diff --git a/implementations/Crafter.Graphics-RenderingElement3D-WebGPU.cpp b/implementations/Crafter.Graphics-RenderingElement3D-WebGPU.cpp index 2cafd67..cb4c950 100644 --- a/implementations/Crafter.Graphics-RenderingElement3D-WebGPU.cpp +++ b/implementations/Crafter.Graphics-RenderingElement3D-WebGPU.cpp @@ -108,9 +108,16 @@ void RenderingElement3D::BuildTLASUpload(WebGPUCommandEncoderRef /*cmd*/, std::u auto& dst = tlas.instanceBuffer.value[i]; const auto& src = elements[i]->instance; if (elements[i]->transformOwnedByGpu) { - // Preserve whatever the GPU compute shader most recently - // wrote into dst.transform. Update only the non-transform - // fields. + // GPU owns the transform: copy only the host-authored + // metadata fields and leave the host mirror's transform bytes + // alone (the application doesn't maintain them). The upload + // below pushes the whole struct, including these transform + // bytes, but the physics-tlas-transform compute pass — which + // runs after this Upload and before Build in the supported + // Upload -> compute -> Build flow — overwrites the transform + // on the GPU before the TLAS build reads it, so the host-side + // value is moot. (Nothing reads instanceBuffer between Upload + // and that compute pass.) dst.instanceCustomIndex = src.instanceCustomIndex; dst.mask = src.mask; dst.instanceShaderBindingTableRecordOffset = src.instanceShaderBindingTableRecordOffset; @@ -122,42 +129,28 @@ void RenderingElement3D::BuildTLASUpload(WebGPUCommandEncoderRef /*cmd*/, std::u tlas.metadataBuffer.value[i] = elements[i]->userMetadata; } - // Upload the instance buffer with partial-write semantics: for runs - // of CPU-driven elements (transformOwnedByGpu=false) we push the - // whole 64-byte struct in one writeBuffer call; for GPU-driven runs - // we push only the trailing 16 metadata bytes per element, leaving - // the transform field intact for the physics-tlas-transform compute - // shader to update. The two arms below produce identical GPU state - // when every element is CPU-driven — this is a no-op refactor until - // 3DForts flips its physics elements to transformOwnedByGpu=true. - constexpr std::uint32_t kInstSize = sizeof(RTInstance); // 64 - constexpr std::uint32_t kTransformSize = sizeof(RTTransformMatrix); // 48 - constexpr std::uint32_t kMetaSize = kInstSize - kTransformSize; // 16 - - std::uint32_t runStart = 0; - bool runOwned = elements[0]->transformOwnedByGpu; - for (std::uint32_t i = 1; i <= primitiveCount; ++i) { - const bool atEnd = (i == primitiveCount); - const bool currOwned = atEnd ? !runOwned : elements[i]->transformOwnedByGpu; - if (currOwned == runOwned && !atEnd) continue; - - if (runOwned) { - // GPU-driven run — metadata only, per element. Cannot batch - // because the metadata bytes are non-contiguous in the - // instance buffer (one 16-byte chunk per 64-byte slot). - for (std::uint32_t j = runStart; j < i; ++j) { - const std::uint32_t off = j * kInstSize + kTransformSize; - tlas.instanceBuffer.FlushDeviceRange(off, off, kMetaSize); - } - } else { - // CPU-driven run — one contiguous writeBuffer. - const std::uint32_t startOff = runStart * kInstSize; - const std::uint32_t bytes = (i - runStart) * kInstSize; - tlas.instanceBuffer.FlushDeviceRange(startOff, startOff, bytes); - } - runStart = i; - runOwned = currOwned; - } + // Upload the active instance range in a single contiguous writeBuffer. + // + // An earlier version split this into per-run arms: contiguous runs of + // CPU-driven elements (transformOwnedByGpu=false) were pushed as one + // block, but GPU-driven runs pushed only the trailing 16 metadata + // bytes of each element — one FlushDeviceRange per element — to avoid + // clobbering the GPU-written transform. The metadata bytes are strided + // (one 16-byte chunk per 64-byte slot), so a GPU-driven run couldn't + // batch; each element paid a separate WebGPU validation / encode / + // JS-boundary cost. + // + // Pushing the whole struct for GPU-driven elements too is harmless: + // the only supported way to drive a transform from the GPU is the + // manual Upload -> physics compute pass -> Build sequence (see the + // file header), and that compute pass runs after this upload and + // rewrites the transform on the GPU before the TLAS build reads it. + // So the stale transform bytes this push carries for those slots are + // overwritten before they matter, and the whole run uploads in one + // call. (The combined BuildTLAS path runs no compute pass and is, as + // documented, only valid for CPU-driven transforms.) + constexpr std::uint32_t kInstSize = sizeof(RTInstance); // 64 + tlas.instanceBuffer.FlushDeviceRange(0, 0, primitiveCount * kInstSize); // Only primitiveCount slots are live; the buffer is padded to kNPadded // (65536). Flushing the whole thing wgpuWriteBuffers all 256 KB through