//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // A VulkanShader releases its shader module. It used to have no destructor, // so every module leaked, and ComputeShader::Load destroyed the module by // hand. The destructor hands the module to the deferred-deletion queue (issue // #101), like the other owners of Vulkan handles, so a static VulkanShader // makes no Vulkan call at exit. A VulkanShader can't be copied any more, since // two copies would both release the module; moves hand it over, which is what // a growing std::vector needs. A file that can't be read throws, // naming it: it used to print a line and create a module from no code, which // surfaced later as an unrelated driver error. // // Needs a Vulkan device and no window: the test steps Device::frameCounter // itself. The validation check only means something with // CRAFTER_GRAPHICS_VALIDATION set. #include #include "vulkan/vulkan.h" import Crafter.Graphics; import std; using namespace Crafter; namespace fs = std::filesystem; namespace { int failures = 0; void Check(bool ok, std::string_view what) { std::println("{} {}", ok ? "PASS" : "FAIL", what); if (!ok) ++failures; } fs::path ShaderPath() { return fs::read_symlink("/proc/self/exe").parent_path() / "noop.comp.spv"; } std::size_t QueuedCount(VkShaderModule module) { return static_cast(std::ranges::count_if(Device::deletionQueue, [&](const Device::PendingDeletion& e) { return e.shaderModule == module; })); } } // namespace static_assert(!std::is_copy_constructible_v && !std::is_copy_assignable_v, "two copies of a VulkanShader would both release its module"); int main() { Device::Initialize(); Device::validationErrorCount = 0; Device::framesInFlight = Window::numFrames; Device::frameCounter = 0; Device::DrainDeletions(); // ── Destroying a VulkanShader queues its module ────────────────────────── VkShaderModule module = VK_NULL_HANDLE; { VulkanShader shader(ShaderPath(), "main", VK_SHADER_STAGE_COMPUTE_BIT, nullptr); module = shader.shader; Check(module != VK_NULL_HANDLE, "the shader module was created"); Check(Device::deletionQueue.empty(), "a live VulkanShader has queued nothing"); } Check(QueuedCount(module) == 1, "destroying a VulkanShader queues its module"); Device::frameCounter = Window::numFrames; Device::ReclaimDeletions(); Check(Device::deletionQueue.empty(), "the module is freed on its retire frame"); // ── Moving hands the module over ───────────────────────────────────────── { VulkanShader source(ShaderPath(), "main", VK_SHADER_STAGE_COMPUTE_BIT, nullptr); module = source.shader; VulkanShader moved(std::move(source)); Check(source.shader == VK_NULL_HANDLE && moved.shader == module, "a move constructor hands the module over"); VulkanShader target(ShaderPath(), "main", VK_SHADER_STAGE_COMPUTE_BIT, nullptr); const VkShaderModule replaced = target.shader; target = std::move(moved); Check(moved.shader == VK_NULL_HANDLE && target.shader == module, "a move assignment hands the module over"); Check(QueuedCount(replaced) == 1, "and queues the module it replaced"); Check(QueuedCount(module) == 0, "the module moved around is not queued while owned"); } Check(QueuedCount(module) == 1, "the module is queued once, by its last owner"); Device::DrainDeletions(); // ── A growing std::vector moves its elements ─────────────── { std::vector modules; { std::vector shaders; for (int i = 0; i < 5; ++i) { shaders.emplace_back(ShaderPath(), "main", VK_SHADER_STAGE_COMPUTE_BIT, nullptr); } for (const VulkanShader& shader : shaders) modules.push_back(shader.shader); Check(Device::deletionQueue.empty(), "growing the vector queued nothing"); } Check(std::ranges::all_of(modules, [](VkShaderModule m) { return QueuedCount(m) == 1; }), "each module in the vector is queued exactly once when the vector goes"); Device::DrainDeletions(); } // ── A file that isn't there throws, naming it ───────────────────────────── { std::string message; try { VulkanShader missing(ShaderPath().parent_path() / "missing.spv", "main", VK_SHADER_STAGE_COMPUTE_BIT, nullptr); } catch (const std::runtime_error& e) { message = e.what(); } Check(message.find("missing.spv") != std::string::npos, std::format("a shader file that isn't there throws, naming it ({})", message.empty() ? "no exception" : message)); } if (std::getenv("CRAFTER_GRAPHICS_VALIDATION") != nullptr) { Check(Device::validationErrorCount == 0, std::format("no Vulkan validation errors across the run ({} seen)", Device::validationErrorCount)); } std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed"); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; }