BuildTLAS
- A rebuild for a new instance count or build preference used to create a
new acceleration structure and destroy the old one at once, though the
command buffer being recorded could still use it (a scene and then a
model built in one setup). The acceleration structure now spans its whole
storage buffer, and a rebuild that fits builds into it again, so the
handle and tlases[i].address stay the same and nothing is destroyed. A
TLAS that outgrows its storage gets new storage and a new handle; the old
handle goes to the deletion queue ahead of the storage it lives in. It is
queued after the scratch is resized, and its address goes with it, so a
Resize or create that throws leaves no address of a queued acceleration
structure behind.
- The barrier before the build also orders earlier traces and compute ray
queries of the TLAS, which a refit or a rebuild in place would otherwise
overwrite while they read it (BuildTlasWriteBarrier). Its source access
mask holds no read bit: one does nothing, but makes synchronization
validation 1.4.363 report the second of two builds in one command buffer
as a write-after-write hazard.
- TlasReadBarrier is now TlasBarrier, shared by both barriers.
- Documented: call BuildTLAS once every earlier submission that used
tlases[index] has completed, and when the address stays the same.
TlasWithBuffer
- Moves hand the acceleration structure over, and destroying or assigning
over one queues it ahead of its storage. The implicit members dropped the
handle over freed storage, or left two owners and a moved-from TLAS that
refit through buffers it no longer had.
Tests
- TLASLifetime: destroying, moving, move-assigning and resetting a
TlasWithBuffer.
- TLASHighWaterMark skips without ray tracing. It asserts the acceleration
structure is rebuilt in place on a shrink or a growth within capacity,
that the old handle is queued before its storage on a growth past it, and
that builds in one command buffer keep the handle while they fit, queue
it once when one outgrows the storage, and free it only on its retire
frame.
- TLASBuildBarrier checks the barrier before the build too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RenderingElement3D::tlases was a plain static array, so its buffers
were freed by its static destructor at process exit. The Vulkan driver
and layers are loaded during Device::Initialize, after the array was
constructed, so their own statics are torn down first, and destroying
the buffers then called into freed driver and layer state: every
ray-traced app run with CRAFTER_GRAPHICS_VALIDATION segfaulted on exit
inside the validation layer. TLASHighWaterMark and
TLASInstanceDirtyTracking worked around it by tearing frame 0's TLAS
down by hand before returning.
tlases now refers to an array that is never destroyed, the choice
Device already makes for the device itself, and exit reclaims the
memory. Indexing it is unchanged for callers. The hand teardown in the
two tests goes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The TLAS metadata buffer is CPU-written every frame (one userMetadata word
per element in BuildTLAS's copy loop) and read by the ray shaders as a
STORAGE_BUFFER via device address every frame. Allocated
HOST_VISIBLE | HOST_COHERENT (system RAM), every per-frame shader read
traversed PCIe — the same cost the sibling instance buffer paid before #65.
Upgrade the request to HOST_VISIBLE with a best-effort DEVICE_LOCAL preference
(BAR/VRAM), keeping the single persistently-mapped buffer. The CPU's writes are
write-only, sequential, never-read-back — the ideal write-combined BAR load —
and the shader reads now hit local VRAM.
Unlike the instance buffer there is no GPU co-write, so the direct upgrade is
unconditional. Staging is still deliberately avoided: this buffer is rewritten
by the CPU every frame, so a per-frame stage+copy+barrier would defeat the
purpose (#75 caveat 2).
Correctness:
- DEVICE_LOCAL is a preference only (depends on #59): GetMemoryType falls back
to plain HOST_VISIBLE (the previous behaviour) on GPUs without resizable BAR.
- HOST_COHERENT is dropped from the required set — the combined type may not be
coherent. A FlushDevice() is added after the copy loop to make the host writes
available on a non-coherent type; it self-gates to a no-op on a coherent type
(#60), so the previous behaviour is preserved wherever the type ends up
coherent. The metadata buffer is not an AS build input and has no GPU
co-write, so it needs neither the build-stage barrier the instance buffer
takes nor a HOST->shader command barrier: the queue submit already orders host
writes made available before it against every command in the submission,
exactly as it did when this buffer was HOST_COHERENT.
Extends the TLASHighWaterMark hardware test with a metadata-buffer assertion
block mirroring #65's instance-buffer check: the chosen memory type must equal
what GetMemoryType resolves for the HOST_VISIBLE | prefer-DEVICE_LOCAL request,
and must be DEVICE_LOCAL wherever a host-visible device-local type is reachable.
The zero-validation-errors check covers the dropped HOST_COHERENT path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The TLAS instance buffer is rebuilt/refit every frame and is co-written by
both the CPU (host-authored fields) and the GPU (the compute shader writes
the transform field in place for transformOwnedByGpu elements). Allocated
HOST_VISIBLE | HOST_COHERENT (system RAM), both GPU accesses crossed PCIe:
the compute write of the transform, and the AS build's read of the instances.
Upgrade the request to HOST_VISIBLE with a best-effort DEVICE_LOCAL preference
(BAR/VRAM), keeping the single shared, persistently-mapped buffer — no staging,
no copy, no extra barrier. A whole-buffer staging copy was rejected: it would
clobber the GPU-written transforms in this in-place co-write design. Now the
compute write and AS build stay in local VRAM; the CPU's write-only,
sequential, never-read-back field writes are the ideal write-combined BAR load.
Correctness:
- DEVICE_LOCAL is a preference only (depends on #59): GetMemoryType falls back
to plain HOST_VISIBLE (the previous behaviour) on GPUs without resizable BAR.
- HOST_COHERENT is dropped from the required set — the combined type may not be
coherent. The existing FlushDevice(cmd, ...) already establishes host->build
visibility and gates its flush-skip on the *chosen* type's flags (#60), so a
non-coherent BAR type still flushes correctly. The barrier is never removed.
metadataBuffer gets the same upgrade separately (#75).
Extends the TLASHighWaterMark hardware test to assert the chosen memory type
matches the HOST_VISIBLE | prefer-DEVICE_LOCAL request and lands on a
DEVICE_LOCAL type where one is reachable; the existing zero-validation-errors
check covers the dropped HOST_COHERENT path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BuildTLAS reallocated the host-visible instanceBuffer and metadataBuffer
on every topology change. They only ever need to hold at least
primitiveCount entries (the AS build reads exactly primitiveCount via
tlasRangeInfo, and the copy loop writes [0, primitiveCount)), so a
shrink — or any growth that still fits the previous capacity — can reuse
the existing allocation. Gate the two Resize calls on a high-water-mark
check, removing two of the four reallocations on a count change. The AS
storage + scratch rebuild below is unchanged: it is tied to the AS
itself and dominates this path regardless.
Adds tests/TLASHighWaterMark driving the real hardware AS-build path: it
asserts a shrink (and within-capacity growth) reuses the buffers while a
growth past the high-water reallocates, that builtInstanceCount still
tracks the live count, and that feeding an oversized instance buffer to
the build produces zero Vulkan validation errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>