From 70d9c2c82a8c23ad258c3a0278aa10b2e52135c2 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Mon, 19 Oct 2026 03:03:16 +0200 Subject: [PATCH] fix(rt): never destroy the static TLASes RenderingElement3D::tlases was a plain static array, so its buffers were freed by its static destructor at process exit. The Vulkan driver and layers are loaded during Device::Initialize, after the array was constructed, so their own statics are torn down first, and destroying the buffers then called into freed driver and layer state: every ray-traced app run with CRAFTER_GRAPHICS_VALIDATION segfaulted on exit inside the validation layer. TLASHighWaterMark and TLASInstanceDirtyTracking worked around it by tearing frame 0's TLAS down by hand before returning. tlases now refers to an array that is never destroyed, the choice Device already makes for the device itself, and exit reclaims the memory. Indexing it is unchanged for callers. The hand teardown in the two tests goes. Co-Authored-By: Claude Opus 5.5 --- .../Crafter.Graphics-RenderingElement3D.cppm | 9 ++++++++- tests/TLASHighWaterMark/main.cpp | 16 ---------------- tests/TLASInstanceDirtyTracking/main.cpp | 14 -------------- 3 files changed, 8 insertions(+), 31 deletions(-) diff --git a/interfaces/Crafter.Graphics-RenderingElement3D.cppm b/interfaces/Crafter.Graphics-RenderingElement3D.cppm index dee9ebf..576736a 100644 --- a/interfaces/Crafter.Graphics-RenderingElement3D.cppm +++ b/interfaces/Crafter.Graphics-RenderingElement3D.cppm @@ -115,7 +115,14 @@ export namespace Crafter { inline static std::uint64_t hostDataVersionCounter = 0; static std::vector elements; - inline static TlasWithBuffer tlases[Window::numFrames]; + // Never destroyed, like Device::device. As a plain static its + // destructor would run at process exit after the Vulkan driver and + // layers, which are loaded during Device::Initialize (after this was + // constructed), have torn down their own state, and destroying the + // buffers would call into that freed state: the validation layer + // segfaults on it. Exit reclaims them instead. + inline static std::array& tlases = + *new std::array(); // Build (or in-place refit) the TLAS for frame `index`. `preference` // maps onto the FAST_TRACE/FAST_BUILD bits exactly as Mesh::Build // does for a BLAS: FastTrace (the default) spends more build time for diff --git a/tests/TLASHighWaterMark/main.cpp b/tests/TLASHighWaterMark/main.cpp index 99174f4..746a66e 100644 --- a/tests/TLASHighWaterMark/main.cpp +++ b/tests/TLASHighWaterMark/main.cpp @@ -351,22 +351,6 @@ int main() { Check(Device::validationErrorCount == 0, std::format("no Vulkan validation errors ({} seen)", Device::validationErrorCount)); - // Tear down the frame-0 TLAS while the Vulkan device is still alive. The - // static tlases[] array is destroyed at process exit, by which point the - // device (also static) may already be gone — so its VulkanBuffer - // destructors would fault. Releasing here keeps the exit path clean. - { - auto& t0 = RenderingElement3D::tlases[0]; - if (t0.accelerationStructure != VK_NULL_HANDLE) { - Device::vkDestroyAccelerationStructureKHR(Device::device, t0.accelerationStructure, nullptr); - t0.accelerationStructure = VK_NULL_HANDLE; - } - if (t0.instanceBuffer.buffer != VK_NULL_HANDLE) t0.instanceBuffer.Clear(); - if (t0.metadataBuffer.buffer != VK_NULL_HANDLE) t0.metadataBuffer.Clear(); - if (t0.scratchBuffer.buffer != VK_NULL_HANDLE) t0.scratchBuffer.Clear(); - if (t0.buffer.buffer != VK_NULL_HANDLE) t0.buffer.Clear(); - } - if (failures != 0) { std::println("{} check(s) failed", failures); return EXIT_FAILURE; diff --git a/tests/TLASInstanceDirtyTracking/main.cpp b/tests/TLASInstanceDirtyTracking/main.cpp index 4ba711a..b2526fc 100644 --- a/tests/TLASInstanceDirtyTracking/main.cpp +++ b/tests/TLASInstanceDirtyTracking/main.cpp @@ -213,20 +213,6 @@ int main() { Check(Device::validationErrorCount == 0, std::format("no Vulkan validation errors ({} seen)", Device::validationErrorCount)); - // Tear down the frame-0 TLAS while the device is still alive (same rationale - // as TLASHighWaterMark — the static tlases[] outlive the static device). - { - auto& t0 = RenderingElement3D::tlases[0]; - if (t0.accelerationStructure != VK_NULL_HANDLE) { - Device::vkDestroyAccelerationStructureKHR(Device::device, t0.accelerationStructure, nullptr); - t0.accelerationStructure = VK_NULL_HANDLE; - } - if (t0.instanceBuffer.buffer != VK_NULL_HANDLE) t0.instanceBuffer.Clear(); - if (t0.metadataBuffer.buffer != VK_NULL_HANDLE) t0.metadataBuffer.Clear(); - if (t0.scratchBuffer.buffer != VK_NULL_HANDLE) t0.scratchBuffer.Clear(); - if (t0.buffer.buffer != VK_NULL_HANDLE) t0.buffer.Clear(); - } - if (failures != 0) { std::println("{} check(s) failed", failures); return EXIT_FAILURE;