feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap
Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
6aa0338c0f
commit
48aa9be5ce
22 changed files with 912 additions and 110 deletions
|
|
@ -7,12 +7,14 @@ module Crafter.Graphics:ComputeShader_impl;
|
|||
import :ComputeShader;
|
||||
import :ShaderVulkan;
|
||||
import :Device;
|
||||
import :DescriptorHeapVulkan;
|
||||
import std;
|
||||
|
||||
using namespace Crafter;
|
||||
|
||||
ComputeShader::ComputeShader(ComputeShader&& other) noexcept
|
||||
: pipeline(other.pipeline),
|
||||
layout(other.layout),
|
||||
workaroundNeedsTlas(other.workaroundNeedsTlas),
|
||||
workaroundTlasPushOffset(other.workaroundTlasPushOffset) {
|
||||
other.pipeline = VK_NULL_HANDLE;
|
||||
|
|
@ -24,6 +26,7 @@ ComputeShader& ComputeShader::operator=(ComputeShader&& other) noexcept {
|
|||
vkDestroyPipeline(Device::device, pipeline, nullptr);
|
||||
}
|
||||
pipeline = other.pipeline;
|
||||
layout = other.layout;
|
||||
workaroundNeedsTlas = other.workaroundNeedsTlas;
|
||||
workaroundTlasPushOffset = other.workaroundTlasPushOffset;
|
||||
other.pipeline = VK_NULL_HANDLE;
|
||||
|
|
@ -38,7 +41,44 @@ ComputeShader::~ComputeShader() {
|
|||
}
|
||||
}
|
||||
|
||||
void ComputeShader::Load(const std::filesystem::path& spvPath) {
|
||||
void ComputeShader::Load(const std::filesystem::path& spvPath, DescriptorHeapVulkan& heap) {
|
||||
if (!Device::descriptorHeapSupported) {
|
||||
// Sets mode. The heap build of the shader uses SPV_EXT_descriptor_heap
|
||||
// and untyped pointers, which this device cannot consume, so load the
|
||||
// descriptor-set build compiled from the same source instead.
|
||||
std::filesystem::path setsPath = spvPath;
|
||||
setsPath.replace_extension("sets.spv");
|
||||
// VulkanShader only prints when a file is missing and then creates an
|
||||
// empty module, which surfaces as an unrelated driver error. A missing
|
||||
// variant is the likely mistake when a bundle is copied to a device
|
||||
// without the heap, so name it.
|
||||
if (!std::filesystem::exists(setsPath)) {
|
||||
throw std::runtime_error(std::format(
|
||||
"ComputeShader::Load: this device has no VK_EXT_descriptor_heap, so the descriptor-set build "
|
||||
"of the shader is required, but {} does not exist. Build the shader a second time with "
|
||||
"CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined and ship it next to {}.",
|
||||
setsPath.string(), spvPath.string()));
|
||||
}
|
||||
if (heap.pipelineLayout == VK_NULL_HANDLE) {
|
||||
throw std::runtime_error("ComputeShader::Load: the descriptor heap must be Initialize()d before a shader is loaded against it");
|
||||
}
|
||||
VulkanShader shader(setsPath, "main", VK_SHADER_STAGE_COMPUTE_BIT, nullptr);
|
||||
layout = heap.pipelineLayout;
|
||||
VkComputePipelineCreateInfo info {
|
||||
.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
|
||||
.stage = {
|
||||
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
|
||||
.stage = VK_SHADER_STAGE_COMPUTE_BIT,
|
||||
.module = shader.shader,
|
||||
.pName = "main",
|
||||
},
|
||||
.layout = layout,
|
||||
};
|
||||
Device::CheckVkResult(vkCreateComputePipelines(
|
||||
Device::device, Device::pipelineCache, 1, &info, nullptr, &pipeline));
|
||||
return;
|
||||
}
|
||||
|
||||
VulkanShader shader(spvPath, "main", VK_SHADER_STAGE_COMPUTE_BIT, nullptr);
|
||||
|
||||
// NVIDIA descriptor-heap AS-read workaround (issue #15 / #7): remember
|
||||
|
|
@ -78,12 +118,38 @@ void ComputeShader::Dispatch(VkCommandBuffer cmd,
|
|||
VkDeviceAddress tlasAddress) const {
|
||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
|
||||
if (push != nullptr && pushBytes > 0) {
|
||||
VkPushDataInfoEXT pushInfo {
|
||||
.sType = VK_STRUCTURE_TYPE_PUSH_DATA_INFO_EXT,
|
||||
.offset = 0,
|
||||
.data = { .address = const_cast<void*>(push), .size = pushBytes },
|
||||
};
|
||||
Device::vkCmdPushDataEXT(cmd, &pushInfo);
|
||||
if (!Device::descriptorHeapSupported) {
|
||||
// Sets mode: the same bytes go through the shared layout's
|
||||
// push-constant range, which DescriptorHeapVulkan sized to the
|
||||
// device limit rounded down to whole words.
|
||||
const std::uint32_t limit = Device::deviceProperties.limits.maxPushConstantsSize & ~3u;
|
||||
const std::uint32_t wholeWords = (pushBytes + 3u) & ~3u;
|
||||
if (wholeWords > limit) {
|
||||
throw std::length_error(std::format(
|
||||
"ComputeShader::Dispatch: {} bytes of push constants exceed this device's limit of {}",
|
||||
pushBytes, limit));
|
||||
}
|
||||
if (wholeWords == pushBytes) {
|
||||
vkCmdPushConstants(cmd, layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, pushBytes, push);
|
||||
} else {
|
||||
// vkCmdPushConstants only takes whole words, and the caller's
|
||||
// buffer cannot be read past its end, so push a padded copy.
|
||||
// vkCmdPushDataEXT has the same rule, but nothing enforces it
|
||||
// on the heap path and drivers accept a ragged size there;
|
||||
// padding keeps such a payload from failing only on a device
|
||||
// without the heap.
|
||||
std::vector<std::byte> padded(wholeWords);
|
||||
std::memcpy(padded.data(), push, pushBytes);
|
||||
vkCmdPushConstants(cmd, layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, wholeWords, padded.data());
|
||||
}
|
||||
} else {
|
||||
VkPushDataInfoEXT pushInfo {
|
||||
.sType = VK_STRUCTURE_TYPE_PUSH_DATA_INFO_EXT,
|
||||
.offset = 0,
|
||||
.data = { .address = const_cast<void*>(push), .size = pushBytes },
|
||||
};
|
||||
Device::vkCmdPushDataEXT(cmd, &pushInfo);
|
||||
}
|
||||
}
|
||||
// NVIDIA descriptor-heap AS-read workaround (issue #15 / #7): if this shader
|
||||
// ray-queries the TLAS through the heap it was rewritten to read the TLAS
|
||||
|
|
|
|||
|
|
@ -41,19 +41,31 @@ const char* const instanceExtensionNames[] = {
|
|||
"VK_KHR_wayland_surface"
|
||||
#endif
|
||||
};
|
||||
const char* const deviceExtensionNames[] = {
|
||||
// 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.
|
||||
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.
|
||||
const char* const descriptorHeapExtensionNames[] = {
|
||||
"VK_EXT_descriptor_heap",
|
||||
"VK_KHR_shader_untyped_pointers",
|
||||
};
|
||||
// Ray tracing additionally needs the descriptor heap: every RT pipeline and
|
||||
// RT shader in this library is heap-only.
|
||||
const char* const rayTracingExtensionNames[] = {
|
||||
"VK_KHR_acceleration_structure",
|
||||
"VK_KHR_ray_tracing_pipeline",
|
||||
"VK_KHR_ray_query",
|
||||
"VK_EXT_shader_atomic_float",
|
||||
"VK_EXT_descriptor_heap",
|
||||
"VK_KHR_deferred_host_operations",
|
||||
"VK_KHR_maintenance5",
|
||||
"VK_KHR_shader_untyped_pointers",
|
||||
"VK_EXT_device_fault"
|
||||
};
|
||||
const char* const layerNames[] = {
|
||||
"VK_LAYER_KHRONOS_validation"
|
||||
|
|
@ -63,7 +75,9 @@ const char* const layerNames[] = {
|
|||
void Device::CheckVkResult(VkResult result) {
|
||||
if (result != VK_SUCCESS)
|
||||
{
|
||||
if(result == VK_ERROR_DEVICE_LOST) {
|
||||
// The fault report needs VK_EXT_device_fault; without it the entry
|
||||
// point below is not callable and the bare VkResult is all there is.
|
||||
if(result == VK_ERROR_DEVICE_LOST && deviceFaultSupported) {
|
||||
VkDeviceFaultCountsEXT faultCounts = {
|
||||
.sType = VK_STRUCTURE_TYPE_DEVICE_FAULT_COUNTS_EXT,
|
||||
.pNext = NULL,
|
||||
|
|
@ -119,6 +133,26 @@ void Device::CheckVkResult(VkResult result) {
|
|||
}
|
||||
}
|
||||
|
||||
void Device::RequireRayTracing() {
|
||||
if (rayTracingSupported) {
|
||||
return;
|
||||
}
|
||||
// Thrown at the first ray tracing entry point (an RT-stage shader module,
|
||||
// a BLAS/TLAS build, the SBT, the pipeline) so the failure names the
|
||||
// cause. Letting the call through would hand heap-only SPIR-V or an
|
||||
// unloaded entry point to a driver that never enabled the feature.
|
||||
if (descriptorHeapSupported) {
|
||||
throw std::runtime_error(
|
||||
"Crafter.Graphics: ray tracing is unavailable: the Vulkan driver lacks one of "
|
||||
"VK_KHR_acceleration_structure, VK_KHR_ray_tracing_pipeline, VK_KHR_ray_query, "
|
||||
"VK_KHR_deferred_host_operations");
|
||||
}
|
||||
throw std::runtime_error(
|
||||
"Crafter.Graphics: ray tracing is unavailable: it needs VK_EXT_descriptor_heap and "
|
||||
"VK_KHR_shader_untyped_pointers, which this driver lacks (or "
|
||||
"CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP is set). Only the compute/UI path runs here.");
|
||||
}
|
||||
|
||||
bool Device::PipelineCacheDataCompatible(std::span<const std::byte> data) {
|
||||
// VkPipelineCacheHeaderVersionOne is a fixed 32-byte little-endian header
|
||||
// (Vulkan spec 16.5.2): u32 headerSize, u32 headerVersion, u32 vendorID,
|
||||
|
|
@ -651,36 +685,130 @@ void Device::Initialize() {
|
|||
}
|
||||
}
|
||||
|
||||
// Enumerate available device extensions so we can opt into
|
||||
// VK_EXT_memory_decompression when the driver advertises it. Drivers
|
||||
// without it (AMD, Intel as of early 2026) get the CPU-decode fallback.
|
||||
// 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) {
|
||||
if (std::strcmp(e.extensionName, VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME) == 0) {
|
||||
memoryDecompressionSupported = true;
|
||||
break;
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
// Properties query: chain memory-decompression props only when supported,
|
||||
// otherwise sType validation flags it as an unrecognized struct on
|
||||
// drivers that don't expose the extension.
|
||||
if (memoryDecompressionSupported) {
|
||||
memoryDecompressionProperties.pNext = const_cast<void*>(rayTracingProperties.pNext);
|
||||
rayTracingProperties.pNext = &memoryDecompressionProperties;
|
||||
// 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);
|
||||
// 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
|
||||
? ~VkPipelineStageFlags{0}
|
||||
: ~VkPipelineStageFlags{VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR};
|
||||
|
||||
// Properties query, built back to front. A struct is chained only when its
|
||||
// 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).
|
||||
void* propertiesChain = &driverProperties;
|
||||
if (descriptorHeapSupported) {
|
||||
descriptorHeapProperties.pNext = propertiesChain;
|
||||
propertiesChain = &descriptorHeapProperties;
|
||||
}
|
||||
if (rayTracingSupported) {
|
||||
rayTracingProperties.pNext = propertiesChain;
|
||||
propertiesChain = &rayTracingProperties;
|
||||
}
|
||||
if (memoryDecompressionSupported) {
|
||||
memoryDecompressionProperties.pNext = propertiesChain;
|
||||
propertiesChain = &memoryDecompressionProperties;
|
||||
}
|
||||
// Chain driver properties onto the tail of the query so we can detect
|
||||
// the NVIDIA proprietary driver for the descriptor-heap AS-read
|
||||
// workaround (issue #15 / #7).
|
||||
descriptorHeapProperties.pNext = &driverProperties;
|
||||
VkPhysicalDeviceProperties2 properties2 {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
|
||||
.pNext = &rayTracingProperties
|
||||
.pNext = propertiesChain
|
||||
};
|
||||
vkGetPhysicalDeviceProperties2(physDevice, &properties2);
|
||||
// Keep the core properties around for the pipeline-cache identity check
|
||||
|
|
@ -694,8 +822,13 @@ void Device::Initialize() {
|
|||
// 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.
|
||||
workaroundDescriptorHeapAS = (driverProperties.driverID == VK_DRIVER_ID_NVIDIA_PROPRIETARY);
|
||||
// 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.
|
||||
workaroundDescriptorHeapAS = descriptorHeapSupported
|
||||
&& driverProperties.driverID == VK_DRIVER_ID_NVIDIA_PROPRIETARY;
|
||||
|
||||
// Sanity-gate: GDeflate 1.0 must actually be in the supported method set.
|
||||
if (memoryDecompressionSupported &&
|
||||
|
|
@ -727,24 +860,18 @@ void Device::Initialize() {
|
|||
queueCreateInfo.queueCount = 1;
|
||||
queueCreateInfo.pQueuePriorities = &priority;
|
||||
|
||||
VkPhysicalDeviceFaultFeaturesEXT faultFeatures2 = {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT,
|
||||
};
|
||||
VkPhysicalDeviceFeatures2 features22 = {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
|
||||
.pNext = &faultFeatures2,
|
||||
};
|
||||
vkGetPhysicalDeviceFeatures2(physDevice, &features22);
|
||||
|
||||
// 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.
|
||||
VkPhysicalDeviceFaultFeaturesEXT faultFeatures = {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT,
|
||||
.deviceFault = VK_TRUE,
|
||||
.deviceFaultVendorBinary = faultFeatures2.deviceFaultVendorBinary,
|
||||
.deviceFaultVendorBinary = faultQuery.deviceFaultVendorBinary,
|
||||
};
|
||||
|
||||
VkPhysicalDeviceShaderUntypedPointersFeaturesKHR untypedPointersFeatures {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_UNTYPED_POINTERS_FEATURES_KHR,
|
||||
.pNext = &faultFeatures,
|
||||
.shaderUntypedPointers = VK_TRUE,
|
||||
};
|
||||
|
||||
|
|
@ -752,37 +879,38 @@ void Device::Initialize() {
|
|||
// VK_ACCESS_2_*) — required for VK_EXT_memory_decompression's sync tokens.
|
||||
VkPhysicalDeviceVulkan13Features features13 {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES,
|
||||
.pNext = &untypedPointersFeatures,
|
||||
.synchronization2 = VK_TRUE,
|
||||
};
|
||||
|
||||
VkPhysicalDeviceMemoryDecompressionFeaturesEXT memoryDecompressionFeatures {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_DECOMPRESSION_FEATURES_EXT,
|
||||
.pNext = &features13,
|
||||
.memoryDecompression = VK_TRUE,
|
||||
};
|
||||
void* postDecompressChain = memoryDecompressionSupported
|
||||
? static_cast<void*>(&memoryDecompressionFeatures)
|
||||
: static_cast<void*>(&features13);
|
||||
|
||||
VkPhysicalDeviceDescriptorHeapFeaturesEXT desciptorHeapFeatures {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_HEAP_FEATURES_EXT,
|
||||
.pNext = postDecompressChain,
|
||||
.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,
|
||||
.pNext = &desciptorHeapFeatures,
|
||||
.storageBuffer16BitAccess = VK_TRUE,
|
||||
};
|
||||
|
||||
VkPhysicalDeviceVulkan12Features features12 {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES,
|
||||
.pNext = &bit16,
|
||||
.shaderFloat16 = VK_TRUE,
|
||||
// Bindless / runtime descriptor array indexing — needed for the
|
||||
// descriptor_heap shader path.
|
||||
// Bindless / runtime descriptor array indexing — needed by both
|
||||
// binding models (heap and descriptor sets).
|
||||
.shaderUniformBufferArrayNonUniformIndexing = VK_TRUE,
|
||||
.shaderSampledImageArrayNonUniformIndexing = VK_TRUE,
|
||||
.shaderStorageBufferArrayNonUniformIndexing = VK_TRUE,
|
||||
|
|
@ -791,28 +919,64 @@ void Device::Initialize() {
|
|||
.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,
|
||||
.pNext = &features12,
|
||||
.rayQuery = VK_TRUE
|
||||
};
|
||||
|
||||
VkPhysicalDeviceRayTracingPipelineFeaturesKHR physicalDeviceRayTracingPipelineFeatures{
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_PIPELINE_FEATURES_KHR,
|
||||
.pNext = &physicalDeviceRayQueryFeatures,
|
||||
.rayTracingPipeline = VK_TRUE
|
||||
};
|
||||
|
||||
VkPhysicalDeviceAccelerationStructureFeaturesKHR deviceAccelerationStructureFeature = {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_FEATURES_KHR,
|
||||
.pNext = &physicalDeviceRayTracingPipelineFeatures,
|
||||
.accelerationStructure = VK_TRUE
|
||||
};
|
||||
|
||||
void* featureChain = nullptr;
|
||||
const auto link = [&featureChain](auto& features) {
|
||||
features.pNext = featureChain;
|
||||
featureChain = &features;
|
||||
};
|
||||
if (deviceFaultSupported) {
|
||||
link(faultFeatures);
|
||||
}
|
||||
if (descriptorHeapSupported) {
|
||||
link(untypedPointersFeatures);
|
||||
}
|
||||
link(features13);
|
||||
if (memoryDecompressionSupported) {
|
||||
link(memoryDecompressionFeatures);
|
||||
}
|
||||
if (descriptorHeapSupported) {
|
||||
link(desciptorHeapFeatures);
|
||||
}
|
||||
link(maintenance5Features);
|
||||
link(bit16);
|
||||
link(features12);
|
||||
if (rayTracingSupported) {
|
||||
link(physicalDeviceRayQueryFeatures);
|
||||
link(physicalDeviceRayTracingPipelineFeatures);
|
||||
link(deviceAccelerationStructureFeature);
|
||||
}
|
||||
|
||||
VkPhysicalDeviceFeatures2 physical_features2 = {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
|
||||
.pNext = &deviceAccelerationStructureFeature,
|
||||
.pNext = featureChain,
|
||||
.features = {
|
||||
// Order matches VkPhysicalDeviceFeatures declaration so the
|
||||
// designated-initializer-order warning stays quiet.
|
||||
|
|
@ -833,12 +997,23 @@ void Device::Initialize() {
|
|||
}
|
||||
};
|
||||
|
||||
// Build the enabled-extension list dynamically so we can append the
|
||||
// optional VK_EXT_memory_decompression entry only when the driver
|
||||
// advertises it.
|
||||
// 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(deviceExtensionNames),
|
||||
std::end(deviceExtensionNames));
|
||||
std::begin(requiredDeviceExtensionNames),
|
||||
std::end(requiredDeviceExtensionNames));
|
||||
if (descriptorHeapSupported) {
|
||||
enabledDeviceExtensions.append_range(descriptorHeapExtensionNames);
|
||||
}
|
||||
if (rayTracingSupported) {
|
||||
enabledDeviceExtensions.append_range(rayTracingExtensionNames);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,6 +191,9 @@ namespace {
|
|||
}
|
||||
|
||||
void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd, RTBuildOptions options) {
|
||||
// Every Build* records an acceleration-structure build; without ray
|
||||
// tracing those entry points are not loaded. Fail with the reason.
|
||||
Device::RequireRayTracing();
|
||||
// Place both inputs in device-local memory (direct map on ReBAR/UMA,
|
||||
// staged copy otherwise — UploadDeviceLocal decides) and barrier the
|
||||
// upload before the BLAS build reads them. Replaces the previous
|
||||
|
|
@ -205,6 +208,7 @@ void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32
|
|||
}
|
||||
|
||||
void Mesh::Build(const CompressedMeshAsset& asset, VkCommandBuffer cmd, RTBuildOptions options) {
|
||||
Device::RequireRayTracing();
|
||||
if (!Device::memoryDecompressionSupported) {
|
||||
// CPU fallback: decompress into temporary host vectors, then take
|
||||
// the existing uncompressed path. The data region is decompressed
|
||||
|
|
@ -333,6 +337,7 @@ namespace {
|
|||
}
|
||||
|
||||
void Mesh::BuildProcedural(std::span<const RTAabb> aabbs, bool opaque, VkCommandBuffer cmd, RTBuildOptions options) {
|
||||
Device::RequireRayTracing();
|
||||
this->opaque = opaque;
|
||||
allowUpdate = options.allowUpdate;
|
||||
builtInputCount = static_cast<std::uint32_t>(aabbs.size());
|
||||
|
|
@ -341,6 +346,7 @@ void Mesh::BuildProcedural(std::span<const RTAabb> aabbs, bool opaque, VkCommand
|
|||
}
|
||||
|
||||
void Mesh::BuildProcedural(VkDeviceAddress aabbAddress, std::uint32_t count, bool opaque, VkCommandBuffer cmd, RTBuildOptions options, std::uint32_t stride) {
|
||||
Device::RequireRayTracing();
|
||||
// Zero-copy procedural build: the AABBs already live in `aabbAddress`
|
||||
// (a device buffer a GPU compute pass wrote). Nothing touches
|
||||
// self.aabbBuffer — the build input is the producer's buffer directly.
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ void RenderingElement3D::Remove(RenderingElement3D* e) {
|
|||
}
|
||||
|
||||
void RenderingElement3D::BuildTLAS(VkCommandBuffer cmd, std::uint32_t index, RTBuildPreference preference) {
|
||||
Device::RequireRayTracing();
|
||||
auto& tlas = tlases[index];
|
||||
const std::uint32_t primitiveCount = static_cast<std::uint32_t>(elements.size());
|
||||
|
||||
|
|
|
|||
|
|
@ -30,11 +30,11 @@ void UIRenderer::Initialize(Window& window, GraphicsDescriptorHeap& heap, Graphi
|
|||
heap_ = &heap;
|
||||
|
||||
// Load the four standard pipelines plus the fused uber-kernel (issue #47).
|
||||
drawQuads.Load(quadsSpv);
|
||||
drawCircles.Load(circlesSpv);
|
||||
drawImages.Load(imagesSpv);
|
||||
drawText.Load(textSpv);
|
||||
drawFused.Load(fusedSpv);
|
||||
drawQuads.Load(quadsSpv, heap);
|
||||
drawCircles.Load(circlesSpv, heap);
|
||||
drawImages.Load(imagesSpv, heap);
|
||||
drawText.Load(textSpv, heap);
|
||||
drawFused.Load(fusedSpv, heap);
|
||||
|
||||
// Allocate one image slot for the swapchain output. Each per-frame heap
|
||||
// copy will hold ITS frame's image at this slot.
|
||||
|
|
@ -275,6 +275,16 @@ void UIRenderer::Dispatch(GraphicsCommandBuffer cmd, const GraphicsComputeShader
|
|||
// ─── descriptor writes ─────────────────────────────────────────────────
|
||||
|
||||
void UIRenderer::WriteSwapchainDescriptors() {
|
||||
if (!Device::descriptorHeapSupported) {
|
||||
// Sets mode: each frame's descriptor set holds ITS swapchain image at
|
||||
// outImageSlot_, through a view the heap object creates and owns.
|
||||
for (std::uint8_t f = 0; f < Window::numFrames; ++f) {
|
||||
heap_->WriteImageSet(f, outImageSlot_, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
||||
window_->imageViews[f], VK_IMAGE_LAYOUT_GENERAL);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Each per-frame heap holds ITS swapchain image at outImageSlot_.
|
||||
std::array<VkImageDescriptorInfoEXT, Window::numFrames> infos{};
|
||||
std::array<VkResourceDescriptorInfoEXT, Window::numFrames> resources{};
|
||||
|
|
@ -334,6 +344,11 @@ void UIRenderer::WriteFontAtlasDescriptor() {
|
|||
void UIRenderer::WriteSampledImageDescriptor(std::uint16_t slot,
|
||||
const VkImageViewCreateInfo& viewInfo,
|
||||
VkImageLayout layout) {
|
||||
if (!Device::descriptorHeapSupported) {
|
||||
heap_->WriteImageSets(slot, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, viewInfo, layout);
|
||||
return;
|
||||
}
|
||||
|
||||
std::array<VkImageDescriptorInfoEXT, Window::numFrames> infos{};
|
||||
std::array<VkResourceDescriptorInfoEXT, Window::numFrames> resources{};
|
||||
std::array<VkHostAddressRangeEXT, Window::numFrames> destinations{};
|
||||
|
|
@ -364,7 +379,14 @@ void UIRenderer::WriteSampledImageDescriptor(std::uint16_t slot,
|
|||
for (auto& h : heap_->resourceHeap) h.FlushDevice(off, sz);
|
||||
}
|
||||
|
||||
void UIRenderer::WriteBufferDescriptor(std::uint16_t slot, VkDeviceAddress address, std::uint32_t size) {
|
||||
void UIRenderer::WriteBufferDescriptor(std::uint16_t slot, VkBuffer buffer, VkDeviceAddress address, std::uint32_t size) {
|
||||
if (!Device::descriptorHeapSupported) {
|
||||
// A descriptor set names the buffer by handle; the heap below names
|
||||
// it by device address.
|
||||
heap_->WriteBufferSets(slot, buffer);
|
||||
return;
|
||||
}
|
||||
|
||||
std::array<VkDeviceAddressRangeEXT, Window::numFrames> ranges{};
|
||||
std::array<VkResourceDescriptorInfoEXT, Window::numFrames> resources{};
|
||||
std::array<VkHostAddressRangeEXT, Window::numFrames> destinations{};
|
||||
|
|
@ -393,6 +415,14 @@ void UIRenderer::WriteBufferDescriptor(std::uint16_t slot, VkDeviceAddress addre
|
|||
|
||||
SamplerSlot UIRenderer::RegisterSampler(const VkSamplerCreateInfo& info) {
|
||||
auto range = heap_->AllocateSamplerSlots(1);
|
||||
if (!Device::descriptorHeapSupported) {
|
||||
// The handle owns the slot before the sampler is created, so a
|
||||
// failed vkCreateSampler returns it to the freelist.
|
||||
SamplerSlot slot{heap_, range.firstElement};
|
||||
heap_->WriteSamplerSets(range.firstElement, info);
|
||||
return slot;
|
||||
}
|
||||
|
||||
std::array<VkSamplerCreateInfo, Window::numFrames> infos{};
|
||||
std::array<VkHostAddressRangeEXT, Window::numFrames> destinations{};
|
||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||
|
|
|
|||
|
|
@ -833,7 +833,11 @@ void Window::Render() {
|
|||
// whole pipeline, so the frame-edge barriers wait on exactly those stages
|
||||
// instead of ALL_COMMANDS. Derived once here and reused for the present
|
||||
// barrier below (passes don't change within a Render()).
|
||||
const VkPipelineStageFlags swapWriterStages = SwapchainStageUnion(passes);
|
||||
// Device::stageMask drops RAY_TRACING_SHADER on devices without ray
|
||||
// tracing: the no-pass fallback and any un-narrowed pass report the
|
||||
// conservative writer union, which names that stage, and a stage whose
|
||||
// feature is not enabled is invalid in a barrier.
|
||||
const VkPipelineStageFlags swapWriterStages = SwapchainStageUnion(passes) & Device::stageMask;
|
||||
|
||||
// dstAccessMask must only contain access flags supported by the dst stage
|
||||
// mask (VUID-vkCmdPipelineBarrier-pImageMemoryBarriers-02820). Derive it
|
||||
|
|
@ -856,7 +860,15 @@ void Window::Render() {
|
|||
// buffer dictates GPU execution order, so the bind must come first.
|
||||
// Pass-side dispatches still run with the same heaps bound — moving
|
||||
// the bind earlier doesn't change anything for them.
|
||||
if (descriptorHeap) {
|
||||
if (descriptorHeap && !Device::descriptorHeapSupported) {
|
||||
// Sets mode: the heap object holds one bindless descriptor set per
|
||||
// frame instead of heap buffers. Every compute pipeline loaded against
|
||||
// this heap shares its pipeline layout, so one bind here stays valid
|
||||
// across all of them, exactly like the heap bind below.
|
||||
vkCmdBindDescriptorSets(drawCmdBuffers[currentBuffer], VK_PIPELINE_BIND_POINT_COMPUTE,
|
||||
descriptorHeap->pipelineLayout, 0, 1,
|
||||
&descriptorHeap->sets[currentBuffer], 0, nullptr);
|
||||
} else if (descriptorHeap) {
|
||||
// Rebuild the cached per-frame bind structs only when the heap changes.
|
||||
// The address/size of each frame's heap and the reservedRangeOffset
|
||||
// bitmask are stable for a given heap, so this runs once per assignment
|
||||
|
|
@ -918,8 +930,11 @@ void Window::Render() {
|
|||
// serialises COMPUTE_SHADER while an RT pass on either side pulls in
|
||||
// RAY_TRACING_SHADER — never the ALL_COMMANDS full-pipeline stall.
|
||||
VkImageMemoryBarrier ib = BuildSwapchainInterPassBarrier(images[currentBuffer]);
|
||||
// Masked like swapWriterStages above: a pass that keeps the
|
||||
// default stage reports a union that names RAY_TRACING_SHADER.
|
||||
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer],
|
||||
passes[i]->SwapchainStage(), passes[i + 1]->SwapchainStage(),
|
||||
passes[i]->SwapchainStage() & Device::stageMask,
|
||||
passes[i + 1]->SwapchainStage() & Device::stageMask,
|
||||
0, 0, nullptr, 0, nullptr, 1, &ib);
|
||||
}
|
||||
}
|
||||
|
|
@ -1100,6 +1115,13 @@ void Window::CreateSwapchain()
|
|||
}
|
||||
uint32_t imageCount{ 0 };
|
||||
Device::CheckVkResult(vkGetSwapchainImagesKHR(Device::device, swapChain, &imageCount, nullptr));
|
||||
// images[] and every other per-frame array hold exactly numFrames
|
||||
// entries. A driver may create more images than minImageCount asked for
|
||||
// (a surface whose minimum is above numFrames must); writing them into
|
||||
// images[] would run off its end, so refuse instead.
|
||||
if (imageCount != numFrames) {
|
||||
throw std::runtime_error(std::format("Window: the swapchain has {} images, but the frame loop is built for exactly {}", imageCount, numFrames));
|
||||
}
|
||||
|
||||
// Get the swap chain images
|
||||
Device::CheckVkResult(vkGetSwapchainImagesKHR(Device::device, swapChain, &imageCount, images));
|
||||
|
|
|
|||
Loading…
Reference in a new issue