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
21
README.md
21
README.md
|
|
@ -89,10 +89,25 @@ shaded through an intersection shader with an any-hit cut-out.
|
|||
`VK_EXT_descriptor_heap`; pipelines are created with
|
||||
`VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT` so there are no
|
||||
descriptor-set layouts and push constants travel via
|
||||
`vkCmdPushDataEXT`. The heap, ray tracing and device-fault extensions
|
||||
are optional groups, detected at start-up
|
||||
`vkCmdPushDataEXT`. Needs Vulkan 1.3. The heap, ray tracing and
|
||||
device-fault extensions are optional groups, detected at start-up
|
||||
(`Device::descriptorHeapSupported`, `rayTracingSupported`,
|
||||
`deviceFaultSupported`).
|
||||
`deviceFaultSupported`). Device features split by who uses them: the
|
||||
ones the binding model and the library's own shaders need are the
|
||||
library's to enable; arithmetic, storage and layout features an
|
||||
application's shaders use (`shaderInt16` for a `uint16_t` spec
|
||||
constant, `scalarBlockLayout`, fp16, 16-bit storage) are the
|
||||
application's to ask for, as core feature structs:
|
||||
`Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}})`. A second
|
||||
argument takes features to use where the GPU has them, and
|
||||
`Device::enabledFeatures` says which it got. A GPU that can't run the
|
||||
application is skipped. `VulkanShader` refuses a module that declares,
|
||||
directly or implicitly, a capability whose core feature, subgroup
|
||||
operation or optional group (ray tracing, the descriptor heap) is off,
|
||||
naming the field to pass or saying the GPU lacks it. Capabilities only
|
||||
an extension the library never enables could allow are left to the
|
||||
driver and the validation layer, as are layout rules
|
||||
(`scalarBlockLayout`), which have no capability.
|
||||
- **DescriptorHeapVulkan / DescriptorHeapWebGPU** — bindless slot
|
||||
allocators. Vulkan side allocates image/buffer/sampler slots in a
|
||||
`VK_EXT_descriptor_heap`; WebGPU side resolves slots to JS-side
|
||||
|
|
|
|||
|
|
@ -39,7 +39,9 @@ int main() {
|
|||
const int instanceCount = kGrid * kGrid * kGrid;
|
||||
std::println("[RTVolume] grid {}^3 = {} procedural spheres", kGrid, instanceCount);
|
||||
|
||||
Device::Initialize();
|
||||
// The raygen takes the heap's buffer start as a uint16_t spec constant,
|
||||
// and aabbs.comp declares its AABB buffer `layout(scalar)`.
|
||||
Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}, .vulkan12 = {.scalarBlockLayout = VK_TRUE}});
|
||||
Window window(1280, 720, "RTVolume");
|
||||
VkCommandBuffer cmd = window.StartInit();
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,9 @@ int main() {
|
|||
loadedMesh.vertexCount, loadedMesh.indexCount,
|
||||
loadedTex.sizeX, loadedTex.sizeY);
|
||||
|
||||
Device::Initialize();
|
||||
// The raygen and closest-hit shaders take heap slots as uint16_t spec
|
||||
// constants.
|
||||
Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}});
|
||||
Window window(1280, 720, "Sponza");
|
||||
VkCommandBuffer cmd = window.StartInit();
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,8 @@ int main() {
|
|||
std::println("[SponzaBench] scene: {} meshes, {} albedos, {}x{} probe albedo",
|
||||
meshCount, albedoCount, loadedTex.sizeX, loadedTex.sizeY);
|
||||
|
||||
Device::Initialize();
|
||||
// The raygen takes the heap's buffer start as a uint16_t spec constant.
|
||||
Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}});
|
||||
Window window(1280, 720, "SponzaBench");
|
||||
VkCommandBuffer cmd = window.StartInit();
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ The minimal ray-traced example. Renders a single static triangle through
|
|||
|
||||
## What it shows
|
||||
|
||||
- `Device::Initialize()` + `Window` + swapchain bring-up.
|
||||
- `Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}})` + `Window` +
|
||||
swapchain bring-up. The raygen takes the heap's buffer start as a
|
||||
`uint16_t` spec constant, which needs `shaderInt16`; without asking for
|
||||
it, loading the shader throws, naming the feature.
|
||||
- A `DescriptorHeapVulkan` sized for one image + one buffer slot, with
|
||||
slot ranges allocated via the bump-allocator API
|
||||
(`AllocateImageSlots`, `AllocateBufferSlots`).
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ import Crafter.Math;
|
|||
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
int main() {
|
||||
Device::Initialize();
|
||||
// The raygen takes the heap's buffer start as a uint16_t spec constant.
|
||||
Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}});
|
||||
Window window(1280, 720, "HelloVulkan");
|
||||
VkCommandBuffer cmd = window.StartInit();
|
||||
DescriptorHeapVulkan descriptorHeap;
|
||||
|
|
|
|||
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
|
||||
|
|
|
|||
|
|
@ -37,8 +37,52 @@ export namespace Crafter {
|
|||
};
|
||||
#endif
|
||||
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
// The core feature structs, one per Vulkan version: every feature promoted
|
||||
// to core is in exactly one of them, and Vulkan forbids naming a feature
|
||||
// through both its version's struct and the struct it was promoted from.
|
||||
// The features an application's own shaders and code use go to
|
||||
// Device::Initialize in these. Only the VkBool32 members are read, so sType
|
||||
// and pNext may be left zero, as `{.vulkan12 = {.shaderFloat16 = VK_TRUE}}`
|
||||
// leaves them. At namespace scope, like KeyRepeatState: a nested type with
|
||||
// member initializers can't be the type of an inline static inside Device.
|
||||
struct DeviceFeatures {
|
||||
VkPhysicalDeviceFeatures vulkan10 {};
|
||||
VkPhysicalDeviceVulkan11Features vulkan11 { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES };
|
||||
VkPhysicalDeviceVulkan12Features vulkan12 { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES };
|
||||
VkPhysicalDeviceVulkan13Features vulkan13 { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES };
|
||||
// Needs a Vulkan 1.4 GPU. Below 1.4 only maintenance5 can be on, which
|
||||
// the library enables through VK_KHR_maintenance5 there.
|
||||
VkPhysicalDeviceVulkan14Features vulkan14 { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_4_FEATURES };
|
||||
};
|
||||
// The features a GPU lacks, named the way an application writes them
|
||||
// ("vulkan12.shaderFloat16"), split by who needs them.
|
||||
struct FeatureGaps {
|
||||
std::vector<std::string> library;
|
||||
std::vector<std::string> application;
|
||||
};
|
||||
// One GPU as Device::ChooseDevice weighs it: no gaps means it can run the
|
||||
// application.
|
||||
struct DeviceChoice {
|
||||
VkPhysicalDeviceType type;
|
||||
std::string name;
|
||||
std::vector<std::string> gaps;
|
||||
};
|
||||
#endif
|
||||
|
||||
struct Device {
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
// Creates the instance, picks a GPU and creates the device. `required`
|
||||
// and `optional` are what the application's own shaders and code use;
|
||||
// the library adds what it needs itself. A GPU without Vulkan 1.3, a
|
||||
// required extension, the library's needs or a required feature is
|
||||
// skipped (saying why, when it ranks above the one chosen); when none is
|
||||
// left this throws, naming each GPU's gaps. Optional features are enabled
|
||||
// where the chosen GPU has them: enabledFeatures says which.
|
||||
static void Initialize(const DeviceFeatures& required = {}, const DeviceFeatures& optional = {});
|
||||
#else
|
||||
static void Initialize();
|
||||
#endif
|
||||
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||
|
|
@ -238,7 +282,10 @@ export namespace Crafter {
|
|||
// (DescriptorHeapVulkan sets mode), the
|
||||
// *.sets.spv shaders, vkCmdPushConstants
|
||||
// Setting the environment variable CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP
|
||||
// forces the second model on any driver, for testing.
|
||||
// forces the second model on any driver, for testing. In the same way
|
||||
// CRAFTER_GRAPHICS_NO_VULKAN_1_4 creates the instance as a Vulkan 1.3
|
||||
// application, so the 1.3 path (maintenance5 through
|
||||
// VK_KHR_maintenance5, no vulkan14 features) runs on a 1.4 GPU.
|
||||
inline static bool descriptorHeapSupported = false;
|
||||
// Ray tracing also needs the descriptor heap (RT pipelines and RT
|
||||
// shaders are heap-only), so this is never true without it.
|
||||
|
|
@ -253,6 +300,56 @@ export namespace Crafter {
|
|||
// tracing is unsupported. Called at every ray tracing entry point.
|
||||
static void RequireRayTracing();
|
||||
|
||||
// ─── Device features ───────────────────────────────────────────
|
||||
// Every feature Initialize enabled: the library's own, the
|
||||
// application's required ones, and whichever optional ones the GPU has.
|
||||
// pNext is null. Read only the fields you asked for: the library's own
|
||||
// may change. All false before Initialize.
|
||||
inline static DeviceFeatures enabledFeatures {};
|
||||
// Every feature the chosen GPU supports, enabled or not. Below Vulkan
|
||||
// 1.4 vulkan14 holds maintenance5 alone, from VK_KHR_maintenance5.
|
||||
inline static DeviceFeatures supportedFeatures {};
|
||||
// The Vulkan version the application may use on the chosen GPU:
|
||||
// min(the instance's, the GPU's). Zero before Initialize.
|
||||
inline static std::uint32_t apiVersion = 0;
|
||||
// The GPU's subgroup size, stages and operations, which
|
||||
// CheckShaderCapabilities holds GroupNonUniform* capabilities to.
|
||||
inline static VkPhysicalDeviceVulkan11Properties vulkan11Properties = {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_PROPERTIES
|
||||
};
|
||||
// True when shaderInt64 is on only for the NVIDIA workaround's SPIR-V
|
||||
// rewrite (below): an application shader that declares Int64 still
|
||||
// needs the application to ask for it, or it would run only on NVIDIA.
|
||||
inline static bool shaderInt64ForWorkaroundOnly = false;
|
||||
// The features to enable on a GPU that supports `supported`: library ∪
|
||||
// required ∪ (optional ∩ supported), never one the GPU lacks. Each one
|
||||
// in `library` or `required` that it lacks goes to `gaps`. Below an
|
||||
// apiVersion of 1.4 a vulkan14 one is marked "(Vulkan 1.4 only)", but
|
||||
// maintenance5, which VK_KHR_maintenance5 provides there, is marked as
|
||||
// that extension's. Pure logic, driven by tests/DeviceFeatures without
|
||||
// a GPU.
|
||||
static DeviceFeatures ResolveFeatures(const DeviceFeatures& library, const DeviceFeatures& required,
|
||||
const DeviceFeatures& optional, const DeviceFeatures& supported,
|
||||
std::uint32_t apiVersion, FeatureGaps& gaps);
|
||||
// The GPU to use: the best device type among those without gaps
|
||||
// (discrete, integrated, virtual, other), a CPU device only when no
|
||||
// other kind is listed. Prints why each better-ranked GPU was passed
|
||||
// over, and throws listing every GPU's gaps when none qualifies. Pure
|
||||
// logic apart from that print, driven by tests/DeviceFeatures.
|
||||
static std::size_t ChooseDevice(std::span<const DeviceChoice> choices);
|
||||
// Throws when `spirv` declares, directly or implicitly (Int64Atomics
|
||||
// declares Int64), a SPIR-V capability that nothing enabled: a core
|
||||
// feature that is off, a subgroup operation the GPU lacks, or one of
|
||||
// the library's optional groups (ray tracing, the descriptor heap) that
|
||||
// is off. Most drivers accept such a module silently. The message names
|
||||
// `file`, the capability, and either the field to pass to Initialize or
|
||||
// that the GPU lacks it. Capabilities a core version enables, and ones
|
||||
// no enabled extension can, are left to the driver and the validation
|
||||
// layer. VulkanShader calls it on every module, before the NVIDIA
|
||||
// rewrite. Reads enabledFeatures, supportedFeatures, apiVersion,
|
||||
// vulkan11Properties and the group flags, so tests can stage those.
|
||||
static void CheckShaderCapabilities(std::span<const std::uint32_t> spirv, const std::filesystem::path& file);
|
||||
|
||||
// The two property structs below are only filled in (and only chained
|
||||
// into the properties query) when their capability group is supported;
|
||||
// otherwise they stay zero.
|
||||
|
|
@ -456,4 +553,12 @@ export namespace Crafter {
|
|||
static void TickKeyRepeats() {}
|
||||
#endif
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
// Not exported: shared by Device's implementation units.
|
||||
namespace Crafter::DeviceDetail {
|
||||
// `parts` one after another, `separator` between them, for gap lists.
|
||||
std::string Join(std::span<const std::string> parts, std::string_view separator);
|
||||
}
|
||||
#endif
|
||||
|
|
@ -58,7 +58,7 @@ export namespace WorkaroundNvidiaAS {
|
|||
OpTypeAccelerationStructureKHR = 5341,
|
||||
CapabilityInt64 = 11,
|
||||
StorageClassPushConstant = 9,
|
||||
DecorationBlock = 2, DecorationMatrixStride = 7,
|
||||
DecorationBlock = 2, DecorationRowMajor = 4, DecorationMatrixStride = 7,
|
||||
DecorationArrayStride = 6, DecorationOffset = 35,
|
||||
};
|
||||
|
||||
|
|
@ -110,6 +110,7 @@ export namespace WorkaroundNvidiaAS {
|
|||
std::map<std::uint32_t, std::uint32_t> arrayStride; // array type id → ArrayStride
|
||||
std::map<std::uint64_t, std::uint32_t> memberOffset; // (struct<<32|idx) → Offset
|
||||
std::map<std::uint64_t, std::uint32_t> memberMatStride; // (struct<<32|idx) → MatrixStride
|
||||
std::set<std::uint64_t> memberRowMajor; // (struct<<32|idx) decorated RowMajor
|
||||
std::map<std::uint32_t, std::uint32_t> ptrPointee; // pointer type id → pointee type id
|
||||
for (std::size_t k = 0; k < instrs.size(); ++k) {
|
||||
const Instr& in = instrs[k];
|
||||
|
|
@ -150,6 +151,7 @@ export namespace WorkaroundNvidiaAS {
|
|||
std::uint64_t key = (static_cast<std::uint64_t>(in[1]) << 32) | in[2];
|
||||
if (in.size() >= 5 && in[3] == DecorationOffset) memberOffset[key] = in[4];
|
||||
if (in.size() >= 5 && in[3] == DecorationMatrixStride) memberMatStride[key] = in[4];
|
||||
if (in.size() >= 4 && in[3] == DecorationRowMajor) memberRowMajor.insert(key);
|
||||
break;
|
||||
}
|
||||
case OpCapability: lastCapIdx = k; break;
|
||||
|
|
@ -199,10 +201,20 @@ export namespace WorkaroundNvidiaAS {
|
|||
std::uint32_t sz;
|
||||
auto mt = typeInstr.find(t[m]);
|
||||
if (mt != typeInstr.end() && (mt->second->at(0) & 0xFFFFu) == OpTypeMatrix
|
||||
&& memberMatStride.count(key))
|
||||
sz = memberMatStride[key] * (*mt->second)[3];
|
||||
else
|
||||
&& memberMatStride.count(key)) {
|
||||
// MatrixStride separates the columns, or the rows
|
||||
// of a RowMajor member: as many as the column
|
||||
// type has components.
|
||||
const Instr& matrix = *mt->second;
|
||||
std::uint32_t vectors = matrix[3];
|
||||
if (memberRowMajor.count(key)) {
|
||||
auto column = typeInstr.find(matrix[2]);
|
||||
if (column != typeInstr.end()) vectors = (*column->second)[3];
|
||||
}
|
||||
sz = memberMatStride[key] * vectors;
|
||||
} else {
|
||||
sz = footprint(t[m]);
|
||||
}
|
||||
end = std::max(end, off + sz);
|
||||
}
|
||||
return end;
|
||||
|
|
@ -386,6 +398,15 @@ export namespace Crafter {
|
|||
|
||||
file.close();
|
||||
|
||||
if (spirv.size() < 5 || spirv[0] != 0x07230203u) {
|
||||
throw std::runtime_error(std::format("VulkanShader: {} is not SPIR-V (no SPIR-V header)", path.string()));
|
||||
}
|
||||
// Most drivers accept a module whose capability Initialize didn't
|
||||
// enable, so a feature the application forgot to ask for would only
|
||||
// show on a strict driver or under the validation layer. Checked on
|
||||
// the words as compiled, before the rewrite below adds its own Int64.
|
||||
Device::CheckShaderCapabilities(spirv, path);
|
||||
|
||||
// NVIDIA descriptor-heap AS-read workaround (issue #15 / #7).
|
||||
// No-op on every other driver and on shaders that don't read an
|
||||
// acceleration structure. Remove with the rest of the workaround
|
||||
|
|
|
|||
16
project.cpp
16
project.cpp
|
|
@ -255,10 +255,11 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
cfg.files.emplace_back(fs::path("additional/dom-webgpu.js"));
|
||||
}
|
||||
} else {
|
||||
std::array<fs::path, 14> impls = {
|
||||
std::array<fs::path, 15> impls = {
|
||||
"implementations/Crafter.Graphics-Clipboard",
|
||||
"implementations/Crafter.Graphics-ComputeShader",
|
||||
"implementations/Crafter.Graphics-Device",
|
||||
"implementations/Crafter.Graphics-Device-Features",
|
||||
"implementations/Crafter.Graphics-Font",
|
||||
"implementations/Crafter.Graphics-FontAtlas",
|
||||
"implementations/Crafter.Graphics-Gamepad",
|
||||
|
|
@ -719,6 +720,19 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
// stamps synthetic device identities + headers and drives it directly,
|
||||
// no GPU device needed at runtime.
|
||||
cfg.tests.push_back(LibraryTest("PipelineCacheValidation"));
|
||||
|
||||
// Which features Device::Initialize enables, which GPU it picks, and
|
||||
// which SPIR-V capabilities VulkanShader lets through:
|
||||
// Device::ResolveFeatures, ChooseDevice and CheckShaderCapabilities are
|
||||
// pure logic, so this test drives them with synthetic GPUs and
|
||||
// hand-built SPIR-V, no GPU device needed at runtime.
|
||||
cfg.tests.push_back(LibraryTest("DeviceFeatures"));
|
||||
|
||||
// The same on a real GPU, end to end: required and optional features
|
||||
// through Device::Initialize, and the Vulkan 1.3 path
|
||||
// (CRAFTER_GRAPHICS_NO_VULKAN_1_4) in a child process. Needs a Vulkan
|
||||
// device.
|
||||
cfg.tests.push_back(LibraryTest("FeatureRequests"));
|
||||
}
|
||||
|
||||
return cfg;
|
||||
|
|
|
|||
364
tests/DeviceFeatures/main.cpp
Normal file
364
tests/DeviceFeatures/main.cpp
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// Device::Initialize enables what the library's own shaders use plus what the
|
||||
// application passes it, skips a GPU that lacks any of that, and VulkanShader
|
||||
// refuses a module that declares a capability nobody enabled. The decisions
|
||||
// behind those are pure logic: Device::ResolveFeatures (the features to
|
||||
// enable, and the gaps a GPU has), Device::ChooseDevice (the GPU to use) and
|
||||
// Device::CheckShaderCapabilities (whether a module's capabilities are
|
||||
// enabled). This test drives them with synthetic GPUs and hand-built SPIR-V,
|
||||
// so it needs no GPU, mirroring PipelineCacheValidation. tests/FeatureRequests
|
||||
// checks the same wiring end to end on a real GPU.
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <stddef.h>
|
||||
#include "vulkan/vulkan.h"
|
||||
|
||||
import Crafter.Graphics;
|
||||
import std;
|
||||
using namespace Crafter;
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
|
||||
void Check(bool ok, std::string_view what) {
|
||||
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
||||
if (!ok) ++failures;
|
||||
}
|
||||
|
||||
// The five core feature structs as DeviceFeatures lays them out: where each
|
||||
// one's VkBool32 members start, and how many it has (vk.xml; a core feature
|
||||
// struct never changes once its version ships).
|
||||
struct Block {
|
||||
std::string_view version;
|
||||
std::size_t first;
|
||||
std::size_t count;
|
||||
};
|
||||
const std::array<Block, 5> blocks {{
|
||||
{ "vulkan10", offsetof(DeviceFeatures, vulkan10) + offsetof(VkPhysicalDeviceFeatures, robustBufferAccess), 55 },
|
||||
{ "vulkan11", offsetof(DeviceFeatures, vulkan11) + offsetof(VkPhysicalDeviceVulkan11Features, storageBuffer16BitAccess), 12 },
|
||||
{ "vulkan12", offsetof(DeviceFeatures, vulkan12) + offsetof(VkPhysicalDeviceVulkan12Features, samplerMirrorClampToEdge), 47 },
|
||||
{ "vulkan13", offsetof(DeviceFeatures, vulkan13) + offsetof(VkPhysicalDeviceVulkan13Features, robustImageAccess), 15 },
|
||||
{ "vulkan14", offsetof(DeviceFeatures, vulkan14) + offsetof(VkPhysicalDeviceVulkan14Features, globalPriorityQuery), 21 },
|
||||
}};
|
||||
|
||||
void SetSlot(DeviceFeatures& features, std::size_t offset) {
|
||||
const VkBool32 on = VK_TRUE;
|
||||
std::memcpy(reinterpret_cast<std::byte*>(&features) + offset, &on, sizeof(on));
|
||||
}
|
||||
|
||||
// Whether a and b have the same VkBool32s on, ignoring sType, pNext and
|
||||
// padding.
|
||||
bool SameFeatures(const DeviceFeatures& a, const DeviceFeatures& b) {
|
||||
for (const Block& block : blocks) {
|
||||
for (std::size_t i = 0; i < block.count; ++i) {
|
||||
const std::size_t offset = block.first + i * sizeof(VkBool32);
|
||||
VkBool32 x;
|
||||
VkBool32 y;
|
||||
std::memcpy(&x, reinterpret_cast<const std::byte*>(&a) + offset, sizeof(x));
|
||||
std::memcpy(&y, reinterpret_cast<const std::byte*>(&b) + offset, sizeof(y));
|
||||
if ((x != VK_FALSE) != (y != VK_FALSE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Every core feature on.
|
||||
DeviceFeatures AllOn() {
|
||||
DeviceFeatures features;
|
||||
for (const Block& block : blocks) {
|
||||
for (std::size_t i = 0; i < block.count; ++i) {
|
||||
SetSlot(features, block.first + i * sizeof(VkBool32));
|
||||
}
|
||||
}
|
||||
return features;
|
||||
}
|
||||
|
||||
// A module that declares Shader and then `capabilities`, ending the
|
||||
// capability section with an OpMemoryModel.
|
||||
std::vector<std::uint32_t> Module(std::initializer_list<std::uint32_t> capabilities) {
|
||||
constexpr std::uint32_t opCapability = 17;
|
||||
constexpr std::uint32_t opMemoryModel = 14;
|
||||
std::vector<std::uint32_t> words { 0x07230203u, 0x00010400u, 0, 1, 0 };
|
||||
words.insert(words.end(), { opCapability | (2u << 16), 1 /* Shader */ });
|
||||
for (std::uint32_t capability : capabilities) {
|
||||
words.insert(words.end(), { opCapability | (2u << 16), capability });
|
||||
}
|
||||
words.insert(words.end(), { opMemoryModel | (3u << 16), 0, 1 });
|
||||
return words;
|
||||
}
|
||||
|
||||
// The message CheckShaderCapabilities throws for `spirv`, or "" when it lets
|
||||
// the module through.
|
||||
std::string Refusal(const std::vector<std::uint32_t>& spirv) {
|
||||
try {
|
||||
Device::CheckShaderCapabilities(spirv, "shaders/test.comp.spv");
|
||||
} catch (const std::runtime_error& error) {
|
||||
return error.what();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// A 1.4 GPU with every core feature, enabled nowhere, and no optional group.
|
||||
void ResetDevice() {
|
||||
Device::enabledFeatures = {};
|
||||
Device::supportedFeatures = AllOn();
|
||||
Device::apiVersion = VK_API_VERSION_1_4;
|
||||
Device::vulkan11Properties = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_PROPERTIES };
|
||||
Device::vulkan11Properties.subgroupSupportedOperations = VK_SUBGROUP_FEATURE_BASIC_BIT;
|
||||
Device::shaderInt64ForWorkaroundOnly = false;
|
||||
Device::descriptorHeapSupported = false;
|
||||
Device::rayTracingSupported = false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
// --- what gets enabled --------------------------------------------------
|
||||
{
|
||||
DeviceFeatures library;
|
||||
library.vulkan10.samplerAnisotropy = VK_TRUE;
|
||||
// Designated initializers leave sType zero, as an application's will.
|
||||
const DeviceFeatures required { .vulkan12 = { .shaderFloat16 = VK_TRUE } };
|
||||
const DeviceFeatures optional {
|
||||
.vulkan11 = { .storageBuffer16BitAccess = VK_TRUE },
|
||||
.vulkan13 = { .synchronization2 = VK_TRUE },
|
||||
};
|
||||
DeviceFeatures supported = AllOn();
|
||||
supported.vulkan13.synchronization2 = VK_FALSE;
|
||||
|
||||
FeatureGaps gaps;
|
||||
const DeviceFeatures enabled = Device::ResolveFeatures(library, required, optional, supported, VK_API_VERSION_1_4, gaps);
|
||||
Check(gaps.library.empty() && gaps.application.empty(), "a GPU with everything needed has no gaps");
|
||||
Check(enabled.vulkan10.samplerAnisotropy && enabled.vulkan12.shaderFloat16,
|
||||
"the library's and the application's required features are enabled");
|
||||
Check(enabled.vulkan11.storageBuffer16BitAccess, "an optional feature the GPU has is enabled");
|
||||
Check(!enabled.vulkan13.synchronization2, "an optional feature the GPU lacks stays off, silently");
|
||||
Check(!enabled.vulkan10.shaderInt16 && !enabled.vulkan14.pushDescriptor,
|
||||
"nothing nobody asked for is enabled");
|
||||
Check(enabled.vulkan12.sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES && enabled.vulkan12.pNext == nullptr,
|
||||
"the result carries the sTypes and no pNext, ready to chain");
|
||||
}
|
||||
|
||||
// --- gaps, by who needs the feature -------------------------------------
|
||||
{
|
||||
DeviceFeatures library;
|
||||
library.vulkan10.samplerAnisotropy = VK_TRUE;
|
||||
const DeviceFeatures required { .vulkan12 = { .shaderFloat16 = VK_TRUE } };
|
||||
DeviceFeatures supported = AllOn();
|
||||
supported.vulkan10.samplerAnisotropy = VK_FALSE;
|
||||
supported.vulkan12.shaderFloat16 = VK_FALSE;
|
||||
|
||||
FeatureGaps gaps;
|
||||
const DeviceFeatures enabled = Device::ResolveFeatures(library, required, {}, supported, VK_API_VERSION_1_4, gaps);
|
||||
Check(gaps.library == std::vector<std::string>{ "vulkan10.samplerAnisotropy" },
|
||||
"a library need the GPU lacks is the library's gap");
|
||||
Check(gaps.application == std::vector<std::string>{ "vulkan12.shaderFloat16" },
|
||||
"a required feature the GPU lacks is the application's gap, named as the application writes it");
|
||||
Check(!enabled.vulkan12.shaderFloat16, "a feature the GPU lacks is never enabled");
|
||||
}
|
||||
|
||||
// --- Vulkan 1.3 GPUs ----------------------------------------------------
|
||||
{
|
||||
DeviceFeatures library;
|
||||
library.vulkan14.maintenance5 = VK_TRUE;
|
||||
const DeviceFeatures required { .vulkan14 = { .pushDescriptor = VK_TRUE } };
|
||||
// Below 1.4 a GPU supports no vulkan14 feature but maintenance5, which
|
||||
// Initialize copies there from VK_KHR_maintenance5's struct (that copy
|
||||
// is tests/FeatureRequests' to check, on a real GPU).
|
||||
DeviceFeatures supported = AllOn();
|
||||
supported.vulkan14 = { .maintenance5 = VK_TRUE };
|
||||
|
||||
FeatureGaps gaps;
|
||||
const DeviceFeatures enabled = Device::ResolveFeatures(library, required, {}, supported, VK_API_VERSION_1_3, gaps);
|
||||
Check(gaps.library.empty() && enabled.vulkan14.maintenance5,
|
||||
"at 1.3 a supported vulkan14.maintenance5 meets the library's need, with no version note");
|
||||
Check(gaps.application == std::vector<std::string>{ "vulkan14.pushDescriptor (Vulkan 1.4 only)" },
|
||||
"at 1.3 a required vulkan14 feature is a gap that names the version");
|
||||
|
||||
FeatureGaps noMaintenance5;
|
||||
DeviceFeatures without = AllOn();
|
||||
without.vulkan14 = {};
|
||||
Device::ResolveFeatures(library, {}, {}, without, VK_API_VERSION_1_3, noMaintenance5);
|
||||
Check(noMaintenance5.library == std::vector<std::string>{ "vulkan14.maintenance5 (VK_KHR_maintenance5 reports it unsupported)" },
|
||||
"at 1.3 a missing maintenance5 blames VK_KHR_maintenance5's feature bit, not the version");
|
||||
|
||||
FeatureGaps gaps14;
|
||||
DeviceFeatures supported14 = AllOn();
|
||||
supported14.vulkan14.pushDescriptor = VK_FALSE;
|
||||
Device::ResolveFeatures(library, required, {}, supported14, VK_API_VERSION_1_4, gaps14);
|
||||
Check(gaps14.application == std::vector<std::string>{ "vulkan14.pushDescriptor" },
|
||||
"at 1.4 the same gap carries no version note");
|
||||
}
|
||||
|
||||
// --- every core feature, by name ----------------------------------------
|
||||
{
|
||||
std::set<std::string> names;
|
||||
bool eachNamedOnce = true;
|
||||
bool eachEnabledAlone = true;
|
||||
bool eachUnderItsVersion = true;
|
||||
for (const Block& block : blocks) {
|
||||
for (std::size_t i = 0; i < block.count; ++i) {
|
||||
DeviceFeatures required;
|
||||
SetSlot(required, block.first + i * sizeof(VkBool32));
|
||||
FeatureGaps gaps;
|
||||
Device::ResolveFeatures({}, required, {}, {}, VK_API_VERSION_1_4, gaps);
|
||||
if (gaps.application.size() != 1 || !names.insert(gaps.application[0]).second) {
|
||||
eachNamedOnce = false;
|
||||
continue;
|
||||
}
|
||||
eachUnderItsVersion = eachUnderItsVersion && gaps.application[0].starts_with(std::format("{}.", block.version));
|
||||
// On a GPU with everything, only that one gets enabled.
|
||||
FeatureGaps none;
|
||||
const DeviceFeatures enabled = Device::ResolveFeatures({}, required, {}, AllOn(), VK_API_VERSION_1_4, none);
|
||||
eachEnabledAlone = eachEnabledAlone && SameFeatures(enabled, required);
|
||||
}
|
||||
}
|
||||
Check(eachNamedOnce && names.size() == 150, "each of the 150 core features has a gap name of its own");
|
||||
Check(eachUnderItsVersion, "each name starts with its struct's member in DeviceFeatures");
|
||||
Check(eachEnabledAlone, "requiring one feature enables that one alone");
|
||||
Check(names.contains("vulkan10.robustBufferAccess") && names.contains("vulkan10.shaderInt16")
|
||||
&& names.contains("vulkan11.storageBuffer16BitAccess") && names.contains("vulkan12.scalarBlockLayout")
|
||||
&& names.contains("vulkan14.pushDescriptor"),
|
||||
"names match the members (spot checks)");
|
||||
}
|
||||
|
||||
// --- choosing the GPU ---------------------------------------------------
|
||||
{
|
||||
const std::vector<std::string> noGaps;
|
||||
const std::vector<std::string> gap { "lacks vulkan12.shaderFloat16, which the application requires" };
|
||||
const DeviceChoice discrete { VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU, "dGPU", noGaps };
|
||||
const DeviceChoice discreteLacking { VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU, "old dGPU", gap };
|
||||
const DeviceChoice integrated { VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU, "iGPU", noGaps };
|
||||
const DeviceChoice integratedLacking { VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU, "old iGPU", gap };
|
||||
const DeviceChoice cpu { VK_PHYSICAL_DEVICE_TYPE_CPU, "llvmpipe", noGaps };
|
||||
|
||||
Check(Device::ChooseDevice(std::vector{ integrated, discrete }) == 1, "a discrete GPU wins over an integrated one");
|
||||
Check(Device::ChooseDevice(std::vector{ discrete, discrete }) == 0, "of two equal GPUs the first is used");
|
||||
Check(Device::ChooseDevice(std::vector{ discreteLacking, integrated, cpu }) == 1,
|
||||
"a GPU that lacks something is skipped for the next one that can run the application");
|
||||
Check(Device::ChooseDevice(std::vector{ cpu }) == 0, "a CPU device runs the application when nothing else is listed");
|
||||
|
||||
std::string message;
|
||||
try {
|
||||
Device::ChooseDevice(std::vector{ discreteLacking, integratedLacking, cpu });
|
||||
} catch (const std::runtime_error& error) {
|
||||
message = error.what();
|
||||
}
|
||||
Check(!message.empty(), "GPUs that lack something don't fall back to a CPU device");
|
||||
Check(message.contains("old dGPU: lacks vulkan12.shaderFloat16") && message.contains("old iGPU: lacks")
|
||||
&& message.contains("llvmpipe: a CPU device"),
|
||||
"the error names every GPU and why it can't be used");
|
||||
|
||||
std::string none;
|
||||
try {
|
||||
Device::ChooseDevice(std::vector<DeviceChoice>{});
|
||||
} catch (const std::runtime_error& error) {
|
||||
none = error.what();
|
||||
}
|
||||
Check(none.contains("lists none"), "no GPU at all is an error that says so");
|
||||
}
|
||||
|
||||
// --- SPIR-V capabilities against the enabled features -------------------
|
||||
{
|
||||
constexpr std::uint32_t int16 = 22;
|
||||
constexpr std::uint32_t int64 = 11;
|
||||
constexpr std::uint32_t int64Atomics = 12;
|
||||
constexpr std::uint32_t storageImageReadWithoutFormat = 55;
|
||||
constexpr std::uint32_t groupNonUniform = 61;
|
||||
constexpr std::uint32_t groupNonUniformArithmetic = 63;
|
||||
constexpr std::uint32_t shaderNonUniform = 5301;
|
||||
constexpr std::uint32_t rayQueryKHR = 4472;
|
||||
constexpr std::uint32_t descriptorHeapEXT = 5128;
|
||||
constexpr std::uint32_t groupNonUniformRotateKHR = 6026;
|
||||
constexpr std::uint32_t floatControls2 = 6029;
|
||||
|
||||
ResetDevice();
|
||||
const std::string refusal = Refusal(Module({ int16 }));
|
||||
Check(refusal.contains("shaders/test.comp.spv") && refusal.contains("Int16")
|
||||
&& refusal.contains("vulkan10.shaderInt16: pass it to Device::Initialize"),
|
||||
"Int16 without shaderInt16, on a GPU that has it, is refused, naming the file and the field to pass");
|
||||
Device::enabledFeatures.vulkan10.shaderInt16 = VK_TRUE;
|
||||
Check(Refusal(Module({ int16 })).empty(), "...and accepted once shaderInt16 is enabled");
|
||||
|
||||
ResetDevice();
|
||||
Device::supportedFeatures.vulkan10.shaderInt16 = VK_FALSE;
|
||||
Check(Refusal(Module({ int16 })).contains("this GPU lacks it"),
|
||||
"on a GPU without shaderInt16 the refusal says the GPU lacks it, not to pass it");
|
||||
|
||||
ResetDevice();
|
||||
Device::apiVersion = VK_API_VERSION_1_3;
|
||||
Device::supportedFeatures.vulkan14 = { .maintenance5 = VK_TRUE };
|
||||
Check(Refusal(Module({ floatControls2 })).contains("a Vulkan 1.4 feature, and this GPU runs Vulkan 1.3"),
|
||||
"a vulkan14 capability on a 1.3 GPU names the version");
|
||||
ResetDevice();
|
||||
Check(Refusal(Module({ floatControls2 })).contains("vulkan14.shaderFloatControls2: pass it"),
|
||||
"FloatControls2 is refused without vulkan14.shaderFloatControls2");
|
||||
Device::enabledFeatures.vulkan14.shaderFloatControls2 = VK_TRUE;
|
||||
Check(Refusal(Module({ floatControls2 })).empty(), "...and accepted with it");
|
||||
|
||||
ResetDevice();
|
||||
Check(Refusal(Module({ storageImageReadWithoutFormat, shaderNonUniform })).empty(),
|
||||
"capabilities a core version enables are left to the driver");
|
||||
|
||||
ResetDevice();
|
||||
Device::enabledFeatures.vulkan10.shaderInt64 = VK_TRUE;
|
||||
Device::shaderInt64ForWorkaroundOnly = true;
|
||||
Check(Refusal(Module({ int64 })).contains("NVIDIA workaround"),
|
||||
"Int64 on only for the NVIDIA workaround doesn't let an application's Int64 through");
|
||||
Device::shaderInt64ForWorkaroundOnly = false;
|
||||
Check(Refusal(Module({ int64 })).empty(), "...but does once the application asked for shaderInt64");
|
||||
|
||||
ResetDevice();
|
||||
Device::enabledFeatures.vulkan12.shaderSharedInt64Atomics = VK_TRUE;
|
||||
Check(Refusal(Module({ int64Atomics })).contains("Int64Atomics, which implicitly declares Int64"),
|
||||
"Int64Atomics alone still needs shaderInt64: it implicitly declares Int64");
|
||||
Device::enabledFeatures.vulkan10.shaderInt64 = VK_TRUE;
|
||||
Check(Refusal(Module({ int64Atomics })).empty(), "a capability with two enabling features takes either");
|
||||
Device::shaderInt64ForWorkaroundOnly = true;
|
||||
Check(Refusal(Module({ int64Atomics })).contains("NVIDIA workaround"),
|
||||
"an implied Int64 doesn't pass on the NVIDIA workaround's shaderInt64 either");
|
||||
|
||||
ResetDevice();
|
||||
Check(Refusal(Module({ groupNonUniformArithmetic })).contains("VK_SUBGROUP_FEATURE_ARITHMETIC_BIT"),
|
||||
"subgroup arithmetic the GPU lacks is refused");
|
||||
Device::vulkan11Properties.subgroupSupportedOperations |= VK_SUBGROUP_FEATURE_ARITHMETIC_BIT;
|
||||
Check(Refusal(Module({ groupNonUniformArithmetic })).empty(),
|
||||
"...and accepted where the GPU has it, whatever the stage: Vulkan gates running subgroup operations by stage, not declaring them");
|
||||
Device::enabledFeatures.vulkan14.shaderSubgroupRotate = VK_TRUE;
|
||||
Device::vulkan11Properties.subgroupSupportedOperations = 0;
|
||||
Check(Refusal(Module({ groupNonUniformRotateKHR })).contains("implicitly declares GroupNonUniform, but this GPU has no"),
|
||||
"GroupNonUniformRotateKHR implicitly declares GroupNonUniform, and is held to it");
|
||||
|
||||
ResetDevice();
|
||||
Check(Refusal(Module({ rayQueryKHR })).contains("Device::rayTracingSupported"),
|
||||
"a ray-query module is refused where ray tracing is off");
|
||||
Device::rayTracingSupported = true;
|
||||
Check(Refusal(Module({ rayQueryKHR })).empty(), "...and accepted where it is on");
|
||||
Check(Refusal(Module({ descriptorHeapEXT })).contains("Device::descriptorHeapSupported"),
|
||||
"a descriptor-heap module is refused where the heap is off");
|
||||
Device::descriptorHeapSupported = true;
|
||||
Check(Refusal(Module({ descriptorHeapEXT })).empty(), "...and accepted where it is on");
|
||||
|
||||
ResetDevice();
|
||||
std::vector<std::uint32_t> afterLayout = Module({});
|
||||
afterLayout.insert(afterLayout.end(), { 17u | (2u << 16), int16 });
|
||||
Check(Refusal(afterLayout).empty(), "only the capability section at the start of the module is read");
|
||||
std::vector<std::uint32_t> malformed { 0x07230203u, 0x00010400u, 0, 1, 0, 17u /* length 0 */ };
|
||||
Check(Refusal(malformed).empty(), "a malformed instruction ends the scan instead of running past the module");
|
||||
Check(Refusal(Module({ groupNonUniform })).empty(), "GroupNonUniform passes on the BASIC operations every GPU has");
|
||||
}
|
||||
ResetDevice();
|
||||
|
||||
if (failures != 0) {
|
||||
std::println("{} check(s) failed", failures);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
std::println("all checks passed");
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
138
tests/FeatureRequests/main.cpp
Normal file
138
tests/FeatureRequests/main.cpp
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// Device::Initialize's feature requests on a real GPU, end to end: a required
|
||||
// feature ends up enabled, an optional one exactly where the GPU supports it,
|
||||
// and one nobody asked for stays off. On the Vulkan 1.3 path
|
||||
// (CRAFTER_GRAPHICS_NO_VULKAN_1_4, run in a child process, since an instance's
|
||||
// version is fixed for the process) maintenance5 comes from
|
||||
// VK_KHR_maintenance5 and no other vulkan14 feature can be had. And a refusal
|
||||
// leaves nothing behind: after Initialize throws for want of a GPU, calling it
|
||||
// again asking for less gives a working window (also a child process).
|
||||
// tests/DeviceFeatures checks the logic behind this without a GPU; this checks
|
||||
// the wiring between Initialize, EvaluateDevice and ResolveFeatures, and the
|
||||
// 1.3 device chain, which nothing else runs. Neither run resets
|
||||
// validationErrorCount after Initialize, so with CRAFTER_GRAPHICS_VALIDATION
|
||||
// set vkCreateDevice is checked too. Needs a Vulkan device.
|
||||
|
||||
#include <cstdlib>
|
||||
#include "vulkan/vulkan.h"
|
||||
|
||||
import Crafter.Graphics;
|
||||
import std;
|
||||
using namespace Crafter;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
|
||||
void Check(bool ok, std::string_view what) {
|
||||
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
||||
if (!ok) ++failures;
|
||||
}
|
||||
|
||||
int RunCommand(const std::string& cmd) {
|
||||
int status = std::system(cmd.c_str());
|
||||
if (status == -1) return -1;
|
||||
return (status & 0x7f) == 0 ? ((status >> 8) & 0xff) : 128 + (status & 0x7f);
|
||||
}
|
||||
|
||||
bool Same(VkBool32 a, VkBool32 b) {
|
||||
return (a != VK_FALSE) == (b != VK_FALSE);
|
||||
}
|
||||
|
||||
// Initializes with one required and three optional features, checks what
|
||||
// was enabled, and makes a buffer: every VulkanBuffer goes through
|
||||
// maintenance5's VkBufferUsageFlags2CreateInfo, whichever way it was enabled.
|
||||
void Run(bool vulkan13) {
|
||||
const std::string_view path = vulkan13 ? "at 1.3: " : "";
|
||||
Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}}, {
|
||||
.vulkan11 = {.storageBuffer16BitAccess = VK_TRUE},
|
||||
.vulkan12 = {.shaderFloat16 = VK_TRUE},
|
||||
.vulkan14 = {.pushDescriptor = VK_TRUE},
|
||||
});
|
||||
const DeviceFeatures& enabled = Device::enabledFeatures;
|
||||
const DeviceFeatures& supported = Device::supportedFeatures;
|
||||
|
||||
Check(enabled.vulkan10.shaderInt16, std::format("{}a required feature is enabled", path));
|
||||
Check(Same(enabled.vulkan11.storageBuffer16BitAccess, supported.vulkan11.storageBuffer16BitAccess)
|
||||
&& Same(enabled.vulkan12.shaderFloat16, supported.vulkan12.shaderFloat16),
|
||||
std::format("{}an optional feature is enabled exactly where the GPU supports it", path));
|
||||
Check(!enabled.vulkan10.geometryShader && !enabled.vulkan12.shaderInt8,
|
||||
std::format("{}a feature nobody asked for stays off", path));
|
||||
Check(enabled.vulkan12.bufferDeviceAddress && enabled.vulkan14.maintenance5,
|
||||
std::format("{}the library's own needs are on, maintenance5 among them", path));
|
||||
if (vulkan13) {
|
||||
Check(VK_API_VERSION_MAJOR(Device::apiVersion) == 1 && VK_API_VERSION_MINOR(Device::apiVersion) == 3,
|
||||
std::format("CRAFTER_GRAPHICS_NO_VULKAN_1_4 runs the application as Vulkan 1.3 (got {}.{})",
|
||||
VK_API_VERSION_MAJOR(Device::apiVersion), VK_API_VERSION_MINOR(Device::apiVersion)));
|
||||
Check(supported.vulkan14.maintenance5 && !supported.vulkan14.pushDescriptor,
|
||||
"at 1.3: vulkan14 holds maintenance5 alone, copied from VK_KHR_maintenance5");
|
||||
Check(!enabled.vulkan14.pushDescriptor, "at 1.3: an optional vulkan14 feature stays off");
|
||||
} else {
|
||||
Check(Same(enabled.vulkan14.pushDescriptor, supported.vulkan14.pushDescriptor),
|
||||
"an optional vulkan14 feature is enabled exactly where the GPU supports it");
|
||||
}
|
||||
|
||||
VulkanBuffer<std::uint32_t, true> buffer;
|
||||
buffer.Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
|
||||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, 16);
|
||||
Check(buffer.address != 0, std::format("{}a buffer made through maintenance5's usage flags has an address", path));
|
||||
|
||||
if (std::getenv("CRAFTER_GRAPHICS_VALIDATION") != nullptr) {
|
||||
Check(Device::validationErrorCount == 0,
|
||||
std::format("{}no Vulkan validation errors, device creation included ({} seen)", path, Device::validationErrorCount));
|
||||
}
|
||||
buffer.Clear();
|
||||
}
|
||||
|
||||
// A refusal takes back what Initialize made, so asking again for less works.
|
||||
// At 1.3 no GPU has vulkan14.pushDescriptor, so requiring it must throw; the
|
||||
// retry must then bring up a window, on a Wayland connection of its own, that
|
||||
// renders.
|
||||
void Retry() {
|
||||
bool refused = false;
|
||||
try {
|
||||
Device::Initialize({.vulkan14 = {.pushDescriptor = VK_TRUE}});
|
||||
} catch (const std::runtime_error& error) {
|
||||
refused = std::string_view(error.what()).contains("vulkan14.pushDescriptor (Vulkan 1.4 only)");
|
||||
}
|
||||
Check(refused, "at 1.3: requiring a vulkan14 feature leaves no GPU, and the error names it");
|
||||
Check(Device::instance == VK_NULL_HANDLE, "the refusal destroyed the instance it had made");
|
||||
|
||||
Device::Initialize();
|
||||
Window window(320, 240, "FeatureRequests retry");
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
window.Render();
|
||||
}
|
||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||
Check(window.frameCounter == 4, "after a refusal, Initialize asking for less brings up a window that renders");
|
||||
if (std::getenv("CRAFTER_GRAPHICS_VALIDATION") != nullptr) {
|
||||
Check(Device::validationErrorCount == 0,
|
||||
std::format("no Vulkan validation errors across the refusal and the retry ({} seen)", Device::validationErrorCount));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--vulkan13") {
|
||||
Run(true);
|
||||
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--retry") {
|
||||
Retry();
|
||||
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
|
||||
const fs::path exe = fs::read_symlink("/proc/self/exe");
|
||||
const int exit = RunCommand(std::format("CRAFTER_GRAPHICS_NO_VULKAN_1_4=1 '{}' --vulkan13", exe.string()));
|
||||
Check(exit == 0, std::format("the Vulkan 1.3 path passes its checks (child exit {})", exit));
|
||||
const int retry = RunCommand(std::format("CRAFTER_GRAPHICS_NO_VULKAN_1_4=1 '{}' --retry", exe.string()));
|
||||
Check(retry == 0, std::format("Initialize can be retried after a refusal (child exit {})", retry));
|
||||
Run(false);
|
||||
|
||||
std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed");
|
||||
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
|
|
@ -119,7 +119,10 @@ constexpr std::array<std::uint8_t, 3> miss = {128, 128, 128};
|
|||
// Renders the triangle through `path` into `out` and checks the pixels.
|
||||
// Returns the exit code.
|
||||
int Render(Path path, const fs::path& out) {
|
||||
Device::Initialize();
|
||||
// The raygen takes the heap's buffer start as a uint16_t spec constant.
|
||||
// Required, not optional: every ray-tracing GPU has shaderInt16, so 77
|
||||
// stays "no ray tracing", which Probe reports as such.
|
||||
Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}});
|
||||
if (!Device::rayTracingSupported) {
|
||||
std::println("SKIP no Vulkan ray-tracing device");
|
||||
return 77;
|
||||
|
|
|
|||
|
|
@ -380,7 +380,12 @@ fs::path OutputDir() {
|
|||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
Device::Initialize();
|
||||
// The shaders take heap slots as uint16_t spec constants, and the
|
||||
// closest-hit indexes buffers non-uniformly.
|
||||
Device::Initialize({
|
||||
.vulkan10 = {.shaderInt16 = VK_TRUE},
|
||||
.vulkan12 = {.shaderStorageBufferArrayNonUniformIndexing = VK_TRUE},
|
||||
});
|
||||
if (!Device::rayTracingSupported) {
|
||||
std::println("SKIP no Vulkan ray-tracing device");
|
||||
return 77;
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ constexpr std::string_view kHeader =
|
|||
"layout(descriptor_heap) uniform writeonly image2D image[];\n"
|
||||
"layout(location = 0) rayPayloadEXT vec3 hitValue;\n";
|
||||
|
||||
const std::array<Case, 5> kCases = {{
|
||||
const std::array<Case, 6> kCases = {{
|
||||
// No push constant at all → Patch synthesizes a fresh single-member block at offset 0.
|
||||
{ "no-push-constant", std::string_view{
|
||||
""
|
||||
|
|
@ -114,6 +114,12 @@ const std::array<Case, 5> kCases = {{
|
|||
"layout(push_constant) uniform PC { vec4 v[2]; uint f; } pc;\n"
|
||||
}, true, true, 40 },
|
||||
|
||||
// Existing block {row-major mat3x4 @0}: 3 columns of 4 rows, stored as 4
|
||||
// rows 16 bytes apart, so it ends at 64, not at the 48 its 3 columns give.
|
||||
{ "merge-row-major-mat3x4", std::string_view{
|
||||
"layout(push_constant) uniform PC { layout(row_major) mat3x4 m; } pc;\n"
|
||||
}, true, true, 64 },
|
||||
|
||||
// Push constant but NO acceleration-structure read → Patch is a no-op; the
|
||||
// single user block must survive untouched and still validate.
|
||||
{ "push-constant-no-as", std::string_view{
|
||||
|
|
@ -133,7 +139,9 @@ std::string BuildSource(const Case& c) {
|
|||
// Reference the push constant so glslang keeps the block in the module.
|
||||
std::string_view g = c.glsl;
|
||||
std::string extra = "vec4(hitValue, 1.0)";
|
||||
if (g.find("mat4 m;") != std::string_view::npos)
|
||||
if (g.find("mat3x4 m;") != std::string_view::npos)
|
||||
extra = "vec4(hitValue, 1.0) + pc.m[2]";
|
||||
else if (g.find("mat4 m;") != std::string_view::npos)
|
||||
extra = "pc.m * vec4(hitValue, 1.0) + vec4(pc.l, float(pc.f))";
|
||||
else if (g.find("uint f; } pc;") != std::string_view::npos && g.find("vec4 v[2]") != std::string_view::npos)
|
||||
extra = "vec4(hitValue, 1.0) + pc.v[0] + pc.v[1] + vec4(float(pc.f))";
|
||||
|
|
|
|||
|
|
@ -64,7 +64,8 @@ struct Push {
|
|||
} // namespace
|
||||
|
||||
int main() {
|
||||
Device::Initialize();
|
||||
// The raygen takes the heap's buffer start as a uint16_t spec constant.
|
||||
Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}});
|
||||
if (!Device::rayTracingSupported) {
|
||||
std::println("SKIP no Vulkan ray-tracing device");
|
||||
return 77;
|
||||
|
|
|
|||
Loading…
Reference in a new issue