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

@@ -33,6 +33,37 @@ if(DLU_OIDN)
target_link_libraries(dUgc PRIVATE OpenImageDenoise)
target_compile_definitions(dUgc PRIVATE DLU_OIDN)
endif()
# Embree on Intel GPUs through SYCL (ray_backend=embree-gpu), when built with DLU_EMBREE_SYCL: libdlu_embree_sycl
# (EmbreeSycl/, Embree built with its SYCL support and the GPU kernels) is its own project built by a SYCL compiler
# (DLU_SYCL_CXX: Intel oneAPI DPC++'s icpx, or the open source DPC++'s clang++; found through ONEAPI_ROOT or
# DPCPP_ROOT), put next to the servers and loaded by the UGC server when asked for, so nothing else needs oneAPI.
option(DLU_EMBREE_SYCL "Build the UGC server's Intel GPU ray tracer (ray_backend=embree-gpu; needs a SYCL compiler)" OFF)
if(DLU_EMBREE_SYCL)
find_program(DLU_SYCL_CXX NAMES icpx clang++ PATHS "$ENV{ONEAPI_ROOT}/compiler/latest/bin" "$ENV{DPCPP_ROOT}/bin" NO_DEFAULT_PATH)
if(NOT DLU_SYCL_CXX)
find_program(DLU_SYCL_CXX NAMES icpx)
endif()
if(NOT DLU_SYCL_CXX)
message(FATAL_ERROR "DLU_EMBREE_SYCL needs a SYCL compiler: set DLU_SYCL_CXX, ONEAPI_ROOT (Intel oneAPI) or DPCPP_ROOT (open source DPC++)")
endif()
get_filename_component(DLU_SYCL_BIN "${DLU_SYCL_CXX}" DIRECTORY)
find_program(DLU_SYCL_CC NAMES icx clang PATHS "${DLU_SYCL_BIN}" NO_DEFAULT_PATH REQUIRED)
include(ExternalProject)
set(DLU_EMBREE_SYCL_LIBRARY "${CMAKE_SHARED_LIBRARY_PREFIX}dlu_embree_sycl${CMAKE_SHARED_LIBRARY_SUFFIX}")
ExternalProject_Add(dlu_embree_sycl
SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/EmbreeSycl"
BINARY_DIR "${CMAKE_BINARY_DIR}/embree_sycl"
CMAKE_ARGS -DCMAKE_CXX_COMPILER=${DLU_SYCL_CXX} -DCMAKE_C_COMPILER=${DLU_SYCL_CC} -DCMAKE_BUILD_TYPE=Release
INSTALL_COMMAND ${CMAKE_COMMAND} -E copy_if_different "<BINARY_DIR>/${DLU_EMBREE_SYCL_LIBRARY}" "${CMAKE_BINARY_DIR}/"
BUILD_ALWAYS TRUE
USES_TERMINAL_BUILD TRUE)
target_sources(dUgc PRIVATE "Render/UgcRaysEmbreeGpu.cpp")
target_compile_definitions(dUgc PRIVATE DLU_EMBREE_SYCL)
target_link_libraries(dUgc PRIVATE ${CMAKE_DL_LIBS})
add_dependencies(dUgc dlu_embree_sycl)
message(STATUS "Intel GPU ray tracing (embree-gpu) built with ${DLU_SYCL_CXX}")
endif()
# HIPRT and Orochi, when built with DLU_HIPRT (thirdparty/CMakeLists.txt): the ray_backend=hiprt setting
if(DLU_HIPRT)
target_sources(dUgc PRIVATE "Render/UgcRaysHiprt.cpp")

View File

@@ -0,0 +1,58 @@
# libdlu_embree_sycl: Embree 4 with its SYCL (Intel GPU) support and the UGC server's GPU ray kernels, for
# ray_backend=embree-gpu. A project of its own, built by a SYCL compiler (Intel oneAPI DPC++'s icpx, or the open source
# DPC++'s clang++) as an external project of the servers' build (DLU_EMBREE_SYCL), so the rest never needs one. Embree
# is linked into it statically and bound inside it (-Bsymbolic, none of its symbols exported), so it never meets the
# servers' own Embree (CPU), whose symbols have the same names.
cmake_minimum_required(VERSION 3.25)
project(dlu_embree_sycl CXX)
include(FetchContent)
set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
set(CMAKE_POLICY_DEFAULT_CMP0126 NEW)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(BUILD_TESTING OFF)
set(EMBREE_SYCL_SUPPORT ON)
set(EMBREE_SYCL_AOT_DEVICES "none") # the kernels are compiled for the GPU found when first used
set(EMBREE_STATIC_LIB ON)
set(EMBREE_TASKING_SYSTEM "INTERNAL")
set(EMBREE_ISPC_SUPPORT OFF)
set(EMBREE_TUTORIALS OFF)
set(EMBREE_ZIP_MODE OFF)
set(EMBREE_INSTALL_DEPENDENCIES OFF)
set(EMBREE_FILTER_FUNCTION OFF)
set(EMBREE_RAY_MASK OFF)
set(EMBREE_RAY_PACKETS OFF)
set(EMBREE_BACKFACE_CULLING OFF)
set(EMBREE_GEOMETRY_TRIANGLE ON)
foreach(geometry QUAD CURVE SUBDIVISION USER INSTANCE INSTANCE_ARRAY GRID POINT)
set(EMBREE_GEOMETRY_${geometry} OFF)
endforeach()
set(EMBREE_MAX_ISA "NONE")
set(EMBREE_ISA_SSE2 ON)
set(EMBREE_ISA_SSE42 OFF)
set(EMBREE_ISA_AVX OFF)
set(EMBREE_ISA_AVX2 OFF)
set(EMBREE_ISA_AVX512 OFF)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
FetchContent_Declare(
embree
GIT_REPOSITORY https://github.com/RenderKit/embree.git
GIT_TAG ff9381774dc99fea81a932ad276677aad6a3d4dd #refs/tags/v4.4.0
GIT_PROGRESS TRUE
GIT_SHALLOW 1
)
FetchContent_MakeAvailable(embree)
add_library(dlu_embree_sycl SHARED UgcEmbreeSycl.cpp)
target_compile_options(dlu_embree_sycl PRIVATE -fsycl -Xclang -fsycl-allow-func-ptr -fsycl-targets=spir64)
target_link_options(dlu_embree_sycl PRIVATE -fsycl -fsycl-targets=spir64 -Wl,-Bsymbolic -Wl,--exclude-libs,ALL)
target_link_libraries(dlu_embree_sycl PRIVATE embree ze_wrapper ${CMAKE_DL_LIBS})
# Only the C interface is seen from outside
set_target_properties(dlu_embree_sycl PROPERTIES CXX_VISIBILITY_PRESET hidden BUILD_RPATH "$ORIGIN" INSTALL_RPATH "$ORIGIN")
install(TARGETS dlu_embree_sycl LIBRARY DESTINATION . RUNTIME DESTINATION .)

View File

@@ -0,0 +1,230 @@
// Embree 4 on an Intel GPU through SYCL, behind the plain C interface in UgcEmbreeSycl.h. Compiled by a SYCL compiler
// (Intel oneAPI DPC++ or the open source DPC++) into libdlu_embree_sycl, which the UGC server loads when asked for it.
#include "UgcEmbreeSycl.h"
#include <sycl/sycl.hpp>
#include <embree4/rtcore.h>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <memory>
#include <string>
#include <vector>
#if defined(RTC_NAMESPACE_USE)
RTC_NAMESPACE_USE
#endif
namespace {
// The ray and hit layouts the UGC server sends (UgcRays::Ray, UgcRays::Hit)
struct Ray {
float ox, oy, oz, minT, dx, dy, dz, maxT;
uint32_t skip, pad0, pad1, pad2;
};
struct Hit {
float t;
uint32_t triangle;
float u, v;
};
static_assert(sizeof(Ray) == 48 && sizeof(Hit) == 16, "the UGC server's layouts");
constexpr uint32_t NONE = 0xFFFFFFFFu;
constexpr float FLOAT_MAX = 3.40282347e38f;
// Triangles only: the kernels are specialized for them
const sycl::specialization_id<RTCFeatureFlags> FEATURES;
constexpr RTCFeatureFlags REQUIRED = RTC_FEATURE_FLAG_TRIANGLE;
struct Gpu {
sycl::device device;
sycl::context context;
std::unique_ptr<sycl::queue> queue;
RTCDevice rtc{};
// Shared (USM) buffers for a batch, grown as needed
Ray* rays{};
void* results{};
size_t capacity{};
};
Gpu* g_Gpu{};
void Reserve(size_t count) {
if (count <= g_Gpu->capacity) return;
if (g_Gpu->rays) sycl::free(g_Gpu->rays, *g_Gpu->queue);
if (g_Gpu->results) sycl::free(g_Gpu->results, *g_Gpu->queue);
g_Gpu->rays = sycl::malloc_shared<Ray>(count, *g_Gpu->queue);
g_Gpu->results = sycl::malloc_shared(count * sizeof(Hit), *g_Gpu->queue);
g_Gpu->capacity = g_Gpu->rays && g_Gpu->results ? count : 0;
}
struct Scene {
RTCScene scene{};
RTCTraversable traversable{};
};
}
extern "C" {
int DluEmbreeSyclInit(int index, char* problem, int problemSize) {
const auto fail = [&](const std::string& why) {
if (problem && problemSize > 0) std::snprintf(problem, static_cast<size_t>(problemSize), "%s", why.c_str());
return 1;
};
if (g_Gpu) return 0;
try {
std::vector<sycl::device> supported;
for (const auto& device : sycl::device::get_devices(sycl::info::device_type::gpu)) {
if (rtcIsSYCLDeviceSupported(device)) supported.push_back(device);
}
if (supported.empty()) return fail("no Intel GPU Embree supports (Arc or Xe, with its compute runtime)");
if (index < 0 || index >= static_cast<int>(supported.size())) {
return fail("no supported GPU " + std::to_string(index) + " (" + std::to_string(supported.size()) + " found)");
}
auto gpu = std::make_unique<Gpu>();
gpu->device = supported[static_cast<size_t>(index)];
gpu->context = sycl::context(gpu->device);
gpu->queue = std::make_unique<sycl::queue>(gpu->context, gpu->device, sycl::property::queue::in_order());
gpu->rtc = rtcNewSYCLDevice(gpu->context, "");
if (!gpu->rtc) return fail(std::string("Embree: no SYCL device (") + rtcGetDeviceLastErrorMessage(nullptr) + ")");
rtcSetDeviceSYCLDevice(gpu->rtc, gpu->device);
g_Gpu = gpu.release();
return 0;
} catch (const std::exception& ex) {
return fail(std::string("SYCL: ") + ex.what());
}
}
void* DluEmbreeSyclScene(const float* positions, uint32_t vertices, const uint32_t* indices, uint32_t triangles) {
if (!g_Gpu) return nullptr;
auto scene = std::make_unique<Scene>();
scene->scene = rtcNewScene(g_Gpu->rtc);
if (!scene->scene) return nullptr;
rtcSetSceneBuildQuality(scene->scene, RTC_BUILD_QUALITY_HIGH);
rtcSetSceneFlags(scene->scene, RTC_SCENE_FLAG_ROBUST);
if (triangles > 0 && vertices > 0) {
const RTCGeometry geometry = rtcNewGeometry(g_Gpu->rtc, RTC_GEOMETRY_TYPE_TRIANGLE);
// Embree's own buffers (USM the GPU reads)
auto* v = static_cast<float*>(rtcSetNewGeometryBuffer(geometry, RTC_BUFFER_TYPE_VERTEX, 0, RTC_FORMAT_FLOAT3, 3 * sizeof(float), vertices));
auto* i = static_cast<uint32_t*>(rtcSetNewGeometryBuffer(geometry, RTC_BUFFER_TYPE_INDEX, 0, RTC_FORMAT_UINT3, 3 * sizeof(uint32_t), triangles));
if (!v || !i) {
rtcReleaseGeometry(geometry);
rtcReleaseScene(scene->scene);
return nullptr;
}
std::memcpy(v, positions, sizeof(float) * 3 * vertices);
std::memcpy(i, indices, sizeof(uint32_t) * 3 * triangles);
rtcCommitGeometry(geometry);
rtcAttachGeometry(scene->scene, geometry);
rtcReleaseGeometry(geometry);
}
rtcCommitScene(scene->scene);
if (rtcGetDeviceError(g_Gpu->rtc) != RTC_ERROR_NONE) {
rtcReleaseScene(scene->scene);
return nullptr;
}
scene->traversable = rtcGetSceneTraversable(scene->scene);
return scene.release();
}
void DluEmbreeSyclRelease(void* handle) {
auto* scene = static_cast<Scene*>(handle);
if (!scene) return;
if (scene->scene) rtcReleaseScene(scene->scene);
delete scene;
}
int DluEmbreeSyclClosest(void* handle, const void* rays, void* hits, uint32_t count) {
auto* scene = static_cast<Scene*>(handle);
if (!g_Gpu || !scene) return 1;
try {
Reserve(count);
if (g_Gpu->capacity < count) return 1;
std::memcpy(g_Gpu->rays, rays, count * sizeof(Ray));
const Ray* in = g_Gpu->rays;
Hit* out = static_cast<Hit*>(g_Gpu->results);
const RTCTraversable traversable = scene->traversable;
g_Gpu->queue->submit([=](sycl::handler& handler) {
handler.set_specialization_constant<FEATURES>(REQUIRED);
handler.parallel_for(sycl::range<1>(count), [=](sycl::item<1> item, sycl::kernel_handler kernel) {
const size_t k = item.get_id(0);
const Ray r = in[k];
Hit hit{ r.maxT, NONE, 0.0f, 0.0f };
float minT = 1.17549435e-38f;
// The ray's skip triangle is never hit: when it is the nearest, the ray goes on from just past it
for (int attempt = 0; attempt < 8; attempt++) {
RTCRayHit query{};
query.ray.org_x = r.ox;
query.ray.org_y = r.oy;
query.ray.org_z = r.oz;
query.ray.dir_x = r.dx;
query.ray.dir_y = r.dy;
query.ray.dir_z = r.dz;
query.ray.tnear = minT;
query.ray.tfar = sycl::fmin(r.maxT, FLOAT_MAX);
query.ray.mask = 0xFFFFFFFFu;
query.hit.geomID = RTC_INVALID_GEOMETRY_ID;
query.hit.primID = RTC_INVALID_GEOMETRY_ID;
RTCIntersectArguments arguments;
rtcInitIntersectArguments(&arguments);
arguments.feature_mask = kernel.get_specialization_constant<FEATURES>();
rtcTraversableIntersect1(traversable, &query, &arguments);
if (query.hit.geomID == RTC_INVALID_GEOMETRY_ID) break;
if (query.hit.primID != r.skip) {
hit = Hit{ query.ray.tfar, query.hit.primID, query.hit.u, query.hit.v };
break;
}
minT = sycl::nextafter(query.ray.tfar, FLOAT_MAX);
}
out[k] = hit;
});
});
g_Gpu->queue->wait_and_throw();
std::memcpy(hits, out, count * sizeof(Hit));
return 0;
} catch (const std::exception&) {
return 1;
}
}
int DluEmbreeSyclOccluded(void* handle, const void* rays, uint8_t* occluded, uint32_t count) {
auto* scene = static_cast<Scene*>(handle);
if (!g_Gpu || !scene) return 1;
try {
Reserve(count);
if (g_Gpu->capacity < count) return 1;
std::memcpy(g_Gpu->rays, rays, count * sizeof(Ray));
const Ray* in = g_Gpu->rays;
uint8_t* out = static_cast<uint8_t*>(g_Gpu->results);
const RTCTraversable traversable = scene->traversable;
g_Gpu->queue->submit([=](sycl::handler& handler) {
handler.set_specialization_constant<FEATURES>(REQUIRED);
handler.parallel_for(sycl::range<1>(count), [=](sycl::item<1> item, sycl::kernel_handler kernel) {
const size_t k = item.get_id(0);
const Ray r = in[k];
RTCRay ray{};
ray.org_x = r.ox;
ray.org_y = r.oy;
ray.org_z = r.oz;
ray.dir_x = r.dx;
ray.dir_y = r.dy;
ray.dir_z = r.dz;
// Hits exactly at minT or maxT don't count, as in the other backends
ray.tnear = sycl::nextafter(r.minT, FLOAT_MAX);
ray.tfar = sycl::nextafter(sycl::fmin(r.maxT, FLOAT_MAX), 0.0f);
ray.mask = 0xFFFFFFFFu;
RTCOccludedArguments arguments;
rtcInitOccludedArguments(&arguments);
arguments.feature_mask = kernel.get_specialization_constant<FEATURES>();
rtcTraversableOccluded1(traversable, &ray, &arguments);
out[k] = ray.tfar < 0.0f ? 1 : 0;
});
});
g_Gpu->queue->wait_and_throw();
std::memcpy(occluded, out, count);
return 0;
} catch (const std::exception&) {
return 1;
}
}
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include <cstdint>
/**
* The plain C interface of libdlu_embree_sycl (built with DLU_EMBREE_SYCL by a SYCL compiler, loaded by the UGC server
* when ray_backend=embree-gpu): Embree 4 on an Intel GPU (Arc, Xe) through SYCL. Rays are 48 bytes and hits 16, as
* UgcRays::Ray and UgcRays::Hit. Calls are made by one thread at a time.
*/
#if defined(_WIN32)
#define DLU_EMBREE_SYCL_API __declspec(dllexport)
#else
#define DLU_EMBREE_SYCL_API __attribute__((visibility("default")))
#endif
extern "C" {
// Picks the `index`-th SYCL GPU Embree supports and sets it up; 0 when it can, else writes why into `problem`
DLU_EMBREE_SYCL_API int DluEmbreeSyclInit(int index, char* problem, int problemSize);
// The mesh on the GPU (positions 3 floats a vertex, indices 3 a triangle); null when it fails
DLU_EMBREE_SYCL_API void* DluEmbreeSyclScene(const float* positions, uint32_t vertices, const uint32_t* indices, uint32_t triangles);
DLU_EMBREE_SYCL_API void DluEmbreeSyclRelease(void* scene);
// The nearest hits (never a ray's skip triangle) and whether anything is hit; 0 when done
DLU_EMBREE_SYCL_API int DluEmbreeSyclClosest(void* scene, const void* rays, void* hits, uint32_t count);
DLU_EMBREE_SYCL_API int DluEmbreeSyclOccluded(void* scene, const void* rays, uint8_t* occluded, uint32_t count);
}

View File

@@ -12,6 +12,9 @@
#ifdef DLU_HIPRT
#include "UgcRaysHiprt.h"
#endif
#ifdef DLU_EMBREE_SYCL
#include "UgcRaysEmbreeGpu.h"
#endif
namespace {
using UgcRays::Hit;
@@ -168,13 +171,17 @@ namespace UgcRays {
}
std::string_view Name(eBackend backend) {
return backend == eBackend::HIPRT ? "hiprt" : "embree";
switch (backend) {
case eBackend::HIPRT: return "hiprt";
case eBackend::EMBREE_GPU: return "embree-gpu";
default: return "embree";
}
}
std::optional<eBackend> Parse(std::string_view name) {
// builtin: the UGC server's own hierarchies, which Embree replaced (settings and options that name it)
if (name == "builtin") return eBackend::EMBREE;
for (const auto backend : { eBackend::EMBREE, eBackend::HIPRT }) {
for (const auto backend : { eBackend::EMBREE, eBackend::HIPRT, eBackend::EMBREE_GPU }) {
if (Name(backend) == name) return backend;
}
return std::nullopt;
@@ -183,6 +190,9 @@ namespace UgcRays {
bool Available(eBackend backend) {
#ifdef DLU_HIPRT
if (backend == eBackend::HIPRT) return UgcRaysHiprt::Available();
#endif
#ifdef DLU_EMBREE_SYCL
if (backend == eBackend::EMBREE_GPU) return UgcRaysEmbreeGpu::Available();
#endif
return backend == eBackend::EMBREE;
}
@@ -196,7 +206,10 @@ namespace UgcRays {
#ifdef DLU_HIPRT
if (backend == eBackend::HIPRT) return UgcRaysHiprt::Problem();
#endif
return "the server was built without it (DLU_HIPRT)";
#ifdef DLU_EMBREE_SYCL
if (backend == eBackend::EMBREE_GPU) return UgcRaysEmbreeGpu::Problem();
#endif
return backend == eBackend::HIPRT ? "the server was built without it (DLU_HIPRT)" : "the server was built without it (DLU_EMBREE_SYCL)";
}
std::unique_ptr<Scene> Make(eBackend backend, const UgcModel::Mesh& mesh) {
@@ -206,16 +219,24 @@ namespace UgcRays {
// A GPU that fails now (out of memory, ...) leaves the job to Embree
if (auto scene = UgcRaysHiprt::Make(mesh)) return scene;
return std::make_unique<EmbreeScene>(mesh);
#endif
#ifdef DLU_EMBREE_SYCL
case eBackend::EMBREE_GPU:
if (auto scene = UgcRaysEmbreeGpu::Make(mesh)) return scene;
return std::make_unique<EmbreeScene>(mesh);
#endif
default: return std::make_unique<EmbreeScene>(mesh);
}
}
void SetGpuDevice(int index) {
void SetGpuDevice(eBackend backend, int index) {
#ifdef DLU_HIPRT
UgcRaysHiprt::SetDevice(index);
#else
(void)index;
if (backend == eBackend::HIPRT) UgcRaysHiprt::SetDevice(index);
#endif
#ifdef DLU_EMBREE_SYCL
if (backend == eBackend::EMBREE_GPU) UgcRaysEmbreeGpu::SetDevice(index);
#endif
(void)backend;
(void)index;
}
}

View File

@@ -18,6 +18,8 @@
* hiprt: AMD's HIPRT on the GPU (AMD through HIP, NVIDIA through CUDA, loaded when first asked for by Orochi),
* when built with DLU_HIPRT and a GPU is there; else embree. One GPU for the process, used by one thread
* at a time; its time is not CPU time.
* embree-gpu: Embree 4 on an Intel GPU (Arc, Xe) through SYCL, when built with DLU_EMBREE_SYCL and such a GPU is
* there; else embree. The same way: one GPU for the process, a thread at a time.
* A scene is built and traced on the thread that asks, so its time counts towards that thread's CPU time
* (UgcThrottle). Scenes aren't shared between threads. docs/UgcServer.md ("Processing options") has the details.
*/
@@ -25,9 +27,9 @@ namespace UgcRays {
constexpr uint32_t NONE = std::numeric_limits<uint32_t>::max();
constexpr float INF = std::numeric_limits<float>::infinity();
enum class eBackend : uint8_t { EMBREE = 0, HIPRT };
enum class eBackend : uint8_t { EMBREE = 0, HIPRT, EMBREE_GPU };
// The setting's name of a backend (embree, hiprt)
// The setting's name of a backend (embree, hiprt, embree-gpu)
std::string_view Name(eBackend backend);
// A backend by its name (case sensitive; builtin, the backend Embree replaced, is embree); nullopt for anything else
std::optional<eBackend> Parse(std::string_view name);
@@ -81,6 +83,7 @@ namespace UgcRays {
*/
std::unique_ptr<Scene> Make(eBackend backend, const UgcModel::Mesh& mesh);
// Which GPU hiprt uses (hiprt_device: 0 is the first HIP or CUDA device); before it is first used
void SetGpuDevice(int index);
// Which GPU a GPU backend uses (hiprt_device: 0 is the first HIP or CUDA device; embree_gpu_device: 0 is the first
// Intel GPU Embree supports); before it is first used
void SetGpuDevice(eBackend backend, int index);
}

View File

@@ -0,0 +1,163 @@
#include "UgcRaysEmbreeGpu.h"
#include <algorithm>
#include <atomic>
#include <mutex>
#include <stdexcept>
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#else
#include <dlfcn.h>
#endif
#include "BinaryPathFinder.h"
#include "EmbreeSycl/UgcEmbreeSycl.h"
namespace {
#if defined(_WIN32)
constexpr const char* LIBRARY = "dlu_embree_sycl.dll";
#else
constexpr const char* LIBRARY = "libdlu_embree_sycl.so";
#endif
// Rays sent to the GPU at once at most (48 MB of rays)
constexpr size_t BATCH = 1u << 20;
std::atomic<int> g_DeviceIndex{ 0 };
std::mutex g_Mutex; // one thread on the GPU at a time; everything below is used under it
struct Library {
bool tried{};
bool ok{};
std::string problem;
decltype(&DluEmbreeSyclInit) init{};
decltype(&DluEmbreeSyclScene) scene{};
decltype(&DluEmbreeSyclRelease) release{};
decltype(&DluEmbreeSyclClosest) closest{};
decltype(&DluEmbreeSyclOccluded) occluded{};
} g_Library;
template<typename T>
bool Find(void* handle, const char* name, T& out) {
#if defined(_WIN32)
out = reinterpret_cast<T>(GetProcAddress(static_cast<HMODULE>(handle), name));
#else
out = reinterpret_cast<T>(dlsym(handle, name));
#endif
return out != nullptr;
}
// Under g_Mutex: loads the library next to the servers and sets the GPU up, once
bool Init() {
if (g_Library.tried) return g_Library.ok;
g_Library.tried = true;
const auto path = (BinaryPathFinder::GetBinaryDir() / LIBRARY).string();
#if defined(_WIN32)
void* handle = LoadLibraryA(path.c_str());
#else
// Local: its symbols (Embree's among them, bound inside it) stay its own
void* handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
#endif
if (!handle) {
#if defined(_WIN32)
g_Library.problem = path + " could not be loaded";
#else
const char* error = dlerror();
g_Library.problem = path + " could not be loaded (" + (error ? error : "?") + ")";
#endif
return false;
}
if (!Find(handle, "DluEmbreeSyclInit", g_Library.init) || !Find(handle, "DluEmbreeSyclScene", g_Library.scene) ||
!Find(handle, "DluEmbreeSyclRelease", g_Library.release) || !Find(handle, "DluEmbreeSyclClosest", g_Library.closest) ||
!Find(handle, "DluEmbreeSyclOccluded", g_Library.occluded)) {
g_Library.problem = path + " is not the UGC server's (functions missing)";
return false;
}
char problem[512]{};
if (g_Library.init(g_DeviceIndex, problem, sizeof(problem)) != 0) {
g_Library.problem = problem;
return false;
}
g_Library.ok = true;
return true;
}
class EmbreeGpuScene final : public UgcRays::Scene {
public:
explicit EmbreeGpuScene(const UgcModel::Mesh& mesh) {
std::lock_guard lock(g_Mutex);
m_Scene = g_Library.scene(reinterpret_cast<const float*>(mesh.positions.data()), static_cast<uint32_t>(mesh.positions.size()), mesh.indices.data(),
static_cast<uint32_t>(mesh.TriangleCount()));
if (!m_Scene) throw std::runtime_error("Embree (GPU): the scene could not be made");
}
~EmbreeGpuScene() override {
std::lock_guard lock(g_Mutex);
g_Library.release(m_Scene);
}
EmbreeGpuScene(const EmbreeGpuScene&) = delete;
EmbreeGpuScene& operator=(const EmbreeGpuScene&) = delete;
UgcRays::Hit Closest(const glm::vec3& origin, const glm::vec3& direction, uint32_t skip, float maxT) const override {
UgcRays::Ray ray{ origin, 0.0f, direction, maxT, skip };
UgcRays::Hit hit;
Closest(&ray, &hit, 1);
return hit;
}
bool Occluded(const glm::vec3& origin, const glm::vec3& direction, float minT, float maxT) const override {
UgcRays::Ray ray{ origin, minT, direction, maxT };
uint8_t occluded = 0;
Occluded(&ray, &occluded, 1);
return occluded != 0;
}
void Closest(const UgcRays::Ray* rays, UgcRays::Hit* hits, size_t count) const override {
std::lock_guard lock(g_Mutex);
for (size_t first = 0; first < count; first += BATCH) {
const auto batch = static_cast<uint32_t>(std::min(BATCH, count - first));
if (g_Library.closest(m_Scene, rays + first, hits + first, batch) != 0) throw std::runtime_error("Embree (GPU): tracing failed");
}
}
void Occluded(const UgcRays::Ray* rays, uint8_t* occluded, size_t count) const override {
std::lock_guard lock(g_Mutex);
for (size_t first = 0; first < count; first += BATCH) {
const auto batch = static_cast<uint32_t>(std::min(BATCH, count - first));
if (g_Library.occluded(m_Scene, rays + first, occluded + first, batch) != 0) throw std::runtime_error("Embree (GPU): tracing failed");
}
}
bool PrefersBatches() const override { return true; }
private:
void* m_Scene{};
};
}
namespace UgcRaysEmbreeGpu {
bool Available() {
std::lock_guard lock(g_Mutex);
return Init();
}
std::string Problem() {
std::lock_guard lock(g_Mutex);
return g_Library.problem;
}
std::unique_ptr<UgcRays::Scene> Make(const UgcModel::Mesh& mesh) {
if (!Available()) return nullptr;
try {
return std::make_unique<EmbreeGpuScene>(mesh);
} catch (const std::exception&) {
return nullptr;
}
}
void SetDevice(int index) {
g_DeviceIndex = index;
}
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include <memory>
#include <string>
#include "UgcRays.h"
/**
* UgcRays' embree-gpu backend (built with DLU_EMBREE_SYCL): Embree 4 on an Intel GPU (Arc, Xe) through SYCL, in
* libdlu_embree_sycl next to the servers (built by a SYCL compiler, dUgcServer/EmbreeSycl), loaded when first asked for.
* One GPU for the process; the threads take turns on it.
*/
namespace UgcRaysEmbreeGpu {
// Whether it can be used: loads the library and sets the GPU up the first time
bool Available();
// Why it can't (empty when it can, or before it was first asked)
std::string Problem();
// The mesh on the GPU; null when the GPU fails (the caller uses another backend)
std::unique_ptr<UgcRays::Scene> Make(const UgcModel::Mesh& mesh);
// Which of the GPUs Embree supports (0: the first); before it is first used
void SetDevice(int index);
}

View File

@@ -159,7 +159,8 @@ namespace {
// What traces the occlusion rays (the icon's too)
settings.ao.rays = UgcRays::Parse(Game::config->GetValue("ray_backend")).value_or(UgcRays::eBackend::EMBREE);
// The GPU hiprt uses (read before it is first used; changing it takes a restart)
UgcRays::SetGpuDevice(std::max(Setting<int32_t>("hiprt_device", 0), 0));
UgcRays::SetGpuDevice(UgcRays::eBackend::HIPRT, std::max(Setting<int32_t>("hiprt_device", 0), 0));
UgcRays::SetGpuDevice(UgcRays::eBackend::EMBREE_GPU, std::max(Setting<int32_t>("embree_gpu_device", 0), 0));
settings.ao.enabled = Setting<int32_t>("bake_ao", 1) != 0;
settings.ao.distance = Setting<float>("ao_distance", 5.0f);
settings.ao.samples = std::clamp(Setting<int32_t>("ao_samples", 64), 1, 1024);
@@ -573,10 +574,13 @@ namespace {
auto settings = ReadSettings();
UgcProcessOptions::Choice choice;
if (!UgcProcessOptions::Parse(options, choice)) {
std::cerr << "Unknown processing options \"" << options << "\" (ray backend embree or hiprt; denoise off or oidn)\n";
std::cerr << "Unknown processing options \"" << options << "\" (ray backend embree, hiprt or embree-gpu; denoise off or oidn)\n";
return EXIT_FAILURE;
}
UgcJobs::ApplyOptions(settings, choice);
if (UgcRays::Resolve(settings.ao.rays) != settings.ao.rays) {
std::cerr << "ray_backend=" << UgcRays::Name(settings.ao.rays) << " can't be used (" << UgcRays::Problem(settings.ao.rays) << "): embree instead\n";
}
UgcBricks::BrickLibrary library(res, 0, ClientReader());
if (!library.LoadMaterials()) std::cerr << "Couldn't read Materials.xml from " << (res / "brickdb.zip") << "; bricks will be grey\n";
UgcJobs::Outcome outcome;