BuildTLASUpload pushed GPU-driven (transformOwnedByGpu) runs one element
at a time — a separate FlushDeviceRange of the 16 strided metadata bytes
per element — each paying WebGPU validation / encode / JS-boundary cost,
while the CPU-driven arm already batched contiguously.
Upload the whole active instance range in a single writeBuffer instead.
Pushing the (stale) transform bytes for GPU-driven slots is harmless: the
only supported way to drive a transform from the GPU is the manual
Upload -> physics compute pass -> Build sequence, and that compute pass
runs after this upload and rewrites the transform on the GPU before the
TLAS build reads it. For the in-repo (all-CPU) usage the bytes uploaded
are identical to before — with one CPU-driven run the old loop already
emitted exactly this single FlushDeviceRange(0, 0, primitiveCount*64).
Verified: native suite (24 passed) and the RTStress wasm example (512
ray-traced instances) render correctly through the new path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>