feat(rt): BuildProcedural — accept a device AABB buffer as build input (#37) #39

Merged
catbot merged 3 commits from claude/issue-37 into master 2026-06-16 16:13:10 +02:00
Showing only changes of commit 8fefb2caef - Show all commits

test(rt): headless device-buffer BuildProcedural + refit (#37)

Extend BLASBuildOptions with a device-local AABB buffer filled via a staging
copy + barrier (standing in for a GPU compute producer) fed into the new
device-address BuildProcedural/RefitProcedural. Asserts the build/refit
succeed, the in-place UPDATE keeps the AS handle + blasAddr, the host
aabbBuffer is never allocated (proof of zero-copy), and the validation layer
reports no errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
catbot 2026-06-16 14:12:12 +00:00

View file

@ -197,6 +197,86 @@ int main() {
Check(proc.accelerationStructure == procHandleBefore && proc.blasAddr == procAddrBefore, Check(proc.accelerationStructure == procHandleBefore && proc.blasAddr == procAddrBefore,
"RefitProcedural kept the same AS handle + blasAddr (in-place UPDATE)"); "RefitProcedural kept the same AS handle + blasAddr (in-place UPDATE)");
// ── Procedural (AABB) BLAS from a DEVICE buffer (issue #37). The boxes
// live in a device-local buffer a GPU pass would write — here filled
// via a staging copy + barrier, standing in for a compute dispatch —
// and fed straight into BuildProcedural/RefitProcedural by device
// address, with no host memcpy through Mesh::aabbBuffer. ────────────
constexpr std::uint32_t kDevCount = 3;
// Device-local AABB build input: the producer's buffer. Needs the AS
// build-input + device-address usage bits, plus TRANSFER_DST so the
// staging upload (our stand-in GPU writer) can fill it.
VulkanBuffer<RTAabb, false> devAabb;
devAabb.Resize(
VK_BUFFER_USAGE_2_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR
| VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
| VK_BUFFER_USAGE_TRANSFER_DST_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, kDevCount);
VulkanBuffer<RTAabb, true> devStaging;
// SHADER_DEVICE_ADDRESS because VulkanBuffer::Create always queries the
// buffer device address (the staging buffer's address itself is unused).
devStaging.Resize(VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, kDevCount);
auto uploadBoxes = [&](VkCommandBuffer cmd, std::span<const RTAabb> boxes) {
std::memcpy(devStaging.value, boxes.data(), boxes.size() * sizeof(RTAabb));
devStaging.FlushDevice();
VkBufferCopy region { .srcOffset = 0, .dstOffset = 0, .size = devAabb.size };
vkCmdCopyBuffer(cmd, devStaging.buffer, devAabb.buffer, 1, &region);
// Order the producing write before the AS build reads the buffer —
// exactly the barrier a real compute producer would need.
VkBufferMemoryBarrier barrier {
.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
.dstAccessMask = VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_KHR,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.buffer = devAabb.buffer, .offset = 0, .size = VK_WHOLE_SIZE,
};
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR,
0, 0, nullptr, 1, &barrier, 0, nullptr);
};
Mesh devProc;
{
std::array<RTAabb, kDevCount> boxes {{
{ .min = {-1,-1,-1}, .max = {1,1,1} },
{ .min = { 2, 2, 2}, .max = {3,3,3} },
{ .min = {-3,-3,-3}, .max = {-2,-2,-2} },
}};
VkCommandBuffer cmd = BeginCmd();
uploadBoxes(cmd, boxes);
devProc.BuildProcedural(devAabb.address, kDevCount, /*opaque*/ false, cmd,
RTBuildOptions{ .preference = RTBuildPreference::FastTrace, .allowUpdate = true });
SubmitWait(cmd);
}
Check(devProc.blasAddr != 0, "device-buffer BuildProcedural produced a non-zero blasAddr");
Check(devProc.builtPrimitiveCount == kDevCount, "device-buffer BLAS reports 3 primitives");
// The host-side aabbBuffer must be untouched — proof there was no memcpy.
Check(devProc.aabbBuffer.buffer == VK_NULL_HANDLE,
"device-buffer path never allocated the host aabbBuffer (zero-copy)");
const VkDeviceAddress devAddrBefore = devProc.blasAddr;
const VkAccelerationStructureKHR devHandleBefore = devProc.accelerationStructure;
{
// Same count, moved boxes (re-written into the same device buffer) →
// in-place UPDATE straight from the device address.
std::array<RTAabb, kDevCount> boxes {{
{ .min = {-2,-2,-2}, .max = {2,2,2} },
{ .min = { 4, 4, 4}, .max = {5,5,5} },
{ .min = {-5,-5,-5}, .max = {-3,-3,-3} },
}};
VkCommandBuffer cmd = BeginCmd();
uploadBoxes(cmd, boxes);
devProc.RefitProcedural(devAabb.address, kDevCount, cmd);
SubmitWait(cmd);
}
Check(devProc.accelerationStructure == devHandleBefore && devProc.blasAddr == devAddrBefore,
"device-buffer RefitProcedural kept the same AS handle + blasAddr (in-place UPDATE)");
Check(devProc.aabbBuffer.buffer == VK_NULL_HANDLE,
"device-buffer refit still never touched the host aabbBuffer");
Check(Device::validationErrorCount == 0, Check(Device::validationErrorCount == 0,
std::format("no Vulkan validation errors ({} seen)", Device::validationErrorCount)); std::format("no Vulkan validation errors ({} seen)", Device::validationErrorCount));