feat(ugc): ray_backend=embree-gpu traces on Intel GPUs with Embree's SYCL (optional build)

The third ray backend, so every machine has a library for it: Embree on x86
CPUs (embree), HIPRT on AMD and NVIDIA GPUs (hiprt), Embree through SYCL on
Intel Arc and Xe GPUs (embree-gpu).

- CMake option DLU_EMBREE_SYCL (off). dUgcServer/EmbreeSycl is a project of
  its own built by a SYCL compiler (DLU_SYCL_CXX; icpx through ONEAPI_ROOT or
  the path, or the open source DPC++'s clang++ through DPCPP_ROOT) as an
  external project: Embree 4.4 with EMBREE_SYCL_SUPPORT, linked statically and
  bound inside (-Bsymbolic, only its C functions exported, so it never meets
  the servers' own Embree), and the GPU kernels (nearest hit skipping a ray's
  triangle, any hit), into libdlu_embree_sycl next to the servers
- UgcRaysEmbreeGpu loads it the first time embree-gpu is asked for; one GPU for
  the process (embree_gpu_device picks it), the workers take turns, the
  occlusion rays in batches as for hiprt
- without the build, the library or a supported Intel GPU it falls back to
  embree and says why (the UGC server's log at start, --make-model on stderr)
- the option names, the settings page, the dashboard's picker, /reprocessproperty

Verified here: the default build and ctest; the SYCL build with the open source
DPC++ 7.1.0 (compiles, links against oneAPI's libsycl.so.9, exports only its C
functions); on this machine (no Intel GPU) it loads, finds no GPU and falls
back to embree. Not verified: tracing on an Intel GPU (none here).

Check: on a machine with an Intel Arc or Xe GPU and oneAPI, configure with
-DDLU_EMBREE_SYCL=ON and run UgcServer --make-model x.lxfml out embree-gpu;
the UGC tests then compare it with Embree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 12:28:33 -05:00
parent a7defdb326
commit db63fd2919
18 changed files with 604 additions and 31 deletions

View File

@@ -1932,10 +1932,12 @@ namespace {
return mesh;
}
// The backends other than embree that this build and machine can use
// The backends other than embree that this build and machine can use (the GPUs')
std::vector<UgcRays::eBackend> OtherBackends() {
std::vector<UgcRays::eBackend> backends;
if (UgcRays::Available(UgcRays::eBackend::HIPRT)) backends.push_back(UgcRays::eBackend::HIPRT);
for (const auto backend : { UgcRays::eBackend::HIPRT, UgcRays::eBackend::EMBREE_GPU }) {
if (UgcRays::Available(backend)) backends.push_back(backend);
}
return backends;
}
}
@@ -2087,17 +2089,26 @@ TEST(UgcRender, DenoisedIconsTraceTheOcclusionPerPixel) {
}
TEST(UgcRays, NamesAndFallback) {
for (const auto backend : { UgcRays::eBackend::EMBREE, UgcRays::eBackend::HIPRT }) {
for (const auto backend : { UgcRays::eBackend::EMBREE, UgcRays::eBackend::HIPRT, UgcRays::eBackend::EMBREE_GPU }) {
EXPECT_EQ(UgcRays::Parse(UgcRays::Name(backend)), backend);
}
EXPECT_EQ(UgcRays::Parse("embree-gpu"), UgcRays::eBackend::EMBREE_GPU);
EXPECT_EQ(UgcRays::Parse("builtin"), UgcRays::eBackend::EMBREE); // the backend Embree replaced
EXPECT_FALSE(UgcRays::Parse("optix"));
EXPECT_TRUE(UgcRays::Available(UgcRays::eBackend::EMBREE));
// A backend this machine can't use falls back to embree
EXPECT_EQ(UgcRays::Resolve(UgcRays::eBackend::HIPRT), UgcRays::Available(UgcRays::eBackend::HIPRT) ? UgcRays::eBackend::HIPRT : UgcRays::eBackend::EMBREE);
// A backend this build or machine can't use falls back to embree, and says why
for (const auto backend : { UgcRays::eBackend::HIPRT, UgcRays::eBackend::EMBREE_GPU }) {
const bool available = UgcRays::Available(backend);
EXPECT_EQ(UgcRays::Resolve(backend), available ? backend : UgcRays::eBackend::EMBREE);
EXPECT_EQ(UgcRays::Problem(backend).empty(), available) << UgcRays::Problem(backend);
// Made anyway: an Embree scene that works
const auto scene = UgcRays::Make(backend, Clutter());
EXPECT_NEAR(scene->Closest({ 0.3f, 0.1f, 0.1f }, { 1, 0, 0 }).t, 1.7f, 1e-5f) << UgcRays::Name(backend);
}
EXPECT_TRUE(UgcRays::Problem(UgcRays::eBackend::EMBREE).empty());
EXPECT_EQ(UgcRays::Resolve(UgcRays::eBackend::EMBREE), UgcRays::eBackend::EMBREE);
// An empty mesh is hit by nothing
for (const auto backend : { UgcRays::eBackend::EMBREE, UgcRays::eBackend::HIPRT }) {
for (const auto backend : { UgcRays::eBackend::EMBREE, UgcRays::eBackend::HIPRT, UgcRays::eBackend::EMBREE_GPU }) {
const auto scene = UgcRays::Make(backend, UgcModel::Mesh{});
EXPECT_EQ(scene->Closest(glm::vec3(0.0f), glm::vec3(0, 1, 0)).triangle, UgcRays::NONE);
EXPECT_FALSE(scene->Occluded(glm::vec3(0.0f), glm::vec3(0, 1, 0), 0.0f, 10.0f));
@@ -2108,7 +2119,7 @@ TEST(UgcRays, FindsTheExpectedHits) {
// The room of the clutter is [-2, 2]^3 (its walls face inwards; triangles 12 on), the box in it [-0.3, 0.3]^3
// (triangles 0 to 11): rays whose hits are known
const auto mesh = Clutter();
for (const auto backend : { UgcRays::eBackend::EMBREE, UgcRays::eBackend::HIPRT }) {
for (const auto backend : { UgcRays::eBackend::EMBREE, UgcRays::eBackend::HIPRT, UgcRays::eBackend::EMBREE_GPU }) {
const auto scene = UgcRays::Make(backend, mesh);
const auto name = std::string(UgcRays::Name(UgcRays::Resolve(backend)));
// From the box's +X face out along +X: the room's +X wall (triangles 16 and 17) 1.7 away