fix(rt): BuildTLAS on an empty scene builds an empty TLAS

The TLAS instance and metadata buffers were sized to the instance
count, so building before anything was in the scene created zero-size
buffers. That is invalid usage: NVIDIA fails the allocation with
VK_ERROR_OUT_OF_DEVICE_MEMORY and BuildTLAS threw, leaving the TLAS with
a buffer handle that has no memory behind it.

Both buffers now get at least one slot, and the scratch buffer at least
one byte, since an empty TLAS may report no scratch at all. An empty
TLAS is valid to trace against; every ray misses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-19 03:03:16 +02:00
commit 3fca002ef5
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB

View file

@ -119,10 +119,15 @@ void RenderingElement3D::BuildTLAS(VkCommandBuffer cmd, std::uint32_t index, RTB
// (no-op when the chosen type is coherent — the previous behaviour),
// and the queue submit carries the host-write -> shader-read ordering as
// it always has.
//
// At least one slot each: a TLAS built before anything is in the scene
// has zero instances, and a zero-size buffer is invalid (NVIDIA fails
// the allocation). The empty build reads no instance anyway.
if (tlas.instanceBuffer.buffer == VK_NULL_HANDLE
|| primitiveCount > tlas.instanceBuffer.size / sizeof(VkAccelerationStructureInstanceKHR)) {
tlas.instanceBuffer.Resize(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, primitiveCount, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
tlas.metadataBuffer.Resize(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, primitiveCount, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
const std::uint32_t capacity = std::max(primitiveCount, 1u);
tlas.instanceBuffer.Resize(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, capacity, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
tlas.metadataBuffer.Resize(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, capacity, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
}
// A topology change reshuffles which element occupies each slot (a count
@ -250,7 +255,9 @@ void RenderingElement3D::BuildTLAS(VkCommandBuffer cmd, std::uint32_t index, RTB
// Scratch buffer must hold at least max(buildScratchSize, updateScratchSize).
// Sizing for buildScratchSize covers both — refit is always smaller.
tlas.scratchBuffer.Resize(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, tlasBuildSizes.buildScratchSize);
// 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