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
5 changed files with 306 additions and 21 deletions
Showing only changes of commit f14441942a - Show all commits

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>
catbot 2026-06-16 14:12:04 +00:00

View file

@ -51,13 +51,14 @@ function stub(name) {
"wgpuFrameBegin", "wgpuFrameEnd", "wgpuFrameBegin", "wgpuFrameEnd",
"wgpuDispatchQuads", "wgpuDispatchCircles", "wgpuDispatchImages", "wgpuDispatchText", "wgpuDispatchQuads", "wgpuDispatchCircles", "wgpuDispatchImages", "wgpuDispatchText",
"wgpuLoadCustomShader", "wgpuDispatchCustom", "wgpuLoadCustomShader", "wgpuDispatchCustom",
"wgpuRegisterMeshBLAS", "wgpuLoadRTPipeline", "wgpuDispatchRT", "wgpuBuildTLAS", "wgpuRegisterMeshBLAS", "wgpuRegisterMeshBLASDeviceAabbs", "wgpuRefitMeshBLASDeviceAabbs",
"wgpuLoadRTPipeline", "wgpuDispatchRT", "wgpuBuildTLAS",
"wgpuLoadComputePipeline", "wgpuDispatchCompute", "wgpuLoadComputePipeline", "wgpuDispatchCompute",
]) { ]) {
// Read-write ints don't need a stub-throw; return 0 for the size queries. // Read-write ints don't need a stub-throw; return 0 for the size queries.
e[n] = n.endsWith("Width") || n.endsWith("Height") e[n] = n.endsWith("Width") || n.endsWith("Height")
? () => 0 ? () => 0
: (n === "wgpuRegisterMeshBLAS" ? () => 0 : stub(n)); : ((n === "wgpuRegisterMeshBLAS" || n === "wgpuRegisterMeshBLASDeviceAabbs") ? () => 0 : stub(n));
} }
} }
@ -2761,6 +2762,12 @@ const rtState = {
// compute shaders at dispatch time. // compute shaders at dispatch time.
currentTlas: 0, currentTlas: 0,
currentTlasInstanceCount: 0, currentTlasInstanceCount: 0,
// handle → { vCursor, bvhNode, primCount } for device-buffer procedural
// BLAS (wgpuRegisterMeshBLASDeviceAabbs). Lets the per-frame refit copy
// fresh boxes into the same vertices-heap region and rewrite the root
// leaf bounds without re-registering the mesh.
deviceAabbRegions: new Map(),
}; };
function rtInit() { function rtInit() {
@ -2933,6 +2940,119 @@ env.wgpuRegisterMeshBLAS = (minX, minY, minZ, maxX, maxY, maxZ,
return handle; return handle;
}; };
// Build a single-leaf BVH node (32 bytes) spanning all `count` prims.
function _rtWriteRootLeaf(bvhCursorBytes, count, minX, minY, minZ, maxX, maxY, maxZ) {
const node = new ArrayBuffer(32);
const nf = new Float32Array(node);
const nu = new Uint32Array(node);
nf[0] = minX; nf[1] = minY; nf[2] = minZ;
nu[3] = 0; // firstChildOrPrim: prim base (rel to primRemapOffset)
nf[4] = maxX; nf[5] = maxY; nf[6] = maxZ;
nu[7] = count >>> 0; // primCount > 0 → leaf
queue.writeBuffer(rtState.bvhHeap.gpu, bvhCursorBytes, node);
}
// Zero-copy procedural BLAS: the boxes already live in a device GPUBuffer
// (a compute pass wrote them). Copy them GPU→GPU into the vertices heap and
// register a single-root-leaf BLAS — no wasm round-trip, no SAH build (there
// is no host copy to build over). See wgpuRegisterMeshBLASDeviceAabbs decl in
// Crafter.Graphics-WebGPU.cppm.
env.wgpuRegisterMeshBLASDeviceAabbs = (aabbBufHandle, count,
minX, minY, minZ, maxX, maxY, maxZ,
opaqueFlag) => {
if (!rtState.vertHeap) rtInit();
const src = buffers.get(aabbBufHandle);
if (!src) {
console.error("[crafter-wgpu] wgpuRegisterMeshBLASDeviceAabbs: unknown buffer handle");
return 0;
}
console.log(`[crafter-wgpu] device-AABB BLAS: ${count} aabbs, bbox=(${minX.toFixed(1)}..${maxX.toFixed(1)}, ${minY.toFixed(1)}..${maxY.toFixed(1)}, ${minZ.toFixed(1)}..${maxZ.toFixed(1)}), opaque=${opaqueFlag}`);
const vBytes = count * 24; // 2 vec3 per box, matching RTAabb/VkAabbPositionsKHR
const nBytes = 32; // one root leaf
const rBytes = count * 4; // identity primRemap
rtHeapEnsure(rtState.vertHeap, vBytes);
rtHeapEnsure(rtState.bvhHeap, nBytes);
rtHeapEnsure(rtState.primRemapHeap, rBytes);
const vCursor = rtState.vertHeap.cursor;
const nCursor = rtState.bvhHeap.cursor;
const rCursor = rtState.primRemapHeap.cursor;
const vOff = vCursor / 12; // in vec3 units
const nOff = nCursor / 32; // in BVHNode units
const rOff = rCursor / 4; // in u32 units
// GPU→GPU copy of the boxes — the data never touches wasm memory.
{
const enc = device.createCommandEncoder();
enc.copyBufferToBuffer(src, 0, rtState.vertHeap.gpu, vCursor, vBytes);
queue.submit([enc.finish()]);
}
// Identity primRemap [0..count) — leaf slot i → box i in the heap.
const remap = new Uint32Array(count);
for (let i = 0; i < count; i++) remap[i] = i;
queue.writeBuffer(rtState.primRemapHeap.gpu, rCursor, remap);
_rtWriteRootLeaf(nCursor, count, minX, minY, minZ, maxX, maxY, maxZ);
rtState.vertHeap.cursor += vBytes;
rtState.bvhHeap.cursor += nBytes;
rtState.primRemapHeap.cursor += rBytes;
const handle = rtState.nextMeshHandle++;
rtMeshRecordsEnsure(handle + 1);
const rec = new ArrayBuffer(64);
const f32 = new Float32Array(rec);
const u32 = new Uint32Array(rec);
f32[0] = minX; f32[1] = minY; f32[2] = minZ;
u32[3] = vOff;
f32[4] = maxX; f32[5] = maxY; f32[6] = maxZ;
u32[7] = 0; // indexOffset (unused for AABBs)
u32[8] = nOff;
u32[9] = rOff;
u32[10] = count >>> 0; // primitive count
u32[11] = 0; // attribsOffset (none)
u32[12] = 1; // geomType = AABBs
u32[13] = opaqueFlag ? 1 : 0;
u32[14] = 0;
u32[15] = 0;
queue.writeBuffer(rtState.meshRecordsBuffer, handle * 64, rec);
rtState.deviceAabbRegions.set(handle, { vCursor, nCursor, primCount: count });
return handle;
};
// Zero-copy procedural refit: re-copy the boxes into the existing heap
// region and refresh the root leaf bounds. Handle (and thus blasAddr) is
// preserved. Count must match the original register.
env.wgpuRefitMeshBLASDeviceAabbs = (meshHandle, aabbBufHandle, count,
minX, minY, minZ, maxX, maxY, maxZ) => {
const region = rtState.deviceAabbRegions.get(meshHandle);
const src = buffers.get(aabbBufHandle);
if (!region || !src) {
console.error("[crafter-wgpu] wgpuRefitMeshBLASDeviceAabbs: unknown mesh/buffer handle");
return;
}
if (count !== region.primCount) {
console.error(`[crafter-wgpu] wgpuRefitMeshBLASDeviceAabbs: count ${count} != built ${region.primCount}`);
return;
}
const enc = device.createCommandEncoder();
enc.copyBufferToBuffer(src, 0, rtState.vertHeap.gpu, region.vCursor, count * 24);
queue.submit([enc.finish()]);
// Refresh the root leaf bounds so traversal's node-level cull stays valid.
_rtWriteRootLeaf(region.nCursor, count, minX, minY, minZ, maxX, maxY, maxZ);
// Refresh the MeshRecord root AABB (used by TLAS build for the world box).
const hdr = new Float32Array(8);
hdr[0] = minX; hdr[1] = minY; hdr[2] = minZ;
new Uint32Array(hdr.buffer)[3] = region.vCursor / 12;
hdr[4] = maxX; hdr[5] = maxY; hdr[6] = maxZ;
queue.writeBuffer(rtState.meshRecordsBuffer, meshHandle * 64, hdr.buffer, 0, 32);
};
env.wgpuBuildTLAS = (instanceBufHandle, instanceCount, tlasOutBufHandle, env.wgpuBuildTLAS = (instanceBufHandle, instanceCount, tlasOutBufHandle,
entryOrderHandle, mortonHandle, binsHandle, entryOrderHandle, mortonHandle, binsHandle,
bvhNodesHandle, sortABufHandle, sortBBufHandle) => { bvhNodesHandle, sortABufHandle, sortBBufHandle) => {

View file

@ -342,6 +342,28 @@ void Mesh::BuildProcedural(std::span<const RTAabb> aabbs,
/*primCount*/ static_cast<std::int32_t>(count)); /*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, void Mesh::Refit(std::span<Vector<float, 3, 3>> vertices,
std::span<std::uint32_t> indices, std::span<std::uint32_t> indices,
WebGPUCommandEncoderRef cmd) { WebGPUCommandEncoderRef cmd) {
@ -359,3 +381,19 @@ void Mesh::RefitProcedural(std::span<const RTAabb> aabbs,
WebGPUCommandEncoderRef cmd) { WebGPUCommandEncoderRef cmd) {
BuildProcedural(aabbs, opaque, 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]);
}

View file

@ -267,26 +267,18 @@ void Mesh::Build(const CompressedMeshAsset& asset, VkCommandBuffer cmd, RTBuildO
} }
namespace { namespace {
// Re-upload AABB build input and record an AABB BLAS build (fresh or // Record an AABB BLAS build (fresh or in-place refit) from a device
// in-place refit). Shared by BuildProcedural and RefitProcedural; the // address that already holds the VkAabbPositionsKHR-compatible boxes.
// geometry's opaque bit is read from self.opaque so an UPDATE keeps the // Geometry-source agnostic: the host-upload path (RecordProceduralBuild)
// exact geometry description of the original build. // points this at self.aabbBuffer, while the device-buffer overloads
void RecordProceduralBuild(Mesh& self, std::span<const RTAabb> aabbs, VkBuildAccelerationStructureFlagsKHR flags, bool update, VkCommandBuffer cmd) { // point it straight at the producer's buffer — no copy involved either
// 24-byte-stride VkAabbPositionsKHR-compatible build input // way. The geometry's opaque bit is read from self.opaque so an UPDATE
// (static_assert'd in the interface). Same usage set as the triangle // keeps the exact geometry description of the original build.
// inputs: AS-build read-only + device address. A refit reuses the void RecordProceduralBuildFromAddress(Mesh& self, VkDeviceAddress aabbAddress, std::uint32_t count, std::uint32_t stride, VkBuildAccelerationStructureFlagsKHR flags, bool update, VkCommandBuffer cmd) {
// existing same-sized buffer (count is unchanged), so the device
// address — and the geometry it feeds — stays stable.
if (!update) {
self.aabbBuffer.Resize(kVertexUsageBase, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, static_cast<std::uint32_t>(aabbs.size()));
}
std::memcpy(self.aabbBuffer.value, aabbs.data(), aabbs.size() * sizeof(RTAabb));
self.aabbBuffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR);
VkAccelerationStructureGeometryAabbsDataKHR aabbsData { VkAccelerationStructureGeometryAabbsDataKHR aabbsData {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_AABBS_DATA_KHR, .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_AABBS_DATA_KHR,
.data = { .deviceAddress = self.aabbBuffer.address }, .data = { .deviceAddress = aabbAddress },
.stride = sizeof(RTAabb) .stride = stride
}; };
VkAccelerationStructureGeometryDataKHR geometryData; VkAccelerationStructureGeometryDataKHR geometryData;
geometryData.aabbs = aabbsData; geometryData.aabbs = aabbsData;
@ -299,7 +291,25 @@ namespace {
.flags = self.opaque ? static_cast<VkGeometryFlagsKHR>(VK_GEOMETRY_OPAQUE_BIT_KHR) : VkGeometryFlagsKHR{} .flags = self.opaque ? static_cast<VkGeometryFlagsKHR>(VK_GEOMETRY_OPAQUE_BIT_KHR) : VkGeometryFlagsKHR{}
}; };
RecordBLASBuildFromGeometry(self, blasGeometry, static_cast<std::uint32_t>(aabbs.size()), flags, update, cmd); RecordBLASBuildFromGeometry(self, blasGeometry, count, flags, update, cmd);
}
// Re-upload AABB build input from host memory and record an AABB BLAS
// build (fresh or in-place refit). Shared by the host-span BuildProcedural
// and RefitProcedural.
void RecordProceduralBuild(Mesh& self, std::span<const RTAabb> aabbs, VkBuildAccelerationStructureFlagsKHR flags, bool update, VkCommandBuffer cmd) {
// 24-byte-stride VkAabbPositionsKHR-compatible build input
// (static_assert'd in the interface). Same usage set as the triangle
// inputs: AS-build read-only + device address. A refit reuses the
// existing same-sized buffer (count is unchanged), so the device
// address — and the geometry it feeds — stays stable.
if (!update) {
self.aabbBuffer.Resize(kVertexUsageBase, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, static_cast<std::uint32_t>(aabbs.size()));
}
std::memcpy(self.aabbBuffer.value, aabbs.data(), aabbs.size() * sizeof(RTAabb));
self.aabbBuffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR);
RecordProceduralBuildFromAddress(self, self.aabbBuffer.address, static_cast<std::uint32_t>(aabbs.size()), sizeof(RTAabb), flags, update, cmd);
} }
} }
@ -311,6 +321,20 @@ void Mesh::BuildProcedural(std::span<const RTAabb> aabbs, bool opaque, VkCommand
RecordProceduralBuild(*this, aabbs, BlasFlags(options), /*update*/ false, cmd); RecordProceduralBuild(*this, aabbs, BlasFlags(options), /*update*/ false, cmd);
} }
void Mesh::BuildProcedural(VkDeviceAddress aabbAddress, std::uint32_t count, bool opaque, VkCommandBuffer cmd, RTBuildOptions options, std::uint32_t stride) {
// Zero-copy procedural build: the AABBs already live in `aabbAddress`
// (a device buffer a GPU compute pass wrote). Nothing touches
// self.aabbBuffer — the build input is the producer's buffer directly.
// The caller owns that buffer's lifetime + usage flags
// (ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY + SHADER_DEVICE_ADDRESS)
// and must barrier the producing writes before this build reads them.
this->opaque = opaque;
allowUpdate = options.allowUpdate;
builtInputCount = count;
RecordProceduralBuildFromAddress(*this, aabbAddress, count, stride, BlasFlags(options), /*update*/ false, cmd);
}
void Mesh::Refit(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd) { void Mesh::Refit(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd) {
// A hardware in-place UPDATE is only valid when the original build asked // A hardware in-place UPDATE is only valid when the original build asked
// for it, an AS already exists, and the topology is unchanged. Otherwise // for it, an AS already exists, and the topology is unchanged. Otherwise
@ -356,3 +380,20 @@ void Mesh::RefitProcedural(std::span<const RTAabb> aabbs, VkCommandBuffer cmd) {
RecordProceduralBuild(*this, aabbs, buildFlags, update, cmd); RecordProceduralBuild(*this, aabbs, buildFlags, update, cmd);
} }
void Mesh::RefitProcedural(VkDeviceAddress aabbAddress, std::uint32_t count, VkCommandBuffer cmd, std::uint32_t stride) {
// Zero-copy refit: the GPU producer rewrote the same device buffer in
// place; re-record the build (UPDATE when allowed + count unchanged,
// else a full rebuild) reading straight from `aabbAddress`. As with the
// device-buffer BuildProcedural, nothing touches self.aabbBuffer and the
// caller is responsible for barriering the producing writes.
const bool sameTopology =
accelerationStructure != VK_NULL_HANDLE
&& count == builtInputCount;
const bool update = allowUpdate && sameTopology;
if (!update && count != builtInputCount) {
builtInputCount = count;
}
RecordProceduralBuildFromAddress(*this, aabbAddress, count, stride, buildFlags, update, cmd);
}

View file

@ -125,6 +125,23 @@ export namespace Crafter {
// exactly like a triangle BLAS. // exactly like a triangle BLAS.
void BuildProcedural(std::span<const RTAabb> aabbs, bool opaque, VkCommandBuffer cmd, RTBuildOptions options = {}); 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 // Refit the triangle BLAS against new geometry of the *same
// topology* (same vertex count and index count as the original // topology* (same vertex count and index count as the original
// Build). When that Build set RTBuildOptions::allowUpdate this issues // 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); 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. // Procedural analog of Refit: new object-space boxes, same count.
void RefitProcedural(std::span<const RTAabb> aabbs, VkCommandBuffer cmd); 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 #endif // !CRAFTER_GRAPHICS_WINDOW_DOM
@ -243,6 +270,25 @@ export namespace Crafter {
WebGPUCommandEncoderRef cmd = 0, WebGPUCommandEncoderRef cmd = 0,
RTBuildOptions options = {}); 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, // 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 // these simply re-run the host BVH build over the new data, so they
// accept the full geometry (not just moved positions). Provided so // accept the full geometry (not just moved positions). Provided so
@ -253,6 +299,16 @@ export namespace Crafter {
WebGPUCommandEncoderRef cmd = 0); WebGPUCommandEncoderRef cmd = 0);
void RefitProcedural(std::span<const RTAabb> aabbs, void RefitProcedural(std::span<const RTAabb> aabbs,
WebGPUCommandEncoderRef cmd = 0); 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 #endif // CRAFTER_GRAPHICS_WINDOW_DOM

View file

@ -195,6 +195,36 @@ namespace Crafter::WebGPU {
const void* attribsPtr, std::int32_t attribsByteCount, const void* attribsPtr, std::int32_t attribsByteCount,
std::int32_t geomType, std::int32_t opaqueFlag, std::int32_t primCount); std::int32_t geomType, std::int32_t opaqueFlag, std::int32_t primCount);
// Zero-copy procedural BLAS registration: the AABB build input already
// lives in an existing device buffer (`aabbBufferHandle`, a GPUBuffer a
// compute pass wrote) rather than a wasm host pointer. The bridge
// copyBufferToBuffer's `count` boxes (24 bytes each, [min,max] vec3 — the
// RTAabb / VkAabbPositionsKHR layout) straight into the vertices heap,
// never round-tripping them through wasm memory. With no host copy of the
// boxes there is nothing to run the SAH builder over, so the BLAS gets a
// single root leaf spanning all `count` primitives bounded by the
// caller-supplied object-space box (min/max) — the traversal then linearly
// intersects every box. `opaqueFlag` is the geometry opaque bit. Returns
// the mesh handle (0 on failure), same contract as wgpuRegisterMeshBLAS.
__attribute__((import_module("env"), import_name("wgpuRegisterMeshBLASDeviceAabbs")))
extern "C" std::uint32_t wgpuRegisterMeshBLASDeviceAabbs(
WebGPUBufferRef aabbBufferHandle, std::int32_t count,
float minX, float minY, float minZ,
float maxX, float maxY, float maxZ,
std::int32_t opaqueFlag);
// Zero-copy procedural BLAS refit: re-copy `count` boxes from the device
// buffer into the existing mesh's vertices-heap region (count unchanged)
// and update the root leaf bounds, preserving the mesh handle so
// RTInstance::accelerationStructureReference stays valid. The device
// counterpart of Mesh::RefitProcedural.
__attribute__((import_module("env"), import_name("wgpuRefitMeshBLASDeviceAabbs")))
extern "C" void wgpuRefitMeshBLASDeviceAabbs(
std::uint32_t meshHandle,
WebGPUBufferRef aabbBufferHandle, std::int32_t count,
float minX, float minY, float minZ,
float maxX, float maxY, float maxZ);
// RT pipeline build. The library composes WGSL by concatenating the // RT pipeline build. The library composes WGSL by concatenating the
// traversal library, generated hit-group switches, and the user- // traversal library, generated hit-group switches, and the user-
// supplied raygen / miss / closesthit / anyhit bodies. `bindings` is // supplied raygen / miss / closesthit / anyhit bodies. `bindings` is