Files
DarkflameServer/dUgcServer/Render/UgcRays.h
Aaron Kimbrell 07dbf30dc8 feat(ugc): ray_backend=hiprt traces on the GPU with HIPRT and Orochi (optional build)
HIPRT (MIT) behind the CMake option DLU_HIPRT (off): its headers come from its
SDK (HIPRT_ROOT, else ROCm's /opt/rocm) and are copied next to the servers; its
library is loaded when first used (hiprtew), as HIP or CUDA are by Orochi
(MIT, fetched pinned by hash; CUDA when its toolkit is found). The trace
kernels (nearest hit skipping the triangle a ray leaves, any hit) are compiled
the first time and kept in cache/hiprt. One GPU context for the process
(hiprt_device picks the GPU); the workers take turns on it. When HIPRT, the
GPU or a scene's upload fails, Embree is used instead, and the UGC server logs
why at start.

For a GPU the rays go in batches (UgcRays::Scene gets batch queries; the CPU
backends answer them a ray at a time):
- hidden faces: with a batch backend the paths are traced side by side, a
  bounce at a time (the path code split into Start, Scatter and Bounce, the one
  by one tracing unchanged); the same paths with the same random numbers, so
  the same triangles are decided (tested with builtin side by side)
- the occlusion bake and the denoised icons' traced occlusion always ask in
  batches (the same rays, the same results)

Its symbols are hidden: the servers export theirs (-rdynamic), and HIPRT's
library, which has an Orochi of its own, would otherwise call ours.

Check: configure with -DDLU_HIPRT=ON on a machine with ROCm (or HIPRT's SDK)
and an AMD RDNA or NVIDIA GPU; UgcServer --make-model x.lxfml out hiprt; the
UGC tests (hits, hidden faces and occlusion against builtin); a build without
it leaves everything as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:29:08 -05:00

88 lines
3.9 KiB
C++

#pragma once
#include <cstdint>
#include <limits>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <glm/glm.hpp>
#include "UgcModel.h"
/**
* Rays against a mesh's triangles, for the hidden faces' paths (the nearest hit) and the ambient occlusion rays
* (whether anything is hit), by one of several backends (the ugc_ray_backend setting, or per job):
* builtin: the UGC server's own bounding volume hierarchies (the ones it always had)
* embree: Intel's Embree 4 on the CPU, on the thread that asks (no threads of its own)
* 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.
* 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.
*/
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 { BUILTIN = 0, EMBREE, HIPRT };
// The setting's name of a backend (builtin, embree, hiprt)
std::string_view Name(eBackend backend);
// A backend by its name (case sensitive); nullopt for anything else
std::optional<eBackend> Parse(std::string_view name);
// Whether this build and machine can use the backend (builtin and embree always)
bool Available(eBackend backend);
// The backend that is used when `wanted` is asked for: itself, or embree when it isn't available
eBackend Resolve(eBackend wanted);
// Why a backend isn't available (empty when it is)
std::string Problem(eBackend backend);
struct Hit {
float t{ INF };
uint32_t triangle{ NONE };
float u{}, v{}; // weights of the triangle's second and third vertex
};
// A ray of a batch (the layout the GPU kernels read too)
struct Ray {
glm::vec3 origin{};
float minT{}; // Occluded: hits further than this count (Closest: further than 0)
glm::vec3 direction{}; // unit
float maxT{ INF }; // hits nearer than this count
uint32_t skip{ NONE }; // Closest: the triangle never hit (the one the ray leaves)
uint32_t padding[3]{};
};
static_assert(sizeof(Ray) == 48, "the GPU kernels read rays as 48 bytes");
static_assert(sizeof(Hit) == 16, "the GPU kernels write hits as 16 bytes");
class Scene {
public:
virtual ~Scene() = default;
// The nearest triangle along the ray (unit direction) further than 0 and before `maxT`, never `skip` (the
// triangle the ray leaves, as in Cycles); triangle NONE (and t = maxT) when there is none
virtual Hit Closest(const glm::vec3& origin, const glm::vec3& direction, uint32_t skip = NONE, float maxT = INF) const = 0;
// Whether the ray (unit direction) hits a triangle further than `minT` and nearer than `maxT`
virtual bool Occluded(const glm::vec3& origin, const glm::vec3& direction, float minT, float maxT) const = 0;
// Many rays at once, as the single ray queries answer them (a GPU answers a batch at the cost of one ray)
virtual void Closest(const Ray* rays, Hit* hits, size_t count) const;
virtual void Occluded(const Ray* rays, uint8_t* occluded, size_t count) const;
// Whether the backend is only fast with big batches (a GPU): the callers then trace many paths side by side
virtual bool PrefersBatches() const { return false; }
};
/**
* The mesh's triangles as they are now (copied) in the backend Resolve(backend) picks. Throws std::runtime_error
* when the backend fails.
*/
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);
}