//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); } } } }