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:
Jorijn van der Graaf 2026-10-31 06:40:02 +01:00
commit bcdc885de8
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
18 changed files with 1721 additions and 333 deletions

View file

@ -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