138 lines
6.6 KiB
C++
138 lines
6.6 KiB
C++
|
|
//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 <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;
|
||
|
|
}
|
||
|
|
|
||
|
|
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<std::uint32_t, true> 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;
|
||
|
|
}
|