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

@@ -79,7 +79,7 @@ namespace UgcDebounce {
* chosen names in this order, separated by spaces ("embree oidn"); a choice left out is the setting's.
*/
namespace UgcProcessOptions {
inline constexpr std::string_view RAYS[] = { "embree", "hiprt" };
inline constexpr std::string_view RAYS[] = { "embree", "hiprt", "embree-gpu" };
inline constexpr std::string_view DENOISE[] = { "off", "oidn" };
// Hidden-face methods earlier versions' options named (stored options still have them): read and ignored
inline constexpr std::string_view RETIRED[] = { "toolbox", "fast" };

View File

@@ -498,8 +498,9 @@ namespace {
c.Add(Bool(UGC, "remove_hidden_faces", "Remove faces nobody can see", "The opaque bricks are rendered from 42 directions around the model; the triangles that show in none are removed. Faces seen only through openings or by bounced light go too.", true));
c.Add(Bool(UGC, "hsr_ground_plane", "Nothing seen from below", "Also removes what can only be seen from under the model.", false));
c.Add(Unit(Int(UGC, "hsr_resolution", "Detail of the visibility renders", "The size of each of the 42 renders; bigger keeps smaller visible faces.", "1024", 64, 4096), "pixels"));
c.Add(Labels(Choice(UGC, "ray_backend", "Ray tracer", "What traces the occlusion rays: embree (Intel Embree on the CPU) or hiprt (the GPU, when the server was built with it and has one; else embree). They give the same results but for rounding. Staff can pick another for one make when making models again.", "embree", { "embree", "hiprt" }), { "Embree (CPU)", "HIPRT (GPU)" }));
c.Add(Labels(Choice(UGC, "ray_backend", "Ray tracer", "What traces the occlusion rays: embree (Intel Embree on the CPU), hiprt (AMD or NVIDIA GPUs through HIPRT) or embree-gpu (Intel Arc or Xe GPUs through Embree's SYCL), each GPU one only when the server was built with it and has such a GPU (else embree). They give the same results but for rounding. Staff can pick another for one make when making models again.", "embree", { "embree", "hiprt", "embree-gpu" }), { "Embree (CPU)", "HIPRT (AMD or NVIDIA GPU)", "Embree (Intel GPU)" }));
c.Add(Int(UGC, "hiprt_device", "GPU for HIPRT", "Which GPU the hiprt ray tracer uses: 0 is the first HIP (AMD) or CUDA (NVIDIA) device.", "0", 0, 16, true));
c.Add(Int(UGC, "embree_gpu_device", "Intel GPU for Embree", "Which Intel GPU the embree-gpu ray tracer uses: 0 is the first Embree supports.", "0", 0, 16, true));
c.Add(Labels(Choice(UGC, "denoise", "Denoise icons", "oidn (when the server was built with Intel Open Image Denoise; else off): a model's icon is drawn from its colors before the occlusion bake, with its occlusion traced per pixel with a few rays and the noise removed by the denoiser, instead of the baked occlusion. The model itself keeps its baked occlusion (a denoiser only works on images).", "off", { "off", "oidn" }), { "Off", "Open Image Denoise" }));
c.Add(Int(UGC, "denoise_samples", "Denoised occlusion rays per pixel", "Occlusion rays traced from each pixel of a denoised icon (before it is scaled down, so 16 times as many per icon pixel); fewer is faster and noisier.", "4", 1, 256));
c.Add(Bool(UGC, "bake_ao", "Darken hidden corners", "Ambient occlusion baked into the vertex colors.", true));

View File

@@ -727,7 +727,7 @@
// ---- processing options: Make again's choices and the comparison of what made the models ----
var OPTION_LABELS = { embree: 'Embree (CPU)', hiprt: 'HIPRT (GPU)', off: 'Off', oidn: 'Open Image Denoise' };
var OPTION_LABELS = { embree: 'Embree (CPU)', hiprt: 'HIPRT (GPU)', 'embree-gpu': 'Embree (Intel GPU)', off: 'Off', oidn: 'Open Image Denoise' };
function loadOptions() {
return api.get('/api/ugc/options').then(function (d) {
if (!d.success) throw new Error(d.error || 'Failed');

View File

@@ -35,8 +35,8 @@
<details class="card mb-3" id="optionsCard">
<summary class="card-header py-2 small fw-semibold">Processing options compared</summary>
<div class="card-body py-2 small">
<p class="text-body-secondary mb-2">The ways of making models to compare: the ray tracer of the occlusion (Embree on the CPU, HIPRT on the
GPU) and denoising of icons. The settings pick the defaults; <b>Make again</b> can use others for a make (the options next
<p class="text-body-secondary mb-2">The ways of making models to compare: the ray tracer of the occlusion (Embree on the CPU, HIPRT on AMD or
NVIDIA GPUs, Embree on Intel GPUs) and denoising of icons. The settings pick the defaults; <b>Make again</b> can use others for a make (the options next
to it, or <code>/reprocessproperty embree oidn</code> in game).
Every make is counted under what made it: the averages per make below, each model's in the List view's Options column.</p>
<div class="table-responsive"><table class="table table-sm align-middle mb-0" id="optionsTable">

View File

@@ -497,7 +497,7 @@ void SlashCommandHandler::Startup() {
Command ReprocessPropertyCommand{
.help = "Make this property's models again and reload it",
.info = "The UGC server makes every model placed on the property you are on again, with the current UGC settings or the processing options given (in any order: embree or hiprt; off or oidn). Once they are made, everyone on the property is sent back into it, so their game loads the new meshes",
.info = "The UGC server makes every model placed on the property you are on again, with the current UGC settings or the processing options given (in any order: embree, hiprt or embree-gpu; off or oidn). Once they are made, everyone on the property is sent back into it, so their game loads the new meshes",
.aliases = { "reprocessproperty", "reloadpropertymodels" },
.handle = DEVGMCommands::ReprocessProperty,
.requiredLevel = eGameMasterLevel::DEVELOPER

View File

@@ -1993,7 +1993,7 @@ namespace DEVGMCommands {
// Optional processing options for this make: ray backend and denoising, in any order
UgcProcessOptions::Choice choice;
if (!UgcProcessOptions::Parse(args, choice)) {
ChatPackets::SendSystemMessage(sysAddr, u"Usage: /reprocessproperty [embree|hiprt] [off|oidn] (left out: the UGC settings')");
ChatPackets::SendSystemMessage(sysAddr, u"Usage: /reprocessproperty [embree|hiprt|embree-gpu] [off|oidn] (left out: the UGC settings')");
return;
}
const auto options = UgcProcessOptions::ToString(choice);

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;

View File

@@ -137,7 +137,7 @@ These commands are primarily for development and testing. The usage of many of t
|resurrect|`/resurrect`|Resurrects the player.|8|
|setminifig|`/setminifig <body part> <minifig item id>`|Alters your player's minifig. Body part can be one of "Eyebrows", "Eyes", "HairColor", "HairStyle", "Pants", "LeftHand", "Mouth", "RightHand", "Shirt", or "Hands". Changing minifig parts could break the character so this command is limited to GMs.|1|
|testmap|`/testmap <zone> (clone-id) (instance-id) (spawn-point)`|Transfers you to the given zone by id and clone id and then spawns you at the specified spawn point if one was specified. Ignores instance-id for now. Aliases: `/tm`.|1|
|reprocessproperty|`/reprocessproperty [embree\|hiprt] [off\|oidn]`|Has the UGC server make every brick built model on the property you are on again, with the current UGC settings or the processing options given, in any order (ray backend, denoising; see docs/UgcServer.md, "Processing options"). When they are all made (at most 15 minutes), everyone on the property is sent back into it, so their game downloads the new meshes. Aliases: `/reloadpropertymodels`.|8|
|reprocessproperty|`/reprocessproperty [embree\|hiprt\|embree-gpu] [off\|oidn]`|Has the UGC server make every brick built model on the property you are on again, with the current UGC settings or the processing options given, in any order (ray backend, denoising; see docs/UgcServer.md, "Processing options"). When they are all made (at most 15 minutes), everyone on the property is sent back into it, so their game downloads the new meshes. Aliases: `/reloadpropertymodels`.|8|
|reportproxphys|`/reportproxphys`|Prints to console the position and radius of proximity sensors.|9|
|spawnphysicsverts|`/spawnphysicsverts`|Spawns a 1x1 brick at all vertices of phantom physics objects|8|
|teleport|`/teleport <x/source player> (y) <z/target player>`|Teleports you. If no Y is given, you are teleported to the height of the terrain or physics object at (x, z). Any of the coordinates can use the syntax of an exact position (10.0), or a relative position (~+10.0). A ~ means use the current value of that axis as the base value. Addition or subtraction is supported (~+10) (~-10). If source player and target player are players that exist in the world, then the source player will be teleported to target player. Aliases: `/tele`, `/tp`.|6|

View File

@@ -136,11 +136,12 @@ remove_hidden_faces=1
hsr_ground_plane=0
hsr_resolution=1024
# What traces the occlusion rays: embree (Intel Embree on the CPU) or
# hiprt (the GPU, when built with DLU_HIPRT and the machine has one; else embree). hiprt_device: which GPU (0: the first
# HIP or CUDA device; restart to change).
# What traces the occlusion rays: embree (Intel Embree on the CPU), hiprt (AMD or NVIDIA GPUs, when built with
# DLU_HIPRT) or embree-gpu (Intel Arc or Xe GPUs, when built with DLU_EMBREE_SYCL); without the build or the GPU,
# embree. hiprt_device and embree_gpu_device: which GPU (0: the first; restart to change).
ray_backend=embree
hiprt_device=0
embree_gpu_device=0
# denoise: off, or oidn (when built with DLU_OIDN; else off): a model's icon is drawn from its colors before the
# occlusion bake with its occlusion traced per pixel (denoise_samples rays from each pixel of the supersampled image)

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