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
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue