feat(rt): BuildProcedural/RefitProcedural from a device AABB buffer (#37)

Add zero-copy procedural BLAS overloads that take the AABB build input
straight from an existing device buffer instead of memcpy-ing a host span
through Mesh::aabbBuffer — the input-source companion to #36's refit work.
A GPU compute producer (e.g. a GPU-resident particle system) can now feed a
moving procedural BLAS that builds/refits each frame with no host round-trip.

Vulkan: new BuildProcedural(VkDeviceAddress, count, ...) and
RefitProcedural(VkDeviceAddress, count, ...) feed the device address directly
into VkAccelerationStructureGeometryAabbsDataKHR; the host-span path is
refactored to share RecordProceduralBuildFromAddress and is otherwise
unchanged.

WebGPU: device-buffer BuildProcedural/RefitProcedural copy the boxes GPU->GPU
into the mesh heap (new wgpuRegisterMeshBLASDeviceAabbs / wgpuRefitMeshBLAS-
DeviceAabbs bridge fns) and wrap them in a single root leaf bounded by a
caller-supplied worldBounds — no wasm round-trip, blasAddr stable across refit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-16 14:12:04 +00:00
commit f14441942a
5 changed files with 306 additions and 21 deletions

View file

@ -342,6 +342,28 @@ void Mesh::BuildProcedural(std::span<const RTAabb> aabbs,
/*primCount*/ static_cast<std::int32_t>(count));
}
void Mesh::BuildProcedural(WebGPUBufferRef aabbBuffer,
std::uint32_t count,
RTAabb worldBounds,
bool opaque_,
WebGPUCommandEncoderRef /*cmd*/,
RTBuildOptions /*options*/) {
// Zero-copy: the boxes are already on the GPU (a compute pass wrote
// them). The bridge copies them GPU→GPU into the vertices heap and
// registers a single-root-leaf BLAS bounded by worldBounds — no SAH
// build (there is no host copy of the boxes to build over) and no host
// round-trip. Traversal linearly intersects all `count` boxes.
opaque = opaque_;
triangleCount = 0; // not a triangle mesh
vertexCount = count * 2; // 2 "vertices" (min,max) per box
blasAddr = WebGPU::wgpuRegisterMeshBLASDeviceAabbs(
aabbBuffer, static_cast<std::int32_t>(count),
worldBounds.min[0], worldBounds.min[1], worldBounds.min[2],
worldBounds.max[0], worldBounds.max[1], worldBounds.max[2],
opaque ? 1 : 0);
}
void Mesh::Refit(std::span<Vector<float, 3, 3>> vertices,
std::span<std::uint32_t> indices,
WebGPUCommandEncoderRef cmd) {
@ -359,3 +381,19 @@ void Mesh::RefitProcedural(std::span<const RTAabb> aabbs,
WebGPUCommandEncoderRef cmd) {
BuildProcedural(aabbs, opaque, cmd);
}
void Mesh::RefitProcedural(WebGPUBufferRef aabbBuffer,
std::uint32_t count,
RTAabb worldBounds,
WebGPUCommandEncoderRef /*cmd*/) {
// Re-copy the GPU boxes into the existing heap region and refresh the
// root bounds — keeps blasAddr stable (no re-register), so TLAS
// instances referencing this mesh stay valid across the per-frame
// update. No host copy.
vertexCount = count * 2;
WebGPU::wgpuRefitMeshBLASDeviceAabbs(
static_cast<std::uint32_t>(blasAddr), aabbBuffer,
static_cast<std::int32_t>(count),
worldBounds.min[0], worldBounds.min[1], worldBounds.min[2],
worldBounds.max[0], worldBounds.max[1], worldBounds.max[2]);
}