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

@ -125,6 +125,23 @@ export namespace Crafter {
// exactly like a triangle BLAS.
void BuildProcedural(std::span<const RTAabb> aabbs, bool opaque, VkCommandBuffer cmd, RTBuildOptions options = {});
// Zero-copy procedural build: take the AABB build input straight from
// an existing device buffer (by device address + primitive count)
// rather than memcpy-ing a host span through aabbBuffer. The intended
// producer is a GPU compute pass that writes the boxes itself — e.g.
// a GPU-resident particle system whose per-frame AABBs never touch the
// CPU. The buffer must carry
// VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR
// and VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, its boxes laid out as
// VkAabbPositionsKHR (`stride`, default sizeof(RTAabb) == 24), and the
// caller must pipeline-barrier the producing writes (e.g. a compute
// dispatch) before the build on `cmd` reads them. The buffer's
// lifetime is the caller's responsibility — it is never copied, so it
// must outlive the build submitted on `cmd`. Everything else (opaque
// bit, hit-group contract, instance wiring) matches the host-span
// BuildProcedural above.
void BuildProcedural(VkDeviceAddress aabbAddress, std::uint32_t count, bool opaque, VkCommandBuffer cmd, RTBuildOptions options = {}, std::uint32_t stride = sizeof(RTAabb));
// Refit the triangle BLAS against new geometry of the *same
// topology* (same vertex count and index count as the original
// Build). When that Build set RTBuildOptions::allowUpdate this issues
@ -139,6 +156,16 @@ export namespace Crafter {
void Refit(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd);
// Procedural analog of Refit: new object-space boxes, same count.
void RefitProcedural(std::span<const RTAabb> aabbs, VkCommandBuffer cmd);
// Zero-copy procedural refit: the device-buffer counterpart of
// RefitProcedural above and the input-source pairing for the device
// BuildProcedural. The GPU producer rewrote the boxes in `aabbAddress`
// in place; this re-records the build straight from that address —
// an in-place hardware UPDATE when the original build set allowUpdate
// and `count` is unchanged (handle / blasAddr preserved), otherwise a
// full rebuild. Same buffer-usage / barrier / lifetime contract as the
// device BuildProcedural. Call per frame to track GPU-written boxes
// with zero host involvement.
void RefitProcedural(VkDeviceAddress aabbAddress, std::uint32_t count, VkCommandBuffer cmd, std::uint32_t stride = sizeof(RTAabb));
};
}
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
@ -243,6 +270,25 @@ export namespace Crafter {
WebGPUCommandEncoderRef cmd = 0,
RTBuildOptions options = {});
// Zero-copy procedural build: the boxes already live in an existing
// device buffer (`aabbBuffer`, a GPUBuffer a compute pass wrote), fed
// straight into the BLAS with no host copy — the WebGPU analog of the
// device-address Vulkan BuildProcedural. The software path has no
// hardware AS and builds its BVH on the host, but here there is no
// host copy to build over, so the device boxes are copied GPU→GPU into
// the mesh heap and wrapped in a single root leaf bounded by
// `worldBounds` (object-space min/max enclosing all `count` boxes —
// the producer supplies it since the CPU never sees the boxes). The
// traversal then linearly intersects every box. `opaque` is the
// geometry opaque bit (false lets any-hit run). `options` has no effect
// (no hardware AS to tune), kept for API symmetry.
void BuildProcedural(WebGPUBufferRef aabbBuffer,
std::uint32_t count,
RTAabb worldBounds,
bool opaque = false,
WebGPUCommandEncoderRef cmd = 0,
RTBuildOptions options = {});
// Refit analogs of the native API. With no hardware AS to update,
// these simply re-run the host BVH build over the new data, so they
// accept the full geometry (not just moved positions). Provided so
@ -253,6 +299,16 @@ export namespace Crafter {
WebGPUCommandEncoderRef cmd = 0);
void RefitProcedural(std::span<const RTAabb> aabbs,
WebGPUCommandEncoderRef cmd = 0);
// Zero-copy procedural refit: re-copy the boxes from the device buffer
// into this mesh's heap region (count unchanged) and update the root
// leaf bounds, keeping blasAddr stable — unlike the host-span
// RefitProcedural, which rebuilds and re-publishes a NEW handle. The
// device counterpart of the device-address Vulkan RefitProcedural;
// call per frame to track GPU-written boxes with no host copy.
void RefitProcedural(WebGPUBufferRef aabbBuffer,
std::uint32_t count,
RTAabb worldBounds,
WebGPUCommandEncoderRef cmd = 0);
};
}
#endif // CRAFTER_GRAPHICS_WINDOW_DOM