feat(device): applications ask for the device features their shaders use
Device
- Device::Initialize(required, optional) takes DeviceFeatures, the five
core feature structs vulkan10 to vulkan14. The library enables only what
its own shaders and code use. Device::enabledFeatures says what was
enabled, supportedFeatures what the GPU has, apiVersion the version in
use.
- shaderInt16, shaderFloat16, storageBuffer16BitAccess, scalarBlockLayout
and the uniform-buffer, storage-buffer and storage-image non-uniform
indexing bits are no longer enabled for everyone. An application whose
shaders take uint16_t spec constants passes
{.vulkan10 = {.shaderInt16 = VK_TRUE}}. scalarBlockLayout stays on with
the NVIDIA workaround, whose TLAS address may sit in a struct's tail
padding.
- Every GPU is weighed before one is chosen: Vulkan 1.3, the swapchain and,
below 1.4, maintenance5 extensions, the library's needs, the required
features. One that can't run the application is skipped, saying why when
it ranks higher. A CPU device runs it only when nothing else is listed,
and CRAFTER_GRAPHICS_DEVICE never falls back. A GPU two drivers both list
is tried through each.
- When no GPU qualifies, Initialize throws naming each GPU's gaps. It
destroys the instance it made, before any Wayland connection, so it can
be called again asking for less.
- maintenance5 is vulkan14's on a 1.4 GPU and VK_KHR_maintenance5's below.
CRAFTER_GRAPHICS_NO_VULKAN_1_4 runs as a 1.3 application, to exercise
that path on a 1.4 GPU.
- VK_KHR_spirv_1_4 and VK_KHR_shader_float_controls, core since 1.2, are no
longer enabled, nor VK_EXT_shader_atomic_float, which was enabled without
its feature struct. memoryDecompression's feature bit is queried before
it is enabled.
Shaders
- VulkanShader checks a module's capabilities, declared directly or
implicitly, against the enabled core features, the GPU's subgroup
operations and the optional groups, and throws naming the field to pass,
or saying the GPU lacks it. The tables are generated from vk.xml and
SPIR-V's grammar.
- WorkaroundNvidiaAS::Patch sized a row-major matrix push member by its
columns, so the TLAS address could land inside it.
Window
- The swapchain format is one whose own feature flags allow storage reads
and writes without a format, which since Vulkan 1.3 each format says for
itself; shaderStorageImageRead/WriteWithoutFormat are no longer required.
Tests
- DeviceFeatures: ResolveFeatures, ChooseDevice and CheckShaderCapabilities,
without a GPU.
- FeatureRequests: required and optional features end to end, the 1.3
path, and a retry after a refusal.
- PushConstantRewrite: a row-major mat3x4 push member.
- HeapASRead, HeapDynamicIndexHit, RTPassPushData and the ray-tracing
examples ask for the features their shaders use.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c62109caf3
commit
bcdc885de8
18 changed files with 1721 additions and 333 deletions
551
implementations/Crafter.Graphics-Device-Features.cpp
Normal file
551
implementations/Crafter.Graphics-Device-Features.cpp
Normal file
|
|
@ -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 <stddef.h>
|
||||
#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<const std::string> 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 <typename S>
|
||||
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<S, VkPhysicalDeviceFeatures>) {
|
||||
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<VkPhysicalDeviceFeatures>(0, 55, offsetof(DeviceFeatures, vulkan10)));
|
||||
static_assert(Covers<VkPhysicalDeviceVulkan11Features>(55, 12, offsetof(DeviceFeatures, vulkan11)));
|
||||
static_assert(Covers<VkPhysicalDeviceVulkan12Features>(67, 47, offsetof(DeviceFeatures, vulkan12)));
|
||||
static_assert(Covers<VkPhysicalDeviceVulkan13Features>(114, 15, offsetof(DeviceFeatures, vulkan13)));
|
||||
static_assert(Covers<VkPhysicalDeviceVulkan14Features>(129, 21, offsetof(DeviceFeatures, vulkan14)));
|
||||
|
||||
bool Get(const DeviceFeatures& features, const FeatureField& field) {
|
||||
VkBool32 value;
|
||||
std::memcpy(&value, reinterpret_cast<const std::byte*>(&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<std::byte*>(&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 <spirvcapabilities>: 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<FeatureField, 2> 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<std::uint32_t, 2> 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<const DeviceChoice> 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<std::size_t> 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<const std::uint32_t> 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<VkSubgroupFeatureFlagBits>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<const char*> requiredExtensions;
|
||||
DeviceFeatures supported;
|
||||
DeviceFeatures enabled;
|
||||
// Why it can't run the application. Empty when it can.
|
||||
std::vector<std::string> 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<std::string> availableExtensions;
|
||||
{
|
||||
std::uint32_t extCount = 0;
|
||||
vkEnumerateDeviceExtensionProperties(physical, nullptr, &extCount, nullptr);
|
||||
std::vector<VkExtensionProperties> exts(extCount);
|
||||
vkEnumerateDeviceExtensionProperties(physical, nullptr, &extCount, exts.data());
|
||||
for (const VkExtensionProperties& e : exts) {
|
||||
availableExtensions.emplace(e.extensionName);
|
||||
}
|
||||
}
|
||||
const auto hasAll = [&availableExtensions](std::span<const char* const> 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<std::string> 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<VkPhysicalDevice> 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<std::vector<VkPhysicalDevice>> gpus;
|
||||
{
|
||||
std::vector<std::array<std::uint8_t, VK_UUID_SIZE>> 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<std::uint8_t, VK_UUID_SIZE> 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<VkPhysicalDevice>& 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<Candidate> candidates;
|
||||
std::vector<DeviceChoice> choices;
|
||||
for (const std::vector<VkPhysicalDevice>& 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<VkPhysicalDevice> 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<VkPhysicalDevice> distinct;
|
||||
std::vector<std::array<std::uint8_t, VK_UUID_SIZE>> 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<std::uint8_t, VK_UUID_SIZE> 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<std::string> availableExtensions;
|
||||
{
|
||||
std::uint32_t extCount = 0;
|
||||
vkEnumerateDeviceExtensionProperties(physDevice, nullptr, &extCount, nullptr);
|
||||
std::vector<VkExtensionProperties> exts(extCount);
|
||||
vkEnumerateDeviceExtensionProperties(physDevice, nullptr, &extCount, exts.data());
|
||||
for (const VkExtensionProperties& e : exts) {
|
||||
availableExtensions.emplace(e.extensionName);
|
||||
}
|
||||
}
|
||||
const auto hasAll = [&availableExtensions](std::span<const char* const> 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<const char*> 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<const char*> 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<VkSurfaceFormatKHR> 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<VkFormat> 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
|
||||
|
|
|
|||
Loading…
Reference in a new issue