//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Device::Initialize's feature requests on a real GPU, end to end: a required // feature ends up enabled, an optional one exactly where the GPU supports it, // and one nobody asked for stays off. On the Vulkan 1.3 path // (CRAFTER_GRAPHICS_NO_VULKAN_1_4, run in a child process, since an instance's // version is fixed for the process) maintenance5 comes from // VK_KHR_maintenance5 and no other vulkan14 feature can be had. And a refusal // leaves nothing behind: after Initialize throws for want of a GPU, calling it // again asking for less gives a working window (also a child process). // tests/DeviceFeatures checks the logic behind this without a GPU; this checks // the wiring between Initialize, EvaluateDevice and ResolveFeatures, and the // 1.3 device chain, which nothing else runs. Neither run resets // validationErrorCount after Initialize, so with CRAFTER_GRAPHICS_VALIDATION // set vkCreateDevice is checked too. Needs a Vulkan device. #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; } int RunCommand(const std::string& cmd) { int status = std::system(cmd.c_str()); if (status == -1) return -1; return (status & 0x7f) == 0 ? ((status >> 8) & 0xff) : 128 + (status & 0x7f); } bool Same(VkBool32 a, VkBool32 b) { return (a != VK_FALSE) == (b != VK_FALSE); } // Initializes with one required and three optional features, checks what // was enabled, and makes a buffer: every VulkanBuffer goes through // maintenance5's VkBufferUsageFlags2CreateInfo, whichever way it was enabled. void Run(bool vulkan13) { const std::string_view path = vulkan13 ? "at 1.3: " : ""; Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}}, { .vulkan11 = {.storageBuffer16BitAccess = VK_TRUE}, .vulkan12 = {.shaderFloat16 = VK_TRUE}, .vulkan14 = {.pushDescriptor = VK_TRUE}, }); const DeviceFeatures& enabled = Device::enabledFeatures; const DeviceFeatures& supported = Device::supportedFeatures; Check(enabled.vulkan10.shaderInt16, std::format("{}a required feature is enabled", path)); Check(Same(enabled.vulkan11.storageBuffer16BitAccess, supported.vulkan11.storageBuffer16BitAccess) && Same(enabled.vulkan12.shaderFloat16, supported.vulkan12.shaderFloat16), std::format("{}an optional feature is enabled exactly where the GPU supports it", path)); Check(!enabled.vulkan10.geometryShader && !enabled.vulkan12.shaderInt8, std::format("{}a feature nobody asked for stays off", path)); Check(enabled.vulkan12.bufferDeviceAddress && enabled.vulkan14.maintenance5, std::format("{}the library's own needs are on, maintenance5 among them", path)); if (vulkan13) { Check(VK_API_VERSION_MAJOR(Device::apiVersion) == 1 && VK_API_VERSION_MINOR(Device::apiVersion) == 3, std::format("CRAFTER_GRAPHICS_NO_VULKAN_1_4 runs the application as Vulkan 1.3 (got {}.{})", VK_API_VERSION_MAJOR(Device::apiVersion), VK_API_VERSION_MINOR(Device::apiVersion))); Check(supported.vulkan14.maintenance5 && !supported.vulkan14.pushDescriptor, "at 1.3: vulkan14 holds maintenance5 alone, copied from VK_KHR_maintenance5"); Check(!enabled.vulkan14.pushDescriptor, "at 1.3: an optional vulkan14 feature stays off"); } else { Check(Same(enabled.vulkan14.pushDescriptor, supported.vulkan14.pushDescriptor), "an optional vulkan14 feature is enabled exactly where the GPU supports it"); } VulkanBuffer buffer; buffer.Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, 16); Check(buffer.address != 0, std::format("{}a buffer made through maintenance5's usage flags has an address", path)); if (std::getenv("CRAFTER_GRAPHICS_VALIDATION") != nullptr) { Check(Device::validationErrorCount == 0, std::format("{}no Vulkan validation errors, device creation included ({} seen)", path, Device::validationErrorCount)); } buffer.Clear(); } // A refusal takes back what Initialize made, so asking again for less works. // At 1.3 no GPU has vulkan14.pushDescriptor, so requiring it must throw; the // retry must then bring up a window, on a Wayland connection of its own, that // renders. void Retry() { bool refused = false; try { Device::Initialize({.vulkan14 = {.pushDescriptor = VK_TRUE}}); } catch (const std::runtime_error& error) { refused = std::string_view(error.what()).contains("vulkan14.pushDescriptor (Vulkan 1.4 only)"); } Check(refused, "at 1.3: requiring a vulkan14 feature leaves no GPU, and the error names it"); Check(Device::instance == VK_NULL_HANDLE, "the refusal destroyed the instance it had made"); Device::Initialize(); Window window(320, 240, "FeatureRequests retry"); for (int i = 0; i < 4; ++i) { window.Render(); } Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); Check(window.frameCounter == 4, "after a refusal, Initialize asking for less brings up a window that renders"); if (std::getenv("CRAFTER_GRAPHICS_VALIDATION") != nullptr) { Check(Device::validationErrorCount == 0, std::format("no Vulkan validation errors across the refusal and the retry ({} seen)", Device::validationErrorCount)); } } } // namespace int main(int argc, char** argv) { if (argc == 2 && std::string_view(argv[1]) == "--vulkan13") { Run(true); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; } if (argc == 2 && std::string_view(argv[1]) == "--retry") { Retry(); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; } const fs::path exe = fs::read_symlink("/proc/self/exe"); const int exit = RunCommand(std::format("CRAFTER_GRAPHICS_NO_VULKAN_1_4=1 '{}' --vulkan13", exe.string())); Check(exit == 0, std::format("the Vulkan 1.3 path passes its checks (child exit {})", exit)); const int retry = RunCommand(std::format("CRAFTER_GRAPHICS_NO_VULKAN_1_4=1 '{}' --retry", exe.string())); Check(retry == 0, std::format("Initialize can be retried after a refusal (child exit {})", retry)); Run(false); std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed"); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; }