diff --git a/README.md b/README.md index fc81db2..be5aeb8 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,9 @@ Vulkan + WebGPU graphics library built around C++20 modules and bindless heaps. Provides window management, ray tracing, and a compute-shader-driven UI on a single, opinionated stack. Native -builds use Vulkan with `VK_EXT_descriptor_heap`; `wasm32-*` builds +builds use Vulkan with `VK_EXT_descriptor_heap` (the compute/UI path +also runs without it, see +[below](#drivers-without-vk_ext_descriptor_heap)); `wasm32-*` builds target the browser via WebGPU and a DOM window backend. ## Backends @@ -79,7 +81,10 @@ shaded through an intersection shader with an any-hit cut-out. `VK_EXT_descriptor_heap`; pipelines are created with `VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT` so there are no descriptor-set layouts and push constants travel via - `vkCmdPushDataEXT`. + `vkCmdPushDataEXT`. The heap, ray tracing and device-fault extensions + are optional groups, detected at start-up + (`Device::descriptorHeapSupported`, `rayTracingSupported`, + `deviceFaultSupported`). - **DescriptorHeapVulkan / DescriptorHeapWebGPU** — bindless slot allocators. Vulkan side allocates image/buffer/sampler slots in a `VK_EXT_descriptor_heap`; WebGPU side resolves slots to JS-side @@ -108,12 +113,13 @@ shaded through an intersection shader with an any-hit cut-out. [WAVEFRONT-DESIGN.md](WAVEFRONT-DESIGN.md). - **ComputeShader / WebGPUComputeShader** — Tier 1 wrapper used by the UI system. Vulkan loads a `.spv` and dispatches with - `vkCmdPushDataEXT`; WebGPU loads a user-supplied `.wgsl` blob at + `vkCmdPushDataEXT` (or the `.sets.spv` build and `vkCmdPushConstants` + without the heap); WebGPU loads a user-supplied `.wgsl` blob at runtime via `wgpuLoadCustomShader`. Use it directly for any custom compute. -- **UI** — three-tier UI system; see below. The standard shaders ship - as four `.spv` blobs on native and four WGSL strings baked into the - WebGPU dispatcher. +- **UI** — three-tier UI system; see below. The standard shaders (four + kernels plus the fused one) ship as `.spv` / `.sets.spv` pairs on + native and as WGSL strings baked into the WebGPU dispatcher. - **FontAtlas** — single-channel SDF atlas (1024×1024, 32pt base, shelf-packed, lazy `Ensure` per codepoint, dirty-flush via `Update`). Backend-agnostic. @@ -168,6 +174,51 @@ overhead. If you need to render thousands of UI items, you want a different shader (tile binning, per-item-list resolve), not more dispatches. +### Drivers without `VK_EXT_descriptor_heap` + +The compute/UI path also runs on Vulkan drivers that lack +`VK_EXT_descriptor_heap` (Mesa Turnip on Qualcomm Adreno, for one). +`Device::Initialize` detects this and sets +`Device::descriptorHeapSupported = false`. The same slots then live in +one bindless descriptor set per frame instead of the heap, and push +constants go through `vkCmdPushConstants`. Slot handles, item structs, +`UIDispatchHeader` and the `UIRenderer` API are unchanged, so +application code does not branch on it. Ray tracing is heap-only: on +such a driver `Device::rayTracingSupported` is false and the RT entry +points throw. + +Each compute shader is shipped in two builds of the same source: +`.spv` for the heap and `.sets.spv` for descriptor sets. +`ComputeShader::Load(path, heap)` takes the `.spv` path and picks the +right file itself. For a custom shader that means three things: + +- Declare bindless resources with the `UI_HEAP_*` macros from + [shaders/ui-shared.glsl](shaders/ui-shared.glsl), not with a literal + `descriptor_heap`: + + ```glsl + layout(UI_HEAP_BUFFERS, std430) readonly buffer MyBuf { MyItem items[]; } myHeap[]; + ``` + +- Register the shader twice in `project.cpp`, the second time with the + define and the variant name: + + ```cpp + cfg.shaders.emplace_back(fs::path("my.comp.glsl"), std::string("main"), ShaderType::Compute); + Shader& sets = cfg.shaders.emplace_back(fs::path("my.comp.glsl"), std::string("main"), ShaderType::Compute); + sets.defines = {"CRAFTER_GRAPHICS_DESCRIPTOR_SETS"}; + sets.variant = "sets"; + ``` + +- Keep the push-constant block within the device's + `maxPushConstantsSize` (256 bytes on common drivers). Without the + heap a larger payload throws in `ComputeShader::Dispatch`. + +Set the environment variable `CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP` to +force the descriptor-set path on a driver that does have the heap; the +`UIFallbackParity` test uses it to compare the two paths frame for +frame. + ## Build The repository is built with `crafter-build` (a project-config based diff --git a/examples/CustomShader/inverse-circle.comp.glsl b/examples/CustomShader/inverse-circle.comp.glsl index 00ada77..4e7a2b3 100644 --- a/examples/CustomShader/inverse-circle.comp.glsl +++ b/examples/CustomShader/inverse-circle.comp.glsl @@ -21,7 +21,10 @@ struct InverseCircleItem { vec4 centerRadius; }; -layout(descriptor_heap, std430) readonly buffer InvCircleBuf { +// UI_HEAP_BUFFERS (ui-shared.glsl) instead of a literal `descriptor_heap`: +// this file is compiled twice, and the macro picks the heap or the +// descriptor-set binding to match. +layout(UI_HEAP_BUFFERS, std430) readonly buffer InvCircleBuf { InverseCircleItem items[]; } invCircleHeap[]; diff --git a/examples/CustomShader/main.cpp b/examples/CustomShader/main.cpp index 960d002..818ec81 100644 --- a/examples/CustomShader/main.cpp +++ b/examples/CustomShader/main.cpp @@ -54,7 +54,11 @@ int main() { // read from `hdr.itemBuffer` in the push data. GraphicsComputeShader inverseCircle; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM - inverseCircle.Load("inverse-circle.comp.spv"); + // The heap argument ties the pipeline to this heap's binding model. On a + // driver without VK_EXT_descriptor_heap, Load picks + // inverse-circle.comp.sets.spv (built from the same source, see + // project.cpp) on its own. + inverseCircle.Load("inverse-circle.comp.spv", heap); #else UICustomBinding invBindings[] = { { .group = 2, diff --git a/examples/CustomShader/project.cpp b/examples/CustomShader/project.cpp index eae0218..dc79778 100644 --- a/examples/CustomShader/project.cpp +++ b/examples/CustomShader/project.cpp @@ -47,6 +47,13 @@ extern "C" Configuration CrafterBuildProject(std::span a } else { cfg.shaders.emplace_back(fs::path("inverse-circle.comp.glsl"), std::string("main"), ShaderType::Compute); + // Second build of the same source for drivers without + // VK_EXT_descriptor_heap: inverse-circle.comp.sets.spv. ComputeShader::Load + // picks it at runtime, so a custom shader needs both to run everywhere. + Shader& sets = cfg.shaders.emplace_back(fs::path("inverse-circle.comp.glsl"), + std::string("main"), ShaderType::Compute); + sets.defines = {"CRAFTER_GRAPHICS_DESCRIPTOR_SETS"}; + sets.variant = "sets"; } return cfg; } diff --git a/examples/RTVolume/main.cpp b/examples/RTVolume/main.cpp index 81e4433..d6242a6 100644 --- a/examples/RTVolume/main.cpp +++ b/examples/RTVolume/main.cpp @@ -125,7 +125,7 @@ int main() { } ComputeShader aabbWriter; - aabbWriter.Load("aabbs.comp.spv"); + aabbWriter.Load("aabbs.comp.spv", descriptorHeap); struct AabbPush { VkDeviceAddress boxes; std::uint32_t count; float time; } push { aabbDevice.address, kBoxCount, 0.0f }; aabbWriter.Dispatch(cmd, &push, sizeof(push), (kBoxCount + 63) / 64); diff --git a/implementations/Crafter.Graphics-ComputeShader.cpp b/implementations/Crafter.Graphics-ComputeShader.cpp index ebc91d6..3af554b 100644 --- a/implementations/Crafter.Graphics-ComputeShader.cpp +++ b/implementations/Crafter.Graphics-ComputeShader.cpp @@ -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(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 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(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 diff --git a/implementations/Crafter.Graphics-Device.cpp b/implementations/Crafter.Graphics-Device.cpp index 075d7ef..138107c 100644 --- a/implementations/Crafter.Graphics-Device.cpp +++ b/implementations/Crafter.Graphics-Device.cpp @@ -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 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 availableExtensions; { std::uint32_t extCount = 0; vkEnumerateDeviceExtensionProperties(physDevice, nullptr, &extCount, nullptr); std::vector 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 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(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(&memoryDecompressionFeatures) - : static_cast(&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 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); } diff --git a/implementations/Crafter.Graphics-Mesh.cpp b/implementations/Crafter.Graphics-Mesh.cpp index b24f3e1..c171e4b 100644 --- a/implementations/Crafter.Graphics-Mesh.cpp +++ b/implementations/Crafter.Graphics-Mesh.cpp @@ -191,6 +191,9 @@ namespace { } void Mesh::Build(std::span> verticies, std::span 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> verticies, std::span aabbs, bool opaque, VkCommandBuffer cmd, RTBuildOptions options) { + Device::RequireRayTracing(); this->opaque = opaque; allowUpdate = options.allowUpdate; builtInputCount = static_cast(aabbs.size()); @@ -341,6 +346,7 @@ void Mesh::BuildProcedural(std::span 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. diff --git a/implementations/Crafter.Graphics-RenderingElement3D.cpp b/implementations/Crafter.Graphics-RenderingElement3D.cpp index 81a7c95..9622c94 100644 --- a/implementations/Crafter.Graphics-RenderingElement3D.cpp +++ b/implementations/Crafter.Graphics-RenderingElement3D.cpp @@ -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(elements.size()); diff --git a/implementations/Crafter.Graphics-UI.cpp b/implementations/Crafter.Graphics-UI.cpp index 05bc318..b2ce600 100644 --- a/implementations/Crafter.Graphics-UI.cpp +++ b/implementations/Crafter.Graphics-UI.cpp @@ -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 infos{}; std::array 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 infos{}; std::array resources{}; std::array 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 ranges{}; std::array resources{}; std::array 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 infos{}; std::array destinations{}; for (std::uint32_t f = 0; f < Window::numFrames; ++f) { diff --git a/implementations/Crafter.Graphics-Window.cpp b/implementations/Crafter.Graphics-Window.cpp index 8ae726a..017121c 100644 --- a/implementations/Crafter.Graphics-Window.cpp +++ b/implementations/Crafter.Graphics-Window.cpp @@ -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)); diff --git a/interfaces/Crafter.Graphics-ComputeShader.cppm b/interfaces/Crafter.Graphics-ComputeShader.cppm index bc00a3c..8900aa6 100644 --- a/interfaces/Crafter.Graphics-ComputeShader.cppm +++ b/interfaces/Crafter.Graphics-ComputeShader.cppm @@ -9,17 +9,29 @@ export module Crafter.Graphics:ComputeShader; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM import std; import :Device; +import :DescriptorHeapVulkan; export namespace Crafter { - // Tier 1: thin compute-pipeline wrapper. Owns one VkPipeline created with - // VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT (no pipeline layout — the - // bindless heap supplies all bindings, push constants travel via - // vkCmdPushDataEXT). Use this to dispatch the four standard UI shaders - // and any user-authored compute shader that follows the ui-shared.glsl - // contract. + // Tier 1: thin compute-pipeline wrapper. Owns one VkPipeline. Use this to + // dispatch the four standard UI shaders and any user-authored compute + // shader that follows the ui-shared.glsl contract. + // + // The pipeline follows the device's binding model + // (Device::descriptorHeapSupported): + // heap -> created with VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT and + // no pipeline layout; the bindless heap supplies all bindings + // and push constants travel via vkCmdPushDataEXT. + // sets -> created from the shader's .sets.spv build against the + // heap object's shared pipeline layout; the bindless descriptor + // set supplies the bindings and push constants travel via + // vkCmdPushConstants. class ComputeShader { public: VkPipeline pipeline = VK_NULL_HANDLE; + // Sets mode only: the pipeline layout of the heap this shader was + // loaded against, needed again by vkCmdPushConstants. Not owned — it + // belongs to that DescriptorHeapVulkan. VK_NULL_HANDLE in heap mode. + VkPipelineLayout layout = VK_NULL_HANDLE; // NVIDIA descriptor-heap AS-read workaround (issue #15 / #7): set by // Load when this shader ray-queries the TLAS through the descriptor @@ -38,14 +50,30 @@ export namespace Crafter { ComputeShader& operator=(ComputeShader&& other) noexcept; ~ComputeShader(); - // Loads a SPIR-V compute shader from disk and creates a pipeline that - // uses the bindless descriptor-heap binding model. - void Load(const std::filesystem::path& spvPath); + // Loads a SPIR-V compute shader from disk and creates its pipeline. + // + // `spvPath` always names the descriptor-heap build (.spv). On a + // device without the heap, the .sets.spv build next to it is + // loaded instead, so the caller never branches on the binding model; + // a missing .sets.spv throws, naming the file. + // + // `heap` must already be Initialize()d and must be the heap assigned + // to Window::descriptorHeap for every frame this shader is dispatched + // in: in sets mode the pipeline is built against that heap's layout, + // and Window::Render binds that heap's descriptor set. + void Load(const std::filesystem::path& spvPath, DescriptorHeapVulkan& heap); // Bind, push constants (if any), dispatch. Caller computes group counts // and is responsible for any inter-dispatch barriers (UIRenderer::Dispatch // wraps this with the standard write-after-write barrier). // + // pushBytes should be a multiple of 4 (Vulkan requires it for both + // push mechanisms). On a device without the descriptor heap the + // payload travels as ordinary push constants, so it must also fit + // the device's maxPushConstantsSize (256 bytes on common drivers); a + // larger one throws std::length_error. Keep push blocks within that + // if the shader is meant to run on both binding models. + // // tlasAddress is the NVIDIA descriptor-heap AS-read workaround hook // (issue #15 / #7): a shader that ray-queries the TLAS through the // descriptor heap is rewritten to read its device address from a push diff --git a/interfaces/Crafter.Graphics-DescriptorHeapVulkan.cppm b/interfaces/Crafter.Graphics-DescriptorHeapVulkan.cppm index 9223853..3163a72 100644 --- a/interfaces/Crafter.Graphics-DescriptorHeapVulkan.cppm +++ b/interfaces/Crafter.Graphics-DescriptorHeapVulkan.cppm @@ -39,7 +39,95 @@ export namespace Crafter { std::vector bufferFreelist; std::vector samplerFreelist; + // ─── Sets mode ────────────────────────────────────────────────── + // Used instead of the heap buffers above when the driver has no + // VK_EXT_descriptor_heap (Device::descriptorHeapSupported == false). + // The same slots then live in one bindless descriptor set per frame, + // mirroring the per-frame heap copies, so slot allocation, the slot + // handles and every shader-side index keep their meaning: + // binding 0 STORAGE_IMAGE[] image slots written as storage images + // binding 1 SAMPLED_IMAGE[] image slots written as sampled images + // binding 2 SAMPLER[] sampler slots + // binding 3 STORAGE_BUFFER[] buffer slots (bufferStartElement is 0) + // These numbers are the UI_HEAP_* macros in shaders/ui-shared.glsl. + // Bindings 0 and 1 share the image slot space the way the two typed + // views of the resource heap do; a slot is valid in whichever of the + // two it was written to. + // + // All bindings are PARTIALLY_BOUND | UPDATE_AFTER_BIND | + // UPDATE_UNUSED_WHILE_PENDING: most slots are never written, and a + // slot is written into every frame's set at once while earlier frames + // may still be executing with their set bound. + static constexpr std::uint32_t storageImageBinding = 0; + static constexpr std::uint32_t sampledImageBinding = 1; + static constexpr std::uint32_t samplerBinding = 2; + static constexpr std::uint32_t storageBufferBinding = 3; + + VkDescriptorPool setPool = VK_NULL_HANDLE; + VkDescriptorSetLayout setLayout = VK_NULL_HANDLE; + // Shared by every compute pipeline created against this heap + // (ComputeShader::Load), so the set Window::Render binds once per + // frame stays bound across all of them. Carries one compute + // push-constant range covering the device's whole push-constant space. + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkDescriptorSet sets[Window::numFrames] {}; + // Descriptor sets reference VkImageView / VkSampler objects, where the + // heap takes create-infos, so in sets mode this struct creates and + // owns them. setImageViews[frame][slot] is the view frame's set uses + // at that image slot; a view shared by all frames is stored under + // frame 0 only. setSamplers[slot] likewise. + std::vector setImageViews[Window::numFrames]; + std::vector setSamplers; + + DescriptorHeapVulkan() = default; + DescriptorHeapVulkan(const DescriptorHeapVulkan&) = delete; + DescriptorHeapVulkan& operator=(const DescriptorHeapVulkan&) = delete; + + // Heap mode owns nothing beyond its VulkanBuffers, which release + // themselves. Sets mode owns the objects above. The frame loop keeps + // frames in flight and nothing waits for them at shutdown, so wait + // here: destroying a view or the pool under a still-executing command + // buffer is a use-after-free on the GPU. + ~DescriptorHeapVulkan() { + if (setLayout == VK_NULL_HANDLE) { + return; + } + vkQueueWaitIdle(Device::queue); + DestroySets(); + } + + // Destroys every sets-mode object and returns to the un-initialized + // state. The caller guarantees nothing on the GPU still uses them. + // Safe on a partly built heap: destroying VK_NULL_HANDLE is a no-op. + void DestroySets() noexcept { + vkDestroyDescriptorPool(Device::device, setPool, nullptr); + for (std::vector& views : setImageViews) { + for (VkImageView view : views) { + vkDestroyImageView(Device::device, view, nullptr); + } + views.clear(); + } + for (VkSampler sampler : setSamplers) { + vkDestroySampler(Device::device, sampler, nullptr); + } + setSamplers.clear(); + vkDestroyDescriptorSetLayout(Device::device, setLayout, nullptr); + vkDestroyPipelineLayout(Device::device, pipelineLayout, nullptr); + setPool = VK_NULL_HANDLE; + setLayout = VK_NULL_HANDLE; + pipelineLayout = VK_NULL_HANDLE; + std::ranges::fill(sets, VkDescriptorSet{VK_NULL_HANDLE}); + } + + // Call once, before any shader is loaded against this heap. In heap + // mode a second call simply re-sizes the heap buffers; in sets mode it + // throws, because the pipelines already built against the old layout + // could not be used with a new one. void Initialize(std::uint16_t images, std::uint16_t buffers, std::uint16_t samplers) { + if (!Device::descriptorHeapSupported) { + InitializeSets(images, buffers, samplers); + return; + } std::uint32_t descriptorRegion = images * Device::descriptorHeapProperties.imageDescriptorSize + buffers * Device::descriptorHeapProperties.bufferDescriptorSize; std::uint32_t alignedDescriptorRegion = (descriptorRegion + Device::descriptorHeapProperties.imageDescriptorAlignment - 1) & ~(Device::descriptorHeapProperties.imageDescriptorAlignment - 1); std::uint32_t resourceSize = alignedDescriptorRegion + Device::descriptorHeapProperties.minResourceHeapReservedRange; @@ -78,6 +166,231 @@ export namespace Crafter { } } + // Sets-mode half of Initialize. Kept apart so the heap sizing above, + // which divides by descriptor sizes that are zero without the + // extension, never runs here. + void InitializeSets(std::uint16_t images, std::uint16_t buffers, std::uint16_t samplers) { + // A second Initialize would leak the first pool and layouts and + // leave every pipeline created against the old layout + // incompatible with the new sets. + if (setLayout != VK_NULL_HANDLE) { + throw std::runtime_error("DescriptorHeapVulkan: Initialize called twice (descriptor-set mode cannot be re-initialized)"); + } + // If a step below throws, undo the ones that succeeded. A + // half-built heap would otherwise keep setLayout set, so a retry + // would report "Initialize called twice" instead of the real + // error, and nothing would release what was created. + struct Rollback { + DescriptorHeapVulkan* heap; + ~Rollback() { if (heap != nullptr) heap->DestroySets(); } + } rollback{this}; + + // Buffer slots are plain array indices into binding 3, so + // BufferSlot's conversion adds nothing. + bufferStartElement = 0; + bufferStartOffset = 0; + + imageCapacity = images; + bufferCapacity = buffers; + samplerCapacity = samplers; + imageNext = 0; + bufferNext = 0; + samplerNext = 0; + imageFreelist.clear(); + imageFreelist.reserve(images); + bufferFreelist.clear(); + bufferFreelist.reserve(buffers); + samplerFreelist.clear(); + samplerFreelist.reserve(samplers); + + // At least one descriptor per binding even when a pool was sized + // zero (Initialize(1, 1, 0) is common): a zero-count pool size is + // invalid, and the UI shaders statically reference all four arrays. + const std::uint32_t imageCount = std::max(images, 1); + const std::uint32_t bufferCount = std::max(buffers, 1); + const std::uint32_t samplerCount = std::max(samplers, 1); + + const VkDescriptorSetLayoutBinding bindings[] = { + { storageImageBinding, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, imageCount, VK_SHADER_STAGE_COMPUTE_BIT, nullptr }, + { sampledImageBinding, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, imageCount, VK_SHADER_STAGE_COMPUTE_BIT, nullptr }, + { samplerBinding, VK_DESCRIPTOR_TYPE_SAMPLER, samplerCount, VK_SHADER_STAGE_COMPUTE_BIT, nullptr }, + { storageBufferBinding, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, bufferCount, VK_SHADER_STAGE_COMPUTE_BIT, nullptr }, + }; + constexpr VkDescriptorBindingFlags slotFlags = + VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT + | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT + | VK_DESCRIPTOR_BINDING_UPDATE_UNUSED_WHILE_PENDING_BIT; + const VkDescriptorBindingFlags bindingFlags[] = { slotFlags, slotFlags, slotFlags, slotFlags }; + const VkDescriptorSetLayoutBindingFlagsCreateInfo bindingFlagsInfo { + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO, + .bindingCount = static_cast(std::size(bindingFlags)), + .pBindingFlags = bindingFlags, + }; + const VkDescriptorSetLayoutCreateInfo layoutInfo { + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, + .pNext = &bindingFlagsInfo, + .flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT, + .bindingCount = static_cast(std::size(bindings)), + .pBindings = bindings, + }; + Device::CheckVkResult(vkCreateDescriptorSetLayout(Device::device, &layoutInfo, nullptr, &setLayout)); + + // One range over the whole push-constant space rather than a + // per-shader size: the layout is shared, and each Dispatch pushes + // only the bytes its shader declares. + const VkPushConstantRange pushRange { + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .offset = 0, + .size = Device::deviceProperties.limits.maxPushConstantsSize & ~3u, + }; + const VkPipelineLayoutCreateInfo pipelineLayoutInfo { + .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .setLayoutCount = 1, + .pSetLayouts = &setLayout, + .pushConstantRangeCount = 1, + .pPushConstantRanges = &pushRange, + }; + Device::CheckVkResult(vkCreatePipelineLayout(Device::device, &pipelineLayoutInfo, nullptr, &pipelineLayout)); + + const VkDescriptorPoolSize poolSizes[] = { + { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, imageCount * Window::numFrames }, + { VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, imageCount * Window::numFrames }, + { VK_DESCRIPTOR_TYPE_SAMPLER, samplerCount * Window::numFrames }, + { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, bufferCount * Window::numFrames }, + }; + const VkDescriptorPoolCreateInfo poolInfo { + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, + .flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT, + .maxSets = Window::numFrames, + .poolSizeCount = static_cast(std::size(poolSizes)), + .pPoolSizes = poolSizes, + }; + Device::CheckVkResult(vkCreateDescriptorPool(Device::device, &poolInfo, nullptr, &setPool)); + + VkDescriptorSetLayout setLayouts[Window::numFrames]; + std::ranges::fill(setLayouts, setLayout); + const VkDescriptorSetAllocateInfo allocInfo { + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, + .descriptorPool = setPool, + .descriptorSetCount = Window::numFrames, + .pSetLayouts = setLayouts, + }; + Device::CheckVkResult(vkAllocateDescriptorSets(Device::device, &allocInfo, sets)); + + for (std::vector& views : setImageViews) { + views.assign(imageCount, VK_NULL_HANDLE); + } + setSamplers.assign(samplerCount, VK_NULL_HANDLE); + rollback.heap = nullptr; + } + + // ─── Sets-mode descriptor writes ──────────────────────────────── + // The counterparts of vkWriteResourceDescriptorsEXT / + // vkWriteSamplerDescriptorsEXT into the heap buffers. Call only when + // !Device::descriptorHeapSupported. `slot` is the raw index the + // Allocate*Slots call returned. + // + // Each write replaces whatever view or sampler the slot held and + // destroys it immediately. That relies on the same contract as slot + // reuse in heap mode: no frame in flight may still reference the slot + // (true after the queue drain that precedes Window::onResize, and for + // a slot that was freed at a scene-switch barrier). + + // Writes `type` (STORAGE_IMAGE or SAMPLED_IMAGE) at an image slot of + // one frame's set, for images that differ per frame: the swapchain. + // A slot written this way must be written for every frame. + void WriteImageSet(std::uint8_t frame, std::uint16_t slot, VkDescriptorType type, + const VkImageViewCreateInfo& viewInfo, VkImageLayout layout) { + VkImageView view = VK_NULL_HANDLE; + Device::CheckVkResult(vkCreateImageView(Device::device, &viewInfo, nullptr, &view)); + vkDestroyImageView(Device::device, setImageViews[frame][slot], nullptr); + setImageViews[frame][slot] = view; + + const VkDescriptorImageInfo imageInfo { .imageView = view, .imageLayout = layout }; + const VkWriteDescriptorSet write { + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .dstSet = sets[frame], + .dstBinding = type == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE ? storageImageBinding : sampledImageBinding, + .dstArrayElement = slot, + .descriptorCount = 1, + .descriptorType = type, + .pImageInfo = &imageInfo, + }; + vkUpdateDescriptorSets(Device::device, 1, &write, 0, nullptr); + } + + // Writes the same image at an image slot of every frame's set, through + // one shared view. + void WriteImageSets(std::uint16_t slot, VkDescriptorType type, + const VkImageViewCreateInfo& viewInfo, VkImageLayout layout) { + VkImageView view = VK_NULL_HANDLE; + Device::CheckVkResult(vkCreateImageView(Device::device, &viewInfo, nullptr, &view)); + for (std::vector& views : setImageViews) { + vkDestroyImageView(Device::device, views[slot], nullptr); + views[slot] = VK_NULL_HANDLE; + } + setImageViews[0][slot] = view; + + const VkDescriptorImageInfo imageInfo { .imageView = view, .imageLayout = layout }; + VkWriteDescriptorSet writes[Window::numFrames]; + for (std::uint8_t f = 0; f < Window::numFrames; ++f) { + writes[f] = { + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .dstSet = sets[f], + .dstBinding = type == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE ? storageImageBinding : sampledImageBinding, + .dstArrayElement = slot, + .descriptorCount = 1, + .descriptorType = type, + .pImageInfo = &imageInfo, + }; + } + vkUpdateDescriptorSets(Device::device, Window::numFrames, writes, 0, nullptr); + } + + // Writes a storage buffer at a buffer slot of every frame's set. The + // whole buffer is bound; the shader's runtime array is bounded by the + // item count the dispatch passes, as in heap mode. + void WriteBufferSets(std::uint16_t slot, VkBuffer buffer) { + const VkDescriptorBufferInfo bufferInfo { .buffer = buffer, .offset = 0, .range = VK_WHOLE_SIZE }; + VkWriteDescriptorSet writes[Window::numFrames]; + for (std::uint8_t f = 0; f < Window::numFrames; ++f) { + writes[f] = { + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .dstSet = sets[f], + .dstBinding = storageBufferBinding, + .dstArrayElement = slot, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, + .pBufferInfo = &bufferInfo, + }; + } + vkUpdateDescriptorSets(Device::device, Window::numFrames, writes, 0, nullptr); + } + + // Creates the sampler and writes it at a sampler slot of every + // frame's set. + void WriteSamplerSets(std::uint16_t slot, const VkSamplerCreateInfo& info) { + VkSampler sampler = VK_NULL_HANDLE; + Device::CheckVkResult(vkCreateSampler(Device::device, &info, nullptr, &sampler)); + vkDestroySampler(Device::device, setSamplers[slot], nullptr); + setSamplers[slot] = sampler; + + const VkDescriptorImageInfo samplerInfo { .sampler = sampler }; + VkWriteDescriptorSet writes[Window::numFrames]; + for (std::uint8_t f = 0; f < Window::numFrames; ++f) { + writes[f] = { + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .dstSet = sets[f], + .dstBinding = samplerBinding, + .dstArrayElement = slot, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER, + .pImageInfo = &samplerInfo, + }; + } + vkUpdateDescriptorSets(Device::device, Window::numFrames, writes, 0, nullptr); + } + ImageSlotRange AllocateImageSlots(std::uint16_t count) { if (count == 1 && !imageFreelist.empty()) { std::uint16_t s = imageFreelist.back(); @@ -158,7 +471,12 @@ export namespace Crafter { return firstElement * Device::descriptorHeapProperties.samplerDescriptorSize; } + // Both are 0 in sets mode: buffer slots are plain indices into their + // own binding, and the heap descriptor sizes divided by below are zero. inline static std::uint32_t GetBufferOffset(std::uint16_t images, std::uint16_t buffers) { + if (!Device::descriptorHeapSupported) { + return 0; + } std::uint32_t bufferStartElement = images * Device::descriptorHeapProperties.imageDescriptorSize / Device::descriptorHeapProperties.bufferDescriptorSize; if(images > 0 && bufferStartElement == 0) { @@ -167,6 +485,9 @@ export namespace Crafter { return bufferStartElement * Device::descriptorHeapProperties.bufferDescriptorSize; } inline static std::uint16_t GetBufferOffsetElement(std::uint16_t images, std::uint16_t buffers) { + if (!Device::descriptorHeapSupported) { + return 0; + } std::uint16_t bufferStartElement = images * Device::descriptorHeapProperties.imageDescriptorSize / Device::descriptorHeapProperties.bufferDescriptorSize; if(images > 0 && bufferStartElement == 0) { diff --git a/interfaces/Crafter.Graphics-Device.cppm b/interfaces/Crafter.Graphics-Device.cppm index d3666f7..d461eb6 100644 --- a/interfaces/Crafter.Graphics-Device.cppm +++ b/interfaces/Crafter.Graphics-Device.cppm @@ -174,12 +174,39 @@ export namespace Crafter { // relocate it. inline static std::filesystem::path pipelineCachePath = "pipeline_cache.bin"; + // ─── Optional capability groups ──────────────────────────────── + // Decided once by Initialize from the extensions the driver + // advertises; false until then. Not every driver has the descriptor + // heap (Mesa Turnip on Adreno has neither it nor ray tracing), so the + // compute/UI path runs in one of two binding models: + // descriptorHeapSupported -> VK_EXT_descriptor_heap, the *.spv shaders + // !descriptorHeapSupported -> one bindless descriptor set per frame + // (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. + 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. + inline static bool rayTracingSupported = false; + // VK_EXT_device_fault: the VK_ERROR_DEVICE_LOST report in CheckVkResult. + inline static bool deviceFaultSupported = false; + // Pipeline stages that are valid on this device: everything, minus + // RAY_TRACING_SHADER when ray tracing is unsupported. AND it into any + // stage mask built from a constant that names the RT stage. + inline static VkPipelineStageFlags stageMask = ~VkPipelineStageFlags{0}; + // Throws std::runtime_error naming the missing capability when ray + // tracing is unsupported. Called at every ray tracing entry point. + static void RequireRayTracing(); + + // 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. inline static VkPhysicalDeviceDescriptorHeapPropertiesEXT descriptorHeapProperties = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_HEAP_PROPERTIES_EXT }; inline static VkPhysicalDeviceRayTracingPipelinePropertiesKHR rayTracingProperties = { - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_PIPELINE_PROPERTIES_KHR, - .pNext = &descriptorHeapProperties + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_PIPELINE_PROPERTIES_KHR }; inline static VkPhysicalDeviceMemoryDecompressionPropertiesEXT memoryDecompressionProperties = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_DECOMPRESSION_PROPERTIES_EXT diff --git a/interfaces/Crafter.Graphics-ImageVulkan.cppm b/interfaces/Crafter.Graphics-ImageVulkan.cppm index 5064c4e..3864d60 100644 --- a/interfaces/Crafter.Graphics-ImageVulkan.cppm +++ b/interfaces/Crafter.Graphics-ImageVulkan.cppm @@ -90,6 +90,15 @@ export namespace Crafter { this->width = width; this->height = height; this->mipLevels = mipLevels; + // The RT default is not a valid pipeline stage on a device without + // ray tracing, where the only shader that can sample an image is a + // compute shader. Resolved here rather than in the member + // initializer, which can run before Device::Initialize. The + // parameter is corrected, not only the member: the transition at + // the end of this function is recorded with it. + if (!Device::rayTracingSupported && consumerStage == VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR) { + consumerStage = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT; + } this->consumerStage = consumerStage; this->streamed = streamed; buffer.Create( diff --git a/interfaces/Crafter.Graphics-PipelineRTVulkan.cppm b/interfaces/Crafter.Graphics-PipelineRTVulkan.cppm index 902bcbb..245ca47 100644 --- a/interfaces/Crafter.Graphics-PipelineRTVulkan.cppm +++ b/interfaces/Crafter.Graphics-PipelineRTVulkan.cppm @@ -39,6 +39,7 @@ export namespace Crafter { // matching the simple examples) and is clamped to the device's // maxRayRecursionDepth. void Init(VkCommandBuffer cmd, std::span raygenGroups, std::span missGroups, std::span hitGroups, ShaderBindingTableVulkan& shaderTable, std::uint32_t maxRecursionDepth = 1) { + Device::RequireRayTracing(); workaroundNeedsTlas = shaderTable.workaroundNeedsTlas; workaroundTlasPushOffset = shaderTable.workaroundTlasPushOffset; diff --git a/interfaces/Crafter.Graphics-ShaderBindingTableVulkan.cppm b/interfaces/Crafter.Graphics-ShaderBindingTableVulkan.cppm index f922544..7b27605 100644 --- a/interfaces/Crafter.Graphics-ShaderBindingTableVulkan.cppm +++ b/interfaces/Crafter.Graphics-ShaderBindingTableVulkan.cppm @@ -26,6 +26,7 @@ export namespace Crafter { bool workaroundNeedsTlas = false; std::uint32_t workaroundTlasPushOffset = 0; void Init(const std::span shaders) { + Device::RequireRayTracing(); shaderStages.reserve(shaders.size()); for(const VulkanShader& shader: shaders) { shaderStages.emplace_back(VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, shader.stage, shader.shader, shader.entrypoint.c_str(), shader.specilizationInfo); diff --git a/interfaces/Crafter.Graphics-ShaderVulkan.cppm b/interfaces/Crafter.Graphics-ShaderVulkan.cppm index 0af5520..4ea2d19 100644 --- a/interfaces/Crafter.Graphics-ShaderVulkan.cppm +++ b/interfaces/Crafter.Graphics-ShaderVulkan.cppm @@ -351,6 +351,17 @@ export namespace Crafter { bool patchedAS = false; std::uint32_t tlasPushOffset = 0; VulkanShader(const std::filesystem::path& path, std::string entrypoint, VkShaderStageFlagBits stage, VkSpecializationInfo* specilizationInfo) : stage(stage), entrypoint(entrypoint), specilizationInfo(specilizationInfo) { + // A ray tracing stage is the first thing every RT setup creates. + // Stop here on a device without ray tracing: its shaders are + // descriptor-heap SPIR-V, which must not reach a driver that never + // enabled the heap or untyped pointers. + constexpr VkShaderStageFlags rayTracingStages = + VK_SHADER_STAGE_RAYGEN_BIT_KHR | VK_SHADER_STAGE_ANY_HIT_BIT_KHR + | VK_SHADER_STAGE_CLOSEST_HIT_BIT_KHR | VK_SHADER_STAGE_MISS_BIT_KHR + | VK_SHADER_STAGE_INTERSECTION_BIT_KHR | VK_SHADER_STAGE_CALLABLE_BIT_KHR; + if ((stage & rayTracingStages) != 0) { + Device::RequireRayTracing(); + } std::ifstream file(path, std::ios::binary); if (!file) { std::cerr << "Error: Could not open file " << path << std::endl; diff --git a/interfaces/Crafter.Graphics-UI.cppm b/interfaces/Crafter.Graphics-UI.cppm index 9675f73..64be449 100644 --- a/interfaces/Crafter.Graphics-UI.cppm +++ b/interfaces/Crafter.Graphics-UI.cppm @@ -260,9 +260,10 @@ export namespace Crafter { const FusedBatch& text); // Generic dispatch for user-authored shaders. On Vulkan, `shader` is - // a SPIR-V compute pipeline (bindless via VK_EXT_descriptor_heap, so - // any resource indices baked into push data resolve through the - // global heap). On DOM, `shader` carries a UICustomBinding list + // a SPIR-V compute pipeline (bindless, via VK_EXT_descriptor_heap or + // the descriptor-set fallback, so any resource indices baked into + // push data resolve through the global heap either way). On DOM, + // `shader` carries a UICustomBinding list // declared at Load time; the renderer reads the listed slot uints // out of `push`, resolves them against heap.bufferTable / // imageTable / samplerTable, and builds the bind groups before @@ -430,7 +431,7 @@ export namespace Crafter { #ifndef CRAFTER_GRAPHICS_WINDOW_DOM void WriteSwapchainDescriptors(); void WriteFontAtlasDescriptor(); - void WriteBufferDescriptor(std::uint16_t slot, VkDeviceAddress address, std::uint32_t size); + void WriteBufferDescriptor(std::uint16_t slot, VkBuffer buffer, VkDeviceAddress address, std::uint32_t size); void WriteSampledImageDescriptor(std::uint16_t slot, const VkImageViewCreateInfo& viewInfo, VkImageLayout layout); @@ -442,7 +443,7 @@ export namespace Crafter { BufferSlot UIRenderer::RegisterBuffer(GraphicsBuffer& buffer) { auto range = heap_->AllocateBufferSlots(1); #ifndef CRAFTER_GRAPHICS_WINDOW_DOM - WriteBufferDescriptor(range.firstElement, buffer.address, buffer.size); + WriteBufferDescriptor(range.firstElement, buffer.buffer, buffer.address, buffer.size); #else heap_->bufferTable[range.firstElement] = buffer.handle; #endif @@ -454,6 +455,10 @@ export namespace Crafter { ImageSlot UIRenderer::RegisterImage(ImageVulkan& image, VkFormat format, VkImageLayout layout) { auto range = heap_->AllocateImageSlots(1); + // The handle owns the slot from here on: in descriptor-set mode the + // write creates an image view, and if that throws the slot returns + // to the freelist instead of leaking. + ImageSlot slot{heap_, range.firstElement}; VkImageViewCreateInfo info { .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = image.image, @@ -472,7 +477,7 @@ export namespace Crafter { }, }; WriteSampledImageDescriptor(range.firstElement, info, layout); - return ImageSlot{heap_, range.firstElement}; + return slot; } #endif } diff --git a/interfaces/Crafter.Graphics-VulkanTransition.cppm b/interfaces/Crafter.Graphics-VulkanTransition.cppm index 23d3bc5..f63ce3a 100644 --- a/interfaces/Crafter.Graphics-VulkanTransition.cppm +++ b/interfaces/Crafter.Graphics-VulkanTransition.cppm @@ -9,6 +9,7 @@ module; export module Crafter.Graphics:VulkanTransition; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM import std; +import :Device; export namespace Crafter { #include "vulkan/vulkan.h" @@ -68,7 +69,10 @@ export namespace Crafter { return VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; case VK_IMAGE_LAYOUT_GENERAL: //assert(false && "Don't know how to get a meaningful VkPipelineStageFlags for VK_IMAGE_LAYOUT_GENERAL! Don't use it!"); - return VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR; + // GENERAL images are written by RT or compute shaders; on a + // device without ray tracing the RT stage is not a valid stage. + return Device::rayTracingSupported ? VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR + : VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT; default: assert(false); return 0; diff --git a/project.cpp b/project.cpp index 7342e2f..c4a4cec 100644 --- a/project.cpp +++ b/project.cpp @@ -236,11 +236,17 @@ extern "C" Configuration CrafterBuildProject(std::span a }; cfg.GetInterfacesAndImplementations(ifaces, impls); - cfg.shaders.emplace_back(fs::path("shaders/ui-quads.comp.glsl"), std::string("main"), ShaderType::Compute); - cfg.shaders.emplace_back(fs::path("shaders/ui-circles.comp.glsl"), std::string("main"), ShaderType::Compute); - cfg.shaders.emplace_back(fs::path("shaders/ui-images.comp.glsl"), std::string("main"), ShaderType::Compute); - cfg.shaders.emplace_back(fs::path("shaders/ui-text.comp.glsl"), std::string("main"), ShaderType::Compute); - cfg.shaders.emplace_back(fs::path("shaders/ui-fused.comp.glsl"), std::string("main"), ShaderType::Compute); + // Every UI kernel is built twice from one source: .comp.spv + // indexes VK_EXT_descriptor_heap, .comp.sets.spv reads the same + // slots from a bindless descriptor set for drivers without the heap + // extension (Mesa Turnip). ui-shared.glsl switches on the define and + // ComputeShader::Load picks the file at runtime. + for (const char* shader : {"shaders/ui-quads.comp.glsl", "shaders/ui-circles.comp.glsl", "shaders/ui-images.comp.glsl", "shaders/ui-text.comp.glsl", "shaders/ui-fused.comp.glsl"}) { + cfg.shaders.emplace_back(fs::path(shader), std::string("main"), ShaderType::Compute); + Shader& sets = cfg.shaders.emplace_back(fs::path(shader), std::string("main"), ShaderType::Compute); + sets.defines = {"CRAFTER_GRAPHICS_DESCRIPTOR_SETS"}; + sets.variant = "sets"; + } cfg.buildFiles.emplace_back(fs::path("shaders/ui-shared.glsl")); // Regression test for issue #18: drive the NVIDIA descriptor-heap diff --git a/shaders/ui-shared.glsl b/shaders/ui-shared.glsl index b06888b..2157978 100644 --- a/shaders/ui-shared.glsl +++ b/shaders/ui-shared.glsl @@ -8,7 +8,31 @@ #extension GL_EXT_shader_image_load_formatted : enable #extension GL_EXT_shader_explicit_arithmetic_types_int16 : enable + +// ─── binding model ────────────────────────────────────────────────────── +// Every shader that includes this file is compiled twice from one source. +// The default build indexes VK_EXT_descriptor_heap directly. The second +// build defines CRAFTER_GRAPHICS_DESCRIPTOR_SETS and reads the same slots +// from one bindless descriptor set, for drivers without the heap extension +// (output: .sets.spv, picked by ComputeShader::Load at runtime). +// +// Declare bindless resources with these macros instead of writing +// `descriptor_heap` or a set/binding pair, and both builds stay in sync. +// Slot numbers mean the same thing in both: UI_HEAP_IMAGES and +// UI_HEAP_TEXTURES share one image slot space, and any number of buffer +// blocks may alias UI_HEAP_BUFFERS. +#ifdef CRAFTER_GRAPHICS_DESCRIPTOR_SETS +#define UI_HEAP_IMAGES set = 0, binding = 0 +#define UI_HEAP_TEXTURES set = 0, binding = 1 +#define UI_HEAP_SAMPLERS set = 0, binding = 2 +#define UI_HEAP_BUFFERS set = 0, binding = 3 +#else #extension GL_EXT_descriptor_heap : enable +#define UI_HEAP_IMAGES descriptor_heap +#define UI_HEAP_TEXTURES descriptor_heap +#define UI_HEAP_SAMPLERS descriptor_heap +#define UI_HEAP_BUFFERS descriptor_heap +#endif #extension GL_EXT_nonuniform_qualifier : enable #extension GL_EXT_buffer_reference : enable @@ -16,9 +40,9 @@ // The same heap slot can be read as either uiImages[] (storage image) or // uiTextures[] (sampled image) depending on which descriptor was written // at that slot. Samplers live in a separate sampler heap. -layout(descriptor_heap) uniform image2D uiImages[]; -layout(descriptor_heap) uniform texture2D uiTextures[]; -layout(descriptor_heap) uniform sampler uiSamplers[]; +layout(UI_HEAP_IMAGES) uniform image2D uiImages[]; +layout(UI_HEAP_TEXTURES) uniform texture2D uiTextures[]; +layout(UI_HEAP_SAMPLERS) uniform sampler uiSamplers[]; // ─── push-constant header ─────────────────────────────────────────────── // Every UI dispatch's push-constant struct begins with this. User shaders @@ -70,10 +94,10 @@ struct GlyphItem { vec4 rect; vec4 uv; vec4 color; }; // ─── SSBO heap views ──────────────────────────────────────────────────── // One declaration per item type; each shader uses the one matching its // dispatch. Indexed by hdr.itemBuffer. -layout(descriptor_heap, std430) readonly buffer UIQuadBuf { QuadItem items[]; } uiQuadHeap[]; -layout(descriptor_heap, std430) readonly buffer UICircleBuf { CircleItem items[]; } uiCircleHeap[]; -layout(descriptor_heap, std430) readonly buffer UIImageBuf { ImageItem items[]; } uiImageHeap[]; -layout(descriptor_heap, std430) readonly buffer UIGlyphBuf { GlyphItem items[]; } uiGlyphHeap[]; +layout(UI_HEAP_BUFFERS, std430) readonly buffer UIQuadBuf { QuadItem items[]; } uiQuadHeap[]; +layout(UI_HEAP_BUFFERS, std430) readonly buffer UICircleBuf { CircleItem items[]; } uiCircleHeap[]; +layout(UI_HEAP_BUFFERS, std430) readonly buffer UIImageBuf { ImageItem items[]; } uiImageHeap[]; +layout(UI_HEAP_BUFFERS, std430) readonly buffer UIGlyphBuf { GlyphItem items[]; } uiGlyphHeap[]; // ──── Driver workaround: per-member SSBO load ────────────────────────────