From 1a81f115c397899446e609554abe7faacf725980 Mon Sep 17 00:00:00 2001 From: catbot Date: Tue, 16 Jun 2026 13:37:46 +0000 Subject: [PATCH] =?UTF-8?q?feat(vulkan-rt):=20BLAS=20build=20options=20?= =?UTF-8?q?=E2=80=94=20fast-build/fast-trace=20+=20in-place=20refit=20(#36?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- implementations/Crafter.Graphics-Device.cpp | 1 + .../Crafter.Graphics-Mesh-WebGPU.cpp | 30 +- implementations/Crafter.Graphics-Mesh.cpp | 260 +++++++++++++----- interfaces/Crafter.Graphics-Device.cppm | 7 + interfaces/Crafter.Graphics-Mesh.cppm | 99 ++++++- 5 files changed, 315 insertions(+), 82 deletions(-) diff --git a/implementations/Crafter.Graphics-Device.cpp b/implementations/Crafter.Graphics-Device.cpp index 016242a..41bf2b1 100644 --- a/implementations/Crafter.Graphics-Device.cpp +++ b/implementations/Crafter.Graphics-Device.cpp @@ -167,6 +167,7 @@ VkBool32 onError(VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMe break; case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT : printf("(error): "); + Device::validationErrorCount++; break; } diff --git a/implementations/Crafter.Graphics-Mesh-WebGPU.cpp b/implementations/Crafter.Graphics-Mesh-WebGPU.cpp index 69f3c9a..5d74198 100644 --- a/implementations/Crafter.Graphics-Mesh-WebGPU.cpp +++ b/implementations/Crafter.Graphics-Mesh-WebGPU.cpp @@ -271,12 +271,17 @@ namespace { void Mesh::Build(std::span> vertices, std::span 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> vertices(asset.vertexCount); std::vector indices(asset.indexCount); std::vector dataBytes( @@ -298,7 +303,8 @@ void Mesh::Build(const CompressedMeshAsset& asset, void Mesh::BuildProcedural(std::span aabbs, bool opaque_, - WebGPUCommandEncoderRef /*cmd*/) { + WebGPUCommandEncoderRef /*cmd*/, + RTBuildOptions /*options*/) { const std::uint32_t count = static_cast(aabbs.size()); opaque = opaque_; triangleCount = 0; // not a triangle mesh @@ -335,3 +341,21 @@ void Mesh::BuildProcedural(std::span aabbs, /*opaqueFlag*/ opaque ? 1 : 0, /*primCount*/ static_cast(count)); } + +void Mesh::Refit(std::span> vertices, + std::span 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 aabbs, + WebGPUCommandEncoderRef cmd) { + BuildProcedural(aabbs, opaque, cmd); +} diff --git a/implementations/Crafter.Graphics-Mesh.cpp b/implementations/Crafter.Graphics-Mesh.cpp index 0e37756..2dbff16 100644 --- a/implementations/Crafter.Graphics-Mesh.cpp +++ b/implementations/Crafter.Graphics-Mesh.cpp @@ -39,51 +39,107 @@ namespace { constexpr VkBufferUsageFlags2 kIndexUsageBase = 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 // agnostic; both the triangle and the AABB (procedural) paths feed - // their single geometry through here. - void RecordBLASBuildFromGeometry(Mesh& self, const VkAccelerationStructureGeometryKHR& blasGeometry, std::uint32_t primitiveCount, VkCommandBuffer cmd) { + // their single geometry through here. `update` selects an in-place + // 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{ .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_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, .pGeometries = &blasGeometry, }; - VkAccelerationStructureBuildSizesInfoKHR blasBuildSizes = { - .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_SIZES_INFO_KHR - }; - Device::vkGetAccelerationStructureBuildSizesKHR( - Device::device, - VK_ACCELERATION_STRUCTURE_BUILD_TYPE_DEVICE_KHR, - &blasBuildGeometryInfo, - &primitiveCount, - &blasBuildSizes - ); + 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 = { + .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_SIZES_INFO_KHR + }; + Device::vkGetAccelerationStructureBuildSizesKHR( + Device::device, + VK_ACCELERATION_STRUCTURE_BUILD_TYPE_DEVICE_KHR, + &blasBuildGeometryInfo, + &primitiveCount, + &blasBuildSizes + ); + + self.scratchBuffer.Resize( + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + blasBuildSizes.buildScratchSize); + + self.blasBuffer.Resize( + VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR + | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT + | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + 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{ + .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_CREATE_INFO_KHR, + .buffer = self.blasBuffer.buffer, + .offset = 0, + .size = blasBuildSizes.accelerationStructureSize, + .type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR, + }; + 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); + } - self.scratchBuffer.Resize( - VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, - VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, - blasBuildSizes.buildScratchSize); blasBuildGeometryInfo.scratchData.deviceAddress = self.scratchBuffer.address; - - self.blasBuffer.Resize( - VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR - | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT - | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR, - VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, - blasBuildSizes.accelerationStructureSize); - - VkAccelerationStructureCreateInfoKHR blasCreateInfo{ - .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_CREATE_INFO_KHR, - .buffer = self.blasBuffer.buffer, - .offset = 0, - .size = blasBuildSizes.accelerationStructureSize, - .type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR, - }; - Device::CheckVkResult(Device::vkCreateAccelerationStructureKHR(Device::device, &blasCreateInfo, nullptr, &self.accelerationStructure)); - 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 { .primitiveCount = primitiveCount, @@ -94,14 +150,11 @@ namespace { VkAccelerationStructureBuildRangeInfoKHR* blasRangeInfoPP = &blasRangeInfo; Device::vkCmdBuildAccelerationStructuresKHR(cmd, 1, &blasBuildGeometryInfo, &blasRangeInfoPP); - VkAccelerationStructureDeviceAddressInfoKHR addrInfo { - .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_DEVICE_ADDRESS_INFO_KHR, - .accelerationStructure = self.accelerationStructure - }; - self.blasAddr = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo); + self.buildFlags = flags; + self.builtPrimitiveCount = primitiveCount; } - 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; vertexAddr.deviceAddress = self.vertexBuffer.address; @@ -126,11 +179,11 @@ namespace { .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> verticies, std::span indicies, VkCommandBuffer cmd) { +void Mesh::Build(std::span> verticies, std::span indicies, VkCommandBuffer cmd, RTBuildOptions options) { vertexBuffer.Resize(kVertexUsageBase, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, verticies.size()); indexBuffer.Resize(kIndexUsageBase, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, indicies.size()); @@ -140,10 +193,12 @@ void Mesh::Build(std::span> verticies, std::span(verticies.size()), static_cast(indicies.size()), cmd); + allowUpdate = options.allowUpdate; + builtInputCount = static_cast(verticies.size()); + RecordBLASBuild(*this, static_cast(verticies.size()), static_cast(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) { // CPU fallback: decompress into temporary host vectors, then take // the existing uncompressed path. The data region is decompressed @@ -159,7 +214,7 @@ void Mesh::Build(const CompressedMeshAsset& asset, VkCommandBuffer cmd) { std::span(dataDiscard), }; Compression::DecompressCPU(asset.blob, std::span(outputs).first(asset.blob.regions.size())); - Build(vertices, indices, cmd); + Build(vertices, indices, cmd, options); return; } @@ -206,35 +261,98 @@ void Mesh::Build(const CompressedMeshAsset& asset, VkCommandBuffer cmd) { VK_PIPELINE_STAGE_2_ACCELERATION_STRUCTURE_BUILD_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 aabbs, bool opaque, VkCommandBuffer cmd) { - this->opaque = opaque; +namespace { + // 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 aabbs, VkBuildAccelerationStructureFlagsKHR flags, bool update, VkCommandBuffer cmd) { + // 24-byte-stride VkAabbPositionsKHR-compatible build input + // (static_assert'd in the interface). Same usage set as the triangle + // inputs: AS-build read-only + device address. A refit reuses the + // existing same-sized buffer (count is unchanged), so the device + // address — and the geometry it feeds — stays stable. + if (!update) { + self.aabbBuffer.Resize(kVertexUsageBase, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, static_cast(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); - // 24-byte-stride VkAabbPositionsKHR-compatible build input - // (static_assert'd in the interface). Same usage set as the triangle - // inputs: AS-build read-only + device address. - aabbBuffer.Resize(kVertexUsageBase, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, static_cast(aabbs.size())); - std::memcpy(aabbBuffer.value, aabbs.data(), aabbs.size() * sizeof(RTAabb)); - aabbBuffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR); + VkAccelerationStructureGeometryAabbsDataKHR aabbsData { + .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_AABBS_DATA_KHR, + .data = { .deviceAddress = self.aabbBuffer.address }, + .stride = sizeof(RTAabb) + }; + VkAccelerationStructureGeometryDataKHR geometryData; + geometryData.aabbs = aabbsData; + VkAccelerationStructureGeometryKHR blasGeometry { + .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR, + .geometryType = VK_GEOMETRY_TYPE_AABBS_KHR, + .geometry = geometryData, + // Non-opaque by default (mirrors the WebGPU path) so any-hit + // shaders run for procedural geometry unless the caller opts out. + .flags = self.opaque ? static_cast(VK_GEOMETRY_OPAQUE_BIT_KHR) : VkGeometryFlagsKHR{} + }; - VkAccelerationStructureGeometryAabbsDataKHR aabbsData { - .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_AABBS_DATA_KHR, - .data = { .deviceAddress = aabbBuffer.address }, - .stride = sizeof(RTAabb) - }; - VkAccelerationStructureGeometryDataKHR geometryData; - geometryData.aabbs = aabbsData; - VkAccelerationStructureGeometryKHR blasGeometry { - .sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR, - .geometryType = VK_GEOMETRY_TYPE_AABBS_KHR, - .geometry = geometryData, - // Non-opaque by default (mirrors the WebGPU path) so any-hit - // shaders run for procedural geometry unless the caller opts out. - .flags = opaque ? static_cast(VK_GEOMETRY_OPAQUE_BIT_KHR) : VkGeometryFlagsKHR{} - }; - - RecordBLASBuildFromGeometry(*this, blasGeometry, static_cast(aabbs.size()), cmd); + RecordBLASBuildFromGeometry(self, blasGeometry, static_cast(aabbs.size()), flags, update, cmd); + } +} + +void Mesh::BuildProcedural(std::span aabbs, bool opaque, VkCommandBuffer cmd, RTBuildOptions options) { + this->opaque = opaque; + allowUpdate = options.allowUpdate; + builtInputCount = static_cast(aabbs.size()); + + RecordProceduralBuild(*this, aabbs, BlasFlags(options), /*update*/ false, cmd); +} + +void Mesh::Refit(std::span> verticies, std::span 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(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)); + 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(verticies.size()), static_cast(indicies.size()), buildFlags, /*update*/ true, cmd); +} + +void Mesh::RefitProcedural(std::span 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(aabbs.size()); + } + RecordProceduralBuild(*this, aabbs, buildFlags, update, cmd); } diff --git a/interfaces/Crafter.Graphics-Device.cppm b/interfaces/Crafter.Graphics-Device.cppm index 8ec80a7..c1a39c1 100644 --- a/interfaces/Crafter.Graphics-Device.cppm +++ b/interfaces/Crafter.Graphics-Device.cppm @@ -184,6 +184,13 @@ export namespace Crafter { // path and RTPass pushes the active TLAS address as push data. Delete // this flag and everything keyed on it once a fixed driver ships. 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 // push-constant block is tracked per-shader (VulkanShader::tlasPushOffset), // not here: a single global is clobbered by whichever shader was patched diff --git a/interfaces/Crafter.Graphics-Mesh.cppm b/interfaces/Crafter.Graphics-Mesh.cppm index b71a359..516d408 100644 --- a/interfaces/Crafter.Graphics-Mesh.cppm +++ b/interfaces/Crafter.Graphics-Mesh.cppm @@ -42,6 +42,31 @@ export namespace Crafter { }; 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 { public: VulkanBuffer scratchBuffer; @@ -61,17 +86,32 @@ export namespace Crafter { VulkanBuffer compressedStaging; VkAccelerationStructureGeometryTrianglesDataKHR blasData; VkAccelerationStructureGeometryKHR blas; - VkAccelerationStructureKHR accelerationStructure; + VkAccelerationStructureKHR accelerationStructure = VK_NULL_HANDLE; VkDeviceAddress blasAddr; bool opaque; - void Build(std::span> verticies, std::span 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> verticies, std::span indicies, VkCommandBuffer cmd, RTBuildOptions options = {}); // GPU path: decompresses vertex (region 0) and index (region 1) streams // from asset.blob into vertexBuffer / indexBuffer using // VK_EXT_memory_decompression. Falls back to CPU decode + the // uncompressed Build if Device::memoryDecompressionSupported is false. // Region 2 (data) is not consumed here — the caller decompresses it // 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 // boxes (VK_GEOMETRY_TYPE_AABBS_KHR). The hit group bound to // instances of this mesh must be a @@ -83,7 +123,22 @@ export namespace Crafter { // default) to let any-hit shaders run. Same scratch/lifetime // handling as the triangle Build; instances reference blasAddr // exactly like a triangle BLAS. - void BuildProcedural(std::span aabbs, bool opaque, VkCommandBuffer cmd); + void BuildProcedural(std::span 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> verticies, std::span indicies, VkCommandBuffer cmd); + // Procedural analog of Refit: new object-space boxes, same count. + void RefitProcedural(std::span aabbs, VkCommandBuffer cmd); }; } #endif // !CRAFTER_GRAPHICS_WINDOW_DOM @@ -125,6 +180,20 @@ export namespace Crafter { }; 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 { public: // BLAS "handle": opaque identity that goes into @@ -147,7 +216,8 @@ export namespace Crafter { // kept for API symmetry with the Vulkan signature. void Build(std::span> vertices, std::span indices, - WebGPUCommandEncoderRef cmd = 0); + WebGPUCommandEncoderRef cmd = 0, + RTBuildOptions options = {}); // CPU-decompress the .cmesh blob (no VK_EXT_memory_decompression // 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 // as `vertexAttribs : array` with a per-mesh u32-word offset. 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 // — the WebGPU analog of a VK_GEOMETRY_TYPE_AABBS_KHR geometry. The @@ -168,8 +239,20 @@ export namespace Crafter { // transparent / volumetric). The `cmd` parameter is unused on // WebGPU — kept for API symmetry with the triangle path. void BuildProcedural(std::span aabbs, - bool opaque = false, - WebGPUCommandEncoderRef cmd = 0); + 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> vertices, + std::span indices, + WebGPUCommandEncoderRef cmd = 0); + void RefitProcedural(std::span aabbs, + WebGPUCommandEncoderRef cmd = 0); }; } #endif // CRAFTER_GRAPHICS_WINDOW_DOM