Native GPU host¶
system.gpu runs natively through the dream-host-gpu capability library (HostCapability::Gpu,
the gpu feature of dream-host). It drives slang-rhi
and loads only the precompiled shader bundles described in
GPU shader bundle. The Slang compiler is never linked or loaded.
Crate layout¶
| Path | Role |
|---|---|
build.rs, build/fetch.rs |
Downloads the pinned inputs, applies patches/, builds native/ with CMake |
patches/ |
slang-rhi patches: MSL source as precompiled Metal code; unbound acceleration structure slots left unwritten on Vulkan devices without nullDescriptor; D3D12 descriptor reclamation and allocation-failure cleanup |
pins.toml |
URLs and SHA-256 of Slang, slang-rhi, Dawn, Vulkan-Headers, metal-cpp, WinPixEventRuntime |
native/replay/ |
The replay library: answers slang-rhi's reflection queries from a recorded layout |
native/recorder/ |
dream-slang-layout, the build-time recorder that writes those layouts |
native/glue/ |
dgpu.h, the flat C surface over slang-rhi that the Rust code calls |
src/ |
Handle table, bundle reader, backend selection, command decoding, futures |
aot_probe/ |
The Milestone 0 proof, kept runnable with python3 aot_probe/build.py |
tests/backends.rs |
The backend matrix (one child process per backend) |
tests/fixtures/ |
Test bundles and make_bundles.py, which writes them with the pinned tools |
Build¶
build.rs downloads each archive listed in pins.toml once into the GPU cache, verifies its
SHA-256, and extracts it. The cache is $DREAM_GPU_CACHE, or <target>/gpu-cache. Offline
builds work once the cache holds the archives; a failed download names the file and the path it
belongs at. The steps are:
- Read
include/from the host's Slang release, or from$DREAM_SLANGwhen that root carries the pinned version's headers. Nothing else in the release is used. - Copy slang-rhi and apply
patches/*.patchwithgit apply.GIT_CEILING_DIRECTORIESkeeps git away from the Dream checkout above the cache. - Configure
native/CMakeLists.txt. Dawn, Vulkan-Headers, metal-cpp and WinPixEventRuntime reach slang-rhi as verifiedfile://URLs with their SHA-256, so CMake downloads nothing. DXC, OptiX, NVAPI, the Agility SDK and Aftermath are disabled. The replay library is slang-rhi'sslangtarget (SLANG_RHI_BUILD_FROM_SLANG_REPO=ON). - Link the static archives (listed in
archives.txt) intolibdream_host_gpu. Copy Dawn's shared library into the profile directory, because slang-rhi loads Dawn from the directory of the library that links it.HostCapability::companion_library_namesmakes the compiler's runtime staging anddreamer packship Dawn next to the host library.
Downloaded archives and verified source trees are shared in the GPU cache. CMake build directories,
including FetchContent's dependency subbuilds, stay under Cargo's OUT_DIR. Generator and CMake
toolchain environment settings select separate directories, so Ninja and Visual Studio never reuse
one another's CMakeCache.txt. The build respects CMAKE_GENERATOR and otherwise uses CMake's
platform default; installing Ninja on PATH does not silently change that choice.
The build needs CMake, a C++20 compiler and git. Building the backends needs no SDK beyond the
platform's: Metal, Vulkan and WebGPU on macOS; Vulkan, CUDA and WebGPU on Linux; D3D12, D3D11,
Vulkan, CUDA and WebGPU on Windows. Vulkan and CUDA drivers are loaded at run time. On macOS,
Vulkan needs the Khronos loader in front of MoltenVK, because MoltenVK alone rejects
VK_KHR_portability_enumeration.
Manual hardware qualification¶
python scripts/qualify-gpu.py --backend dx12 (or vulkan) runs the existing compute, bindless,
ray-query and ray-pipeline assertions on that backend alone. --backend cuda runs the compute
assertions. The command preserves its project and log under target/gpu-qualification/ and
fails when a requested check skips, so missing drivers cannot count as hardware qualification.
On Windows, install the CUDA toolkit and make its bin directory available through PATH and
CUDA_PATH for the CUDA host. Copy the toolkit's nvrtc*.dll files, including the builtins DLL,
into the Slang toolchain's bin directory so the tool sandbox can compile PTX. Windows
qualification used NVRTC/NVCC 12.9.86 with runtime headers 12.9.79. CUDA cooperative vectors
are unavailable in the pinned build without OptiX.
The CUDA host uses NVRTC for slang-rhi's internal texture-clear kernels at device creation; Dream's shader entry points are precompiled PTX. CUDA does not have the same compiler-free runtime dependency set as Vulkan, D3D12 or Metal.
dream-slang-layout¶
dreamer toolchain install slang extracts the Slang release, then builds
<root>/bin/dream-slang-layout from native/recorder/ and native/replay/layout_blob.*. Those
sources are embedded in dreamer. The build links the release's slang-compiler library, and the
host C++ compiler is found the way Cargo build scripts find it (the cc crate). The tool must link
the exact compiler it reflects, so it is built at install time rather than shipped. It is the
second executable of the slang pinned tool, so generators reach it with
@uses_tool("dream-slang-layout").
Re-running dreamer toolchain install slang refreshes the recorder when its embedded C++
sources or build instructions change, even if the Slang release is already installed. The
source fingerprint is written only after a successful build.
The tool takes slangc's options: positional .slang modules, -entry with an optional -stage,
and -target. The last -o names the layout. An earlier -o right after an entry writes that
entry's code from the same linked program, which is useful when one run must produce code and
layout together. -reflection-json is accepted and ignored.
Threading and futures¶
- One process-wide lock guards the handle table and every glue call that touches a device.
- Each device owns a completion thread, reached through the shared device core rather than the
device's handle, so reads and submissions on live resources work after the guest released the
device itself. A submission returns a fence value; the thread waits for it without the lock,
then takes the lock to finish the job and resolves the guest future with
dream_host_complete_foreign. Queued jobs keep the device alive. Every async import is@async_host. - Reads allocate their guest byte array on the guest thread when they start. Completion threads only fill it, because they must not allocate from the guest's thread-local arena. A failed read resolves to the same array with its length set to 0.
- Device creation runs on a short-lived thread, because creating a device can take long enough to stall a frame.
- Surface creation, acquire and present run on the guest thread, which is the process main thread. They complete their futures immediately.
Resources¶
Handles come from dream_host_abi::handles::HandleTable: they are positive, 0 means failure,
and a released slot comes back with a new generation, so a stale handle misses instead of naming
a newer resource and the id space never runs out. The guest keeps each handle in a
GpuOwnedHandle, and command encoders and GpuArgs keep the owned handles of every resource
their commands name, so a resource whose last user reference is gone is released only after
submit handed its commands to the host. Each entry records its owning device,
and using a resource with another device reports EWRONGDEVICE. Each submission stamps every
resource it references with its fence value. gpuRelease frees a resource at once when its last
submission has finished. Otherwise the resource moves to a deferred list, which the completion
thread drains as fences complete. Devices are reference-counted by their resources and their
completion thread, so a device is destroyed only after all of its work and resources are gone.
Backend selection¶
backend::select is pure and unit-tested:
Autotries, in order, Metal, Vulkan, WebGPU on macOS; D3D12, Vulkan, D3D11, WebGPU on Windows; Vulkan, WebGPU elsewhere. It skips backends missing from the guest's compiled mask, so a program without shaders (mask0) may open any of them. CUDA runs compute only, soAutonever picks it.- An explicit backend that is not in the compiled mask reports
ENOTCOMPILEDwith the backend number. One that cannot open (no adapter or driver, or a host built without it) reportsEUNAVAILABLEwith the reason. Neither ever falls back to another backend. - Cooperative vectors (
GpuFeature.CooperativeVectors) are reported only on Vulkan, D3D12 and CUDA devices whose slang-rhiFeature::CooperativeVectoris present. The pinned build leaves out NVAPI and OptiX, so in practice only Vulkan devices withVK_NV_cooperative_vectorreport it. - slang-rhi's Metal device reports neither wave operations nor half precision, which every GPU it
opens has, so Metal devices report
SubgroupsandFloat16themselves. It never reports cooperative matrices either; Metal devices in the Apple7 family or later (Apple silicon) reportCooperativeMatrices, because Slang lowers 8x8CoopMattosimdgroup_matrix. Bindless handles exist only where slang-rhi implements descriptor handles (Vulkan, D3D12, CUDA). - Creating a pipeline checks the bundle capabilities of its entries (see
GPU shader bundle) and reports
EUNSUPPORTEDwith the missing feature.
Buffers and MLP layers¶
- Buffers are allocated in whole 4-byte words on every backend, because WebGPU sizes buffers,
fills and copies that way. Reads copy whole words into the padding. A write that ends the
buffer is zero-padded into the padding; on WebGPU any other write must start and end on a
word boundary (
EINVALIDotherwise). The browser host follows the same rules. gpuMlpUpload(src/mlp.rs) stores one layer as biases at offset 0 and the matrix at the next multiple of 64 bytes, the layout the generatedcoopVecMatMulAddreads (see GPU shader bundle).mlp::layout_forpicks the matrix layout. On Vulkan and D3D12 devices with cooperative vectors it is the inferencing-optimal layout, whichdgpu_mlp_convert_optimalproduces on the CPU through slang-rhi'sgetCooperativeVectorMatrixSizeandconvertCooperativeVectorMatrix. Everything else keeps row-major weights: CUDA, because its PTX is compiled without OptiX and Slang emulates the multiply on row-major data, and devices without the feature, which cannot run the code anyway.
Bindings¶
A pipeline's program contains exactly the entries it uses, which come from one pipeline group
(see GPU shader bundle). The replay library re-points the group
layout's entry list at them by name (createPrecompiledProgram). Each bundle parameter
row is resolved once, at pipeline creation, to a slang-rhi cursor path. The rules, in priority
order:
- A field with the parameter's name and a matching binding.
- A field with the parameter's name.
- A field with a matching binding number.
Only fields that can hold the parameter's kind are candidates: samplers match sampler fields, textures match texture-shaped resources, and so on. Binding numbers alone are not unique, because Metal numbers buffers, textures and samplers in separate index spaces. The entry's scope is searched before the globals, because reflection reports entry-point bindings relative to the entry. A value parameter matches the uniform block it was packed into, and its bytes land at the recorded offset inside that block. WebGPU rejects a bind group with an empty slot, while the other backends ignore slots the code never reads. Because a render or ray-tracing group's layout covers the globals of all its shaders, the glue fills every slot with per-device placeholders right after binding a WebGPU pipeline, and the real bindings then overwrite them. Read-only slots share one placeholder per kind; each writable slot gets its own, because WebGPU rejects one buffer or texture bound to two writable slots.
Ray tracing¶
src/raytracing.rs decodes and validates the guest's encodings; the glue
(native/glue/raytracing.cpp) only translates them.
gpuBottomLevelBuildbuilds triangles (GpuVec3positions, 12-byte stride, optional 32-bit indices) or boxes (min/maxGpuVec3pairs) into an acceleration structure.gpuTopLevelBuildconverts each instance to the device's native instance record (D3D12, Vulkan, Metal or OptiX) with its 3x4 row-major transform, id, mask, hit group offset and flags. Both record the build into their own submission, insert the handle at once, and resolve the future with it when the fence completes; scratch and instance buffers live until then. A top-level structure keeps its bottom levels alive, and only top-level structures bind to shaders.gpuRayTracingPipelineCreatedecodes theGpuDevice.ray_tracing_pipelinelayout into the shader binding table: one ray generation shader, misses, hit groups namedhit_group_<i>(closest hit, optional any hit and intersection), and callables. All shaders must come from one shader module. The program lists ray generation first and each shader once; only ray generation takes parameters. Pipelines reserve a 128-byte payload, 32-byte hit attributes and a recursion depth of 1 (rays traced from ray generation only).- The command stream traces inside a compute pass (
SetPipeline,Bindwith stageRayGeneration,TraceRays); binding a ray tracing pipeline turns the pass into a slang-rhi ray tracing pass. - Acceleration structures need
GpuFeature.RayTracingorGpuFeature.RayQuery; ray tracing pipelines needRayTracing. Buffers on devices with acceleration structures may feed builds. Native render passes bind top-level structures through the acceleration-structure overload ofGpuRenderPass.bind; fragment shaders can issue inline ray queries directly.
Strict ahead-of-time check¶
With DREAM_GPU_STRICT_AOT=1 the loader check becomes an assertion: once a Slang compiler image
is mapped, the next device creation, bundle load or pipeline creation ends the process with the
image's path. scripts/test_corpus.py sets it and removes DREAM_SLANG for every run.
Errors¶
The glue returns a status and a thread-local message. Driver messages reach a per-device sink,
which keeps the first error since the last failure was reported: later errors are usually
consequences of it, such as Dawn's "invalid pipeline" after a pipeline creation failed. The Rust side maps these to the
CODE\ndetail text that gpuLastError returns (see gpu_error_from_host). Driver messages that
report device loss make the device sticky-lost, and every later call on it reports EDEVICELOST.
testing::simulate_device_loss drives the same path in tests.
Recording can fail through slang-rhi's debug callback even when finish succeeds. Submission
checks the sink before sending the command buffer, so failed binding allocation cannot silently
drop draw state. The D3D12 patch retires completed command buffers before a new encoder reserves
descriptors and excludes failed arena allocations from later frees. Its regression fills the
sampler heap, expects a recoverable error, then verifies subsequent valid submissions succeed.
Tests¶
cargo test -p dream-host-gpu --libcovers:- the bundle reader, including property tests that feed it arbitrary and mutated bytes
- the command stream decoder, with fuzzing
- backend selection
- the handle table
- render pipeline description decoding
cargo test -p dream-host-gpu --test backendsruns device-free selection and validation checks, then re-runs itself once per backend. Each backend dispatches compute in all three binding styles (globals plus a parameter block, entry-point parameters, and the generator's<entry>_<param>globals). It also renders an offscreen triangle and a textured, indexed quad with a depth attachment, reads them back, checks error mapping and simulated device loss, and asserts that no Slang compiler image was mapped. A backend that the host or machine cannot run is reported as skipped. On macOS the Vulkan child uses the Khronos loader and MoltenVK from the GPU cache unlessVK_DRIVER_FILESis set.DREAM_GPU_SURFACE_TEST=1adds a real window fromdream-host-windowand presents three frames to it. WhenDREAM_SLANG(or~/.dream/toolchains/slang-2026.20) is present, a loader self-test maps the Slang compiler on purpose to prove that the check detects it.- Regenerate the fixtures with
python3 tests/fixtures/make_bundles.py. The output is deterministic. cargo test -p dream-host-gpu --libalso unit-tests the ray tracing decoders (geometry, instance records and transforms, binding table layout and program order, with fuzzing). The backend test's strict-AOT self-test maps the Slang compiler withDREAM_GPU_STRICT_AOT=1and checks that device creation ends the process.- The Dream suites in
tests/projects/gpu_runtime/run real programs through the generator and this host on every compiled backend: compute, workgroup and subgroup reductions, an offscreen render read back pixel by pixel, bindless sampling, inline ray queries, a ray tracing pipeline, backend selection, forward- and reverse-mode autodiff checked against finite differences,float16buffers and half arithmetic, and cooperative vectors and matrices (which also check that devices without them reportUnsupportedCapability). A backend the machine cannot run printsSKIP <test> on <backend>: <reason>and the test continues, so machines without a GPU pass. On macOS the corpus script points Vulkan at the GPU cache's Khronos loader and MoltenVK.
Not yet implemented¶
- Ray queries and ray tracing pipelines were qualified on D3D12 and Vulkan on an RTX 4080
under Windows on 2026-10-19 with
scripts/qualify-gpu.py. Metal has no ray tracing passes and MoltenVK has no ray tracing extensions, so macOS uses inline queries on Metal. The pipeline suite skips on devices withoutGpuFeature.RayTracing. - Payload size, hit attribute size and recursion depth are fixed (128 bytes, 32 bytes, 1); the bundle does not record them yet.
- The inferencing-optimal MLP conversion has not run on hardware: no available machine has a Vulkan or D3D12 device with cooperative vectors.
- A render or ray tracing pipeline binds the globals of all of its module's shaders of those stages, because they share one recorded layout. On WebGPU, their storage buffers together count toward the per-stage limit (8 by default, 10 on Dawn over Metal); the generator warns past 8. Compute pipelines bind only their own shader's globals.
- Vertex and fragment shaders of one pipeline must come from the same shader module.
- Vertex attribute locations must be dense (
0..n-1): slang-rhi's Vulkan, Metal and WebGPU backends take attributeias shader locationi. D3D binds by the generator'sLOCATION<n>semantics. - Wayland windows have no slang-rhi surface. GLFW runs them through XWayland, which gives an Xlib handle.