example(RTVolume): GPU-written device-buffer procedural AABBs (#37)

A compute shader (aabbs.comp.{glsl,wgsl}) writes the procedural box into a
device buffer that BuildProcedural consumes by device address/handle with no
host copy; the WebGPU path also refits from it each frame so the box breathes.
Verified rendering on both Vulkan (native) and WebGPU (browser).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-16 14:12:12 +00:00
commit 0d1312ac09
4 changed files with 173 additions and 13 deletions

View file

@ -88,14 +88,62 @@ int main() {
PipelineRTVulkan pipeline;
pipeline.Init(cmd, raygenGroups, missGroups, hitGroups, shaderTable);
// ── One procedural unit-box BLAS. The intersection shader treats the
// box as the bounding volume of a radius-1 sphere centred at the
// object origin. opaque=false so the any-hit cut-out runs. ─────────
std::array<RTAabb, 1> boxes {{
{ .min = {-1.0f, -1.0f, -1.0f}, .max = {1.0f, 1.0f, 1.0f} },
}};
// ── One procedural unit-box BLAS, built from a DEVICE buffer the GPU
// writes (issue #37). A compute shader (aabbs.comp.glsl) writes the
// box [-r,r]^3 into a device-local buffer; that buffer is fed straight
// into Mesh::BuildProcedural by device address — no host copy of the
// AABBs. The intersection shader then treats the box as the bounding
// volume of a sphere centred at the object origin. opaque=false so the
// any-hit cut-out runs. ──────────────────────────────────────────────
constexpr std::uint32_t kBoxCount = 1;
VulkanBuffer<RTAabb, false> aabbDevice;
aabbDevice.Resize(
VK_BUFFER_USAGE_2_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR
| VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
| VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, kBoxCount);
// The producer reaches its output through a buffer_reference (device
// address pushed below). It binds no heap descriptors, but a
// descriptor-heap compute pipeline still expects a heap bound for its
// push-data path — the render loop binds it every frame, but the init
// command buffer hasn't, so bind it here before dispatching.
{
VkBindHeapInfoEXT resourceHeapInfo {
.sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT,
.heapRange = {
.address = descriptorHeap.resourceHeap[window.currentBuffer].address,
.size = static_cast<std::uint32_t>(descriptorHeap.resourceHeap[window.currentBuffer].size),
},
.reservedRangeOffset = (descriptorHeap.resourceHeap[window.currentBuffer].size - Device::descriptorHeapProperties.minResourceHeapReservedRange) & ~(Device::descriptorHeapProperties.imageDescriptorAlignment - 1),
.reservedRangeSize = Device::descriptorHeapProperties.minResourceHeapReservedRange,
};
Device::vkCmdBindResourceHeapEXT(cmd, &resourceHeapInfo);
}
ComputeShader aabbWriter;
aabbWriter.Load("aabbs.comp.spv");
struct AabbPush { VkDeviceAddress boxes; std::uint32_t count; float time; } push {
aabbDevice.address, kBoxCount, 0.0f };
aabbWriter.Dispatch(cmd, &push, sizeof(push), (kBoxCount + 63) / 64);
// Order the producing writes before the BLAS build reads the buffer.
VkMemoryBarrier aabbBarrier {
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
.dstAccessMask = VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_KHR,
};
vkCmdPipelineBarrier(cmd,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR,
0, 1, &aabbBarrier, 0, nullptr, 0, nullptr);
Mesh sphere;
sphere.BuildProcedural(boxes, /*opaque*/ false, cmd);
// allowUpdate so the GPU-written boxes can be refit in place each frame
// (the per-frame companion to this build — see the headless
// BLASBuildOptions test for the device-buffer refit path).
sphere.BuildProcedural(aabbDevice.address, kBoxCount, /*opaque*/ false, cmd,
RTBuildOptions{ .allowUpdate = true });
// ── Instance grid. ─────────────────────────────────────────────────
static std::vector<RenderingElement3D> renderers;
@ -241,14 +289,37 @@ int main() {
PipelineRTWebGPU pipeline;
pipeline.Init(cmd, raygenGroups, missGroups, hitGroups, sbt, bindings);
// ── One procedural unit-box BLAS. The intersection shader treats the
// box as the bounding volume of a radius-1 sphere centred at the
// object origin. opaque=false so the any-hit cut-out runs. ─────────
static std::array<RTAabb, 1> boxes {{
{ .min = {-1.0f, -1.0f, -1.0f}, .max = {1.0f, 1.0f, 1.0f} },
// ── One procedural unit-box BLAS, built from a DEVICE buffer the GPU
// writes (issue #37). A compute shader (aabbs.comp.wgsl) writes the
// box [-r,r]^3 into a device storage buffer; that buffer is fed
// straight into Mesh::BuildProcedural by handle — no host copy of the
// AABBs — and RefitProcedural re-consumes it each frame so the boxes
// can breathe. The intersection shader treats the box as the bounding
// volume of a sphere; opaque=false so the any-hit cut-out runs. ──────
constexpr std::uint32_t kBoxCount = 1;
// worldBounds must enclose every box the producer can write (r ≤ 1.15).
constexpr RTAabb kWorldBounds { .min = {-1.2f, -1.2f, -1.2f}, .max = {1.2f, 1.2f, 1.2f} };
static WebGPUBuffer<RTAabb, false> aabbBuf;
aabbBuf.Create(kBoxCount);
// GPU producer: writes the boxes into aabbBuf. No rayQuery → the storage
// buffer binding lives at group 1.
struct AabbParams { std::uint32_t count; float time; std::uint32_t _0, _1; };
std::array<UICustomBinding, 1> aabbBindings {{
{ .group = 1, .binding = 0, .kind = UICustomBindingKind::BufferReadWrite, .pushOffset = 0 },
}};
static PlainComputeShader aabbWriter;
aabbWriter.Load(fs::path("aabbs.comp.wgsl"), sizeof(AabbParams), aabbBindings);
static std::array<std::uint32_t, 1> aabbHandles { aabbBuf.handle };
{
AabbParams params { kBoxCount, 0.0f, 0, 0 };
aabbWriter.Dispatch(&params, sizeof(params), aabbHandles, (kBoxCount + 63u) / 64u);
}
static Mesh sphere;
sphere.BuildProcedural(boxes, /*opaque*/ false, cmd);
sphere.BuildProcedural(aabbBuf.handle, kBoxCount, kWorldBounds, /*opaque*/ false, cmd);
// ── Camera buffer + handle array. ─────────────────────────────────
WebGPUBuffer<CameraGPU, true> cameraBuf;
@ -352,6 +423,20 @@ int main() {
cameraBuf.FlushDevice();
});
// ── Per-frame procedural refit from the device buffer (issue #37). The
// GPU producer rewrites the breathing box into aabbBuf each frame and
// RefitProcedural re-consumes it by handle — no host copy, and the
// BLAS handle (blasAddr) stays stable so the TLAS instances built
// above keep referencing it. worldBounds is constant so the TLAS need
// not rebuild. ───────────────────────────────────────────────────────
EventListener<void> aabbTick(&window.onBeforeUpdate, [&]() {
static float t = 0.0f;
t += kDt;
AabbParams params { kBoxCount, t, 0, 0 };
aabbWriter.Dispatch(&params, sizeof(params), aabbHandles, (kBoxCount + 63u) / 64u);
sphere.RefitProcedural(aabbBuf.handle, kBoxCount, kWorldBounds);
});
window.Render();
window.StartUpdate();
window.StartSync();