diff --git a/project.cpp b/project.cpp index 7c1ba12..9780c23 100644 --- a/project.cpp +++ b/project.cpp @@ -528,6 +528,32 @@ extern "C" Configuration CrafterBuildProject(std::span a tbc.GetInterfacesAndImplementations(ifaces, tlasBarrierImpls); 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 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 // (and flushes) only the slots whose element's host-authored data // changed, tracked via RenderingElement3D::hostDataVersion against the diff --git a/tests/TLASEmptyBuild/main.cpp b/tests/TLASEmptyBuild/main.cpp new file mode 100644 index 0000000..94e9595 --- /dev/null +++ b/tests/TLASEmptyBuild/main.cpp @@ -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 +#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, 3> verts {{{-1, -1, 0}, {0, 1, 0}, {1, -1, 0}}}; + std::array 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::Identity().Store(reinterpret_cast(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; +}