feat(vulkan-rt): BLAS build options — fast-build/fast-trace + in-place refit (#36)

Mesh::Build / BuildProcedural now take an RTBuildOptions { preference,
allowUpdate }. `preference` maps to PREFER_FAST_TRACE (default) or
PREFER_FAST_BUILD; `allowUpdate` sets ALLOW_UPDATE so the BLAS can be
refit later.

Adds Mesh::Refit / RefitProcedural: when the original build opted into
allowUpdate and the topology is unchanged, they record an in-place
UPDATE-mode build (src == dst) — much cheaper than a rebuild, and the
AS handle + blasAddr are preserved so TLAS instances stay valid. They
fall back to a full rebuild otherwise. The shared build tail also now
destroys a stale AS handle on re-Build (previously leaked) and guards
the in-place update with an AS read→write barrier.

The portable RTBuildPreference/RTBuildOptions types and Refit methods
also exist on the WebGPU backend for API symmetry; the software BVH has
no hardware AS, so the preference is a no-op and a "refit" rebuilds the
BVH (re-registering the handle).

Also adds Device::validationErrorCount, bumped by the debug-messenger
callback on ERROR-severity messages, so tests can assert a Vulkan
operation produced no validation errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-16 13:37:46 +00:00
commit 1a81f115c3
5 changed files with 315 additions and 82 deletions

View file

@ -271,12 +271,17 @@ namespace {
void Mesh::Build(std::span<Vector<float, 3, 3>> vertices,
std::span<std::uint32_t> indices,
WebGPUCommandEncoderRef /*cmd*/) {
WebGPUCommandEncoderRef /*cmd*/,
RTBuildOptions /*options*/) {
// The build preference (FastTrace/FastBuild) and allowUpdate have no
// effect on the software-RT path — there is no hardware AS to tune or
// refit. The SAH BVH2 is always built the same way.
BuildBVHAndRegister(*this, vertices, indices, {});
}
void Mesh::Build(const CompressedMeshAsset& asset,
WebGPUCommandEncoderRef /*cmd*/) {
WebGPUCommandEncoderRef /*cmd*/,
RTBuildOptions /*options*/) {
std::vector<Vector<float, 3, 3>> vertices(asset.vertexCount);
std::vector<std::uint32_t> indices(asset.indexCount);
std::vector<std::byte> dataBytes(
@ -298,7 +303,8 @@ void Mesh::Build(const CompressedMeshAsset& asset,
void Mesh::BuildProcedural(std::span<const RTAabb> aabbs,
bool opaque_,
WebGPUCommandEncoderRef /*cmd*/) {
WebGPUCommandEncoderRef /*cmd*/,
RTBuildOptions /*options*/) {
const std::uint32_t count = static_cast<std::uint32_t>(aabbs.size());
opaque = opaque_;
triangleCount = 0; // not a triangle mesh
@ -335,3 +341,21 @@ void Mesh::BuildProcedural(std::span<const RTAabb> aabbs,
/*opaqueFlag*/ opaque ? 1 : 0,
/*primCount*/ static_cast<std::int32_t>(count));
}
void Mesh::Refit(std::span<Vector<float, 3, 3>> vertices,
std::span<std::uint32_t> indices,
WebGPUCommandEncoderRef cmd) {
// No hardware AS to update in place — the software path rebuilds the
// host BVH and registers it afresh. Unlike the Vulkan UPDATE path, this
// assigns a NEW blasAddr (the JS heap append is not in-place), so any
// RTInstance::accelerationStructureReference pointing at this mesh must
// be re-pointed at the updated mesh.blasAddr afterwards. Refit is far
// better suited to the hardware backend; on WebGPU prefer rebuilding
// and re-publishing the handle.
Build(vertices, indices, cmd);
}
void Mesh::RefitProcedural(std::span<const RTAabb> aabbs,
WebGPUCommandEncoderRef cmd) {
BuildProcedural(aabbs, opaque, cmd);
}