121 lines
5.4 KiB
C++
121 lines
5.4 KiB
C++
|
|
//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<VulkanShader> 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 <cstdlib>
|
||
|
|
#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::size_t>(std::ranges::count_if(Device::deletionQueue,
|
||
|
|
[&](const Device::PendingDeletion& e) { return e.shaderModule == module; }));
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
static_assert(!std::is_copy_constructible_v<VulkanShader> && !std::is_copy_assignable_v<VulkanShader>,
|
||
|
|
"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<VulkanShader> moves its elements ───────────────
|
||
|
|
{
|
||
|
|
std::vector<VkShaderModule> modules;
|
||
|
|
{
|
||
|
|
std::vector<VulkanShader> 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;
|
||
|
|
}
|