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:
parent
079b6b0e1f
commit
397ff2db51
6 changed files with 516 additions and 74 deletions
|
|
@ -9,6 +9,40 @@ import std;
|
|||
|
||||
using namespace Crafter;
|
||||
|
||||
TlasWithBuffer::TlasWithBuffer(TlasWithBuffer&& other)
|
||||
: address(std::exchange(other.address, 0))
|
||||
, buffer(std::move(other.buffer))
|
||||
, accelerationStructure(std::exchange(other.accelerationStructure, VK_NULL_HANDLE))
|
||||
, instanceBuffer(std::move(other.instanceBuffer))
|
||||
, scratchBuffer(std::move(other.scratchBuffer))
|
||||
, metadataBuffer(std::move(other.metadataBuffer))
|
||||
, builtInstanceCount(other.builtInstanceCount)
|
||||
, builtFlags(other.builtFlags)
|
||||
, uploadedVersion(std::move(other.uploadedVersion)) {
|
||||
}
|
||||
|
||||
TlasWithBuffer& TlasWithBuffer::operator=(TlasWithBuffer&& other) {
|
||||
if (this == &other) return *this;
|
||||
// Ahead of buffer, which it lives in and which queues its own as it
|
||||
// takes other's over.
|
||||
Device::EnqueueDeletion(accelerationStructure);
|
||||
address = std::exchange(other.address, 0);
|
||||
buffer = std::move(other.buffer);
|
||||
accelerationStructure = std::exchange(other.accelerationStructure, VK_NULL_HANDLE);
|
||||
instanceBuffer = std::move(other.instanceBuffer);
|
||||
scratchBuffer = std::move(other.scratchBuffer);
|
||||
metadataBuffer = std::move(other.metadataBuffer);
|
||||
builtInstanceCount = other.builtInstanceCount;
|
||||
builtFlags = other.builtFlags;
|
||||
uploadedVersion = std::move(other.uploadedVersion);
|
||||
return *this;
|
||||
}
|
||||
|
||||
TlasWithBuffer::~TlasWithBuffer() {
|
||||
// The body runs before the members' destructors, so the handle is queued
|
||||
// ahead of buffer, which it lives in.
|
||||
Device::EnqueueDeletion(accelerationStructure);
|
||||
}
|
||||
|
||||
std::vector<RenderingElement3D*> RenderingElement3D::elements;
|
||||
|
||||
|
|
@ -50,8 +84,8 @@ void RenderingElement3D::BuildTLAS(VkCommandBuffer cmd, std::uint32_t index, RTB
|
|||
|
||||
// Refit (UPDATE) is allowed when the count matches the count this AS
|
||||
// was last built for and the build flags are unchanged. A change forces
|
||||
// a full rebuild because the AS storage and instance buffer were sized
|
||||
// for the old count (and a refit must inherit the original build flags).
|
||||
// a full rebuild, since a refit must keep the primitive count and the
|
||||
// build flags of the build it updates.
|
||||
// Refit is dramatically cheaper at scale (millions of instances) — it
|
||||
// walks the existing BVH and updates AABBs rather than reconstructing
|
||||
// topology.
|
||||
|
|
@ -60,17 +94,8 @@ void RenderingElement3D::BuildTLAS(VkCommandBuffer cmd, std::uint32_t index, RTB
|
|||
|| primitiveCount != tlas.builtInstanceCount
|
||||
|| buildFlags != tlas.builtFlags;
|
||||
|
||||
{
|
||||
VkMemoryBarrier asBarrier {
|
||||
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
|
||||
.srcAccessMask = VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_KHR,
|
||||
.dstAccessMask = VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_KHR | VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_KHR
|
||||
};
|
||||
vkCmdPipelineBarrier(cmd,
|
||||
VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR,
|
||||
VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR,
|
||||
0, 1, &asBarrier, 0, nullptr, 0, nullptr);
|
||||
}
|
||||
const TlasBarrier writeBarrier = BuildTlasWriteBarrier();
|
||||
vkCmdPipelineBarrier(cmd, writeBarrier.srcStage, writeBarrier.dstStage, 0, 1, &writeBarrier.memory, 0, nullptr, 0, nullptr);
|
||||
|
||||
if (topologyChanged) {
|
||||
// Grow the host-visible inputs on a high-water mark rather than
|
||||
|
|
@ -81,8 +106,9 @@ void RenderingElement3D::BuildTLAS(VkCommandBuffer cmd, std::uint32_t index, RTB
|
|||
// over from a larger earlier frame is reused as-is. This drops the
|
||||
// two host-buffer reallocations on a count decrease (and on an
|
||||
// increase that still fits the previous high-water capacity),
|
||||
// leaving only the AS storage + scratch rebuild below, which is tied
|
||||
// to the AS itself and dominates this path regardless.
|
||||
// leaving only the AS build below, which dominates this path
|
||||
// regardless; the AS storage and scratch follow a high-water mark
|
||||
// of their own.
|
||||
//
|
||||
// instanceBuffer and metadataBuffer grow in lockstep (always resized
|
||||
// together to the same entry count), so one capacity check covers
|
||||
|
|
@ -241,7 +267,7 @@ void RenderingElement3D::BuildTLAS(VkCommandBuffer cmd, std::uint32_t index, RTB
|
|||
};
|
||||
|
||||
if (topologyChanged) {
|
||||
// Query sizes for the fresh build, allocate AS storage + scratch.
|
||||
// Query sizes for the fresh build, and make room for it.
|
||||
VkAccelerationStructureBuildSizesInfoKHR tlasBuildSizes {
|
||||
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_SIZES_INFO_KHR
|
||||
};
|
||||
|
|
@ -258,36 +284,50 @@ void RenderingElement3D::BuildTLAS(VkCommandBuffer cmd, std::uint32_t index, RTB
|
|||
// An empty TLAS may need no scratch at all, but the buffer can't be
|
||||
// zero-size.
|
||||
tlas.scratchBuffer.Resize(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, std::max<VkDeviceSize>(tlasBuildSizes.buildScratchSize, 1));
|
||||
tlas.buffer.Resize(VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, tlasBuildSizes.accelerationStructureSize);
|
||||
|
||||
// Destroy the previous AS handle before creating a new one — the
|
||||
// pre-refit path leaked here on every frame.
|
||||
if (tlas.accelerationStructure != VK_NULL_HANDLE) {
|
||||
Device::vkDestroyAccelerationStructureKHR(Device::device, tlas.accelerationStructure, nullptr);
|
||||
// The acceleration structure spans all of its storage, so a build
|
||||
// that fits the storage builds into it again: a BUILD may write any
|
||||
// acceleration structure created at least as large as it needs. The
|
||||
// handle and address then stay the same across count and flag
|
||||
// changes, and nothing is queued. Only a TLAS that outgrows its
|
||||
// storage gets new storage and a new acceleration structure.
|
||||
if (tlas.accelerationStructure == VK_NULL_HANDLE
|
||||
|| tlasBuildSizes.accelerationStructureSize > tlas.buffer.capacity) {
|
||||
// The old one may still be used earlier in this command buffer,
|
||||
// by an earlier build of this index or a trace of it, so it's
|
||||
// queued rather than destroyed; ahead of the storage it lives in,
|
||||
// which the Resize below queues. Nothing in flight uses it (see
|
||||
// BuildTLAS's precondition). The address goes with it, so a
|
||||
// Resize or create that throws doesn't leave the address of a
|
||||
// queued acceleration structure behind.
|
||||
Device::EnqueueDeletion(tlas.accelerationStructure);
|
||||
tlas.accelerationStructure = VK_NULL_HANDLE;
|
||||
tlas.address = 0;
|
||||
|
||||
tlas.buffer.Resize(VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, tlasBuildSizes.accelerationStructureSize);
|
||||
|
||||
VkAccelerationStructureCreateInfoKHR tlasCreateInfo {
|
||||
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_CREATE_INFO_KHR,
|
||||
.buffer = tlas.buffer.buffer,
|
||||
.offset = 0,
|
||||
.size = tlas.buffer.capacity,
|
||||
.type = VK_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL_KHR,
|
||||
};
|
||||
Device::CheckVkResult(Device::vkCreateAccelerationStructureKHR(Device::device, &tlasCreateInfo, nullptr, &tlas.accelerationStructure));
|
||||
|
||||
VkAccelerationStructureDeviceAddressInfoKHR addrInfo {
|
||||
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_DEVICE_ADDRESS_INFO_KHR,
|
||||
.accelerationStructure = tlas.accelerationStructure
|
||||
};
|
||||
tlas.address = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo);
|
||||
}
|
||||
|
||||
VkAccelerationStructureCreateInfoKHR tlasCreateInfo {
|
||||
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_CREATE_INFO_KHR,
|
||||
.buffer = tlas.buffer.buffer,
|
||||
.offset = 0,
|
||||
.size = tlasBuildSizes.accelerationStructureSize,
|
||||
.type = VK_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL_KHR,
|
||||
};
|
||||
Device::CheckVkResult(Device::vkCreateAccelerationStructureKHR(Device::device, &tlasCreateInfo, nullptr, &tlas.accelerationStructure));
|
||||
|
||||
VkAccelerationStructureDeviceAddressInfoKHR addrInfo {
|
||||
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_DEVICE_ADDRESS_INFO_KHR,
|
||||
.accelerationStructure = tlas.accelerationStructure
|
||||
};
|
||||
tlas.address = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo);
|
||||
|
||||
tlas.builtInstanceCount = primitiveCount;
|
||||
tlas.builtFlags = buildFlags;
|
||||
}
|
||||
|
||||
// For UPDATE mode, src == dst (in-place refit). For BUILD, src is
|
||||
// VK_NULL_HANDLE and dst is the freshly-created handle.
|
||||
// VK_NULL_HANDLE and dst is the current handle, kept or fresh.
|
||||
tlasBuildGeometryInfo.scratchData.deviceAddress = tlas.scratchBuffer.address;
|
||||
tlasBuildGeometryInfo.dstAccelerationStructure = tlas.accelerationStructure;
|
||||
tlasBuildGeometryInfo.srcAccelerationStructure =
|
||||
|
|
@ -302,6 +342,6 @@ void RenderingElement3D::BuildTLAS(VkCommandBuffer cmd, std::uint32_t index, RTB
|
|||
VkAccelerationStructureBuildRangeInfoKHR* tlasRangeInfoPP = &tlasRangeInfo;
|
||||
Device::vkCmdBuildAccelerationStructuresKHR(cmd, 1, &tlasBuildGeometryInfo, &tlasRangeInfoPP);
|
||||
|
||||
const TlasReadBarrier readBarrier = BuildTlasReadBarrier();
|
||||
const TlasBarrier readBarrier = BuildTlasReadBarrier();
|
||||
vkCmdPipelineBarrier(cmd, readBarrier.srcStage, readBarrier.dstStage, 0, 1, &readBarrier.memory, 0, nullptr, 0, nullptr);
|
||||
}
|
||||
Loading…
Reference in a new issue