diff --git a/README.md b/README.md index 642c626..bea28ec 100644 --- a/README.md +++ b/README.md @@ -89,10 +89,25 @@ shaded through an intersection shader with an any-hit cut-out. `VK_EXT_descriptor_heap`; pipelines are created with `VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT` so there are no descriptor-set layouts and push constants travel via - `vkCmdPushDataEXT`. The heap, ray tracing and device-fault extensions - are optional groups, detected at start-up + `vkCmdPushDataEXT`. Needs Vulkan 1.3. The heap, ray tracing and + device-fault extensions are optional groups, detected at start-up (`Device::descriptorHeapSupported`, `rayTracingSupported`, - `deviceFaultSupported`). + `deviceFaultSupported`). Device features split by who uses them: the + ones the binding model and the library's own shaders need are the + library's to enable; arithmetic, storage and layout features an + application's shaders use (`shaderInt16` for a `uint16_t` spec + constant, `scalarBlockLayout`, fp16, 16-bit storage) are the + application's to ask for, as core feature structs: + `Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}})`. A second + argument takes features to use where the GPU has them, and + `Device::enabledFeatures` says which it got. A GPU that can't run the + application is skipped. `VulkanShader` refuses a module that declares, + directly or implicitly, a capability whose core feature, subgroup + operation or optional group (ray tracing, the descriptor heap) is off, + naming the field to pass or saying the GPU lacks it. Capabilities only + an extension the library never enables could allow are left to the + driver and the validation layer, as are layout rules + (`scalarBlockLayout`), which have no capability. - **DescriptorHeapVulkan / DescriptorHeapWebGPU** — bindless slot allocators. Vulkan side allocates image/buffer/sampler slots in a `VK_EXT_descriptor_heap`; WebGPU side resolves slots to JS-side diff --git a/examples/RTVolume/main.cpp b/examples/RTVolume/main.cpp index 5fdfeb7..7b91828 100644 --- a/examples/RTVolume/main.cpp +++ b/examples/RTVolume/main.cpp @@ -39,7 +39,9 @@ int main() { const int instanceCount = kGrid * kGrid * kGrid; std::println("[RTVolume] grid {}^3 = {} procedural spheres", kGrid, instanceCount); - Device::Initialize(); + // The raygen takes the heap's buffer start as a uint16_t spec constant, + // and aabbs.comp declares its AABB buffer `layout(scalar)`. + Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}, .vulkan12 = {.scalarBlockLayout = VK_TRUE}}); Window window(1280, 720, "RTVolume"); VkCommandBuffer cmd = window.StartInit(); diff --git a/examples/Sponza/main.cpp b/examples/Sponza/main.cpp index 49ca559..afd8a87 100644 --- a/examples/Sponza/main.cpp +++ b/examples/Sponza/main.cpp @@ -72,7 +72,9 @@ int main() { loadedMesh.vertexCount, loadedMesh.indexCount, loadedTex.sizeX, loadedTex.sizeY); - Device::Initialize(); + // The raygen and closest-hit shaders take heap slots as uint16_t spec + // constants. + Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}}); Window window(1280, 720, "Sponza"); VkCommandBuffer cmd = window.StartInit(); diff --git a/examples/SponzaBench/main.cpp b/examples/SponzaBench/main.cpp index e06754c..377a474 100644 --- a/examples/SponzaBench/main.cpp +++ b/examples/SponzaBench/main.cpp @@ -100,7 +100,8 @@ int main() { std::println("[SponzaBench] scene: {} meshes, {} albedos, {}x{} probe albedo", meshCount, albedoCount, loadedTex.sizeX, loadedTex.sizeY); - Device::Initialize(); + // The raygen takes the heap's buffer start as a uint16_t spec constant. + Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}}); Window window(1280, 720, "SponzaBench"); VkCommandBuffer cmd = window.StartInit(); diff --git a/examples/VulkanTriangle/README.md b/examples/VulkanTriangle/README.md index 6201ff7..b416c82 100644 --- a/examples/VulkanTriangle/README.md +++ b/examples/VulkanTriangle/README.md @@ -5,7 +5,10 @@ The minimal ray-traced example. Renders a single static triangle through ## What it shows -- `Device::Initialize()` + `Window` + swapchain bring-up. +- `Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}})` + `Window` + + swapchain bring-up. The raygen takes the heap's buffer start as a + `uint16_t` spec constant, which needs `shaderInt16`; without asking for + it, loading the shader throws, naming the feature. - A `DescriptorHeapVulkan` sized for one image + one buffer slot, with slot ranges allocated via the bump-allocator API (`AllocateImageSlots`, `AllocateBufferSlots`). diff --git a/examples/VulkanTriangle/main.cpp b/examples/VulkanTriangle/main.cpp index 6b6d453..af0cd5b 100644 --- a/examples/VulkanTriangle/main.cpp +++ b/examples/VulkanTriangle/main.cpp @@ -14,7 +14,8 @@ import Crafter.Math; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM int main() { - Device::Initialize(); + // The raygen takes the heap's buffer start as a uint16_t spec constant. + Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}}); Window window(1280, 720, "HelloVulkan"); VkCommandBuffer cmd = window.StartInit(); DescriptorHeapVulkan descriptorHeap; diff --git a/implementations/Crafter.Graphics-Device-Features.cpp b/implementations/Crafter.Graphics-Device-Features.cpp new file mode 100644 index 0000000..ef6abc9 --- /dev/null +++ b/implementations/Crafter.Graphics-Device-Features.cpp @@ -0,0 +1,551 @@ +//SPDX-License-Identifier: LGPL-3.0-only +//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +// The parts of device bring-up that are pure logic, kept apart from the +// Vulkan calls so tests/DeviceFeatures can drive them without a GPU: which +// features to enable, which GPU to use, and whether a SPIR-V module only +// declares capabilities the device was created for. + +module; + +#include +#include "vulkan/vulkan.h" +#include "vulkan/vk_enum_string_helper.h" + +module Crafter.Graphics:Device_features_impl; +import :Device; +import std; +using namespace Crafter; + +std::string DeviceDetail::Join(std::span parts, std::string_view separator) { + std::string joined; + for (const std::string& part : parts) { + if (!joined.empty()) { + joined += separator; + } + joined += part; + } + return joined; +} + +namespace { + // One VkBool32 of DeviceFeatures: where it is, and the name an + // application writes it by. + struct FeatureField { + std::size_t offset; + std::string_view name; + }; + + #define CRAFTER_FIELD_OF(version, type, field) \ + FeatureField { offsetof(DeviceFeatures, version) + offsetof(type, field), #version "." #field } + + // Every VkBool32 member of the five core feature structs, in declaration + // order. Generated once from vk.xml: a core feature struct never changes + // after its version ships. + #define CRAFTER_DEVICE_FEATURES(X) \ + X(vulkan10, VkPhysicalDeviceFeatures, robustBufferAccess) \ + X(vulkan10, VkPhysicalDeviceFeatures, fullDrawIndexUint32) \ + X(vulkan10, VkPhysicalDeviceFeatures, imageCubeArray) \ + X(vulkan10, VkPhysicalDeviceFeatures, independentBlend) \ + X(vulkan10, VkPhysicalDeviceFeatures, geometryShader) \ + X(vulkan10, VkPhysicalDeviceFeatures, tessellationShader) \ + X(vulkan10, VkPhysicalDeviceFeatures, sampleRateShading) \ + X(vulkan10, VkPhysicalDeviceFeatures, dualSrcBlend) \ + X(vulkan10, VkPhysicalDeviceFeatures, logicOp) \ + X(vulkan10, VkPhysicalDeviceFeatures, multiDrawIndirect) \ + X(vulkan10, VkPhysicalDeviceFeatures, drawIndirectFirstInstance) \ + X(vulkan10, VkPhysicalDeviceFeatures, depthClamp) \ + X(vulkan10, VkPhysicalDeviceFeatures, depthBiasClamp) \ + X(vulkan10, VkPhysicalDeviceFeatures, fillModeNonSolid) \ + X(vulkan10, VkPhysicalDeviceFeatures, depthBounds) \ + X(vulkan10, VkPhysicalDeviceFeatures, wideLines) \ + X(vulkan10, VkPhysicalDeviceFeatures, largePoints) \ + X(vulkan10, VkPhysicalDeviceFeatures, alphaToOne) \ + X(vulkan10, VkPhysicalDeviceFeatures, multiViewport) \ + X(vulkan10, VkPhysicalDeviceFeatures, samplerAnisotropy) \ + X(vulkan10, VkPhysicalDeviceFeatures, textureCompressionETC2) \ + X(vulkan10, VkPhysicalDeviceFeatures, textureCompressionASTC_LDR) \ + X(vulkan10, VkPhysicalDeviceFeatures, textureCompressionBC) \ + X(vulkan10, VkPhysicalDeviceFeatures, occlusionQueryPrecise) \ + X(vulkan10, VkPhysicalDeviceFeatures, pipelineStatisticsQuery) \ + X(vulkan10, VkPhysicalDeviceFeatures, vertexPipelineStoresAndAtomics) \ + X(vulkan10, VkPhysicalDeviceFeatures, fragmentStoresAndAtomics) \ + X(vulkan10, VkPhysicalDeviceFeatures, shaderTessellationAndGeometryPointSize) \ + X(vulkan10, VkPhysicalDeviceFeatures, shaderImageGatherExtended) \ + X(vulkan10, VkPhysicalDeviceFeatures, shaderStorageImageExtendedFormats) \ + X(vulkan10, VkPhysicalDeviceFeatures, shaderStorageImageMultisample) \ + X(vulkan10, VkPhysicalDeviceFeatures, shaderStorageImageReadWithoutFormat) \ + X(vulkan10, VkPhysicalDeviceFeatures, shaderStorageImageWriteWithoutFormat) \ + X(vulkan10, VkPhysicalDeviceFeatures, shaderUniformBufferArrayDynamicIndexing) \ + X(vulkan10, VkPhysicalDeviceFeatures, shaderSampledImageArrayDynamicIndexing) \ + X(vulkan10, VkPhysicalDeviceFeatures, shaderStorageBufferArrayDynamicIndexing) \ + X(vulkan10, VkPhysicalDeviceFeatures, shaderStorageImageArrayDynamicIndexing) \ + X(vulkan10, VkPhysicalDeviceFeatures, shaderClipDistance) \ + X(vulkan10, VkPhysicalDeviceFeatures, shaderCullDistance) \ + X(vulkan10, VkPhysicalDeviceFeatures, shaderFloat64) \ + X(vulkan10, VkPhysicalDeviceFeatures, shaderInt64) \ + X(vulkan10, VkPhysicalDeviceFeatures, shaderInt16) \ + X(vulkan10, VkPhysicalDeviceFeatures, shaderResourceResidency) \ + X(vulkan10, VkPhysicalDeviceFeatures, shaderResourceMinLod) \ + X(vulkan10, VkPhysicalDeviceFeatures, sparseBinding) \ + X(vulkan10, VkPhysicalDeviceFeatures, sparseResidencyBuffer) \ + X(vulkan10, VkPhysicalDeviceFeatures, sparseResidencyImage2D) \ + X(vulkan10, VkPhysicalDeviceFeatures, sparseResidencyImage3D) \ + X(vulkan10, VkPhysicalDeviceFeatures, sparseResidency2Samples) \ + X(vulkan10, VkPhysicalDeviceFeatures, sparseResidency4Samples) \ + X(vulkan10, VkPhysicalDeviceFeatures, sparseResidency8Samples) \ + X(vulkan10, VkPhysicalDeviceFeatures, sparseResidency16Samples) \ + X(vulkan10, VkPhysicalDeviceFeatures, sparseResidencyAliased) \ + X(vulkan10, VkPhysicalDeviceFeatures, variableMultisampleRate) \ + X(vulkan10, VkPhysicalDeviceFeatures, inheritedQueries) \ + X(vulkan11, VkPhysicalDeviceVulkan11Features, storageBuffer16BitAccess) \ + X(vulkan11, VkPhysicalDeviceVulkan11Features, uniformAndStorageBuffer16BitAccess) \ + X(vulkan11, VkPhysicalDeviceVulkan11Features, storagePushConstant16) \ + X(vulkan11, VkPhysicalDeviceVulkan11Features, storageInputOutput16) \ + X(vulkan11, VkPhysicalDeviceVulkan11Features, multiview) \ + X(vulkan11, VkPhysicalDeviceVulkan11Features, multiviewGeometryShader) \ + X(vulkan11, VkPhysicalDeviceVulkan11Features, multiviewTessellationShader) \ + X(vulkan11, VkPhysicalDeviceVulkan11Features, variablePointersStorageBuffer) \ + X(vulkan11, VkPhysicalDeviceVulkan11Features, variablePointers) \ + X(vulkan11, VkPhysicalDeviceVulkan11Features, protectedMemory) \ + X(vulkan11, VkPhysicalDeviceVulkan11Features, samplerYcbcrConversion) \ + X(vulkan11, VkPhysicalDeviceVulkan11Features, shaderDrawParameters) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, samplerMirrorClampToEdge) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, drawIndirectCount) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, storageBuffer8BitAccess) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, uniformAndStorageBuffer8BitAccess) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, storagePushConstant8) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, shaderBufferInt64Atomics) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, shaderSharedInt64Atomics) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, shaderFloat16) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, shaderInt8) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, descriptorIndexing) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, shaderInputAttachmentArrayDynamicIndexing) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, shaderUniformTexelBufferArrayDynamicIndexing) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, shaderStorageTexelBufferArrayDynamicIndexing) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, shaderUniformBufferArrayNonUniformIndexing) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, shaderSampledImageArrayNonUniformIndexing) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, shaderStorageBufferArrayNonUniformIndexing) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, shaderStorageImageArrayNonUniformIndexing) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, shaderInputAttachmentArrayNonUniformIndexing) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, shaderUniformTexelBufferArrayNonUniformIndexing) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, shaderStorageTexelBufferArrayNonUniformIndexing) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, descriptorBindingUniformBufferUpdateAfterBind) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, descriptorBindingSampledImageUpdateAfterBind) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, descriptorBindingStorageImageUpdateAfterBind) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, descriptorBindingStorageBufferUpdateAfterBind) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, descriptorBindingUniformTexelBufferUpdateAfterBind) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, descriptorBindingStorageTexelBufferUpdateAfterBind) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, descriptorBindingUpdateUnusedWhilePending) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, descriptorBindingPartiallyBound) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, descriptorBindingVariableDescriptorCount) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, runtimeDescriptorArray) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, samplerFilterMinmax) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, scalarBlockLayout) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, imagelessFramebuffer) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, uniformBufferStandardLayout) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, shaderSubgroupExtendedTypes) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, separateDepthStencilLayouts) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, hostQueryReset) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, timelineSemaphore) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, bufferDeviceAddress) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, bufferDeviceAddressCaptureReplay) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, bufferDeviceAddressMultiDevice) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, vulkanMemoryModel) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, vulkanMemoryModelDeviceScope) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, vulkanMemoryModelAvailabilityVisibilityChains) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, shaderOutputViewportIndex) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, shaderOutputLayer) \ + X(vulkan12, VkPhysicalDeviceVulkan12Features, subgroupBroadcastDynamicId) \ + X(vulkan13, VkPhysicalDeviceVulkan13Features, robustImageAccess) \ + X(vulkan13, VkPhysicalDeviceVulkan13Features, inlineUniformBlock) \ + X(vulkan13, VkPhysicalDeviceVulkan13Features, descriptorBindingInlineUniformBlockUpdateAfterBind) \ + X(vulkan13, VkPhysicalDeviceVulkan13Features, pipelineCreationCacheControl) \ + X(vulkan13, VkPhysicalDeviceVulkan13Features, privateData) \ + X(vulkan13, VkPhysicalDeviceVulkan13Features, shaderDemoteToHelperInvocation) \ + X(vulkan13, VkPhysicalDeviceVulkan13Features, shaderTerminateInvocation) \ + X(vulkan13, VkPhysicalDeviceVulkan13Features, subgroupSizeControl) \ + X(vulkan13, VkPhysicalDeviceVulkan13Features, computeFullSubgroups) \ + X(vulkan13, VkPhysicalDeviceVulkan13Features, synchronization2) \ + X(vulkan13, VkPhysicalDeviceVulkan13Features, textureCompressionASTC_HDR) \ + X(vulkan13, VkPhysicalDeviceVulkan13Features, shaderZeroInitializeWorkgroupMemory) \ + X(vulkan13, VkPhysicalDeviceVulkan13Features, dynamicRendering) \ + X(vulkan13, VkPhysicalDeviceVulkan13Features, shaderIntegerDotProduct) \ + X(vulkan13, VkPhysicalDeviceVulkan13Features, maintenance4) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, globalPriorityQuery) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, shaderSubgroupRotate) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, shaderSubgroupRotateClustered) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, shaderFloatControls2) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, shaderExpectAssume) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, rectangularLines) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, bresenhamLines) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, smoothLines) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, stippledRectangularLines) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, stippledBresenhamLines) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, stippledSmoothLines) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, vertexAttributeInstanceRateDivisor) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, vertexAttributeInstanceRateZeroDivisor) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, indexTypeUint8) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, dynamicRenderingLocalRead) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, maintenance5) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, maintenance6) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, pipelineProtectedAccess) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, pipelineRobustness) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, hostImageCopy) \ + X(vulkan14, VkPhysicalDeviceVulkan14Features, pushDescriptor) + + #define CRAFTER_FIELD(version, type, field) CRAFTER_FIELD_OF(version, type, field), + constexpr FeatureField featureFields[] = { CRAFTER_DEVICE_FEATURES(CRAFTER_FIELD) }; + #undef CRAFTER_FIELD + #undef CRAFTER_DEVICE_FEATURES + + // featureFields[begin, begin + count) are all of S's VkBool32 members, in + // order: one after another from S's first, nothing but padding after the + // last. With the counts below, a member missing from the list fails here. + template + consteval bool Covers(std::size_t begin, std::size_t count, std::size_t structOffset) { + std::size_t header = 0; // VkPhysicalDeviceFeatures starts with a VkBool32 + if constexpr (!std::is_same_v) { + header = offsetof(S, pNext) + sizeof(void*); + } + for (std::size_t i = 0; i < count; ++i) { + if (featureFields[begin + i].offset != structOffset + header + i * sizeof(VkBool32)) { + return false; + } + } + return sizeof(S) - (header + count * sizeof(VkBool32)) < alignof(S); + } + static_assert(std::size(featureFields) == 55 + 12 + 47 + 15 + 21); + static_assert(Covers(0, 55, offsetof(DeviceFeatures, vulkan10))); + static_assert(Covers(55, 12, offsetof(DeviceFeatures, vulkan11))); + static_assert(Covers(67, 47, offsetof(DeviceFeatures, vulkan12))); + static_assert(Covers(114, 15, offsetof(DeviceFeatures, vulkan13))); + static_assert(Covers(129, 21, offsetof(DeviceFeatures, vulkan14))); + + bool Get(const DeviceFeatures& features, const FeatureField& field) { + VkBool32 value; + std::memcpy(&value, reinterpret_cast(&features) + field.offset, sizeof(value)); + return value != VK_FALSE; + } + + void Enable(DeviceFeatures& features, const FeatureField& field) { + const VkBool32 value = VK_TRUE; + std::memcpy(reinterpret_cast(&features) + field.offset, &value, sizeof(value)); + } + + // A SPIR-V capability that Vulkan gates by a core feature, a subgroup + // operation, or an extension of one of the library's optional groups, + // from vk.xml's : the ones with no other way to be + // enabled. One that a core version enables isn't listed, nor is one only + // an extension the library never enables can: those are left to the + // driver and the validation layer. + struct CapabilityRule { + std::uint32_t capability; + std::string_view name; + // Any of these enables it. An empty name ends the list. + std::array features; + // Nonzero: enabled instead by this bit of subgroupSupportedOperations. + VkSubgroupFeatureFlags subgroupOperation; + // Non-null: enabled instead by the optional group this Device flag + // says is on. + const bool* group; + }; + + // Generated once from vk.xml, with the values from SPIRV-Headers' + // spirv.core.grammar.json. + constexpr CapabilityRule capabilityRules[] = { + { 2, "Geometry", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, geometryShader) }, 0, nullptr }, + { 3, "Tessellation", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, tessellationShader) }, 0, nullptr }, + { 9, "Float16", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, shaderFloat16) }, 0, nullptr }, + { 10, "Float64", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, shaderFloat64) }, 0, nullptr }, + { 11, "Int64", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, shaderInt64) }, 0, nullptr }, + { 12, "Int64Atomics", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, shaderBufferInt64Atomics), CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, shaderSharedInt64Atomics) }, 0, nullptr }, + { 22, "Int16", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, shaderInt16) }, 0, nullptr }, + { 23, "TessellationPointSize", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, shaderTessellationAndGeometryPointSize) }, 0, nullptr }, + { 24, "GeometryPointSize", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, shaderTessellationAndGeometryPointSize) }, 0, nullptr }, + { 25, "ImageGatherExtended", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, shaderImageGatherExtended) }, 0, nullptr }, + { 27, "StorageImageMultisample", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, shaderStorageImageMultisample) }, 0, nullptr }, + { 28, "UniformBufferArrayDynamicIndexing", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, shaderUniformBufferArrayDynamicIndexing) }, 0, nullptr }, + { 29, "SampledImageArrayDynamicIndexing", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, shaderSampledImageArrayDynamicIndexing) }, 0, nullptr }, + { 30, "StorageBufferArrayDynamicIndexing", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, shaderStorageBufferArrayDynamicIndexing) }, 0, nullptr }, + { 31, "StorageImageArrayDynamicIndexing", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, shaderStorageImageArrayDynamicIndexing) }, 0, nullptr }, + { 32, "ClipDistance", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, shaderClipDistance) }, 0, nullptr }, + { 33, "CullDistance", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, shaderCullDistance) }, 0, nullptr }, + { 34, "ImageCubeArray", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, imageCubeArray) }, 0, nullptr }, + { 35, "SampleRateShading", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, sampleRateShading) }, 0, nullptr }, + { 39, "Int8", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, shaderInt8) }, 0, nullptr }, + { 41, "SparseResidency", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, shaderResourceResidency) }, 0, nullptr }, + { 42, "MinLod", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, shaderResourceMinLod) }, 0, nullptr }, + { 45, "SampledCubeArray", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, imageCubeArray) }, 0, nullptr }, + { 48, "ImageMSArray", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, shaderStorageImageMultisample) }, 0, nullptr }, + { 52, "InterpolationFunction", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, sampleRateShading) }, 0, nullptr }, + { 57, "MultiViewport", { CRAFTER_FIELD_OF(vulkan10, VkPhysicalDeviceFeatures, multiViewport) }, 0, nullptr }, + { 61, "GroupNonUniform", { }, VK_SUBGROUP_FEATURE_BASIC_BIT, nullptr }, + { 62, "GroupNonUniformVote", { }, VK_SUBGROUP_FEATURE_VOTE_BIT, nullptr }, + { 63, "GroupNonUniformArithmetic", { }, VK_SUBGROUP_FEATURE_ARITHMETIC_BIT, nullptr }, + { 64, "GroupNonUniformBallot", { }, VK_SUBGROUP_FEATURE_BALLOT_BIT, nullptr }, + { 65, "GroupNonUniformShuffle", { }, VK_SUBGROUP_FEATURE_SHUFFLE_BIT, nullptr }, + { 66, "GroupNonUniformShuffleRelative", { }, VK_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT, nullptr }, + { 67, "GroupNonUniformClustered", { }, VK_SUBGROUP_FEATURE_CLUSTERED_BIT, nullptr }, + { 68, "GroupNonUniformQuad", { }, VK_SUBGROUP_FEATURE_QUAD_BIT, nullptr }, + { 69, "ShaderLayer", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, shaderOutputLayer) }, 0, nullptr }, + { 70, "ShaderViewportIndex", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, shaderOutputViewportIndex) }, 0, nullptr }, + { 4427, "DrawParameters", { CRAFTER_FIELD_OF(vulkan11, VkPhysicalDeviceVulkan11Features, shaderDrawParameters) }, 0, nullptr }, + { 4433, "StorageBuffer16BitAccess", { CRAFTER_FIELD_OF(vulkan11, VkPhysicalDeviceVulkan11Features, storageBuffer16BitAccess) }, 0, nullptr }, + { 4434, "UniformAndStorageBuffer16BitAccess", { CRAFTER_FIELD_OF(vulkan11, VkPhysicalDeviceVulkan11Features, uniformAndStorageBuffer16BitAccess) }, 0, nullptr }, + { 4435, "StoragePushConstant16", { CRAFTER_FIELD_OF(vulkan11, VkPhysicalDeviceVulkan11Features, storagePushConstant16) }, 0, nullptr }, + { 4436, "StorageInputOutput16", { CRAFTER_FIELD_OF(vulkan11, VkPhysicalDeviceVulkan11Features, storageInputOutput16) }, 0, nullptr }, + { 4439, "MultiView", { CRAFTER_FIELD_OF(vulkan11, VkPhysicalDeviceVulkan11Features, multiview) }, 0, nullptr }, + { 4441, "VariablePointersStorageBuffer", { CRAFTER_FIELD_OF(vulkan11, VkPhysicalDeviceVulkan11Features, variablePointersStorageBuffer) }, 0, nullptr }, + { 4442, "VariablePointers", { CRAFTER_FIELD_OF(vulkan11, VkPhysicalDeviceVulkan11Features, variablePointers) }, 0, nullptr }, + { 4448, "StorageBuffer8BitAccess", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, storageBuffer8BitAccess) }, 0, nullptr }, + { 4449, "UniformAndStorageBuffer8BitAccess", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, uniformAndStorageBuffer8BitAccess) }, 0, nullptr }, + { 4450, "StoragePushConstant8", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, storagePushConstant8) }, 0, nullptr }, + { 4472, "RayQueryKHR", { }, 0, &Device::rayTracingSupported }, + { 4473, "UntypedPointersKHR", { }, 0, &Device::descriptorHeapSupported }, + { 4478, "RayTraversalPrimitiveCullingKHR", { }, 0, &Device::rayTracingSupported }, + { 4479, "RayTracingKHR", { }, 0, &Device::rayTracingSupported }, + { 5128, "DescriptorHeapEXT", { }, 0, &Device::descriptorHeapSupported }, + { 5302, "RuntimeDescriptorArray", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, runtimeDescriptorArray) }, 0, nullptr }, + { 5303, "InputAttachmentArrayDynamicIndexing", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, shaderInputAttachmentArrayDynamicIndexing) }, 0, nullptr }, + { 5304, "UniformTexelBufferArrayDynamicIndexing", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, shaderUniformTexelBufferArrayDynamicIndexing) }, 0, nullptr }, + { 5305, "StorageTexelBufferArrayDynamicIndexing", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, shaderStorageTexelBufferArrayDynamicIndexing) }, 0, nullptr }, + { 5306, "UniformBufferArrayNonUniformIndexing", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, shaderUniformBufferArrayNonUniformIndexing) }, 0, nullptr }, + { 5307, "SampledImageArrayNonUniformIndexing", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, shaderSampledImageArrayNonUniformIndexing) }, 0, nullptr }, + { 5308, "StorageBufferArrayNonUniformIndexing", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, shaderStorageBufferArrayNonUniformIndexing) }, 0, nullptr }, + { 5309, "StorageImageArrayNonUniformIndexing", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, shaderStorageImageArrayNonUniformIndexing) }, 0, nullptr }, + { 5310, "InputAttachmentArrayNonUniformIndexing", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, shaderInputAttachmentArrayNonUniformIndexing) }, 0, nullptr }, + { 5311, "UniformTexelBufferArrayNonUniformIndexing", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, shaderUniformTexelBufferArrayNonUniformIndexing) }, 0, nullptr }, + { 5312, "StorageTexelBufferArrayNonUniformIndexing", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, shaderStorageTexelBufferArrayNonUniformIndexing) }, 0, nullptr }, + { 5345, "VulkanMemoryModel", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, vulkanMemoryModel) }, 0, nullptr }, + { 5346, "VulkanMemoryModelDeviceScope", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, vulkanMemoryModelDeviceScope) }, 0, nullptr }, + { 5347, "PhysicalStorageBufferAddresses", { CRAFTER_FIELD_OF(vulkan12, VkPhysicalDeviceVulkan12Features, bufferDeviceAddress) }, 0, nullptr }, + { 5379, "DemoteToHelperInvocation", { CRAFTER_FIELD_OF(vulkan13, VkPhysicalDeviceVulkan13Features, shaderDemoteToHelperInvocation) }, 0, nullptr }, + { 5629, "ExpectAssumeKHR", { CRAFTER_FIELD_OF(vulkan14, VkPhysicalDeviceVulkan14Features, shaderExpectAssume) }, 0, nullptr }, + { 6016, "DotProductInputAll", { CRAFTER_FIELD_OF(vulkan13, VkPhysicalDeviceVulkan13Features, shaderIntegerDotProduct) }, 0, nullptr }, + { 6017, "DotProductInput4x8Bit", { CRAFTER_FIELD_OF(vulkan13, VkPhysicalDeviceVulkan13Features, shaderIntegerDotProduct) }, 0, nullptr }, + { 6018, "DotProductInput4x8BitPacked", { CRAFTER_FIELD_OF(vulkan13, VkPhysicalDeviceVulkan13Features, shaderIntegerDotProduct) }, 0, nullptr }, + { 6019, "DotProduct", { CRAFTER_FIELD_OF(vulkan13, VkPhysicalDeviceVulkan13Features, shaderIntegerDotProduct) }, 0, nullptr }, + { 6026, "GroupNonUniformRotateKHR", { CRAFTER_FIELD_OF(vulkan14, VkPhysicalDeviceVulkan14Features, shaderSubgroupRotate) }, 0, nullptr }, + { 6029, "FloatControls2", { CRAFTER_FIELD_OF(vulkan14, VkPhysicalDeviceVulkan14Features, shaderFloatControls2) }, 0, nullptr }, + }; + #undef CRAFTER_FIELD_OF + + // The listed capabilities that declaring one implicitly declares, as + // SPIR-V's grammar has it ("Int64Atomics" depends on "Int64"): a module + // may declare Int64Atomics alone and still use 64-bit integers. Generated + // with capabilityRules, keeping only dependencies that have a rule. + struct ImpliedCapabilities { + std::uint32_t capability; + std::string_view name; + std::uint32_t count; + std::array implied; + }; + constexpr ImpliedCapabilities impliedCapabilities[] = { + { 12, "Int64Atomics", 1, { 11 } }, // Int64 + { 23, "TessellationPointSize", 1, { 3 } }, // Tessellation + { 24, "GeometryPointSize", 1, { 2 } }, // Geometry + { 34, "ImageCubeArray", 1, { 45 } }, // SampledCubeArray + { 54, "GeometryStreams", 1, { 2 } }, // Geometry + { 57, "MultiViewport", 1, { 2 } }, // Geometry + { 62, "GroupNonUniformVote", 1, { 61 } }, // GroupNonUniform + { 63, "GroupNonUniformArithmetic", 1, { 61 } }, // GroupNonUniform + { 64, "GroupNonUniformBallot", 1, { 61 } }, // GroupNonUniform + { 65, "GroupNonUniformShuffle", 1, { 61 } }, // GroupNonUniform + { 66, "GroupNonUniformShuffleRelative", 1, { 61 } }, // GroupNonUniform + { 67, "GroupNonUniformClustered", 1, { 61 } }, // GroupNonUniform + { 68, "GroupNonUniformQuad", 1, { 61 } }, // GroupNonUniform + { 4434, "UniformAndStorageBuffer16BitAccess", 1, { 4433 } }, // StorageBuffer16BitAccess + { 4442, "VariablePointers", 1, { 4441 } }, // VariablePointersStorageBuffer + { 4449, "UniformAndStorageBuffer8BitAccess", 1, { 4448 } }, // StorageBuffer8BitAccess + { 4478, "RayTraversalPrimitiveCullingKHR", 2, { 4472, 4479 } }, // RayQueryKHR, RayTracingKHR + { 5016, "Int64ImageEXT", 1, { 11 } }, // Int64 + { 5128, "DescriptorHeapEXT", 1, { 4473 } }, // UntypedPointersKHR + { 5249, "SampleMaskOverrideCoverageNV", 1, { 35 } }, // SampleRateShading + { 5251, "GeometryShaderPassthroughNV", 1, { 2 } }, // Geometry + { 5254, "ShaderViewportIndexLayerEXT", 2, { 2, 57 } }, // Geometry, MultiViewport + { 5255, "ShaderViewportMaskNV", 2, { 2, 57 } }, // Geometry, MultiViewport + { 5259, "ShaderStereoViewNV", 2, { 2, 57 } }, // Geometry, MultiViewport + { 5260, "PerViewAttributesNV", 1, { 4439 } }, // MultiView + { 5383, "ShaderInvocationReorderNV", 1, { 4479 } }, // RayTracingKHR + { 5388, "ShaderInvocationReorderEXT", 1, { 4479 } }, // RayTracingKHR + { 5409, "RayTracingDisplacementMicromapNV", 1, { 4479 } }, // RayTracingKHR + { 5437, "RayTracingClusterAccelerationStructureNV", 1, { 4479 } }, // RayTracingKHR + { 6017, "DotProductInput4x8Bit", 1, { 39 } }, // Int8 + { 6026, "GroupNonUniformRotateKHR", 1, { 61 } }, // GroupNonUniform + { 6243, "UntypedVariableLengthArrayINTEL", 1, { 4473 } }, // UntypedPointersKHR + { 6912, "DotProductFloat16AccFloat32VALVE", 1, { 9 } }, // Float16 + { 6913, "DotProductFloat16AccFloat16VALVE", 1, { 9 } }, // Float16 + }; + + std::string_view GroupName(const bool* group) { + return group == &Device::rayTracingSupported ? "ray tracing (Device::rayTracingSupported)" + : group == &Device::descriptorHeapSupported ? "the descriptor heap (Device::descriptorHeapSupported)" + : group == &Device::deviceFaultSupported ? "device fault reports (Device::deviceFaultSupported)" + : "GPU memory decompression (Device::memoryDecompressionSupported)"; + } + + // Device types by preference. Zero: never chosen. + int Rank(VkPhysicalDeviceType type) { + switch (type) { + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 5; + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 4; + case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 3; + case VK_PHYSICAL_DEVICE_TYPE_CPU: return 2; + case VK_PHYSICAL_DEVICE_TYPE_OTHER: return 1; + default: return 0; + } + } +} + +DeviceFeatures Device::ResolveFeatures(const DeviceFeatures& library, const DeviceFeatures& required, + const DeviceFeatures& optional, const DeviceFeatures& supported, + std::uint32_t apiVersion, FeatureGaps& gaps) { + DeviceFeatures enabled; + for (const FeatureField& field : featureFields) { + const bool byLibrary = Get(library, field); + const bool needed = byLibrary || Get(required, field); + if (Get(supported, field)) { + if (needed || Get(optional, field)) { + Enable(enabled, field); + } + } else if (needed) { + std::string gap(field.name); + if (apiVersion < VK_API_VERSION_1_4 && field.name == "vulkan14.maintenance5") { + gap += " (VK_KHR_maintenance5 reports it unsupported)"; + } else if (apiVersion < VK_API_VERSION_1_4 && field.name.starts_with("vulkan14.")) { + gap += " (Vulkan 1.4 only)"; + } + (byLibrary ? gaps.library : gaps.application).push_back(std::move(gap)); + } + } + return enabled; +} + +std::size_t Device::ChooseDevice(std::span choices) { + // A CPU device (llvmpipe) runs the application only when nothing else is + // listed: a GPU that lacks something is a reason to say so, not to fall + // back to rendering in software. + const bool gpuListed = std::ranges::any_of(choices, [](const DeviceChoice& choice) { + return Rank(choice.type) > 0 && choice.type != VK_PHYSICAL_DEVICE_TYPE_CPU; + }); + std::optional best; + for (std::size_t i = 0; i < choices.size(); ++i) { + const DeviceChoice& choice = choices[i]; + if (!choice.gaps.empty() || Rank(choice.type) == 0 + || (gpuListed && choice.type == VK_PHYSICAL_DEVICE_TYPE_CPU)) { + continue; + } + if (!best || Rank(choice.type) > Rank(choices[*best].type)) { + best = i; + } + } + + if (!best) { + std::string message = "Crafter.Graphics: no GPU can run this application"; + if (choices.empty()) { + message += ": Vulkan lists none"; + } + for (const DeviceChoice& choice : choices) { + std::string why = !choice.gaps.empty() ? DeviceDetail::Join(choice.gaps, "; ") + : choice.type == VK_PHYSICAL_DEVICE_TYPE_CPU + ? "a CPU device, used only when no GPU is listed (pick it with CRAFTER_GRAPHICS_DEVICE)" + : "a device of unknown type"; + message += std::format("\n {}: {}", choice.name, why); + } + throw std::runtime_error(message); + } + + // Say why each GPU that would have come first was passed over. + for (std::size_t i = 0; i < choices.size(); ++i) { + const DeviceChoice& choice = choices[i]; + const int rank = Rank(choice.type); + const int bestRank = Rank(choices[*best].type); + if (!choice.gaps.empty() && (rank > bestRank || (rank == bestRank && i < *best))) { + std::println(stderr, "Crafter.Graphics: skipping {}: {}", choice.name, DeviceDetail::Join(choice.gaps, "; ")); + } + } + return *best; +} + +void Device::CheckShaderCapabilities(std::span spirv, const std::filesystem::path& file) { + constexpr std::uint32_t opCapability = 17; + constexpr std::uint32_t capabilityInt64 = 11; + + // Throws when `capability` has a rule nothing satisfies. `declaredBy` is + // the capability the module named, when it implies this one. + const auto check = [&](std::uint32_t capability, const ImpliedCapabilities* declaredBy) { + const CapabilityRule* rule = std::ranges::find(capabilityRules, capability, &CapabilityRule::capability); + if (rule == std::ranges::end(capabilityRules)) { + return; + } + const std::string what = declaredBy != nullptr + ? std::format("{} declares SPIR-V capability {}, which implicitly declares {}", file.string(), declaredBy->name, rule->name) + : std::format("{} declares SPIR-V capability {}", file.string(), rule->name); + + if (rule->subgroupOperation != 0) { + if ((vulkan11Properties.subgroupSupportedOperations & rule->subgroupOperation) == 0) { + throw std::runtime_error(std::format("{}, but this GPU has no {}", what, + string_VkSubgroupFeatureFlagBits(static_cast(rule->subgroupOperation)))); + } + return; + } + if (rule->group != nullptr) { + if (!*rule->group) { + throw std::runtime_error(std::format("{}, which needs {}, off on this GPU", what, GroupName(rule->group))); + } + return; + } + + bool enabled = false; + bool supported = false; + bool onlyVulkan14 = true; + std::string needs; + for (const FeatureField& field : rule->features) { + if (field.name.empty()) { + break; + } + enabled = enabled || Get(enabledFeatures, field); + supported = supported || Get(supportedFeatures, field); + onlyVulkan14 = onlyVulkan14 && field.name.starts_with("vulkan14."); + needs += needs.empty() ? std::string(field.name) : std::format(" or {}", field.name); + } + // The NVIDIA workaround's own shaderInt64 doesn't count: the shader + // would run on NVIDIA and nowhere else. + if (capability == capabilityInt64 && shaderInt64ForWorkaroundOnly) { + throw std::runtime_error(std::format( + "{}, which needs {}: pass it to Device::Initialize (the NVIDIA workaround enables it for itself only)", + what, needs)); + } + if (enabled) { + return; + } + if (supported) { + throw std::runtime_error(std::format("{}, which needs {}: pass it to Device::Initialize", what, needs)); + } + if (onlyVulkan14 && apiVersion < VK_API_VERSION_1_4) { + throw std::runtime_error(std::format("{}, which needs {}, a Vulkan 1.4 feature, and this GPU runs Vulkan {}.{}", + what, needs, VK_API_VERSION_MAJOR(apiVersion), VK_API_VERSION_MINOR(apiVersion))); + } + throw std::runtime_error(std::format("{}, which needs {}, and this GPU lacks it", what, needs)); + }; + + // A module's OpCapability instructions come first, right after its + // five-word header (the logical layout in SPIR-V's section 2.4). + for (std::size_t i = 5; i < spirv.size();) { + const std::uint32_t length = spirv[i] >> 16; + if ((spirv[i] & 0xFFFFu) != opCapability || length < 2 || length > spirv.size() - i) { + break; + } + const std::uint32_t capability = spirv[i + 1]; + i += length; + + check(capability, nullptr); + const ImpliedCapabilities* implied = std::ranges::find(impliedCapabilities, capability, &ImpliedCapabilities::capability); + if (implied != std::ranges::end(impliedCapabilities)) { + for (std::uint32_t k = 0; k < implied->count; ++k) { + check(implied->implied[k], implied); + } + } + } +} diff --git a/implementations/Crafter.Graphics-Device.cpp b/implementations/Crafter.Graphics-Device.cpp index 3e6606e..0142585 100644 --- a/implementations/Crafter.Graphics-Device.cpp +++ b/implementations/Crafter.Graphics-Device.cpp @@ -43,16 +43,17 @@ const char* const instanceExtensionNames[] = { #endif }; // Device extensions are split by what breaks without them. Only the first -// group is needed to put UI pixels on screen; Device::Initialize enables each -// of the others only when the driver advertises all of its members, and -// records the outcome in Device::descriptorHeapSupported / rayTracingSupported -// / deviceFaultSupported. Mesa Turnip (Adreno), for one, has none of the heap, -// ray tracing or fault extensions. +// group is needed to put UI pixels on screen, and a GPU without it is skipped, +// as is one below Vulkan 1.4 without VK_KHR_maintenance5 (core from 1.4). +// VK_KHR_spirv_1_4 and VK_KHR_shader_float_controls, which ray tracing needs +// on Vulkan 1.1, are core since 1.2. Device::Initialize enables each of the +// other groups only when the driver advertises all of its members and their +// feature bits, and records the outcome in Device::descriptorHeapSupported / +// rayTracingSupported / deviceFaultSupported / memoryDecompressionSupported. +// Mesa Turnip (Adreno), for one, has none of the heap, ray tracing or fault +// extensions. const char* const requiredDeviceExtensionNames[] = { "VK_KHR_swapchain", - "VK_KHR_spirv_1_4", - "VK_KHR_shader_float_controls", - "VK_KHR_maintenance5", }; // Without these, descriptors come from a bindless descriptor set instead // (DescriptorHeapVulkan's sets mode) and shaders load their .sets.spv build. @@ -771,35 +772,350 @@ void Device::seat_handle_capabilities(void*, wl_seat* seat, uint32_t capabilitie #endif -void Device::Initialize() { - #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND - display = wl_display_connect(NULL); - if (display == nullptr) { - throw std::runtime_error("Could not connect to wayland display"); - } - - wl_registry* registry = wl_display_get_registry(display); - wl_registry_add_listener(registry, ®istry_listener, nullptr); +namespace { + // What Initialize learns about one GPU before choosing it. + struct Candidate { + VkPhysicalDevice device = VK_NULL_HANDLE; + VkPhysicalDeviceProperties properties {}; + // min(the instance's apiVersion, the GPU's): the version the + // application may use on it. + std::uint32_t apiVersion = 0; + // "NVIDIA GeForce RTX 4090 (NVIDIA, Vulkan 1.4)", as messages name it. + // The driver tells apart one GPU that two drivers both list. + std::string label; + VkPhysicalDeviceVulkan11Properties vulkan11 { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_PROPERTIES }; + // The capability groups: heap, rayTracing, fault and decompression + // become descriptorHeapSupported, rayTracingSupported, + // deviceFaultSupported and memoryDecompressionSupported, and + // workaroundAS workaroundDescriptorHeapAS. + bool heap = false; + bool rayTracing = false; + bool fault = false; + bool faultVendorBinary = false; + bool decompression = false; + bool workaroundAS = false; + // The extensions every device needs, maintenance5 among them below + // Vulkan 1.4. Initialize adds the groups'. + std::vector requiredExtensions; + DeviceFeatures supported; + DeviceFeatures enabled; + // Why it can't run the application. Empty when it can. + std::vector gaps; + }; - if (wl_display_roundtrip(display) == -1) { - exit(EXIT_FAILURE); - } + Candidate EvaluateDevice(VkPhysicalDevice physical, std::uint32_t instanceVersion, + const DeviceFeatures& required, const DeviceFeatures& optional) { + Candidate candidate { .device = physical }; + vkGetPhysicalDeviceProperties(physical, &candidate.properties); + candidate.apiVersion = std::min(instanceVersion, candidate.properties.apiVersion); + const std::uint32_t major = VK_API_VERSION_MAJOR(candidate.apiVersion); + const std::uint32_t minor = VK_API_VERSION_MINOR(candidate.apiVersion); + // Below 1.3 there is nothing more to ask: a core feature struct is not + // legal in a query below its version. + if (candidate.apiVersion < VK_API_VERSION_1_3) { + candidate.label = std::format("{} (Vulkan {}.{})", candidate.properties.deviceName, major, minor); + candidate.gaps.push_back("Crafter.Graphics needs Vulkan 1.3"); + return candidate; + } + const bool vulkan14 = candidate.apiVersion >= VK_API_VERSION_1_4; - if (shm == NULL || compositor == NULL || xdgWmBase == NULL) { - throw std::runtime_error("No wl_shm, wl_compositor or xdg_wm_base support"); - } - // After the registry roundtrip the data_device (if the compositor - // exposes one) is bound. Clipboard::Initialize attaches the - // selection listener that Clipboard::GetText reads from; doing it - // before the first wl_data_device.selection arrives is what lets - // GetText work the instant a frame is rendered. - Clipboard::Initialize(); - #endif + // The driver identifies NVIDIA's, for the descriptor-heap AS-read + // workaround; Vulkan 1.1's properties hold the subgroup operations. + VkPhysicalDeviceDriverProperties driver { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES, + .pNext = &candidate.vulkan11, + }; + VkPhysicalDeviceProperties2 properties2 { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &driver }; + vkGetPhysicalDeviceProperties2(physical, &properties2); + candidate.label = std::format("{} ({}, Vulkan {}.{})", candidate.properties.deviceName, driver.driverName, major, minor); + // Enumerate the driver's device extensions once; every optional + // capability below is decided from this set. Drivers without + // VK_EXT_memory_decompression (AMD, Intel as of early 2026) get the + // CPU-decode fallback; drivers without the descriptor heap (Mesa + // Turnip) get the descriptor-set path and no ray tracing. + std::unordered_set availableExtensions; + { + std::uint32_t extCount = 0; + vkEnumerateDeviceExtensionProperties(physical, nullptr, &extCount, nullptr); + std::vector exts(extCount); + vkEnumerateDeviceExtensionProperties(physical, nullptr, &extCount, exts.data()); + for (const VkExtensionProperties& e : exts) { + availableExtensions.emplace(e.extensionName); + } + } + const auto hasAll = [&availableExtensions](std::span names) { + return std::ranges::all_of(names, [&](const char* name) { return availableExtensions.contains(name); }); + }; + + // Name the gap here rather than fail in vkCreateDevice with a bare + // VK_ERROR_EXTENSION_NOT_PRESENT. maintenance5 is core from 1.4. + candidate.requiredExtensions.assign(std::begin(requiredDeviceExtensionNames), std::end(requiredDeviceExtensionNames)); + if (!vulkan14) { + candidate.requiredExtensions.push_back(VK_KHR_MAINTENANCE_5_EXTENSION_NAME); + } + std::vector missing; + for (const char* name : candidate.requiredExtensions) { + if (!availableExtensions.contains(name)) { + missing.emplace_back(name); + } + } + if (!missing.empty()) { + candidate.gaps.push_back(std::format("lacks {}", DeviceDetail::Join(missing, ", "))); + return candidate; + } + + // One features query: the core structs for what the library and the + // application need, and the feature bits of the optional groups whose + // extensions are present. A listed extension is not the whole answer: + // a driver may list one whose feature bit is still off, and such a + // driver takes the fallback instead of failing in vkCreateDevice. + const bool heapExtensions = hasAll(descriptorHeapExtensionNames); + const bool rayTracingExtensions = hasAll(rayTracingExtensionNames); + const bool faultExtension = availableExtensions.contains(VK_EXT_DEVICE_FAULT_EXTENSION_NAME); + const bool decompressionExtension = availableExtensions.contains(VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME); + DeviceFeatures& supported = candidate.supported; + VkPhysicalDeviceMaintenance5Features maintenance5Query { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES, + }; + VkPhysicalDeviceDescriptorHeapFeaturesEXT heapQuery { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_HEAP_FEATURES_EXT, + }; + VkPhysicalDeviceShaderUntypedPointersFeaturesKHR untypedPointersQuery { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_UNTYPED_POINTERS_FEATURES_KHR, + }; + VkPhysicalDeviceAccelerationStructureFeaturesKHR accelerationStructureQuery { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_FEATURES_KHR, + }; + VkPhysicalDeviceRayTracingPipelineFeaturesKHR rayTracingPipelineQuery { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_PIPELINE_FEATURES_KHR, + }; + VkPhysicalDeviceRayQueryFeaturesKHR rayQueryQuery { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_QUERY_FEATURES_KHR, + }; + VkPhysicalDeviceFaultFeaturesEXT faultQuery { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT, + }; + VkPhysicalDeviceMemoryDecompressionFeaturesEXT decompressionQuery { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_DECOMPRESSION_FEATURES_EXT, + }; + VkPhysicalDeviceFeatures2 featureQuery { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, + }; + const auto ask = [&featureQuery](auto& features) { + features.pNext = featureQuery.pNext; + featureQuery.pNext = &features; + }; + ask(supported.vulkan11); + ask(supported.vulkan12); + ask(supported.vulkan13); + if (vulkan14) { + ask(supported.vulkan14); + } else { + ask(maintenance5Query); + } + if (heapExtensions) { + ask(heapQuery); + ask(untypedPointersQuery); + } + if (rayTracingExtensions) { + ask(accelerationStructureQuery); + ask(rayTracingPipelineQuery); + ask(rayQueryQuery); + } + if (faultExtension) { + ask(faultQuery); + } + if (decompressionExtension) { + ask(decompressionQuery); + } + vkGetPhysicalDeviceFeatures2(physical, &featureQuery); + supported.vulkan10 = featureQuery.features; + // The chain pointed at this function's locals. + supported.vulkan11.pNext = supported.vulkan12.pNext = supported.vulkan13.pNext = supported.vulkan14.pNext = nullptr; + // Below 1.4 maintenance5 is VK_KHR_maintenance5's, and counts as the + // vulkan14 feature it became. + if (!vulkan14) { + supported.vulkan14.maintenance5 = maintenance5Query.maintenance5; + } + + // CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the descriptor-set path on + // a driver that does have the heap, so the fallback can be exercised on + // a desktop GPU. It takes ray tracing down with it, exactly as on a + // driver that really lacks the extension. + candidate.heap = heapExtensions + && heapQuery.descriptorHeap && untypedPointersQuery.shaderUntypedPointers + && std::getenv("CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP") == nullptr; + candidate.rayTracing = candidate.heap && rayTracingExtensions + && accelerationStructureQuery.accelerationStructure + && rayTracingPipelineQuery.rayTracingPipeline + && rayQueryQuery.rayQuery; + candidate.fault = faultExtension && faultQuery.deviceFault; + candidate.faultVendorBinary = faultQuery.deviceFaultVendorBinary; + candidate.decompression = decompressionExtension && decompressionQuery.memoryDecompression; + // NVIDIA's brand-new VK_EXT_descriptor_heap acceleration-structure read + // path faults (see #7); enable the SPIR-V rewrite workaround there. + // Other drivers (and any future fixed NVIDIA driver, once this gate is + // removed) take the normal heap-bound AS path unchanged. The workaround + // is about the heap, so it is off whenever the heap is: otherwise a + // forced-fallback run on NVIDIA would still rewrite SPIR-V, enable + // shaderInt64 and push the TLAS address through vkCmdPushDataEXT, none + // of which a real heap-less driver does. Still needed on 615.78.08. + // CRAFTER_GRAPHICS_NO_AS_WORKAROUND, set to any value like the other + // CRAFTER_GRAPHICS_* switches, turns it off, which is how a new driver + // gets checked (tests/HeapASRead --no-workaround). + candidate.workaroundAS = candidate.heap + && driver.driverID == VK_DRIVER_ID_NVIDIA_PROPRIETARY + && std::getenv("CRAFTER_GRAPHICS_NO_AS_WORKAROUND") == nullptr; + + // What the library's own shaders and code use on this GPU. Everything + // else is the application's to ask for: shaderInt16 for a uint16_t spec + // constant carrying a heap slot, scalarBlockLayout, fp16, ... + DeviceFeatures library; + // Enabled where the GPU has them, never a reason to skip it. + DeviceFeatures wanted = optional; + // SamplerVulkan turns anisotropic filtering on. + library.vulkan10.samplerAnisotropy = VK_TRUE; + // Every UI kernel reads and writes its storage images without a + // format. From Vulkan 1.3 those capabilities need no feature: each + // format says whether it allows it + // (VK_FORMAT_FEATURE_2_STORAGE_READ/WRITE_WITHOUT_FORMAT_BIT), and + // Window picks a swapchain format that does. The 1.0 bits, which only + // say every format does, are a bonus where the GPU has them. + wanted.vulkan10.shaderStorageImageReadWithoutFormat = VK_TRUE; + wanted.vulkan10.shaderStorageImageWriteWithoutFormat = VK_TRUE; + // Bindless indexing: `images[]`, `textures[]`, `samplers[]` and + // `itemHeap[]` are indexed with a runtime value, in both binding models. + library.vulkan10.shaderSampledImageArrayDynamicIndexing = VK_TRUE; + library.vulkan10.shaderStorageBufferArrayDynamicIndexing = VK_TRUE; + library.vulkan10.shaderStorageImageArrayDynamicIndexing = VK_TRUE; + library.vulkan12.shaderSampledImageArrayNonUniformIndexing = VK_TRUE; + library.vulkan12.runtimeDescriptorArray = VK_TRUE; + // shaderInt64 and scalarBlockLayout: only for the NVIDIA descriptor-heap + // AS-read workaround (issue #15 / #7), which loads the TLAS device + // address as a 64-bit push constant placed right after the shader's + // own push members, inside a struct member's std430 tail padding where + // there is any, which only the scalar layout allows. Remove with the + // workaround. + library.vulkan10.shaderInt64 = candidate.workaroundAS ? VK_TRUE : VK_FALSE; + library.vulkan12.scalarBlockLayout = candidate.workaroundAS ? VK_TRUE : VK_FALSE; + // VulkanBuffer addresses, the descriptor heap and ray tracing. + library.vulkan12.bufferDeviceAddress = VK_TRUE; + // The VkMemoryBarrier2 around vkCmdDecompressMemoryEXT. + library.vulkan13.synchronization2 = VK_TRUE; + // Every VulkanBuffer is created with usage = 0 plus a + // VkBufferUsageFlags2CreateInfo, and heap pipelines carry a + // VkPipelineCreateFlags2CreateInfo: maintenance5 functionality. + library.vulkan14.maintenance5 = VK_TRUE; + if (!candidate.heap) { + // Descriptor-set path (DescriptorHeapVulkan sets mode): one + // bindless set whose slots are written while earlier frames may + // still be executing with it bound, and where most slots are never + // written. The sampled-image bit also covers SAMPLER descriptors. + library.vulkan12.descriptorIndexing = VK_TRUE; + library.vulkan12.descriptorBindingSampledImageUpdateAfterBind = VK_TRUE; + library.vulkan12.descriptorBindingStorageImageUpdateAfterBind = VK_TRUE; + library.vulkan12.descriptorBindingStorageBufferUpdateAfterBind = VK_TRUE; + library.vulkan12.descriptorBindingUpdateUnusedWhilePending = VK_TRUE; + library.vulkan12.descriptorBindingPartiallyBound = VK_TRUE; + } + // The UI kernels' subgroup arithmetic is not asked for here: only an + // application with a UIRenderer runs them, and VulkanShader refuses + // them, naming the operation, on a GPU without it. + + FeatureGaps featureGaps; + candidate.enabled = Device::ResolveFeatures(library, required, wanted, supported, candidate.apiVersion, featureGaps); + if (!featureGaps.library.empty()) { + candidate.gaps.push_back(std::format("lacks {}, which Crafter.Graphics needs", DeviceDetail::Join(featureGaps.library, ", "))); + } + if (!featureGaps.application.empty()) { + candidate.gaps.push_back(std::format("lacks {}, which the application requires", DeviceDetail::Join(featureGaps.application, ", "))); + } + return candidate; + } + + // The GPU Initialize uses, or a throw naming why there is none. + Candidate ChooseGpu(VkInstance instance, std::uint32_t instanceVersion, + const DeviceFeatures& required, const DeviceFeatures& optional) { + std::uint32_t physDeviceCount = 0; + Device::CheckVkResult(vkEnumeratePhysicalDevices(instance, &physDeviceCount, nullptr)); + std::vector physDevices(physDeviceCount); + Device::CheckVkResult(vkEnumeratePhysicalDevices(instance, &physDeviceCount, physDevices.data())); + + // The handles, grouped by GPU: a GPU two drivers both list (a Flatpak + // sees the runtime's and the GL extension's manifests; RADV beside + // AMDVLK) has one device UUID and two handles. The numbers below count + // GPUs, so one means the same GPU inside a sandbox and out. + std::vector> gpus; + { + std::vector> seen; + for (VkPhysicalDevice device : physDevices) { + VkPhysicalDeviceIDProperties id { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES }; + VkPhysicalDeviceProperties2 properties { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &id }; + vkGetPhysicalDeviceProperties2(device, &properties); + std::array uuid; + std::ranges::copy(id.deviceUUID, uuid.begin()); + const auto at = std::ranges::find(seen, uuid); + if (at == seen.end()) { + seen.push_back(uuid); + gpus.push_back({ device }); + } else { + gpus[at - seen.begin()].push_back(device); + } + } + } + // A GPU's first handle that can run the application, else its first + // handle, gaps and all. + const auto evaluate = [&](const std::vector& handles) { + Candidate first = EvaluateDevice(handles.front(), instanceVersion, required, optional); + for (std::size_t i = 1; i < handles.size() && !first.gaps.empty(); ++i) { + Candidate other = EvaluateDevice(handles[i], instanceVersion, required, optional); + if (other.gaps.empty()) { + return other; + } + } + return first; + }; + + // CRAFTER_GRAPHICS_DEVICE picks a GPU by its number: with two of the + // same GPU nothing else tells them apart, and the first may be too busy + // to use. A GPU picked that way is never swapped for another; when it + // can't run the application, that is the error. Without it every GPU is + // weighed and ChooseDevice picks among those that can. + if (const char* chosenDevice = std::getenv("CRAFTER_GRAPHICS_DEVICE")) { + const std::string_view text = chosenDevice; + std::size_t index = 0; + const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), index); + if (error != std::errc() || end != text.data() + text.size() || index >= gpus.size()) { + throw std::runtime_error(gpus.empty() + ? std::format("CRAFTER_GRAPHICS_DEVICE={} isn't the number of a GPU: Vulkan lists none", chosenDevice) + : std::format("CRAFTER_GRAPHICS_DEVICE={} isn't the number of a GPU: Vulkan lists {} GPU(s), 0 to {}", + chosenDevice, gpus.size(), gpus.size() - 1)); + } + Candidate chosen = evaluate(gpus[index]); + if (!chosen.gaps.empty()) { + throw std::runtime_error(std::format("Crafter.Graphics: CRAFTER_GRAPHICS_DEVICE={} is {}, which can't run this application: {}", + chosenDevice, chosen.label, DeviceDetail::Join(chosen.gaps, "; "))); + } + return chosen; + } + std::vector candidates; + std::vector choices; + for (const std::vector& handles : gpus) { + const Candidate& candidate = candidates.emplace_back(evaluate(handles)); + choices.push_back({ candidate.properties.deviceType, candidate.label, candidate.gaps }); + } + return std::move(candidates[Device::ChooseDevice(choices)]); + } +} + +void Device::Initialize(const DeviceFeatures& required, const DeviceFeatures& optional) { VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO}; app.pApplicationName = ""; app.pEngineName = "Crafter.Graphics"; - app.apiVersion = VK_MAKE_VERSION(1, 4, 0); + // CRAFTER_GRAPHICS_NO_VULKAN_1_4 runs as a 1.3 application, so the 1.3 + // path (maintenance5 through its extension) can be exercised on a 1.4 GPU. + app.apiVersion = std::getenv("CRAFTER_GRAPHICS_NO_VULKAN_1_4") != nullptr ? VK_API_VERSION_1_3 : VK_API_VERSION_1_4; // GPU-Assisted Validation: shader-execution-time instrumentation that // catches out-of-bounds descriptor / buffer access the standard layer @@ -872,176 +1188,55 @@ void Device::Initialize() { debugUtilsMessengerCreateInfo.pfnUserCallback = onError; CheckVkResult(GET_EXTENSION_FUNCTION(vkCreateDebugUtilsMessengerEXT)(instance, &debugUtilsMessengerCreateInfo, NULL, &debugMessenger)); - uint32_t physDeviceCount; - vkEnumeratePhysicalDevices(instance, &physDeviceCount, NULL); - - std::vector physDevices(physDeviceCount); - vkEnumeratePhysicalDevices(instance, &physDeviceCount, physDevices.data()); - - uint32_t bestScore = 0; - - // CRAFTER_GRAPHICS_DEVICE picks a GPU by its number among the distinct - // ones in this list: with two of the same GPU nothing else tells them - // apart, and the first may be too busy to use. A GPU two driver manifests - // both list (a Flatpak sees the runtime's and the GL extension's) counts - // once, so the number means the same GPU inside a sandbox and out. - const char* chosenDevice = std::getenv("CRAFTER_GRAPHICS_DEVICE"); - if (chosenDevice != nullptr) { - std::vector distinct; - std::vector> seen; - for (VkPhysicalDevice device : physDevices) { - VkPhysicalDeviceIDProperties id { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES }; - VkPhysicalDeviceProperties2 properties { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &id }; - vkGetPhysicalDeviceProperties2(device, &properties); - std::array uuid; - std::ranges::copy(id.deviceUUID, uuid.begin()); - if (std::ranges::find(seen, uuid) == seen.end()) { - seen.push_back(uuid); - distinct.push_back(device); - } - } - const std::string_view text = chosenDevice; - std::size_t index = 0; - const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), index); - if (error != std::errc() || end != text.data() + text.size() || index >= distinct.size()) { - throw std::runtime_error(std::format("CRAFTER_GRAPHICS_DEVICE={} isn't the number of a GPU: Vulkan lists {} GPU(s), 0 to {}", - chosenDevice, distinct.size(), distinct.size() - 1)); - } - physDevice = distinct[index]; + // Pick the GPU before anything this call can't take back. When none can + // run the application this throws, and takes the instance with it, so + // the application may call Initialize again asking for less. + Candidate chosen; + try { + chosen = ChooseGpu(instance, app.apiVersion, required, optional); + } catch (...) { + GET_EXTENSION_FUNCTION(vkDestroyDebugUtilsMessengerEXT)(instance, debugMessenger, nullptr); + vkDestroyInstance(instance, nullptr); + debugMessenger = VK_NULL_HANDLE; + instance = VK_NULL_HANDLE; + throw; } - for (uint32_t i = 0; chosenDevice == nullptr && i < physDeviceCount; i++) - { - VkPhysicalDevice device = physDevices[i]; + #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND + display = wl_display_connect(NULL); + if (display == nullptr) { + throw std::runtime_error("Could not connect to wayland display"); + } + + wl_registry* registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, ®istry_listener, nullptr); - uint32_t score; + if (wl_display_roundtrip(display) == -1) { + exit(EXIT_FAILURE); + } - VkPhysicalDeviceProperties properties; - vkGetPhysicalDeviceProperties(device, &properties); - switch (properties.deviceType) - { - default : - continue; - case VK_PHYSICAL_DEVICE_TYPE_OTHER : - score = 1; - break; - case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU : - score = 4; - break; - case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU : - score = 5; - break; - case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU : - score = 3; - break; - case VK_PHYSICAL_DEVICE_TYPE_CPU : - score = 2; - break; - } + if (shm == NULL || compositor == NULL || xdgWmBase == NULL) { + throw std::runtime_error("No wl_shm, wl_compositor or xdg_wm_base support"); + } + // After the registry roundtrip the data_device (if the compositor + // exposes one) is bound. Clipboard::Initialize attaches the + // selection listener that Clipboard::GetText reads from; doing it + // before the first wl_data_device.selection arrives is what lets + // GetText work the instant a frame is rendered. + Clipboard::Initialize(); + #endif - if (score > bestScore) - { - physDevice = device; - bestScore = score; - } - } - - // Enumerate the driver's device extensions once; every optional capability - // below is decided from this set. Drivers without - // VK_EXT_memory_decompression (AMD, Intel as of early 2026) get the - // CPU-decode fallback; drivers without the descriptor heap (Mesa Turnip) - // get the descriptor-set path and no ray tracing. - std::unordered_set availableExtensions; - { - std::uint32_t extCount = 0; - vkEnumerateDeviceExtensionProperties(physDevice, nullptr, &extCount, nullptr); - std::vector exts(extCount); - vkEnumerateDeviceExtensionProperties(physDevice, nullptr, &extCount, exts.data()); - for (const VkExtensionProperties& e : exts) { - availableExtensions.emplace(e.extensionName); - } - } - const auto hasAll = [&availableExtensions](std::span names) { - return std::ranges::all_of(names, [&](const char* name) { return availableExtensions.contains(name); }); - }; - - // Fail here, naming the gap, rather than in vkCreateDevice with a bare - // VK_ERROR_EXTENSION_NOT_PRESENT. - { - std::string missing; - for (const char* name : requiredDeviceExtensionNames) { - if (!availableExtensions.contains(name)) { - missing += ' '; - missing += name; - } - } - if (!missing.empty()) { - throw std::runtime_error(std::format("Crafter.Graphics: the Vulkan driver lacks required device extension(s):{}", missing)); - } - } - - // CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the descriptor-set path on a - // driver that does have the heap, so the fallback can be exercised on a - // desktop GPU. It takes ray tracing down with it, exactly as on a driver - // that really lacks the extension. - // - // A listed extension is not the whole answer: a driver may list one whose - // feature bit is still off. Query the feature bits of the optional groups - // whose extensions are present (only those structs are chained) and - // require both, so such a driver takes the fallback instead of failing - // in vkCreateDevice. - const bool heapExtensions = hasAll(descriptorHeapExtensionNames); - const bool rayTracingExtensions = hasAll(rayTracingExtensionNames); - const bool faultExtension = availableExtensions.contains(VK_EXT_DEVICE_FAULT_EXTENSION_NAME); - VkPhysicalDeviceDescriptorHeapFeaturesEXT heapQuery { - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_HEAP_FEATURES_EXT, - }; - VkPhysicalDeviceShaderUntypedPointersFeaturesKHR untypedPointersQuery { - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_UNTYPED_POINTERS_FEATURES_KHR, - }; - VkPhysicalDeviceAccelerationStructureFeaturesKHR accelerationStructureQuery { - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_FEATURES_KHR, - }; - VkPhysicalDeviceRayTracingPipelineFeaturesKHR rayTracingPipelineQuery { - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_PIPELINE_FEATURES_KHR, - }; - VkPhysicalDeviceRayQueryFeaturesKHR rayQueryQuery { - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_QUERY_FEATURES_KHR, - }; - VkPhysicalDeviceFaultFeaturesEXT faultQuery { - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT, - }; - VkPhysicalDeviceFeatures2 featureQuery { - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, - }; - const auto ask = [&featureQuery](auto& features) { - features.pNext = featureQuery.pNext; - featureQuery.pNext = &features; - }; - if (heapExtensions) { - ask(heapQuery); - ask(untypedPointersQuery); - } - if (rayTracingExtensions) { - ask(accelerationStructureQuery); - ask(rayTracingPipelineQuery); - ask(rayQueryQuery); - } - if (faultExtension) { - ask(faultQuery); - } - vkGetPhysicalDeviceFeatures2(physDevice, &featureQuery); - - descriptorHeapSupported = heapExtensions - && heapQuery.descriptorHeap && untypedPointersQuery.shaderUntypedPointers - && std::getenv("CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP") == nullptr; - rayTracingSupported = descriptorHeapSupported && rayTracingExtensions - && accelerationStructureQuery.accelerationStructure - && rayTracingPipelineQuery.rayTracingPipeline - && rayQueryQuery.rayQuery; - deviceFaultSupported = faultExtension && faultQuery.deviceFault; - memoryDecompressionSupported = availableExtensions.contains(VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME); - const bool atomicFloatSupported = availableExtensions.contains(VK_EXT_SHADER_ATOMIC_FLOAT_EXTENSION_NAME); + physDevice = chosen.device; + apiVersion = chosen.apiVersion; + supportedFeatures = chosen.supported; + descriptorHeapSupported = chosen.heap; + rayTracingSupported = chosen.rayTracing; + deviceFaultSupported = chosen.fault; + memoryDecompressionSupported = chosen.decompression; + workaroundDescriptorHeapAS = chosen.workaroundAS; + shaderInt64ForWorkaroundOnly = chosen.workaroundAS + && required.vulkan10.shaderInt64 == VK_FALSE && optional.vulkan10.shaderInt64 == VK_FALSE; + vulkan11Properties = chosen.vulkan11; // RAY_TRACING_SHADER is not a valid pipeline stage unless the ray tracing // pipeline feature is enabled; barrier code masks its stage flags with this. stageMask = rayTracingSupported @@ -1052,8 +1247,9 @@ void Device::Initialize() { // extension is being used: an unrecognized sType is a validation error on // drivers that don't expose the extension, and the heap properties must // stay zero when the heap is forced off. Driver properties are core and - // always queried; they identify the NVIDIA proprietary driver for the - // descriptor-heap AS-read workaround (issue #15 / #7). + // always queried, for Device::driverProperties: the driver's ID and + // version, which callers and tests read (EvaluateDevice took its own copy + // to decide the AS-read workaround). void* propertiesChain = &driverProperties; if (descriptorHeapSupported) { descriptorHeapProperties.pNext = propertiesChain; @@ -1080,26 +1276,12 @@ void Device::Initialize() { // (VulkanBuffer::FlushDevice(offset,bytes)). nonCoherentAtomSize = properties2.properties.limits.nonCoherentAtomSize; - // NVIDIA's brand-new VK_EXT_descriptor_heap acceleration-structure read - // path faults (see #7); enable the SPIR-V rewrite workaround there. Other - // drivers (and any future fixed NVIDIA driver, once this gate is removed) - // take the normal heap-bound AS path unchanged. The workaround is about - // the heap, so it is off whenever the heap is: otherwise a forced-fallback - // run on NVIDIA would still rewrite SPIR-V, enable shaderInt64 and push the - // TLAS address through vkCmdPushDataEXT, none of which a real heap-less - // driver does. Still needed on 615.78.08. CRAFTER_GRAPHICS_NO_AS_WORKAROUND, - // set to any value like the other CRAFTER_GRAPHICS_* switches, turns it off, - // which is how a new driver gets checked (tests/HeapASRead --no-workaround). - workaroundDescriptorHeapAS = descriptorHeapSupported - && driverProperties.driverID == VK_DRIVER_ID_NVIDIA_PROPRIETARY - && std::getenv("CRAFTER_GRAPHICS_NO_AS_WORKAROUND") == nullptr; - // Sanity-gate: GDeflate 1.0 must actually be in the supported method set. if (memoryDecompressionSupported && (memoryDecompressionProperties.decompressionMethods & VK_MEMORY_DECOMPRESSION_METHOD_GDEFLATE_1_0_BIT_EXT) == 0) { memoryDecompressionSupported = false; } - + uint32_t queueFamilyCount; vkGetPhysicalDeviceQueueFamilyProperties(physDevice, &queueFamilyCount, NULL); @@ -1114,8 +1296,8 @@ void Device::Initialize() { break; } } - - + + float priority = 1; VkDeviceQueueCreateInfo queueCreateInfo = {}; @@ -1124,14 +1306,26 @@ void Device::Initialize() { queueCreateInfo.queueCount = 1; queueCreateInfo.pQueuePriorities = &priority; - // Feature structs below are declared without a pNext and linked into one - // chain afterwards, each only when its extension is being enabled: a - // feature struct for an extension that is not enabled is invalid, and - // which ones are enabled now depends on the driver. + // The core features to enable, as EvaluateDevice resolved them: the + // library's own on this GPU, the required ones, and the optional ones it + // has. On a 1.4 GPU maintenance5 is enabled through vulkan14; below 1.4 + // through VK_KHR_maintenance5's struct instead, since Vulkan14Features + // isn't legal there and a feature may not be named through both structs + // (VUID-VkDeviceCreateInfo-pNext-10360). + const bool vulkan14 = chosen.apiVersion >= VK_API_VERSION_1_4; + DeviceFeatures enabled = chosen.enabled; + VkPhysicalDeviceMaintenance5Features maintenance5Features { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES, + .maintenance5 = enabled.vulkan14.maintenance5, + }; + + // The extension groups' feature structs, linked into the same chain only + // when their group is being enabled: a feature struct for an extension + // that is not enabled is invalid. VkPhysicalDeviceFaultFeaturesEXT faultFeatures = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT, .deviceFault = VK_TRUE, - .deviceFaultVendorBinary = faultQuery.deviceFaultVendorBinary, + .deviceFaultVendorBinary = chosen.faultVendorBinary ? VK_TRUE : VK_FALSE, }; VkPhysicalDeviceShaderUntypedPointersFeaturesKHR untypedPointersFeatures { @@ -1139,13 +1333,6 @@ void Device::Initialize() { .shaderUntypedPointers = VK_TRUE, }; - // Enables synchronization2 sentinels (VkMemoryBarrier2, VK_PIPELINE_STAGE_2_*, - // VK_ACCESS_2_*) — required for VK_EXT_memory_decompression's sync tokens. - VkPhysicalDeviceVulkan13Features features13 { - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES, - .synchronization2 = VK_TRUE, - }; - VkPhysicalDeviceMemoryDecompressionFeaturesEXT memoryDecompressionFeatures { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_DECOMPRESSION_FEATURES_EXT, .memoryDecompression = VK_TRUE, @@ -1156,46 +1343,6 @@ void Device::Initialize() { .descriptorHeap = VK_TRUE, }; - // Every VulkanBuffer is created with usage = 0 plus a - // VkBufferUsageFlags2CreateInfo, and heap pipelines carry a - // VkPipelineCreateFlags2CreateInfo. Both are maintenance5 functionality, - // which needs the feature bit as well as the extension name. - VkPhysicalDeviceMaintenance5Features maintenance5Features { - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES, - .maintenance5 = VK_TRUE, - }; - - VkPhysicalDevice16BitStorageFeatures bit16 { - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES, - .storageBuffer16BitAccess = VK_TRUE, - }; - - VkPhysicalDeviceVulkan12Features features12 { - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, - .shaderFloat16 = VK_TRUE, - // Bindless / runtime descriptor array indexing — needed by both - // binding models (heap and descriptor sets). - .shaderUniformBufferArrayNonUniformIndexing = VK_TRUE, - .shaderSampledImageArrayNonUniformIndexing = VK_TRUE, - .shaderStorageBufferArrayNonUniformIndexing = VK_TRUE, - .shaderStorageImageArrayNonUniformIndexing = VK_TRUE, - .runtimeDescriptorArray = VK_TRUE, - .scalarBlockLayout = VK_TRUE, - .bufferDeviceAddress = VK_TRUE - }; - if (!descriptorHeapSupported) { - // Descriptor-set path (DescriptorHeapVulkan sets mode): one bindless - // set whose slots are written while earlier frames may still be - // executing with it bound, and where most slots are never written. - // The sampled-image bit also covers SAMPLER descriptors. - features12.descriptorIndexing = VK_TRUE; - features12.descriptorBindingSampledImageUpdateAfterBind = VK_TRUE; - features12.descriptorBindingStorageImageUpdateAfterBind = VK_TRUE; - features12.descriptorBindingStorageBufferUpdateAfterBind = VK_TRUE; - features12.descriptorBindingUpdateUnusedWhilePending = VK_TRUE; - features12.descriptorBindingPartiallyBound = VK_TRUE; - } - VkPhysicalDeviceRayQueryFeaturesKHR physicalDeviceRayQueryFeatures{ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_QUERY_FEATURES_KHR, .rayQuery = VK_TRUE @@ -1221,51 +1368,37 @@ void Device::Initialize() { } if (descriptorHeapSupported) { link(untypedPointersFeatures); + link(desciptorHeapFeatures); } - link(features13); if (memoryDecompressionSupported) { link(memoryDecompressionFeatures); } - if (descriptorHeapSupported) { - link(desciptorHeapFeatures); - } - link(maintenance5Features); - link(bit16); - link(features12); if (rayTracingSupported) { link(physicalDeviceRayQueryFeatures); link(physicalDeviceRayTracingPipelineFeatures); link(deviceAccelerationStructureFeature); } + link(enabled.vulkan11); + link(enabled.vulkan12); + link(enabled.vulkan13); + if (vulkan14) { + link(enabled.vulkan14); + } else { + link(maintenance5Features); + } VkPhysicalDeviceFeatures2 physical_features2 = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, .pNext = featureChain, - .features = { - // Order matches VkPhysicalDeviceFeatures declaration so the - // designated-initializer-order warning stays quiet. - .samplerAnisotropy = VK_TRUE, - .shaderStorageImageReadWithoutFormat = VK_TRUE, - .shaderStorageImageWriteWithoutFormat = VK_TRUE, - // Bindless dynamic indexing — required to index `images[]`, - // `textures[]`, `samplers[]`, `itemHeap[]` with a runtime value. - .shaderSampledImageArrayDynamicIndexing = VK_TRUE, - .shaderStorageBufferArrayDynamicIndexing = VK_TRUE, - .shaderStorageImageArrayDynamicIndexing = VK_TRUE, - // shaderInt64: only needed for the NVIDIA descriptor-heap AS-read - // workaround (issue #15 / #7), which loads the TLAS device address - // as a 64-bit push constant. Gated so it isn't required on drivers - // that don't take the workaround path. Remove with the workaround. - .shaderInt64 = workaroundDescriptorHeapAS ? VK_TRUE : VK_FALSE, - .shaderInt16 = VK_TRUE - } + .features = enabled.vulkan10, }; - // The enabled-extension list is the required set plus each optional group - // the driver advertises in full (decided above from the enumeration). - std::vector enabledDeviceExtensions( - std::begin(requiredDeviceExtensionNames), - std::end(requiredDeviceExtensionNames)); + // What the application can read back, without the chain above. + enabledFeatures = chosen.enabled; + + // The enabled-extension list is the required set (maintenance5 among it + // below 1.4) plus each optional group the driver advertises in full. + std::vector enabledDeviceExtensions = chosen.requiredExtensions; if (descriptorHeapSupported) { enabledDeviceExtensions.append_range(descriptorHeapExtensionNames); } @@ -1275,9 +1408,6 @@ void Device::Initialize() { if (deviceFaultSupported) { enabledDeviceExtensions.push_back(VK_EXT_DEVICE_FAULT_EXTENSION_NAME); } - if (atomicFloatSupported) { - enabledDeviceExtensions.push_back(VK_EXT_SHADER_ATOMIC_FLOAT_EXTENSION_NAME); - } if (memoryDecompressionSupported) { enabledDeviceExtensions.push_back(VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME); } diff --git a/implementations/Crafter.Graphics-Window.cpp b/implementations/Crafter.Graphics-Window.cpp index ec98c7e..0cf2ed6 100644 --- a/implementations/Crafter.Graphics-Window.cpp +++ b/implementations/Crafter.Graphics-Window.cpp @@ -34,6 +34,7 @@ module; #endif #ifndef CRAFTER_GRAPHICS_WINDOW_DOM #include "vulkan/vulkan.h" +#include "vulkan/vk_enum_string_helper.h" #endif #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND #include "vulkan/vulkan_wayland.h" @@ -448,23 +449,46 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height std::vector surfaceFormats(formatCount); Device::CheckVkResult(vkGetPhysicalDeviceSurfaceFormatsKHR(Device::physDevice, vulkanSurface, &formatCount, surfaceFormats.data())); - // We want to get a format that best suits our needs, so we try to get one from a set of preferred formats - // Initialize the format to the first one returned by the implementation in case we can't find one of the preffered formats - VkSurfaceFormatKHR selectedFormat = surfaceFormats[0]; - std::vector preferredImageFormats = { - VK_FORMAT_R8G8B8A8_UNORM, - VK_FORMAT_B8G8R8A8_UNORM + // The swapchain image is a storage image that compute and ray tracing + // shaders write, and the UI kernels also read, without a format. From + // Vulkan 1.3 that is up to each format + // (VK_FORMAT_FEATURE_2_STORAGE_READ/WRITE_WITHOUT_FORMAT_BIT), not a device + // feature. Take the first surface format that allows both, R8G8B8A8_UNORM + // or B8G8R8A8_UNORM before others; failing that, one that allows writing. + const auto formatFeatures = [](VkFormat format) { + VkFormatProperties3 properties3 { .sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_3 }; + VkFormatProperties2 properties2 { .sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2, .pNext = &properties3 }; + vkGetPhysicalDeviceFormatProperties2(Device::physDevice, format, &properties2); + return properties3.optimalTilingFeatures; }; - - for (auto& availableFormat : surfaceFormats) { - if (std::find(preferredImageFormats.begin(), preferredImageFormats.end(), availableFormat.format) != preferredImageFormats.end()) { - selectedFormat = availableFormat; - break; + constexpr VkFormatFeatureFlags2 writable = VK_FORMAT_FEATURE_2_STORAGE_IMAGE_BIT | VK_FORMAT_FEATURE_2_STORAGE_WRITE_WITHOUT_FORMAT_BIT; + constexpr VkFormatFeatureFlags2 readWritable = writable | VK_FORMAT_FEATURE_2_STORAGE_READ_WITHOUT_FORMAT_BIT; + const VkSurfaceFormatKHR* selectedFormat = nullptr; + int selectedScore = -1; + for (const VkSurfaceFormatKHR& available : surfaceFormats) { + const VkFormatFeatureFlags2 features = formatFeatures(available.format); + if ((features & writable) != writable) { + continue; + } + const bool preferred = available.format == VK_FORMAT_R8G8B8A8_UNORM || available.format == VK_FORMAT_B8G8R8A8_UNORM; + const int score = ((features & readWritable) == readWritable ? 2 : 0) + (preferred ? 1 : 0); + if (score > selectedScore) { + selectedFormat = &available; + selectedScore = score; } } + if (selectedFormat == nullptr) { + throw std::runtime_error("Crafter.Graphics: no format this window's surface offers can be written as a storage image " + "without a format (VK_FORMAT_FEATURE_2_STORAGE_WRITE_WITHOUT_FORMAT_BIT), which drawing into " + "the swapchain needs"); + } + if (selectedScore < 2) { + std::println(stderr, "Crafter.Graphics: the swapchain format {} can't be read as a storage image without a format, " + "which the UI kernels do", string_VkFormat(selectedFormat->format)); + } - colorFormat = selectedFormat.format; - colorSpace = selectedFormat.colorSpace; + colorFormat = selectedFormat->format; + colorSpace = selectedFormat->colorSpace; // Select a supported composite alpha format (not all devices support alpha // opaque). This is a surface property, so pick it once here rather than on diff --git a/interfaces/Crafter.Graphics-Device.cppm b/interfaces/Crafter.Graphics-Device.cppm index 2b25dda..3fef58c 100644 --- a/interfaces/Crafter.Graphics-Device.cppm +++ b/interfaces/Crafter.Graphics-Device.cppm @@ -37,8 +37,52 @@ export namespace Crafter { }; #endif + #ifndef CRAFTER_GRAPHICS_WINDOW_DOM + // The core feature structs, one per Vulkan version: every feature promoted + // to core is in exactly one of them, and Vulkan forbids naming a feature + // through both its version's struct and the struct it was promoted from. + // The features an application's own shaders and code use go to + // Device::Initialize in these. Only the VkBool32 members are read, so sType + // and pNext may be left zero, as `{.vulkan12 = {.shaderFloat16 = VK_TRUE}}` + // leaves them. At namespace scope, like KeyRepeatState: a nested type with + // member initializers can't be the type of an inline static inside Device. + struct DeviceFeatures { + VkPhysicalDeviceFeatures vulkan10 {}; + VkPhysicalDeviceVulkan11Features vulkan11 { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES }; + VkPhysicalDeviceVulkan12Features vulkan12 { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES }; + VkPhysicalDeviceVulkan13Features vulkan13 { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES }; + // Needs a Vulkan 1.4 GPU. Below 1.4 only maintenance5 can be on, which + // the library enables through VK_KHR_maintenance5 there. + VkPhysicalDeviceVulkan14Features vulkan14 { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_4_FEATURES }; + }; + // The features a GPU lacks, named the way an application writes them + // ("vulkan12.shaderFloat16"), split by who needs them. + struct FeatureGaps { + std::vector library; + std::vector application; + }; + // One GPU as Device::ChooseDevice weighs it: no gaps means it can run the + // application. + struct DeviceChoice { + VkPhysicalDeviceType type; + std::string name; + std::vector gaps; + }; + #endif + struct Device { + #ifndef CRAFTER_GRAPHICS_WINDOW_DOM + // Creates the instance, picks a GPU and creates the device. `required` + // and `optional` are what the application's own shaders and code use; + // the library adds what it needs itself. A GPU without Vulkan 1.3, a + // required extension, the library's needs or a required feature is + // skipped (saying why, when it ranks above the one chosen); when none is + // left this throws, naming each GPU's gaps. Optional features are enabled + // where the chosen GPU has them: enabledFeatures says which. + static void Initialize(const DeviceFeatures& required = {}, const DeviceFeatures& optional = {}); + #else static void Initialize(); + #endif #ifndef CRAFTER_GRAPHICS_WINDOW_DOM #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND @@ -238,7 +282,10 @@ export namespace Crafter { // (DescriptorHeapVulkan sets mode), the // *.sets.spv shaders, vkCmdPushConstants // Setting the environment variable CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP - // forces the second model on any driver, for testing. + // forces the second model on any driver, for testing. In the same way + // CRAFTER_GRAPHICS_NO_VULKAN_1_4 creates the instance as a Vulkan 1.3 + // application, so the 1.3 path (maintenance5 through + // VK_KHR_maintenance5, no vulkan14 features) runs on a 1.4 GPU. inline static bool descriptorHeapSupported = false; // Ray tracing also needs the descriptor heap (RT pipelines and RT // shaders are heap-only), so this is never true without it. @@ -253,6 +300,56 @@ export namespace Crafter { // tracing is unsupported. Called at every ray tracing entry point. static void RequireRayTracing(); + // ─── Device features ─────────────────────────────────────────── + // Every feature Initialize enabled: the library's own, the + // application's required ones, and whichever optional ones the GPU has. + // pNext is null. Read only the fields you asked for: the library's own + // may change. All false before Initialize. + inline static DeviceFeatures enabledFeatures {}; + // Every feature the chosen GPU supports, enabled or not. Below Vulkan + // 1.4 vulkan14 holds maintenance5 alone, from VK_KHR_maintenance5. + inline static DeviceFeatures supportedFeatures {}; + // The Vulkan version the application may use on the chosen GPU: + // min(the instance's, the GPU's). Zero before Initialize. + inline static std::uint32_t apiVersion = 0; + // The GPU's subgroup size, stages and operations, which + // CheckShaderCapabilities holds GroupNonUniform* capabilities to. + inline static VkPhysicalDeviceVulkan11Properties vulkan11Properties = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_PROPERTIES + }; + // True when shaderInt64 is on only for the NVIDIA workaround's SPIR-V + // rewrite (below): an application shader that declares Int64 still + // needs the application to ask for it, or it would run only on NVIDIA. + inline static bool shaderInt64ForWorkaroundOnly = false; + // The features to enable on a GPU that supports `supported`: library ∪ + // required ∪ (optional ∩ supported), never one the GPU lacks. Each one + // in `library` or `required` that it lacks goes to `gaps`. Below an + // apiVersion of 1.4 a vulkan14 one is marked "(Vulkan 1.4 only)", but + // maintenance5, which VK_KHR_maintenance5 provides there, is marked as + // that extension's. Pure logic, driven by tests/DeviceFeatures without + // a GPU. + static DeviceFeatures ResolveFeatures(const DeviceFeatures& library, const DeviceFeatures& required, + const DeviceFeatures& optional, const DeviceFeatures& supported, + std::uint32_t apiVersion, FeatureGaps& gaps); + // The GPU to use: the best device type among those without gaps + // (discrete, integrated, virtual, other), a CPU device only when no + // other kind is listed. Prints why each better-ranked GPU was passed + // over, and throws listing every GPU's gaps when none qualifies. Pure + // logic apart from that print, driven by tests/DeviceFeatures. + static std::size_t ChooseDevice(std::span choices); + // Throws when `spirv` declares, directly or implicitly (Int64Atomics + // declares Int64), a SPIR-V capability that nothing enabled: a core + // feature that is off, a subgroup operation the GPU lacks, or one of + // the library's optional groups (ray tracing, the descriptor heap) that + // is off. Most drivers accept such a module silently. The message names + // `file`, the capability, and either the field to pass to Initialize or + // that the GPU lacks it. Capabilities a core version enables, and ones + // no enabled extension can, are left to the driver and the validation + // layer. VulkanShader calls it on every module, before the NVIDIA + // rewrite. Reads enabledFeatures, supportedFeatures, apiVersion, + // vulkan11Properties and the group flags, so tests can stage those. + static void CheckShaderCapabilities(std::span spirv, const std::filesystem::path& file); + // The two property structs below are only filled in (and only chained // into the properties query) when their capability group is supported; // otherwise they stay zero. @@ -456,4 +553,12 @@ export namespace Crafter { static void TickKeyRepeats() {} #endif }; -} \ No newline at end of file +} + +#ifndef CRAFTER_GRAPHICS_WINDOW_DOM +// Not exported: shared by Device's implementation units. +namespace Crafter::DeviceDetail { + // `parts` one after another, `separator` between them, for gap lists. + std::string Join(std::span parts, std::string_view separator); +} +#endif \ No newline at end of file diff --git a/interfaces/Crafter.Graphics-ShaderVulkan.cppm b/interfaces/Crafter.Graphics-ShaderVulkan.cppm index de67f7e..f5b84a9 100644 --- a/interfaces/Crafter.Graphics-ShaderVulkan.cppm +++ b/interfaces/Crafter.Graphics-ShaderVulkan.cppm @@ -58,7 +58,7 @@ export namespace WorkaroundNvidiaAS { OpTypeAccelerationStructureKHR = 5341, CapabilityInt64 = 11, StorageClassPushConstant = 9, - DecorationBlock = 2, DecorationMatrixStride = 7, + DecorationBlock = 2, DecorationRowMajor = 4, DecorationMatrixStride = 7, DecorationArrayStride = 6, DecorationOffset = 35, }; @@ -110,6 +110,7 @@ export namespace WorkaroundNvidiaAS { std::map arrayStride; // array type id → ArrayStride std::map memberOffset; // (struct<<32|idx) → Offset std::map memberMatStride; // (struct<<32|idx) → MatrixStride + std::set memberRowMajor; // (struct<<32|idx) decorated RowMajor std::map ptrPointee; // pointer type id → pointee type id for (std::size_t k = 0; k < instrs.size(); ++k) { const Instr& in = instrs[k]; @@ -150,6 +151,7 @@ export namespace WorkaroundNvidiaAS { std::uint64_t key = (static_cast(in[1]) << 32) | in[2]; if (in.size() >= 5 && in[3] == DecorationOffset) memberOffset[key] = in[4]; if (in.size() >= 5 && in[3] == DecorationMatrixStride) memberMatStride[key] = in[4]; + if (in.size() >= 4 && in[3] == DecorationRowMajor) memberRowMajor.insert(key); break; } case OpCapability: lastCapIdx = k; break; @@ -199,10 +201,20 @@ export namespace WorkaroundNvidiaAS { std::uint32_t sz; auto mt = typeInstr.find(t[m]); if (mt != typeInstr.end() && (mt->second->at(0) & 0xFFFFu) == OpTypeMatrix - && memberMatStride.count(key)) - sz = memberMatStride[key] * (*mt->second)[3]; - else + && memberMatStride.count(key)) { + // MatrixStride separates the columns, or the rows + // of a RowMajor member: as many as the column + // type has components. + const Instr& matrix = *mt->second; + std::uint32_t vectors = matrix[3]; + if (memberRowMajor.count(key)) { + auto column = typeInstr.find(matrix[2]); + if (column != typeInstr.end()) vectors = (*column->second)[3]; + } + sz = memberMatStride[key] * vectors; + } else { sz = footprint(t[m]); + } end = std::max(end, off + sz); } return end; @@ -386,6 +398,15 @@ export namespace Crafter { file.close(); + if (spirv.size() < 5 || spirv[0] != 0x07230203u) { + throw std::runtime_error(std::format("VulkanShader: {} is not SPIR-V (no SPIR-V header)", path.string())); + } + // Most drivers accept a module whose capability Initialize didn't + // enable, so a feature the application forgot to ask for would only + // show on a strict driver or under the validation layer. Checked on + // the words as compiled, before the rewrite below adds its own Int64. + Device::CheckShaderCapabilities(spirv, path); + // NVIDIA descriptor-heap AS-read workaround (issue #15 / #7). // No-op on every other driver and on shaders that don't read an // acceleration structure. Remove with the rest of the workaround diff --git a/project.cpp b/project.cpp index 4a8b7e1..cee0a3d 100644 --- a/project.cpp +++ b/project.cpp @@ -255,10 +255,11 @@ extern "C" Configuration CrafterBuildProject(std::span a cfg.files.emplace_back(fs::path("additional/dom-webgpu.js")); } } else { - std::array impls = { + std::array impls = { "implementations/Crafter.Graphics-Clipboard", "implementations/Crafter.Graphics-ComputeShader", "implementations/Crafter.Graphics-Device", + "implementations/Crafter.Graphics-Device-Features", "implementations/Crafter.Graphics-Font", "implementations/Crafter.Graphics-FontAtlas", "implementations/Crafter.Graphics-Gamepad", @@ -719,6 +720,19 @@ extern "C" Configuration CrafterBuildProject(std::span a // stamps synthetic device identities + headers and drives it directly, // no GPU device needed at runtime. cfg.tests.push_back(LibraryTest("PipelineCacheValidation")); + + // Which features Device::Initialize enables, which GPU it picks, and + // which SPIR-V capabilities VulkanShader lets through: + // Device::ResolveFeatures, ChooseDevice and CheckShaderCapabilities are + // pure logic, so this test drives them with synthetic GPUs and + // hand-built SPIR-V, no GPU device needed at runtime. + cfg.tests.push_back(LibraryTest("DeviceFeatures")); + + // The same on a real GPU, end to end: required and optional features + // through Device::Initialize, and the Vulkan 1.3 path + // (CRAFTER_GRAPHICS_NO_VULKAN_1_4) in a child process. Needs a Vulkan + // device. + cfg.tests.push_back(LibraryTest("FeatureRequests")); } return cfg; diff --git a/tests/DeviceFeatures/main.cpp b/tests/DeviceFeatures/main.cpp new file mode 100644 index 0000000..b5df0d8 --- /dev/null +++ b/tests/DeviceFeatures/main.cpp @@ -0,0 +1,364 @@ +//SPDX-License-Identifier: LGPL-3.0-only +//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +// Device::Initialize enables what the library's own shaders use plus what the +// application passes it, skips a GPU that lacks any of that, and VulkanShader +// refuses a module that declares a capability nobody enabled. The decisions +// behind those are pure logic: Device::ResolveFeatures (the features to +// enable, and the gaps a GPU has), Device::ChooseDevice (the GPU to use) and +// Device::CheckShaderCapabilities (whether a module's capabilities are +// enabled). This test drives them with synthetic GPUs and hand-built SPIR-V, +// so it needs no GPU, mirroring PipelineCacheValidation. tests/FeatureRequests +// checks the same wiring end to end on a real GPU. + +#include +#include +#include +#include "vulkan/vulkan.h" + +import Crafter.Graphics; +import std; +using namespace Crafter; + +namespace { + +int failures = 0; + +void Check(bool ok, std::string_view what) { + std::println("{} {}", ok ? "PASS" : "FAIL", what); + if (!ok) ++failures; +} + +// The five core feature structs as DeviceFeatures lays them out: where each +// one's VkBool32 members start, and how many it has (vk.xml; a core feature +// struct never changes once its version ships). +struct Block { + std::string_view version; + std::size_t first; + std::size_t count; +}; +const std::array blocks {{ + { "vulkan10", offsetof(DeviceFeatures, vulkan10) + offsetof(VkPhysicalDeviceFeatures, robustBufferAccess), 55 }, + { "vulkan11", offsetof(DeviceFeatures, vulkan11) + offsetof(VkPhysicalDeviceVulkan11Features, storageBuffer16BitAccess), 12 }, + { "vulkan12", offsetof(DeviceFeatures, vulkan12) + offsetof(VkPhysicalDeviceVulkan12Features, samplerMirrorClampToEdge), 47 }, + { "vulkan13", offsetof(DeviceFeatures, vulkan13) + offsetof(VkPhysicalDeviceVulkan13Features, robustImageAccess), 15 }, + { "vulkan14", offsetof(DeviceFeatures, vulkan14) + offsetof(VkPhysicalDeviceVulkan14Features, globalPriorityQuery), 21 }, +}}; + +void SetSlot(DeviceFeatures& features, std::size_t offset) { + const VkBool32 on = VK_TRUE; + std::memcpy(reinterpret_cast(&features) + offset, &on, sizeof(on)); +} + +// Whether a and b have the same VkBool32s on, ignoring sType, pNext and +// padding. +bool SameFeatures(const DeviceFeatures& a, const DeviceFeatures& b) { + for (const Block& block : blocks) { + for (std::size_t i = 0; i < block.count; ++i) { + const std::size_t offset = block.first + i * sizeof(VkBool32); + VkBool32 x; + VkBool32 y; + std::memcpy(&x, reinterpret_cast(&a) + offset, sizeof(x)); + std::memcpy(&y, reinterpret_cast(&b) + offset, sizeof(y)); + if ((x != VK_FALSE) != (y != VK_FALSE)) { + return false; + } + } + } + return true; +} + +// Every core feature on. +DeviceFeatures AllOn() { + DeviceFeatures features; + for (const Block& block : blocks) { + for (std::size_t i = 0; i < block.count; ++i) { + SetSlot(features, block.first + i * sizeof(VkBool32)); + } + } + return features; +} + +// A module that declares Shader and then `capabilities`, ending the +// capability section with an OpMemoryModel. +std::vector Module(std::initializer_list capabilities) { + constexpr std::uint32_t opCapability = 17; + constexpr std::uint32_t opMemoryModel = 14; + std::vector words { 0x07230203u, 0x00010400u, 0, 1, 0 }; + words.insert(words.end(), { opCapability | (2u << 16), 1 /* Shader */ }); + for (std::uint32_t capability : capabilities) { + words.insert(words.end(), { opCapability | (2u << 16), capability }); + } + words.insert(words.end(), { opMemoryModel | (3u << 16), 0, 1 }); + return words; +} + +// The message CheckShaderCapabilities throws for `spirv`, or "" when it lets +// the module through. +std::string Refusal(const std::vector& spirv) { + try { + Device::CheckShaderCapabilities(spirv, "shaders/test.comp.spv"); + } catch (const std::runtime_error& error) { + return error.what(); + } + return ""; +} + +// A 1.4 GPU with every core feature, enabled nowhere, and no optional group. +void ResetDevice() { + Device::enabledFeatures = {}; + Device::supportedFeatures = AllOn(); + Device::apiVersion = VK_API_VERSION_1_4; + Device::vulkan11Properties = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_PROPERTIES }; + Device::vulkan11Properties.subgroupSupportedOperations = VK_SUBGROUP_FEATURE_BASIC_BIT; + Device::shaderInt64ForWorkaroundOnly = false; + Device::descriptorHeapSupported = false; + Device::rayTracingSupported = false; +} + +} // namespace + +int main() { + // --- what gets enabled -------------------------------------------------- + { + DeviceFeatures library; + library.vulkan10.samplerAnisotropy = VK_TRUE; + // Designated initializers leave sType zero, as an application's will. + const DeviceFeatures required { .vulkan12 = { .shaderFloat16 = VK_TRUE } }; + const DeviceFeatures optional { + .vulkan11 = { .storageBuffer16BitAccess = VK_TRUE }, + .vulkan13 = { .synchronization2 = VK_TRUE }, + }; + DeviceFeatures supported = AllOn(); + supported.vulkan13.synchronization2 = VK_FALSE; + + FeatureGaps gaps; + const DeviceFeatures enabled = Device::ResolveFeatures(library, required, optional, supported, VK_API_VERSION_1_4, gaps); + Check(gaps.library.empty() && gaps.application.empty(), "a GPU with everything needed has no gaps"); + Check(enabled.vulkan10.samplerAnisotropy && enabled.vulkan12.shaderFloat16, + "the library's and the application's required features are enabled"); + Check(enabled.vulkan11.storageBuffer16BitAccess, "an optional feature the GPU has is enabled"); + Check(!enabled.vulkan13.synchronization2, "an optional feature the GPU lacks stays off, silently"); + Check(!enabled.vulkan10.shaderInt16 && !enabled.vulkan14.pushDescriptor, + "nothing nobody asked for is enabled"); + Check(enabled.vulkan12.sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES && enabled.vulkan12.pNext == nullptr, + "the result carries the sTypes and no pNext, ready to chain"); + } + + // --- gaps, by who needs the feature ------------------------------------- + { + DeviceFeatures library; + library.vulkan10.samplerAnisotropy = VK_TRUE; + const DeviceFeatures required { .vulkan12 = { .shaderFloat16 = VK_TRUE } }; + DeviceFeatures supported = AllOn(); + supported.vulkan10.samplerAnisotropy = VK_FALSE; + supported.vulkan12.shaderFloat16 = VK_FALSE; + + FeatureGaps gaps; + const DeviceFeatures enabled = Device::ResolveFeatures(library, required, {}, supported, VK_API_VERSION_1_4, gaps); + Check(gaps.library == std::vector{ "vulkan10.samplerAnisotropy" }, + "a library need the GPU lacks is the library's gap"); + Check(gaps.application == std::vector{ "vulkan12.shaderFloat16" }, + "a required feature the GPU lacks is the application's gap, named as the application writes it"); + Check(!enabled.vulkan12.shaderFloat16, "a feature the GPU lacks is never enabled"); + } + + // --- Vulkan 1.3 GPUs ---------------------------------------------------- + { + DeviceFeatures library; + library.vulkan14.maintenance5 = VK_TRUE; + const DeviceFeatures required { .vulkan14 = { .pushDescriptor = VK_TRUE } }; + // Below 1.4 a GPU supports no vulkan14 feature but maintenance5, which + // Initialize copies there from VK_KHR_maintenance5's struct (that copy + // is tests/FeatureRequests' to check, on a real GPU). + DeviceFeatures supported = AllOn(); + supported.vulkan14 = { .maintenance5 = VK_TRUE }; + + FeatureGaps gaps; + const DeviceFeatures enabled = Device::ResolveFeatures(library, required, {}, supported, VK_API_VERSION_1_3, gaps); + Check(gaps.library.empty() && enabled.vulkan14.maintenance5, + "at 1.3 a supported vulkan14.maintenance5 meets the library's need, with no version note"); + Check(gaps.application == std::vector{ "vulkan14.pushDescriptor (Vulkan 1.4 only)" }, + "at 1.3 a required vulkan14 feature is a gap that names the version"); + + FeatureGaps noMaintenance5; + DeviceFeatures without = AllOn(); + without.vulkan14 = {}; + Device::ResolveFeatures(library, {}, {}, without, VK_API_VERSION_1_3, noMaintenance5); + Check(noMaintenance5.library == std::vector{ "vulkan14.maintenance5 (VK_KHR_maintenance5 reports it unsupported)" }, + "at 1.3 a missing maintenance5 blames VK_KHR_maintenance5's feature bit, not the version"); + + FeatureGaps gaps14; + DeviceFeatures supported14 = AllOn(); + supported14.vulkan14.pushDescriptor = VK_FALSE; + Device::ResolveFeatures(library, required, {}, supported14, VK_API_VERSION_1_4, gaps14); + Check(gaps14.application == std::vector{ "vulkan14.pushDescriptor" }, + "at 1.4 the same gap carries no version note"); + } + + // --- every core feature, by name ---------------------------------------- + { + std::set names; + bool eachNamedOnce = true; + bool eachEnabledAlone = true; + bool eachUnderItsVersion = true; + for (const Block& block : blocks) { + for (std::size_t i = 0; i < block.count; ++i) { + DeviceFeatures required; + SetSlot(required, block.first + i * sizeof(VkBool32)); + FeatureGaps gaps; + Device::ResolveFeatures({}, required, {}, {}, VK_API_VERSION_1_4, gaps); + if (gaps.application.size() != 1 || !names.insert(gaps.application[0]).second) { + eachNamedOnce = false; + continue; + } + eachUnderItsVersion = eachUnderItsVersion && gaps.application[0].starts_with(std::format("{}.", block.version)); + // On a GPU with everything, only that one gets enabled. + FeatureGaps none; + const DeviceFeatures enabled = Device::ResolveFeatures({}, required, {}, AllOn(), VK_API_VERSION_1_4, none); + eachEnabledAlone = eachEnabledAlone && SameFeatures(enabled, required); + } + } + Check(eachNamedOnce && names.size() == 150, "each of the 150 core features has a gap name of its own"); + Check(eachUnderItsVersion, "each name starts with its struct's member in DeviceFeatures"); + Check(eachEnabledAlone, "requiring one feature enables that one alone"); + Check(names.contains("vulkan10.robustBufferAccess") && names.contains("vulkan10.shaderInt16") + && names.contains("vulkan11.storageBuffer16BitAccess") && names.contains("vulkan12.scalarBlockLayout") + && names.contains("vulkan14.pushDescriptor"), + "names match the members (spot checks)"); + } + + // --- choosing the GPU --------------------------------------------------- + { + const std::vector noGaps; + const std::vector gap { "lacks vulkan12.shaderFloat16, which the application requires" }; + const DeviceChoice discrete { VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU, "dGPU", noGaps }; + const DeviceChoice discreteLacking { VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU, "old dGPU", gap }; + const DeviceChoice integrated { VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU, "iGPU", noGaps }; + const DeviceChoice integratedLacking { VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU, "old iGPU", gap }; + const DeviceChoice cpu { VK_PHYSICAL_DEVICE_TYPE_CPU, "llvmpipe", noGaps }; + + Check(Device::ChooseDevice(std::vector{ integrated, discrete }) == 1, "a discrete GPU wins over an integrated one"); + Check(Device::ChooseDevice(std::vector{ discrete, discrete }) == 0, "of two equal GPUs the first is used"); + Check(Device::ChooseDevice(std::vector{ discreteLacking, integrated, cpu }) == 1, + "a GPU that lacks something is skipped for the next one that can run the application"); + Check(Device::ChooseDevice(std::vector{ cpu }) == 0, "a CPU device runs the application when nothing else is listed"); + + std::string message; + try { + Device::ChooseDevice(std::vector{ discreteLacking, integratedLacking, cpu }); + } catch (const std::runtime_error& error) { + message = error.what(); + } + Check(!message.empty(), "GPUs that lack something don't fall back to a CPU device"); + Check(message.contains("old dGPU: lacks vulkan12.shaderFloat16") && message.contains("old iGPU: lacks") + && message.contains("llvmpipe: a CPU device"), + "the error names every GPU and why it can't be used"); + + std::string none; + try { + Device::ChooseDevice(std::vector{}); + } catch (const std::runtime_error& error) { + none = error.what(); + } + Check(none.contains("lists none"), "no GPU at all is an error that says so"); + } + + // --- SPIR-V capabilities against the enabled features ------------------- + { + constexpr std::uint32_t int16 = 22; + constexpr std::uint32_t int64 = 11; + constexpr std::uint32_t int64Atomics = 12; + constexpr std::uint32_t storageImageReadWithoutFormat = 55; + constexpr std::uint32_t groupNonUniform = 61; + constexpr std::uint32_t groupNonUniformArithmetic = 63; + constexpr std::uint32_t shaderNonUniform = 5301; + constexpr std::uint32_t rayQueryKHR = 4472; + constexpr std::uint32_t descriptorHeapEXT = 5128; + constexpr std::uint32_t groupNonUniformRotateKHR = 6026; + constexpr std::uint32_t floatControls2 = 6029; + + ResetDevice(); + const std::string refusal = Refusal(Module({ int16 })); + Check(refusal.contains("shaders/test.comp.spv") && refusal.contains("Int16") + && refusal.contains("vulkan10.shaderInt16: pass it to Device::Initialize"), + "Int16 without shaderInt16, on a GPU that has it, is refused, naming the file and the field to pass"); + Device::enabledFeatures.vulkan10.shaderInt16 = VK_TRUE; + Check(Refusal(Module({ int16 })).empty(), "...and accepted once shaderInt16 is enabled"); + + ResetDevice(); + Device::supportedFeatures.vulkan10.shaderInt16 = VK_FALSE; + Check(Refusal(Module({ int16 })).contains("this GPU lacks it"), + "on a GPU without shaderInt16 the refusal says the GPU lacks it, not to pass it"); + + ResetDevice(); + Device::apiVersion = VK_API_VERSION_1_3; + Device::supportedFeatures.vulkan14 = { .maintenance5 = VK_TRUE }; + Check(Refusal(Module({ floatControls2 })).contains("a Vulkan 1.4 feature, and this GPU runs Vulkan 1.3"), + "a vulkan14 capability on a 1.3 GPU names the version"); + ResetDevice(); + Check(Refusal(Module({ floatControls2 })).contains("vulkan14.shaderFloatControls2: pass it"), + "FloatControls2 is refused without vulkan14.shaderFloatControls2"); + Device::enabledFeatures.vulkan14.shaderFloatControls2 = VK_TRUE; + Check(Refusal(Module({ floatControls2 })).empty(), "...and accepted with it"); + + ResetDevice(); + Check(Refusal(Module({ storageImageReadWithoutFormat, shaderNonUniform })).empty(), + "capabilities a core version enables are left to the driver"); + + ResetDevice(); + Device::enabledFeatures.vulkan10.shaderInt64 = VK_TRUE; + Device::shaderInt64ForWorkaroundOnly = true; + Check(Refusal(Module({ int64 })).contains("NVIDIA workaround"), + "Int64 on only for the NVIDIA workaround doesn't let an application's Int64 through"); + Device::shaderInt64ForWorkaroundOnly = false; + Check(Refusal(Module({ int64 })).empty(), "...but does once the application asked for shaderInt64"); + + ResetDevice(); + Device::enabledFeatures.vulkan12.shaderSharedInt64Atomics = VK_TRUE; + Check(Refusal(Module({ int64Atomics })).contains("Int64Atomics, which implicitly declares Int64"), + "Int64Atomics alone still needs shaderInt64: it implicitly declares Int64"); + Device::enabledFeatures.vulkan10.shaderInt64 = VK_TRUE; + Check(Refusal(Module({ int64Atomics })).empty(), "a capability with two enabling features takes either"); + Device::shaderInt64ForWorkaroundOnly = true; + Check(Refusal(Module({ int64Atomics })).contains("NVIDIA workaround"), + "an implied Int64 doesn't pass on the NVIDIA workaround's shaderInt64 either"); + + ResetDevice(); + Check(Refusal(Module({ groupNonUniformArithmetic })).contains("VK_SUBGROUP_FEATURE_ARITHMETIC_BIT"), + "subgroup arithmetic the GPU lacks is refused"); + Device::vulkan11Properties.subgroupSupportedOperations |= VK_SUBGROUP_FEATURE_ARITHMETIC_BIT; + Check(Refusal(Module({ groupNonUniformArithmetic })).empty(), + "...and accepted where the GPU has it, whatever the stage: Vulkan gates running subgroup operations by stage, not declaring them"); + Device::enabledFeatures.vulkan14.shaderSubgroupRotate = VK_TRUE; + Device::vulkan11Properties.subgroupSupportedOperations = 0; + Check(Refusal(Module({ groupNonUniformRotateKHR })).contains("implicitly declares GroupNonUniform, but this GPU has no"), + "GroupNonUniformRotateKHR implicitly declares GroupNonUniform, and is held to it"); + + ResetDevice(); + Check(Refusal(Module({ rayQueryKHR })).contains("Device::rayTracingSupported"), + "a ray-query module is refused where ray tracing is off"); + Device::rayTracingSupported = true; + Check(Refusal(Module({ rayQueryKHR })).empty(), "...and accepted where it is on"); + Check(Refusal(Module({ descriptorHeapEXT })).contains("Device::descriptorHeapSupported"), + "a descriptor-heap module is refused where the heap is off"); + Device::descriptorHeapSupported = true; + Check(Refusal(Module({ descriptorHeapEXT })).empty(), "...and accepted where it is on"); + + ResetDevice(); + std::vector afterLayout = Module({}); + afterLayout.insert(afterLayout.end(), { 17u | (2u << 16), int16 }); + Check(Refusal(afterLayout).empty(), "only the capability section at the start of the module is read"); + std::vector malformed { 0x07230203u, 0x00010400u, 0, 1, 0, 17u /* length 0 */ }; + Check(Refusal(malformed).empty(), "a malformed instruction ends the scan instead of running past the module"); + Check(Refusal(Module({ groupNonUniform })).empty(), "GroupNonUniform passes on the BASIC operations every GPU has"); + } + ResetDevice(); + + if (failures != 0) { + std::println("{} check(s) failed", failures); + return EXIT_FAILURE; + } + std::println("all checks passed"); + return EXIT_SUCCESS; +} diff --git a/tests/FeatureRequests/main.cpp b/tests/FeatureRequests/main.cpp new file mode 100644 index 0000000..4d9f142 --- /dev/null +++ b/tests/FeatureRequests/main.cpp @@ -0,0 +1,138 @@ +//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; +} diff --git a/tests/HeapASRead/main.cpp b/tests/HeapASRead/main.cpp index ca178b0..885a914 100644 --- a/tests/HeapASRead/main.cpp +++ b/tests/HeapASRead/main.cpp @@ -119,7 +119,10 @@ constexpr std::array miss = {128, 128, 128}; // Renders the triangle through `path` into `out` and checks the pixels. // Returns the exit code. int Render(Path path, const fs::path& out) { - Device::Initialize(); + // The raygen takes the heap's buffer start as a uint16_t spec constant. + // Required, not optional: every ray-tracing GPU has shaderInt16, so 77 + // stays "no ray tracing", which Probe reports as such. + Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}}); if (!Device::rayTracingSupported) { std::println("SKIP no Vulkan ray-tracing device"); return 77; diff --git a/tests/HeapDynamicIndexHit/main.cpp b/tests/HeapDynamicIndexHit/main.cpp index 653810a..a46d583 100644 --- a/tests/HeapDynamicIndexHit/main.cpp +++ b/tests/HeapDynamicIndexHit/main.cpp @@ -380,7 +380,12 @@ fs::path OutputDir() { } // namespace int main(int argc, char** argv) { - Device::Initialize(); + // The shaders take heap slots as uint16_t spec constants, and the + // closest-hit indexes buffers non-uniformly. + Device::Initialize({ + .vulkan10 = {.shaderInt16 = VK_TRUE}, + .vulkan12 = {.shaderStorageBufferArrayNonUniformIndexing = VK_TRUE}, + }); if (!Device::rayTracingSupported) { std::println("SKIP no Vulkan ray-tracing device"); return 77; diff --git a/tests/PushConstantRewrite/main.cpp b/tests/PushConstantRewrite/main.cpp index 486cec1..79db598 100644 --- a/tests/PushConstantRewrite/main.cpp +++ b/tests/PushConstantRewrite/main.cpp @@ -93,7 +93,7 @@ constexpr std::string_view kHeader = "layout(descriptor_heap) uniform writeonly image2D image[];\n" "layout(location = 0) rayPayloadEXT vec3 hitValue;\n"; -const std::array kCases = {{ +const std::array kCases = {{ // No push constant at all → Patch synthesizes a fresh single-member block at offset 0. { "no-push-constant", std::string_view{ "" @@ -114,6 +114,12 @@ const std::array kCases = {{ "layout(push_constant) uniform PC { vec4 v[2]; uint f; } pc;\n" }, true, true, 40 }, + // Existing block {row-major mat3x4 @0}: 3 columns of 4 rows, stored as 4 + // rows 16 bytes apart, so it ends at 64, not at the 48 its 3 columns give. + { "merge-row-major-mat3x4", std::string_view{ + "layout(push_constant) uniform PC { layout(row_major) mat3x4 m; } pc;\n" + }, true, true, 64 }, + // Push constant but NO acceleration-structure read → Patch is a no-op; the // single user block must survive untouched and still validate. { "push-constant-no-as", std::string_view{ @@ -133,7 +139,9 @@ std::string BuildSource(const Case& c) { // Reference the push constant so glslang keeps the block in the module. std::string_view g = c.glsl; std::string extra = "vec4(hitValue, 1.0)"; - if (g.find("mat4 m;") != std::string_view::npos) + if (g.find("mat3x4 m;") != std::string_view::npos) + extra = "vec4(hitValue, 1.0) + pc.m[2]"; + else if (g.find("mat4 m;") != std::string_view::npos) extra = "pc.m * vec4(hitValue, 1.0) + vec4(pc.l, float(pc.f))"; else if (g.find("uint f; } pc;") != std::string_view::npos && g.find("vec4 v[2]") != std::string_view::npos) extra = "vec4(hitValue, 1.0) + pc.v[0] + pc.v[1] + vec4(float(pc.f))"; diff --git a/tests/RTPassPushData/main.cpp b/tests/RTPassPushData/main.cpp index 08c5449..d939db7 100644 --- a/tests/RTPassPushData/main.cpp +++ b/tests/RTPassPushData/main.cpp @@ -64,7 +64,8 @@ struct Push { } // namespace int main() { - Device::Initialize(); + // The raygen takes the heap's buffer start as a uint16_t spec constant. + Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}}); if (!Device::rayTracingSupported) { std::println("SKIP no Vulkan ray-tracing device"); return 77;