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

Merged
catbot merged 2 commits from claude/issue-36 into master 2026-06-16 15:38:35 +02:00
5 changed files with 315 additions and 82 deletions
Showing only changes of commit 1a81f115c3 - Show all commits

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>
catbot 2026-06-16 13:37:46 +00:00

View file

@ -167,6 +167,7 @@ VkBool32 onError(VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMe
break; break;
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT : case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT :
printf("(error): "); printf("(error): ");
Device::validationErrorCount++;
break; break;
} }

View file

@ -271,12 +271,17 @@ namespace {
void Mesh::Build(std::span<Vector<float, 3, 3>> vertices, void Mesh::Build(std::span<Vector<float, 3, 3>> vertices,
std::span<std::uint32_t> indices, 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, {}); BuildBVHAndRegister(*this, vertices, indices, {});
} }
void Mesh::Build(const CompressedMeshAsset& asset, void Mesh::Build(const CompressedMeshAsset& asset,
WebGPUCommandEncoderRef /*cmd*/) { WebGPUCommandEncoderRef /*cmd*/,
RTBuildOptions /*options*/) {
std::vector<Vector<float, 3, 3>> vertices(asset.vertexCount); std::vector<Vector<float, 3, 3>> vertices(asset.vertexCount);
std::vector<std::uint32_t> indices(asset.indexCount); std::vector<std::uint32_t> indices(asset.indexCount);
std::vector<std::byte> dataBytes( std::vector<std::byte> dataBytes(
@ -298,7 +303,8 @@ void Mesh::Build(const CompressedMeshAsset& asset,
void Mesh::BuildProcedural(std::span<const RTAabb> aabbs, void Mesh::BuildProcedural(std::span<const RTAabb> aabbs,
bool opaque_, bool opaque_,
WebGPUCommandEncoderRef /*cmd*/) { WebGPUCommandEncoderRef /*cmd*/,
RTBuildOptions /*options*/) {
const std::uint32_t count = static_cast<std::uint32_t>(aabbs.size()); const std::uint32_t count = static_cast<std::uint32_t>(aabbs.size());
opaque = opaque_; opaque = opaque_;
triangleCount = 0; // not a triangle mesh triangleCount = 0; // not a triangle mesh
@ -335,3 +341,21 @@ void Mesh::BuildProcedural(std::span<const RTAabb> aabbs,
/*opaqueFlag*/ opaque ? 1 : 0, /*opaqueFlag*/ opaque ? 1 : 0,
/*primCount*/ static_cast<std::int32_t>(count)); /*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);
}

View file

@ -39,18 +39,43 @@ namespace {
constexpr VkBufferUsageFlags2 kIndexUsageBase = constexpr VkBufferUsageFlags2 kIndexUsageBase =
kVertexUsageBase | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; kVertexUsageBase | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
// Translate the portable RTBuildOptions to Vulkan build flags. The two
// preferences are mutually exclusive; ALLOW_UPDATE is layered on top so
// a later in-place refit (UPDATE mode) is permitted.
VkBuildAccelerationStructureFlagsKHR BlasFlags(const RTBuildOptions& options) {
VkBuildAccelerationStructureFlagsKHR flags =
options.preference == RTBuildPreference::FastBuild
? VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_BUILD_BIT_KHR
: VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR;
if (options.allowUpdate)
flags |= VK_BUILD_ACCELERATION_STRUCTURE_ALLOW_UPDATE_BIT_KHR;
return flags;
}
// Shared BLAS sizing / creation / build-record tail — geometry-type // Shared BLAS sizing / creation / build-record tail — geometry-type
// agnostic; both the triangle and the AABB (procedural) paths feed // agnostic; both the triangle and the AABB (procedural) paths feed
// their single geometry through here. // their single geometry through here. `update` selects an in-place
void RecordBLASBuildFromGeometry(Mesh& self, const VkAccelerationStructureGeometryKHR& blasGeometry, std::uint32_t primitiveCount, VkCommandBuffer cmd) { // refit (VK_BUILD_ACCELERATION_STRUCTURE_MODE_UPDATE_KHR, src == dst)
// over a fresh build; the caller guarantees the geometry topology and
// primitiveCount match the existing AS and that `flags` carried
// ALLOW_UPDATE on the original build.
void RecordBLASBuildFromGeometry(Mesh& self, const VkAccelerationStructureGeometryKHR& blasGeometry, std::uint32_t primitiveCount, VkBuildAccelerationStructureFlagsKHR flags, bool update, VkCommandBuffer cmd) {
VkAccelerationStructureBuildGeometryInfoKHR blasBuildGeometryInfo{ VkAccelerationStructureBuildGeometryInfoKHR blasBuildGeometryInfo{
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_KHR, .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_KHR,
.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR, .type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR,
.mode = VK_BUILD_ACCELERATION_STRUCTURE_MODE_BUILD_KHR, .flags = flags,
.mode = update
? VK_BUILD_ACCELERATION_STRUCTURE_MODE_UPDATE_KHR
: VK_BUILD_ACCELERATION_STRUCTURE_MODE_BUILD_KHR,
.geometryCount = 1, .geometryCount = 1,
.pGeometries = &blasGeometry, .pGeometries = &blasGeometry,
}; };
if (!update) {
// Fresh build: query sizes, (re)allocate scratch + AS storage,
// create a new AS handle. The scratch buffer is sized for the
// build (always >= the update scratch requirement), so it also
// covers later in-place refits without resizing.
VkAccelerationStructureBuildSizesInfoKHR blasBuildSizes = { VkAccelerationStructureBuildSizesInfoKHR blasBuildSizes = {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_SIZES_INFO_KHR .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_SIZES_INFO_KHR
}; };
@ -66,7 +91,6 @@ namespace {
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
blasBuildSizes.buildScratchSize); blasBuildSizes.buildScratchSize);
blasBuildGeometryInfo.scratchData.deviceAddress = self.scratchBuffer.address;
self.blasBuffer.Resize( self.blasBuffer.Resize(
VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR
@ -75,6 +99,13 @@ namespace {
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
blasBuildSizes.accelerationStructureSize); blasBuildSizes.accelerationStructureSize);
// Re-Build of an existing Mesh: destroy the stale handle first
// so the previous AS storage isn't leaked.
if (self.accelerationStructure != VK_NULL_HANDLE) {
Device::vkDestroyAccelerationStructureKHR(Device::device, self.accelerationStructure, nullptr);
self.accelerationStructure = VK_NULL_HANDLE;
}
VkAccelerationStructureCreateInfoKHR blasCreateInfo{ VkAccelerationStructureCreateInfoKHR blasCreateInfo{
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_CREATE_INFO_KHR, .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_CREATE_INFO_KHR,
.buffer = self.blasBuffer.buffer, .buffer = self.blasBuffer.buffer,
@ -83,7 +114,32 @@ namespace {
.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR, .type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR,
}; };
Device::CheckVkResult(Device::vkCreateAccelerationStructureKHR(Device::device, &blasCreateInfo, nullptr, &self.accelerationStructure)); Device::CheckVkResult(Device::vkCreateAccelerationStructureKHR(Device::device, &blasCreateInfo, nullptr, &self.accelerationStructure));
VkAccelerationStructureDeviceAddressInfoKHR addrInfo {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_DEVICE_ADDRESS_INFO_KHR,
.accelerationStructure = self.accelerationStructure
};
self.blasAddr = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo);
} else {
// In-place refit: the AS may have been read by a prior TLAS
// build / trace, so order that read before the build overwrites
// it. Scratch reuse needs the same write-after-read guard.
VkMemoryBarrier asBarrier {
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
.srcAccessMask = VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_KHR | VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_KHR,
.dstAccessMask = VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_KHR | VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_KHR
};
vkCmdPipelineBarrier(cmd,
VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR | VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR,
VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR,
0, 1, &asBarrier, 0, nullptr, 0, nullptr);
}
blasBuildGeometryInfo.scratchData.deviceAddress = self.scratchBuffer.address;
blasBuildGeometryInfo.dstAccelerationStructure = self.accelerationStructure; blasBuildGeometryInfo.dstAccelerationStructure = self.accelerationStructure;
// UPDATE refits in place (src == dst); a fresh build has no source.
blasBuildGeometryInfo.srcAccelerationStructure =
update ? self.accelerationStructure : VK_NULL_HANDLE;
VkAccelerationStructureBuildRangeInfoKHR blasRangeInfo { VkAccelerationStructureBuildRangeInfoKHR blasRangeInfo {
.primitiveCount = primitiveCount, .primitiveCount = primitiveCount,
@ -94,14 +150,11 @@ namespace {
VkAccelerationStructureBuildRangeInfoKHR* blasRangeInfoPP = &blasRangeInfo; VkAccelerationStructureBuildRangeInfoKHR* blasRangeInfoPP = &blasRangeInfo;
Device::vkCmdBuildAccelerationStructuresKHR(cmd, 1, &blasBuildGeometryInfo, &blasRangeInfoPP); Device::vkCmdBuildAccelerationStructuresKHR(cmd, 1, &blasBuildGeometryInfo, &blasRangeInfoPP);
VkAccelerationStructureDeviceAddressInfoKHR addrInfo { self.buildFlags = flags;
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_DEVICE_ADDRESS_INFO_KHR, self.builtPrimitiveCount = primitiveCount;
.accelerationStructure = self.accelerationStructure
};
self.blasAddr = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo);
} }
void RecordBLASBuild(Mesh& self, std::uint32_t vertexCount, std::uint32_t indexCount, VkCommandBuffer cmd) { void RecordBLASBuild(Mesh& self, std::uint32_t vertexCount, std::uint32_t indexCount, VkBuildAccelerationStructureFlagsKHR flags, bool update, VkCommandBuffer cmd) {
VkDeviceOrHostAddressConstKHR vertexAddr; VkDeviceOrHostAddressConstKHR vertexAddr;
vertexAddr.deviceAddress = self.vertexBuffer.address; vertexAddr.deviceAddress = self.vertexBuffer.address;
@ -126,11 +179,11 @@ namespace {
.flags = VK_GEOMETRY_OPAQUE_BIT_KHR .flags = VK_GEOMETRY_OPAQUE_BIT_KHR
}; };
RecordBLASBuildFromGeometry(self, blasGeometry, indexCount / 3, cmd); RecordBLASBuildFromGeometry(self, blasGeometry, indexCount / 3, flags, update, cmd);
} }
} }
void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd) { void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd, RTBuildOptions options) {
vertexBuffer.Resize(kVertexUsageBase, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, verticies.size()); vertexBuffer.Resize(kVertexUsageBase, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, verticies.size());
indexBuffer.Resize(kIndexUsageBase, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, indicies.size()); indexBuffer.Resize(kIndexUsageBase, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, indicies.size());
@ -140,10 +193,12 @@ void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32
vertexBuffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR); vertexBuffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR);
indexBuffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR); indexBuffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR);
RecordBLASBuild(*this, static_cast<std::uint32_t>(verticies.size()), static_cast<std::uint32_t>(indicies.size()), cmd); allowUpdate = options.allowUpdate;
builtInputCount = static_cast<std::uint32_t>(verticies.size());
RecordBLASBuild(*this, static_cast<std::uint32_t>(verticies.size()), static_cast<std::uint32_t>(indicies.size()), BlasFlags(options), /*update*/ false, cmd);
} }
void Mesh::Build(const CompressedMeshAsset& asset, VkCommandBuffer cmd) { void Mesh::Build(const CompressedMeshAsset& asset, VkCommandBuffer cmd, RTBuildOptions options) {
if (!Device::memoryDecompressionSupported) { if (!Device::memoryDecompressionSupported) {
// CPU fallback: decompress into temporary host vectors, then take // CPU fallback: decompress into temporary host vectors, then take
// the existing uncompressed path. The data region is decompressed // the existing uncompressed path. The data region is decompressed
@ -159,7 +214,7 @@ void Mesh::Build(const CompressedMeshAsset& asset, VkCommandBuffer cmd) {
std::span<std::byte>(dataDiscard), std::span<std::byte>(dataDiscard),
}; };
Compression::DecompressCPU(asset.blob, std::span(outputs).first(asset.blob.regions.size())); Compression::DecompressCPU(asset.blob, std::span(outputs).first(asset.blob.regions.size()));
Build(vertices, indices, cmd); Build(vertices, indices, cmd, options);
return; return;
} }
@ -206,22 +261,31 @@ void Mesh::Build(const CompressedMeshAsset& asset, VkCommandBuffer cmd) {
VK_PIPELINE_STAGE_2_ACCELERATION_STRUCTURE_BUILD_BIT_KHR, VK_PIPELINE_STAGE_2_ACCELERATION_STRUCTURE_BUILD_BIT_KHR,
VK_ACCESS_2_ACCELERATION_STRUCTURE_READ_BIT_KHR); VK_ACCESS_2_ACCELERATION_STRUCTURE_READ_BIT_KHR);
RecordBLASBuild(*this, asset.vertexCount, asset.indexCount, cmd); allowUpdate = options.allowUpdate;
builtInputCount = asset.vertexCount;
RecordBLASBuild(*this, asset.vertexCount, asset.indexCount, BlasFlags(options), /*update*/ false, cmd);
} }
void Mesh::BuildProcedural(std::span<const RTAabb> aabbs, bool opaque, VkCommandBuffer cmd) { namespace {
this->opaque = opaque; // Re-upload AABB build input and record an AABB BLAS build (fresh or
// in-place refit). Shared by BuildProcedural and RefitProcedural; the
// geometry's opaque bit is read from self.opaque so an UPDATE keeps the
// exact geometry description of the original build.
void RecordProceduralBuild(Mesh& self, std::span<const RTAabb> aabbs, VkBuildAccelerationStructureFlagsKHR flags, bool update, VkCommandBuffer cmd) {
// 24-byte-stride VkAabbPositionsKHR-compatible build input // 24-byte-stride VkAabbPositionsKHR-compatible build input
// (static_assert'd in the interface). Same usage set as the triangle // (static_assert'd in the interface). Same usage set as the triangle
// inputs: AS-build read-only + device address. // inputs: AS-build read-only + device address. A refit reuses the
aabbBuffer.Resize(kVertexUsageBase, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, static_cast<std::uint32_t>(aabbs.size())); // existing same-sized buffer (count is unchanged), so the device
std::memcpy(aabbBuffer.value, aabbs.data(), aabbs.size() * sizeof(RTAabb)); // address — and the geometry it feeds — stays stable.
aabbBuffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR); if (!update) {
self.aabbBuffer.Resize(kVertexUsageBase, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, static_cast<std::uint32_t>(aabbs.size()));
}
std::memcpy(self.aabbBuffer.value, aabbs.data(), aabbs.size() * sizeof(RTAabb));
self.aabbBuffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR);
VkAccelerationStructureGeometryAabbsDataKHR aabbsData { VkAccelerationStructureGeometryAabbsDataKHR aabbsData {
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_AABBS_DATA_KHR, .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_AABBS_DATA_KHR,
.data = { .deviceAddress = aabbBuffer.address }, .data = { .deviceAddress = self.aabbBuffer.address },
.stride = sizeof(RTAabb) .stride = sizeof(RTAabb)
}; };
VkAccelerationStructureGeometryDataKHR geometryData; VkAccelerationStructureGeometryDataKHR geometryData;
@ -232,9 +296,63 @@ void Mesh::BuildProcedural(std::span<const RTAabb> aabbs, bool opaque, VkCommand
.geometry = geometryData, .geometry = geometryData,
// Non-opaque by default (mirrors the WebGPU path) so any-hit // Non-opaque by default (mirrors the WebGPU path) so any-hit
// shaders run for procedural geometry unless the caller opts out. // shaders run for procedural geometry unless the caller opts out.
.flags = opaque ? static_cast<VkGeometryFlagsKHR>(VK_GEOMETRY_OPAQUE_BIT_KHR) : VkGeometryFlagsKHR{} .flags = self.opaque ? static_cast<VkGeometryFlagsKHR>(VK_GEOMETRY_OPAQUE_BIT_KHR) : VkGeometryFlagsKHR{}
}; };
RecordBLASBuildFromGeometry(*this, blasGeometry, static_cast<std::uint32_t>(aabbs.size()), cmd); RecordBLASBuildFromGeometry(self, blasGeometry, static_cast<std::uint32_t>(aabbs.size()), flags, update, cmd);
}
}
void Mesh::BuildProcedural(std::span<const RTAabb> aabbs, bool opaque, VkCommandBuffer cmd, RTBuildOptions options) {
this->opaque = opaque;
allowUpdate = options.allowUpdate;
builtInputCount = static_cast<std::uint32_t>(aabbs.size());
RecordProceduralBuild(*this, aabbs, BlasFlags(options), /*update*/ false, cmd);
}
void Mesh::Refit(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd) {
// A hardware in-place UPDATE is only valid when the original build asked
// for it, an AS already exists, and the topology is unchanged. Otherwise
// fall back to a full rebuild (mode BUILD) of the same buffers — still
// correct, just not the cheap path.
const bool sameTopology =
accelerationStructure != VK_NULL_HANDLE
&& verticies.size() == builtInputCount
&& indicies.size() == static_cast<std::size_t>(builtPrimitiveCount) * 3;
const bool update = allowUpdate && sameTopology;
if (!update) {
// Counts may have changed (or update wasn't permitted): take the
// resizing build path, preserving the caller's original flags.
Build(verticies, indicies, cmd, RTBuildOptions{
.preference = (buildFlags & VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_BUILD_BIT_KHR)
? RTBuildPreference::FastBuild : RTBuildPreference::FastTrace,
.allowUpdate = allowUpdate,
});
return;
}
// Same-sized buffers: overwrite in place so the device addresses (and
// thus the geometry the UPDATE reads) stay stable.
std::memcpy(vertexBuffer.value, verticies.data(), verticies.size() * sizeof(Vector<float, 3, 3>));
std::memcpy(indexBuffer.value, indicies.data(), indicies.size() * sizeof(std::uint32_t));
vertexBuffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR);
indexBuffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR);
RecordBLASBuild(*this, static_cast<std::uint32_t>(verticies.size()), static_cast<std::uint32_t>(indicies.size()), buildFlags, /*update*/ true, cmd);
}
void Mesh::RefitProcedural(std::span<const RTAabb> aabbs, VkCommandBuffer cmd) {
const bool sameTopology =
accelerationStructure != VK_NULL_HANDLE
&& aabbs.size() == builtInputCount;
const bool update = allowUpdate && sameTopology;
if (!update && aabbs.size() != builtInputCount) {
// Topology changed — a full rebuild needs the new count.
builtInputCount = static_cast<std::uint32_t>(aabbs.size());
}
RecordProceduralBuild(*this, aabbs, buildFlags, update, cmd);
} }

View file

@ -184,6 +184,13 @@ export namespace Crafter {
// path and RTPass pushes the active TLAS address as push data. Delete // path and RTPass pushes the active TLAS address as push data. Delete
// this flag and everything keyed on it once a fixed driver ships. // this flag and everything keyed on it once a fixed driver ships.
inline static bool workaroundDescriptorHeapAS = false; inline static bool workaroundDescriptorHeapAS = false;
// Count of ERROR-severity validation messages seen by the debug
// messenger callback since instance creation. The callback only
// prints (it never aborts), so tests that want to fail on a
// validation error — e.g. a malformed acceleration-structure build —
// assert this stays zero across the operation under test.
inline static std::uint32_t validationErrorCount = 0;
// The byte offset of the TLAS-address member inside a patched shader's // The byte offset of the TLAS-address member inside a patched shader's
// push-constant block is tracked per-shader (VulkanShader::tlasPushOffset), // push-constant block is tracked per-shader (VulkanShader::tlasPushOffset),
// not here: a single global is clobbered by whichever shader was patched // not here: a single global is clobbered by whichever shader was patched

View file

@ -42,6 +42,31 @@ export namespace Crafter {
}; };
static_assert(sizeof(RTAabb) == sizeof(VkAabbPositionsKHR)); static_assert(sizeof(RTAabb) == sizeof(VkAabbPositionsKHR));
// Build-time tuning for a BLAS, mapped onto the preference bits of
// VkBuildAccelerationStructureFlagBitsKHR.
enum class RTBuildPreference {
// VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR — spend
// more time building for faster traversal. The right default for
// static geometry that is traced many times.
FastTrace,
// VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_BUILD_BIT_KHR — build
// quickly at the cost of traversal speed. For geometry rebuilt
// (or first-frame streamed) often enough that build time dominates.
FastBuild,
};
// Per-build options for Mesh::Build / BuildProcedural.
struct RTBuildOptions {
RTBuildPreference preference = RTBuildPreference::FastTrace;
// Set VK_BUILD_ACCELERATION_STRUCTURE_ALLOW_UPDATE_BIT_KHR so the
// BLAS can later be refit in place via Refit()/RefitProcedural().
// Required for any subsequent refit; refitting a BLAS built without
// it falls back to a full rebuild. PREFER_FAST_TRACE and
// PREFER_FAST_BUILD are mutually exclusive, so `preference` selects
// exactly one — allowUpdate is layered on top of whichever is set.
bool allowUpdate = false;
};
class Mesh { class Mesh {
public: public:
VulkanBuffer<char, false> scratchBuffer; VulkanBuffer<char, false> scratchBuffer;
@ -61,17 +86,32 @@ export namespace Crafter {
VulkanBuffer<std::byte, true> compressedStaging; VulkanBuffer<std::byte, true> compressedStaging;
VkAccelerationStructureGeometryTrianglesDataKHR blasData; VkAccelerationStructureGeometryTrianglesDataKHR blasData;
VkAccelerationStructureGeometryKHR blas; VkAccelerationStructureGeometryKHR blas;
VkAccelerationStructureKHR accelerationStructure; VkAccelerationStructureKHR accelerationStructure = VK_NULL_HANDLE;
VkDeviceAddress blasAddr; VkDeviceAddress blasAddr;
bool opaque; bool opaque;
void Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd); // ─── Build-option / refit state ──────────────────────────────────
// Flags the BLAS was last (re)built with — carried so Refit can
// re-issue an identical-flags UPDATE build (the FAST_TRACE/FAST_BUILD
// preference plus, when opted in, ALLOW_UPDATE).
VkBuildAccelerationStructureFlagsKHR buildFlags = 0;
// Primitive count of the current build (triangles = indexCount/3,
// procedural = aabb count). An in-place UPDATE must preserve it.
std::uint32_t builtPrimitiveCount = 0;
// Element count the vertex / aabb input buffer was sized for. Refit
// re-uploads in place only when the new data matches this; a change
// forces a full rebuild (topology changed).
std::uint32_t builtInputCount = 0;
// Whether ALLOW_UPDATE was set on the last build, i.e. whether an
// in-place refit is possible at all.
bool allowUpdate = false;
void Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd, RTBuildOptions options = {});
// GPU path: decompresses vertex (region 0) and index (region 1) streams // GPU path: decompresses vertex (region 0) and index (region 1) streams
// from asset.blob into vertexBuffer / indexBuffer using // from asset.blob into vertexBuffer / indexBuffer using
// VK_EXT_memory_decompression. Falls back to CPU decode + the // VK_EXT_memory_decompression. Falls back to CPU decode + the
// uncompressed Build if Device::memoryDecompressionSupported is false. // uncompressed Build if Device::memoryDecompressionSupported is false.
// Region 2 (data) is not consumed here — the caller decompresses it // Region 2 (data) is not consumed here — the caller decompresses it
// into their own buffer if needed (or uses Compression::DecompressCPU). // into their own buffer if needed (or uses Compression::DecompressCPU).
void Build(const ::Crafter::CompressedMeshAsset& asset, VkCommandBuffer cmd); void Build(const ::Crafter::CompressedMeshAsset& asset, VkCommandBuffer cmd, RTBuildOptions options = {});
// Build an AABB (procedural) BLAS from a list of object-space // Build an AABB (procedural) BLAS from a list of object-space
// boxes (VK_GEOMETRY_TYPE_AABBS_KHR). The hit group bound to // boxes (VK_GEOMETRY_TYPE_AABBS_KHR). The hit group bound to
// instances of this mesh must be a // instances of this mesh must be a
@ -83,7 +123,22 @@ export namespace Crafter {
// default) to let any-hit shaders run. Same scratch/lifetime // default) to let any-hit shaders run. Same scratch/lifetime
// handling as the triangle Build; instances reference blasAddr // handling as the triangle Build; instances reference blasAddr
// exactly like a triangle BLAS. // exactly like a triangle BLAS.
void BuildProcedural(std::span<const RTAabb> aabbs, bool opaque, VkCommandBuffer cmd); void BuildProcedural(std::span<const RTAabb> aabbs, bool opaque, VkCommandBuffer cmd, RTBuildOptions options = {});
// Refit the triangle BLAS against new geometry of the *same
// topology* (same vertex count and index count as the original
// Build). When that Build set RTBuildOptions::allowUpdate this issues
// a hardware UPDATE-mode build — much cheaper than a rebuild, and the
// BLAS handle / blasAddr are preserved, so TLAS instances referencing
// it stay valid. A hardware update may only move vertex positions,
// not change connectivity; the indices are re-uploaded for symmetry
// with the rebuild path but must describe the same topology. Without
// allowUpdate (or if the counts changed) it falls back to a full
// rebuild. Lifetime contract matches Build: the spans need only
// outlive this call. Call this per frame to track a deforming mesh.
void Refit(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd);
// Procedural analog of Refit: new object-space boxes, same count.
void RefitProcedural(std::span<const RTAabb> aabbs, VkCommandBuffer cmd);
}; };
} }
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM #endif // !CRAFTER_GRAPHICS_WINDOW_DOM
@ -125,6 +180,20 @@ export namespace Crafter {
}; };
static_assert(sizeof(RTAabb) == 24); static_assert(sizeof(RTAabb) == 24);
// Mirror of the native build-option types so portable code compiles
// unchanged. The software-RT path has no hardware acceleration
// structure, so the FastTrace/FastBuild preference has no effect (the
// SAH BVH2 is always built the same way) and a "refit" simply rebuilds
// the BVH on the host. The types exist purely for API symmetry.
enum class RTBuildPreference {
FastTrace,
FastBuild,
};
struct RTBuildOptions {
RTBuildPreference preference = RTBuildPreference::FastTrace;
bool allowUpdate = false;
};
class Mesh { class Mesh {
public: public:
// BLAS "handle": opaque identity that goes into // BLAS "handle": opaque identity that goes into
@ -147,7 +216,8 @@ export namespace Crafter {
// kept for API symmetry with the Vulkan signature. // kept for API symmetry with the Vulkan signature.
void Build(std::span<Crafter::Vector<float, 3, 3>> vertices, void Build(std::span<Crafter::Vector<float, 3, 3>> vertices,
std::span<std::uint32_t> indices, std::span<std::uint32_t> indices,
WebGPUCommandEncoderRef cmd = 0); WebGPUCommandEncoderRef cmd = 0,
RTBuildOptions options = {});
// CPU-decompress the .cmesh blob (no VK_EXT_memory_decompression // CPU-decompress the .cmesh blob (no VK_EXT_memory_decompression
// equivalent in WebGPU) and forward to the positions+indices path, // equivalent in WebGPU) and forward to the positions+indices path,
@ -156,7 +226,8 @@ export namespace Crafter {
// The data layout is example-defined — the heap is exposed in WGSL // The data layout is example-defined — the heap is exposed in WGSL
// as `vertexAttribs : array<u32>` with a per-mesh u32-word offset. // as `vertexAttribs : array<u32>` with a per-mesh u32-word offset.
void Build(const ::Crafter::CompressedMeshAsset& asset, void Build(const ::Crafter::CompressedMeshAsset& asset,
WebGPUCommandEncoderRef cmd = 0); WebGPUCommandEncoderRef cmd = 0,
RTBuildOptions options = {});
// Build an AABB (procedural) BLAS from a list of object-space boxes // Build an AABB (procedural) BLAS from a list of object-space boxes
// — the WebGPU analog of a VK_GEOMETRY_TYPE_AABBS_KHR geometry. The // — the WebGPU analog of a VK_GEOMETRY_TYPE_AABBS_KHR geometry. The
@ -169,6 +240,18 @@ export namespace Crafter {
// WebGPU — kept for API symmetry with the triangle path. // WebGPU — kept for API symmetry with the triangle path.
void BuildProcedural(std::span<const RTAabb> aabbs, void BuildProcedural(std::span<const RTAabb> aabbs,
bool opaque = false, bool opaque = false,
WebGPUCommandEncoderRef cmd = 0,
RTBuildOptions options = {});
// Refit analogs of the native API. With no hardware AS to update,
// these simply re-run the host BVH build over the new data, so they
// accept the full geometry (not just moved positions). Provided so
// portable code that calls Refit/RefitProcedural compiles and
// behaves correctly on the WebGPU backend.
void Refit(std::span<Crafter::Vector<float, 3, 3>> vertices,
std::span<std::uint32_t> indices,
WebGPUCommandEncoderRef cmd = 0);
void RefitProcedural(std::span<const RTAabb> aabbs,
WebGPUCommandEncoderRef cmd = 0); WebGPUCommandEncoderRef cmd = 0);
}; };
} }