fix(rt): rebuild TLASes in place, after earlier traces, and queue their handles like a Mesh's

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>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 02:32:48 +01:00
commit 397ff2db51
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
6 changed files with 516 additions and 74 deletions

View file

@ -1,15 +1,23 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// The dependency RenderingElement3D::BuildTLAS records after its build, which
// orders the build before the shaders that read the TLAS: traceRayEXT in a
// ray-tracing pass, rayQueryEXT in a compute pass, recorded after it in the
// same command buffer (BuildTLAS in onUpdate, then RTPass, is the documented
// per-frame flow). It used to be an execution-only barrier with no memory
// barrier, which orders the commands but never makes the build's
// acceleration-structure writes available and visible to those reads. The
// dependency is built by BuildTlasReadBarrier, pure CPU logic that this test
// drives directly with no GPU device, like SwapchainBarrierScope.
// The dependencies RenderingElement3D::BuildTLAS records around its build.
//
// After it, the build is ordered before the shaders that read the TLAS:
// traceRayEXT in a ray-tracing pass, rayQueryEXT in a compute pass, recorded
// after it in the same command buffer (BuildTLAS in onUpdate, then RTPass, is
// the documented per-frame flow). It used to be an execution-only barrier with
// no memory barrier, which orders the commands but never makes the build's
// acceleration-structure writes available and visible to those reads.
//
// Before it, the build is ordered after the earlier work it must follow. That
// used to cover only earlier acceleration-structure builds, so a refit, or a
// rebuild into the same storage, recorded after a trace or a compute ray query
// of the TLAS could overwrite it while they still read it.
//
// The dependencies are built by BuildTlasWriteBarrier and BuildTlasReadBarrier,
// pure CPU logic that this test drives directly with no GPU device, like
// SwapchainBarrierScope.
#include <cstdlib>
#include "vulkan/vulkan.h"
@ -31,19 +39,39 @@ void Check(bool ok, std::string_view what) {
} // namespace
int main() {
const TlasReadBarrier barrier = BuildTlasReadBarrier();
const TlasBarrier before = BuildTlasWriteBarrier();
Check(barrier.srcStage == VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR,
"the dependency starts at the acceleration-structure build");
Check((barrier.dstStage & VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR) != 0,
Check(before.dstStage == VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR,
"the dependency before the build ends at the acceleration-structure build");
Check((before.srcStage & VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR) != 0,
"it follows earlier builds (the BLASes it reads, an earlier build of this TLAS)");
Check((before.srcStage & VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR) != 0,
"and earlier ray-tracing shaders (traceRayEXT reading the TLAS)");
Check((before.srcStage & VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT) != 0,
"and earlier compute shaders (rayQueryEXT reading the TLAS)");
Check(before.memory.sType == VK_STRUCTURE_TYPE_MEMORY_BARRIER, "it carries a memory barrier");
Check(before.memory.srcAccessMask == VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_KHR,
"earlier builds' acceleration-structure writes are made available, and no reads "
"(a read bit does nothing but trip synchronization validation)");
constexpr VkAccessFlags kBuildAccess =
VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_KHR | VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_KHR;
Check((before.memory.dstAccessMask & kBuildAccess) == kBuildAccess,
"and visible to the build's reads and writes");
const TlasBarrier after = BuildTlasReadBarrier();
Check(after.srcStage == VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR,
"the dependency after the build starts at the acceleration-structure build");
Check((after.dstStage & VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR) != 0,
"it reaches ray-tracing shaders (traceRayEXT)");
Check((barrier.dstStage & VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT) != 0,
Check((after.dstStage & VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT) != 0,
"it reaches compute shaders (rayQueryEXT)");
Check(barrier.memory.sType == VK_STRUCTURE_TYPE_MEMORY_BARRIER, "it carries a memory barrier");
Check((barrier.memory.srcAccessMask & VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_KHR) != 0,
Check(after.memory.sType == VK_STRUCTURE_TYPE_MEMORY_BARRIER, "it carries a memory barrier");
Check((after.memory.srcAccessMask & VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_KHR) != 0,
"the build's acceleration-structure writes are made available");
Check((barrier.memory.dstAccessMask & VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_KHR) != 0,
Check((after.memory.dstAccessMask & VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_KHR) != 0,
"and visible to acceleration-structure reads");
std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed");