test(rt): an empty scene's TLAS builds, then grows and shrinks
TLASEmptyBuild builds frame 0's TLAS with no elements registered and checks it is a real acceleration structure, then adds an instance and removes it again, rebuilding each time. Before the fix the first build threw on NVIDIA (VK_ERROR_OUT_OF_DEVICE_MEMORY). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3fca002ef5
commit
e5cfbd249c
2 changed files with 159 additions and 0 deletions
26
project.cpp
26
project.cpp
|
|
@ -528,6 +528,32 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
tbc.GetInterfacesAndImplementations(ifaces, tlasBarrierImpls);
|
tbc.GetInterfacesAndImplementations(ifaces, tlasBarrierImpls);
|
||||||
cfg.tests.push_back(std::move(tlasBarrierTest));
|
cfg.tests.push_back(std::move(tlasBarrierTest));
|
||||||
|
|
||||||
|
// BuildTLAS on an empty scene (no elements registered yet) builds a
|
||||||
|
// valid empty TLAS instead of creating zero-size buffers, and adding
|
||||||
|
// and removing instances afterwards still rebuilds it. Needs a Vulkan
|
||||||
|
// RT device at runtime, like TLASHighWaterMark.
|
||||||
|
Test tlasEmptyTest;
|
||||||
|
Configuration& tec = tlasEmptyTest.config;
|
||||||
|
tec.path = cfg.path;
|
||||||
|
tec.name = "TLASEmptyBuild";
|
||||||
|
tec.outputName = "TLASEmptyBuild";
|
||||||
|
tec.type = ConfigurationType::Executable;
|
||||||
|
tec.target = cfg.target;
|
||||||
|
tec.march = cfg.march;
|
||||||
|
tec.mtune = cfg.mtune;
|
||||||
|
tec.debug = cfg.debug;
|
||||||
|
tec.sysroot = cfg.sysroot;
|
||||||
|
tec.dependencies = cfg.dependencies;
|
||||||
|
tec.externalDependencies = cfg.externalDependencies;
|
||||||
|
tec.compileFlags = cfg.compileFlags;
|
||||||
|
tec.linkFlags = cfg.linkFlags;
|
||||||
|
tec.defines = cfg.defines;
|
||||||
|
tec.cFiles = cfg.cFiles;
|
||||||
|
std::vector<fs::path> tlasEmptyImpls(impls.begin(), impls.end());
|
||||||
|
tlasEmptyImpls.emplace_back("tests/TLASEmptyBuild/main");
|
||||||
|
tec.GetInterfacesAndImplementations(ifaces, tlasEmptyImpls);
|
||||||
|
cfg.tests.push_back(std::move(tlasEmptyTest));
|
||||||
|
|
||||||
// Issue #118: the per-frame TLAS instance+metadata host rebuild copies
|
// Issue #118: the per-frame TLAS instance+metadata host rebuild copies
|
||||||
// (and flushes) only the slots whose element's host-authored data
|
// (and flushes) only the slots whose element's host-authored data
|
||||||
// changed, tracked via RenderingElement3D::hostDataVersion against the
|
// changed, tracked via RenderingElement3D::hostDataVersion against the
|
||||||
|
|
|
||||||
133
tests/TLASEmptyBuild/main.cpp
Normal file
133
tests/TLASEmptyBuild/main.cpp
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
//SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
// RenderingElement3D::BuildTLAS with no elements registered, i.e. an app that
|
||||||
|
// builds its TLAS before the scene has anything in it. The TLAS instance and
|
||||||
|
// metadata buffers used to be sized to the instance count, so the first build
|
||||||
|
// of an empty scene created zero-size buffers: invalid usage, which NVIDIA
|
||||||
|
// answers with VK_ERROR_OUT_OF_DEVICE_MEMORY, so BuildTLAS threw. An empty
|
||||||
|
// TLAS is valid to trace against (every ray misses), so it has to build, and
|
||||||
|
// adding and removing instances afterwards has to keep working.
|
||||||
|
//
|
||||||
|
// Needs a Vulkan RT device, like BLASBuildOptions; without RT it skips.
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include "vulkan/vulkan.h"
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds frame 0's TLAS from whatever is registered; false if it threw.
|
||||||
|
bool BuildFrame0() {
|
||||||
|
VkCommandBuffer cmd = BeginCmd();
|
||||||
|
try {
|
||||||
|
RenderingElement3D::BuildTLAS(cmd, 0);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::println(" BuildTLAS threw: {}", e.what());
|
||||||
|
vkEndCommandBuffer(cmd);
|
||||||
|
vkFreeCommandBuffers(Device::device, Device::commandPool, 1, &cmd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SubmitWait(cmd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
Device::Initialize();
|
||||||
|
if (!Device::rayTracingSupported) {
|
||||||
|
std::println("SKIP no Vulkan ray-tracing device");
|
||||||
|
return 77;
|
||||||
|
}
|
||||||
|
Device::validationErrorCount = 0;
|
||||||
|
const TlasWithBuffer& tlas = RenderingElement3D::tlases[0];
|
||||||
|
|
||||||
|
Check(RenderingElement3D::elements.empty(), "the scene starts empty");
|
||||||
|
if (!BuildFrame0()) {
|
||||||
|
Check(false, "an empty scene's TLAS builds");
|
||||||
|
std::println("check(s) failed");
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
Check(true, "an empty scene's TLAS builds");
|
||||||
|
Check(tlas.accelerationStructure != VK_NULL_HANDLE && tlas.address != 0,
|
||||||
|
"it is a real acceleration structure with a device address");
|
||||||
|
Check(tlas.builtInstanceCount == 0, "built for zero instances");
|
||||||
|
|
||||||
|
// One instance added to the scene afterwards.
|
||||||
|
Mesh triangle;
|
||||||
|
{
|
||||||
|
VkCommandBuffer cmd = BeginCmd();
|
||||||
|
std::array<Vector<float, 3, 3>, 3> verts {{{-1, -1, 0}, {0, 1, 0}, {1, -1, 0}}};
|
||||||
|
std::array<std::uint32_t, 3> index {{2, 1, 0}};
|
||||||
|
triangle.Build(verts, index, cmd);
|
||||||
|
SubmitWait(cmd);
|
||||||
|
}
|
||||||
|
RenderingElement3D element;
|
||||||
|
element.instance = {
|
||||||
|
.instanceCustomIndex = 0,
|
||||||
|
.mask = 0xFF,
|
||||||
|
.instanceShaderBindingTableRecordOffset = 0,
|
||||||
|
.flags = VK_GEOMETRY_INSTANCE_FORCE_OPAQUE_BIT_KHR,
|
||||||
|
.accelerationStructureReference = triangle.blasAddr,
|
||||||
|
};
|
||||||
|
MatrixRowMajor<float, 4, 3, 1>::Identity().Store(reinterpret_cast<float*>(element.instance.transform.matrix));
|
||||||
|
RenderingElement3D::Add(&element);
|
||||||
|
Check(BuildFrame0(), "the TLAS rebuilds once an instance is added");
|
||||||
|
Check(tlas.builtInstanceCount == 1, "built for one instance");
|
||||||
|
|
||||||
|
// And back to empty.
|
||||||
|
RenderingElement3D::Remove(&element);
|
||||||
|
Check(BuildFrame0(), "the TLAS rebuilds once the scene is empty again");
|
||||||
|
Check(tlas.builtInstanceCount == 0, "built for zero instances again");
|
||||||
|
|
||||||
|
if (std::getenv("CRAFTER_GRAPHICS_VALIDATION") != nullptr) {
|
||||||
|
Check(Device::validationErrorCount == 0,
|
||||||
|
std::format("no Vulkan validation errors ({} seen)", Device::validationErrorCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed");
|
||||||
|
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue