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 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-16 13:37:46 +00:00
commit ce0114607a
2 changed files with 239 additions and 0 deletions

View file

@ -265,6 +265,36 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> 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<fs::path> blasImpls(impls.begin(), impls.end());
blasImpls.emplace_back("tests/BLASBuildOptions/main");
bc.GetInterfacesAndImplementations(ifaces, blasImpls);
cfg.tests.push_back(std::move(blasTest));
}
return cfg;

View file

@ -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 <cstdlib>
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<Vector<float, 3, 3>> 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<std::uint32_t> 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<RTAabb, 2> 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<RTAabb, 2> 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;
}