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:
parent
00d14d67cb
commit
f14441942a
5 changed files with 306 additions and 21 deletions
|
|
@ -51,13 +51,14 @@ function stub(name) {
|
|||
"wgpuFrameBegin", "wgpuFrameEnd",
|
||||
"wgpuDispatchQuads", "wgpuDispatchCircles", "wgpuDispatchImages", "wgpuDispatchText",
|
||||
"wgpuLoadCustomShader", "wgpuDispatchCustom",
|
||||
"wgpuRegisterMeshBLAS", "wgpuLoadRTPipeline", "wgpuDispatchRT", "wgpuBuildTLAS",
|
||||
"wgpuRegisterMeshBLAS", "wgpuRegisterMeshBLASDeviceAabbs", "wgpuRefitMeshBLASDeviceAabbs",
|
||||
"wgpuLoadRTPipeline", "wgpuDispatchRT", "wgpuBuildTLAS",
|
||||
"wgpuLoadComputePipeline", "wgpuDispatchCompute",
|
||||
]) {
|
||||
// Read-write ints don't need a stub-throw; return 0 for the size queries.
|
||||
e[n] = n.endsWith("Width") || n.endsWith("Height")
|
||||
? () => 0
|
||||
: (n === "wgpuRegisterMeshBLAS" ? () => 0 : stub(n));
|
||||
: ((n === "wgpuRegisterMeshBLAS" || n === "wgpuRegisterMeshBLASDeviceAabbs") ? () => 0 : stub(n));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2761,6 +2762,12 @@ const rtState = {
|
|||
// compute shaders at dispatch time.
|
||||
currentTlas: 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() {
|
||||
|
|
@ -2933,6 +2940,119 @@ env.wgpuRegisterMeshBLAS = (minX, minY, minZ, maxX, maxY, maxZ,
|
|||
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,
|
||||
entryOrderHandle, mortonHandle, binsHandle,
|
||||
bvhNodesHandle, sortABufHandle, sortBBufHandle) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue