Fallbacks and limits
The fallback chain
The package uses subgroups first, with one declared fallback: tinybvh on the CPU. There are no parallel GPU
paths.
The bvh-lbvh-fallback GPU page covers the no-subgroups path.
createMeshLayoutBuilder's fallbackReason is set whenever its kind is 'tinybvh', including when you asked for
tinybvh (prefer: 'tinybvh': "the CPU builder was asked for"). builder.fallback is true only for a real fallback (a
GPU builder was wanted and the device can't run it), so an overlay shows fallback ? fallbackReason : null. With
SceneTrees, gpuFirstTrees: false is a choice too: gpuFirstTrees.reason says so.
The walk's stack
The walk keeps one 64-entry stack per thread for both levels (TRACE_STACK_SIZE). A mesh tree under a top level gets
47 levels (GPU_TREE_MAX_DEPTH); SceneTrees refuses deeper trees. tinybvh's trees on real scenes are about 28 deep
(Sponza). A deeper tree takes a degenerate mesh, such as thousands of identical triangles, which H-PLOC's default
tieBreak: 'pairs' keeps balanced.
Device limits
- One storage binding: 128 MiB on a default device, about 2.7 million triangles at 48 bytes each. Ask for the
adapter's
maxStorageBufferBindingSizeandmaxBufferSize(Installation). - A GPU batch:
planGpuHplocBatchesandplanGpuLbvhBatchessplit meshes into batches the device can bind. - Errors name the limit. A buffer past the limit throws a
RangeErrorwith the limit and the size needed.
The build-peak cap
A GPU build batch holds its inputs, its scratch and its outputs at once, so on a scene load the batch is the biggest memory number. One H-PLOC batch of all of Sponza (5.74 million triangles) is 1,482 MiB: about 270 bytes per triangle (inputs 18.5, scratch 136, outputs 116), against 470 MiB for the scene once settled.
SceneTrees caps each batch at 256 MiB by default (gpuMaxBatchBytes, DEFAULT_GPU_MAX_BATCH_BYTES):
- the update's meshes are split into batches of at most that many bytes;
- the batches build one after another on one scratch pool;
- each batch's trees are copied into the scene's storage, and its outputs freed, before the next batch starts;
- the storage reserves room for the rest at the first batch, so it grows once.
So the peak is about the scene's storage plus one batch. gpuMaxBatchBytes: Infinity turns the cap off (one batch per
update, as big as the device allows). A mesh bigger than the cap builds alone: the cap is a target, not a guarantee.
The standalone builders keep no cap unless given one: createMeshLayoutBuilder(device, { maxBatchBytes }), and
planGpuHplocBatches(device, meshes, { maxBatchBytes }) with builder.createScratchPool(batches) and
createBatch(meshes, options, pool). hplocBatchBytes and lbvhBatchBytes give every buffer's bytes for a batch
before you build it.
Measured on Sponza, Apple M5, Chrome (scene-trees-sponza page, copy mode, f32 nodes, one update per animation
frame after a warm-up load; indicative):
The GPU walk gave the CPU reference's hits on 20,000 rays in every row. With quantized nodes, the default now, the same page measured 613.2 MiB at peak and 360.8 settled under the default cap: GPU batches still write 64-byte nodes, so the batches are the same size, and the storage under them is smaller. The RTX 3060 and the Intel UHD have not run these rows yet (the RTX 3060 has run the trace benches, not this page).