From ce0114607a2c896cfc8625f67dfb39aa3c6435d5 Mon Sep 17 00:00:00 2001 From: catbot Date: Tue, 16 Jun 2026 13:37:46 +0000 Subject: [PATCH] test(vulkan-rt): headless BLAS build-options + refit test (#36) Exercises the real hardware AS-build path: builds and refits triangle and procedural BLASes with fast-build/fast-trace + allow-update flags via one-time command buffers, asserting the requested flags land, that an allowUpdate refit keeps the same AS handle/blasAddr (in-place UPDATE), that the no-update / topology-change fallback still produces a valid BLAS, and that the validation layer reports zero errors. Co-Authored-By: Claude Opus 4.8 --- project.cpp | 30 +++++ tests/BLASBuildOptions/main.cpp | 209 ++++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 tests/BLASBuildOptions/main.cpp diff --git a/project.cpp b/project.cpp index e7c0541..7d45dd3 100644 --- a/project.cpp +++ b/project.cpp @@ -265,6 +265,36 @@ extern "C" Configuration CrafterBuildProject(std::span a sc.GetInterfacesAndImplementations(ifaces, scrollImpls); cfg.tests.push_back(std::move(scrollTest)); } + + // Issue #36: BLAS build options. Drives the real hardware AS-build + // path — records Mesh::Build / Refit / BuildProcedural / + // RefitProcedural with fast-build/fast-trace + allow-update flags + // into one-time command buffers and submits them, asserting the + // requested flags land, that an allowUpdate refit keeps the AS + // handle (in-place UPDATE), and that the validation layer reports no + // errors. Needs a Vulkan RT device at runtime (same as the RT + // examples), so it shares the native build settings. + Test blasTest; + Configuration& bc = blasTest.config; + bc.path = cfg.path; + bc.name = "BLASBuildOptions"; + bc.outputName = "BLASBuildOptions"; + bc.type = ConfigurationType::Executable; + bc.target = cfg.target; + bc.march = cfg.march; + bc.mtune = cfg.mtune; + bc.debug = cfg.debug; + bc.sysroot = cfg.sysroot; + bc.dependencies = cfg.dependencies; + bc.externalDependencies = cfg.externalDependencies; + bc.compileFlags = cfg.compileFlags; + bc.linkFlags = cfg.linkFlags; + bc.defines = cfg.defines; + bc.cFiles = cfg.cFiles; + std::vector blasImpls(impls.begin(), impls.end()); + blasImpls.emplace_back("tests/BLASBuildOptions/main"); + bc.GetInterfacesAndImplementations(ifaces, blasImpls); + cfg.tests.push_back(std::move(blasTest)); } return cfg; diff --git a/tests/BLASBuildOptions/main.cpp b/tests/BLASBuildOptions/main.cpp new file mode 100644 index 0000000..f8e842e --- /dev/null +++ b/tests/BLASBuildOptions/main.cpp @@ -0,0 +1,209 @@ +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +// Issue #36: BLAS build options — fast-build / fast-trace preference and +// in-place refit (UPDATE-mode rebuild). This exercises the real hardware +// path: it spins up a headless Vulkan device (no swapchain / window needed +// — a BLAS build only touches the queue + command pool), records BLAS +// builds and refits into one-time command buffers, and submits them. +// +// What is asserted: +// - Build() with RTBuildOptions records the requested preference + the +// ALLOW_UPDATE bit, and produces a non-zero device address. +// - Refit() on an allowUpdate BLAS keeps the SAME acceleration-structure +// handle and blasAddr (proof it took the in-place UPDATE path, so TLAS +// instances referencing it stay valid). +// - Refit() without allowUpdate, or after a topology change, falls back to +// a fresh build (new handle / address are fine; it must still succeed). +// - The procedural (AABB) path supports the same options + refit. +// - The Vulkan validation layer reports ZERO errors across all of the +// above (Device::validationErrorCount) — the strongest check that the +// UPDATE builds are spec-correct (scratch sizing, ALLOW_UPDATE present, +// src==dst, matching topology, …). +// +// Validation layers are required for the last check to be meaningful; the +// build marks this test as needing the SDK layers. + +#include "vulkan/vulkan.h" +#include + +import Crafter.Graphics; +import Crafter.Math; +import std; + +using namespace Crafter; + +namespace { + +int failures = 0; + +void Check(bool ok, std::string_view what) { + std::println("{} {}", ok ? "PASS" : "FAIL", what); + if (!ok) ++failures; +} + +// One-time command buffer helpers — record a BLAS build, submit, block. +VkCommandBuffer BeginCmd() { + VkCommandBufferAllocateInfo allocInfo { + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + .commandPool = Device::commandPool, + .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, + .commandBufferCount = 1, + }; + VkCommandBuffer cmd = VK_NULL_HANDLE; + Device::CheckVkResult(vkAllocateCommandBuffers(Device::device, &allocInfo, &cmd)); + VkCommandBufferBeginInfo beginInfo { + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, + }; + Device::CheckVkResult(vkBeginCommandBuffer(cmd, &beginInfo)); + return cmd; +} + +void SubmitWait(VkCommandBuffer cmd) { + Device::CheckVkResult(vkEndCommandBuffer(cmd)); + VkSubmitInfo submitInfo { + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, + .commandBufferCount = 1, + .pCommandBuffers = &cmd, + }; + Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, VK_NULL_HANDLE)); + Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); + vkFreeCommandBuffers(Device::device, Device::commandPool, 1, &cmd); +} + +// A unit cube (8 verts, 12 triangles) — enough topology for a real BLAS. +std::vector> CubeVerts(float s) { + return { + {-s,-s,-s}, { s,-s,-s}, { s, s,-s}, {-s, s,-s}, + {-s,-s, s}, { s,-s, s}, { s, s, s}, {-s, s, s}, + }; +} +std::vector CubeIndices() { + return { + 0,1,2, 0,2,3, 4,6,5, 4,7,6, + 0,4,5, 0,5,1, 3,2,6, 3,6,7, + 1,5,6, 1,6,2, 0,3,7, 0,7,4, + }; +} + +} // namespace + +int main() { + Device::Initialize(); + Device::validationErrorCount = 0; + + // ── Triangle BLAS: fast-trace + allow-update, then in-place refit. ── + Mesh tri; + { + auto verts = CubeVerts(1.0f); + auto idx = CubeIndices(); + VkCommandBuffer cmd = BeginCmd(); + tri.Build(verts, idx, cmd, RTBuildOptions{ + .preference = RTBuildPreference::FastTrace, + .allowUpdate = true, + }); + SubmitWait(cmd); + } + Check(tri.blasAddr != 0, "triangle Build produced a non-zero blasAddr"); + Check(tri.accelerationStructure != VK_NULL_HANDLE, "triangle Build created an AS handle"); + Check((tri.buildFlags & VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR) != 0, + "FastTrace preference → PREFER_FAST_TRACE bit set"); + Check((tri.buildFlags & VK_BUILD_ACCELERATION_STRUCTURE_ALLOW_UPDATE_BIT_KHR) != 0, + "allowUpdate=true → ALLOW_UPDATE bit set"); + Check(tri.builtPrimitiveCount == 12, "triangle BLAS reports 12 primitives"); + + const VkDeviceAddress triAddrBefore = tri.blasAddr; + const VkAccelerationStructureKHR triHandleBefore = tri.accelerationStructure; + { + // Same topology, deformed positions → must take the UPDATE path. + auto verts = CubeVerts(1.5f); + auto idx = CubeIndices(); + VkCommandBuffer cmd = BeginCmd(); + tri.Refit(verts, idx, cmd); + SubmitWait(cmd); + } + Check(tri.accelerationStructure == triHandleBefore, + "Refit kept the same AS handle (in-place UPDATE)"); + Check(tri.blasAddr == triAddrBefore, + "Refit kept the same blasAddr (instances stay valid)"); + + // ── Triangle BLAS: fast-build, no update → flags reflect the choice. ─ + Mesh triFast; + { + auto verts = CubeVerts(1.0f); + auto idx = CubeIndices(); + VkCommandBuffer cmd = BeginCmd(); + triFast.Build(verts, idx, cmd, RTBuildOptions{ .preference = RTBuildPreference::FastBuild }); + SubmitWait(cmd); + } + Check((triFast.buildFlags & VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_BUILD_BIT_KHR) != 0, + "FastBuild preference → PREFER_FAST_BUILD bit set"); + Check((triFast.buildFlags & VK_BUILD_ACCELERATION_STRUCTURE_ALLOW_UPDATE_BIT_KHR) == 0, + "allowUpdate=false → ALLOW_UPDATE bit clear"); + + // Refit without allowUpdate must still succeed via the rebuild fallback. + { + auto verts = CubeVerts(0.5f); + auto idx = CubeIndices(); + VkCommandBuffer cmd = BeginCmd(); + triFast.Refit(verts, idx, cmd); + SubmitWait(cmd); + } + Check(triFast.blasAddr != 0, "Refit fallback rebuild still produced a valid BLAS"); + + // ── Procedural (AABB) BLAS: build with options, then refit. ───────── + Mesh proc; + { + std::array boxes {{ + { .min = {-1,-1,-1}, .max = {1,1,1} }, + { .min = { 2, 2, 2}, .max = {3,3,3} }, + }}; + VkCommandBuffer cmd = BeginCmd(); + proc.BuildProcedural(boxes, /*opaque*/ false, cmd, RTBuildOptions{ + .preference = RTBuildPreference::FastTrace, .allowUpdate = true }); + SubmitWait(cmd); + } + Check(proc.blasAddr != 0, "procedural Build produced a non-zero blasAddr"); + Check(proc.builtPrimitiveCount == 2, "procedural BLAS reports 2 primitives"); + + const VkDeviceAddress procAddrBefore = proc.blasAddr; + const VkAccelerationStructureKHR procHandleBefore = proc.accelerationStructure; + { + std::array boxes {{ + { .min = {-2,-2,-2}, .max = {2,2,2} }, + { .min = { 4, 4, 4}, .max = {5,5,5} }, + }}; + VkCommandBuffer cmd = BeginCmd(); + proc.RefitProcedural(boxes, cmd); + SubmitWait(cmd); + } + Check(proc.accelerationStructure == procHandleBefore && proc.blasAddr == procAddrBefore, + "RefitProcedural kept the same AS handle + blasAddr (in-place UPDATE)"); + + Check(Device::validationErrorCount == 0, + std::format("no Vulkan validation errors ({} seen)", Device::validationErrorCount)); + + if (failures != 0) { + std::println("{} check(s) failed", failures); + return EXIT_FAILURE; + } + std::println("all checks passed"); + return EXIT_SUCCESS; +}