diff --git a/README.md b/README.md index be5aeb8..fc81db2 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,7 @@ 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` (the compute/UI path -also runs without it, see -[below](#drivers-without-vk_ext_descriptor_heap)); `wasm32-*` builds +builds use Vulkan with `VK_EXT_descriptor_heap`; `wasm32-*` builds target the browser via WebGPU and a DOM window backend. ## Backends @@ -81,10 +79,7 @@ shaded through an intersection shader with an any-hit cut-out. `VK_EXT_descriptor_heap`; pipelines are created with `VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT` so there are no descriptor-set layouts and push constants travel via - `vkCmdPushDataEXT`. The heap, ray tracing and device-fault extensions - are optional groups, detected at start-up - (`Device::descriptorHeapSupported`, `rayTracingSupported`, - `deviceFaultSupported`). + `vkCmdPushDataEXT`. - **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 @@ -113,13 +108,12 @@ 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` (or the `.sets.spv` build and `vkCmdPushConstants` - without the heap); WebGPU loads a user-supplied `.wgsl` blob at + `vkCmdPushDataEXT`; 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 (four - kernels plus the fused one) ship as `.spv` / `.sets.spv` pairs on - native and as WGSL strings baked into the WebGPU dispatcher. +- **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. - **FontAtlas** — single-channel SDF atlas (1024×1024, 32pt base, shelf-packed, lazy `Ensure` per codepoint, dirty-flush via `Update`). Backend-agnostic. @@ -174,51 +168,6 @@ 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 4e7a2b3..00ada77 100644 --- a/examples/CustomShader/inverse-circle.comp.glsl +++ b/examples/CustomShader/inverse-circle.comp.glsl @@ -21,10 +21,7 @@ struct InverseCircleItem { vec4 centerRadius; }; -// 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 { +layout(descriptor_heap, std430) readonly buffer InvCircleBuf { InverseCircleItem items[]; } invCircleHeap[]; diff --git a/examples/CustomShader/main.cpp b/examples/CustomShader/main.cpp index 818ec81..960d002 100644 --- a/examples/CustomShader/main.cpp +++ b/examples/CustomShader/main.cpp @@ -54,11 +54,7 @@ int main() { // read from `hdr.itemBuffer` in the push data. GraphicsComputeShader inverseCircle; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM - // 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); + inverseCircle.Load("inverse-circle.comp.spv"); #else UICustomBinding invBindings[] = { { .group = 2, diff --git a/examples/CustomShader/project.cpp b/examples/CustomShader/project.cpp index dc79778..eae0218 100644 --- a/examples/CustomShader/project.cpp +++ b/examples/CustomShader/project.cpp @@ -47,13 +47,6 @@ 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 d6242a6..81e4433 100644 --- a/examples/RTVolume/main.cpp +++ b/examples/RTVolume/main.cpp @@ -125,7 +125,7 @@ int main() { } ComputeShader aabbWriter; - aabbWriter.Load("aabbs.comp.spv", descriptorHeap); + aabbWriter.Load("aabbs.comp.spv"); 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 3af554b..ebc91d6 100644 --- a/implementations/Crafter.Graphics-ComputeShader.cpp +++ b/implementations/Crafter.Graphics-ComputeShader.cpp @@ -7,14 +7,12 @@ 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; @@ -26,7 +24,6 @@ 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; @@ -41,44 +38,7 @@ ComputeShader::~ComputeShader() { } } -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; - } - +void ComputeShader::Load(const std::filesystem::path& spvPath) { VulkanShader shader(spvPath, "main", VK_SHADER_STAGE_COMPUTE_BIT, nullptr); // NVIDIA descriptor-heap AS-read workaround (issue #15 / #7): remember @@ -118,38 +78,12 @@ void ComputeShader::Dispatch(VkCommandBuffer cmd, VkDeviceAddress tlasAddress) const { vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); if (push != nullptr && pushBytes > 0) { - 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); - } + 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 138107c..075d7ef 100644 --- a/implementations/Crafter.Graphics-Device.cpp +++ b/implementations/Crafter.Graphics-Device.cpp @@ -41,31 +41,19 @@ const char* const instanceExtensionNames[] = { "VK_KHR_wayland_surface" #endif }; -// Device extensions are split by what breaks without them. Only the first -// group is needed to put UI pixels on screen; Device::Initialize enables each -// of the others only when the driver advertises all of its members, and -// records the outcome in Device::descriptorHeapSupported / rayTracingSupported -// / deviceFaultSupported. Mesa Turnip (Adreno), for one, has none of the heap, -// ray tracing or fault extensions. -const char* const requiredDeviceExtensionNames[] = { +const char* const deviceExtensionNames[] = { "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" @@ -75,9 +63,7 @@ const char* const layerNames[] = { void Device::CheckVkResult(VkResult result) { if (result != VK_SUCCESS) { - // 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) { + if(result == VK_ERROR_DEVICE_LOST) { VkDeviceFaultCountsEXT faultCounts = { .sType = VK_STRUCTURE_TYPE_DEVICE_FAULT_COUNTS_EXT, .pNext = NULL, @@ -133,26 +119,6 @@ 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, @@ -685,130 +651,36 @@ void Device::Initialize() { } } - // 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; + // 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. { 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) { - 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 (std::strcmp(e.extensionName, VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME) == 0) { + memoryDecompressionSupported = true; + break; } } - if (!missing.empty()) { - throw std::runtime_error(std::format("Crafter.Graphics: the Vulkan driver lacks required device extension(s):{}", missing)); - } } - // CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the descriptor-set path on a - // driver that does have the heap, so the fallback can be exercised on a - // desktop GPU. It takes ray tracing down with it, exactly as on a driver - // that really lacks the extension. - // - // A listed extension is not the whole answer: a driver may list one whose - // feature bit is still off. Query the feature bits of the optional groups - // whose extensions are present (only those structs are chained) and - // require both, so such a driver takes the fallback instead of failing - // in vkCreateDevice. - const bool heapExtensions = hasAll(descriptorHeapExtensionNames); - const bool rayTracingExtensions = hasAll(rayTracingExtensionNames); - const bool faultExtension = availableExtensions.contains(VK_EXT_DEVICE_FAULT_EXTENSION_NAME); - VkPhysicalDeviceDescriptorHeapFeaturesEXT heapQuery { - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_HEAP_FEATURES_EXT, - }; - VkPhysicalDeviceShaderUntypedPointersFeaturesKHR untypedPointersQuery { - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_UNTYPED_POINTERS_FEATURES_KHR, - }; - VkPhysicalDeviceAccelerationStructureFeaturesKHR accelerationStructureQuery { - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_FEATURES_KHR, - }; - VkPhysicalDeviceRayTracingPipelineFeaturesKHR rayTracingPipelineQuery { - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_PIPELINE_FEATURES_KHR, - }; - VkPhysicalDeviceRayQueryFeaturesKHR rayQueryQuery { - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_QUERY_FEATURES_KHR, - }; - VkPhysicalDeviceFaultFeaturesEXT faultQuery { - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT, - }; - VkPhysicalDeviceFeatures2 featureQuery { - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, - }; - const auto ask = [&featureQuery](auto& features) { - features.pNext = featureQuery.pNext; - featureQuery.pNext = &features; - }; - if (heapExtensions) { - ask(heapQuery); - ask(untypedPointersQuery); - } - if (rayTracingExtensions) { - ask(accelerationStructureQuery); - ask(rayTracingPipelineQuery); - ask(rayQueryQuery); - } - if (faultExtension) { - ask(faultQuery); - } - vkGetPhysicalDeviceFeatures2(physDevice, &featureQuery); - - descriptorHeapSupported = heapExtensions - && heapQuery.descriptorHeap && untypedPointersQuery.shaderUntypedPointers - && std::getenv("CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP") == nullptr; - rayTracingSupported = descriptorHeapSupported && rayTracingExtensions - && accelerationStructureQuery.accelerationStructure - && rayTracingPipelineQuery.rayTracingPipeline - && rayQueryQuery.rayQuery; - deviceFaultSupported = faultExtension && faultQuery.deviceFault; - memoryDecompressionSupported = availableExtensions.contains(VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME); - const bool atomicFloatSupported = availableExtensions.contains(VK_EXT_SHADER_ATOMIC_FLOAT_EXTENSION_NAME); - // 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; - } + // 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 = propertiesChain; - propertiesChain = &memoryDecompressionProperties; + memoryDecompressionProperties.pNext = const_cast(rayTracingProperties.pNext); + rayTracingProperties.pNext = &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 = propertiesChain + .pNext = &rayTracingProperties }; vkGetPhysicalDeviceProperties2(physDevice, &properties2); // Keep the core properties around for the pipeline-cache identity check @@ -822,13 +694,8 @@ 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. 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; + // take the normal heap-bound AS path unchanged. + workaroundDescriptorHeapAS = (driverProperties.driverID == VK_DRIVER_ID_NVIDIA_PROPRIETARY); // Sanity-gate: GDeflate 1.0 must actually be in the supported method set. if (memoryDecompressionSupported && @@ -860,18 +727,24 @@ void Device::Initialize() { queueCreateInfo.queueCount = 1; queueCreateInfo.pQueuePriorities = &priority; - // Feature structs below are declared without a pNext and linked into one - // chain afterwards, each only when its extension is being enabled: a - // feature struct for an extension that is not enabled is invalid, and - // which ones are enabled now depends on the driver. + 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); + VkPhysicalDeviceFaultFeaturesEXT faultFeatures = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT, .deviceFault = VK_TRUE, - .deviceFaultVendorBinary = faultQuery.deviceFaultVendorBinary, + .deviceFaultVendorBinary = faultFeatures2.deviceFaultVendorBinary, }; VkPhysicalDeviceShaderUntypedPointersFeaturesKHR untypedPointersFeatures { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_UNTYPED_POINTERS_FEATURES_KHR, + .pNext = &faultFeatures, .shaderUntypedPointers = VK_TRUE, }; @@ -879,38 +752,37 @@ 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 by both - // binding models (heap and descriptor sets). + // Bindless / runtime descriptor array indexing — needed for the + // descriptor_heap shader path. .shaderUniformBufferArrayNonUniformIndexing = VK_TRUE, .shaderSampledImageArrayNonUniformIndexing = VK_TRUE, .shaderStorageBufferArrayNonUniformIndexing = VK_TRUE, @@ -919,64 +791,28 @@ 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 = featureChain, + .pNext = &deviceAccelerationStructureFeature, .features = { // Order matches VkPhysicalDeviceFeatures declaration so the // designated-initializer-order warning stays quiet. @@ -997,23 +833,12 @@ void Device::Initialize() { } }; - // The enabled-extension list is the required set plus each optional group - // the driver advertises in full (decided above from the enumeration). + // Build the enabled-extension list dynamically so we can append the + // optional VK_EXT_memory_decompression entry only when the driver + // advertises it. std::vector enabledDeviceExtensions( - 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); - } + std::begin(deviceExtensionNames), + std::end(deviceExtensionNames)); 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 c171e4b..b24f3e1 100644 --- a/implementations/Crafter.Graphics-Mesh.cpp +++ b/implementations/Crafter.Graphics-Mesh.cpp @@ -191,9 +191,6 @@ 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 @@ -208,7 +205,6 @@ 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()); @@ -346,7 +341,6 @@ 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 9622c94..81a7c95 100644 --- a/implementations/Crafter.Graphics-RenderingElement3D.cpp +++ b/implementations/Crafter.Graphics-RenderingElement3D.cpp @@ -32,7 +32,6 @@ 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 b2ce600..05bc318 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, heap); - drawCircles.Load(circlesSpv, heap); - drawImages.Load(imagesSpv, heap); - drawText.Load(textSpv, heap); - drawFused.Load(fusedSpv, heap); + drawQuads.Load(quadsSpv); + drawCircles.Load(circlesSpv); + drawImages.Load(imagesSpv); + drawText.Load(textSpv); + drawFused.Load(fusedSpv); // Allocate one image slot for the swapchain output. Each per-frame heap // copy will hold ITS frame's image at this slot. @@ -275,16 +275,6 @@ 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{}; @@ -344,11 +334,6 @@ 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{}; @@ -379,14 +364,7 @@ void UIRenderer::WriteSampledImageDescriptor(std::uint16_t slot, for (auto& h : heap_->resourceHeap) h.FlushDevice(off, sz); } -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; - } - +void UIRenderer::WriteBufferDescriptor(std::uint16_t slot, VkDeviceAddress address, std::uint32_t size) { std::array ranges{}; std::array resources{}; std::array destinations{}; @@ -415,14 +393,6 @@ void UIRenderer::WriteBufferDescriptor(std::uint16_t slot, VkBuffer buffer, VkDe 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 017121c..8ae726a 100644 --- a/implementations/Crafter.Graphics-Window.cpp +++ b/implementations/Crafter.Graphics-Window.cpp @@ -833,11 +833,7 @@ 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()). - // 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; + const VkPipelineStageFlags swapWriterStages = SwapchainStageUnion(passes); // dstAccessMask must only contain access flags supported by the dst stage // mask (VUID-vkCmdPipelineBarrier-pImageMemoryBarriers-02820). Derive it @@ -860,15 +856,7 @@ 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 && !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) { + 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 @@ -930,11 +918,8 @@ 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() & Device::stageMask, - passes[i + 1]->SwapchainStage() & Device::stageMask, + passes[i]->SwapchainStage(), passes[i + 1]->SwapchainStage(), 0, 0, nullptr, 0, nullptr, 1, &ib); } } @@ -1115,13 +1100,6 @@ 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 8900aa6..bc00a3c 100644 --- a/interfaces/Crafter.Graphics-ComputeShader.cppm +++ b/interfaces/Crafter.Graphics-ComputeShader.cppm @@ -9,29 +9,17 @@ 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. 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. + // 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. 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 @@ -50,30 +38,14 @@ export namespace Crafter { ComputeShader& operator=(ComputeShader&& other) noexcept; ~ComputeShader(); - // 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); + // 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); // 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 3163a72..9223853 100644 --- a/interfaces/Crafter.Graphics-DescriptorHeapVulkan.cppm +++ b/interfaces/Crafter.Graphics-DescriptorHeapVulkan.cppm @@ -39,95 +39,7 @@ 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; @@ -166,231 +78,6 @@ 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(); @@ -471,12 +158,7 @@ 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) { @@ -485,9 +167,6 @@ 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 d461eb6..d3666f7 100644 --- a/interfaces/Crafter.Graphics-Device.cppm +++ b/interfaces/Crafter.Graphics-Device.cppm @@ -174,39 +174,12 @@ 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 + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_PIPELINE_PROPERTIES_KHR, + .pNext = &descriptorHeapProperties }; 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 3864d60..5064c4e 100644 --- a/interfaces/Crafter.Graphics-ImageVulkan.cppm +++ b/interfaces/Crafter.Graphics-ImageVulkan.cppm @@ -90,15 +90,6 @@ 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 245ca47..902bcbb 100644 --- a/interfaces/Crafter.Graphics-PipelineRTVulkan.cppm +++ b/interfaces/Crafter.Graphics-PipelineRTVulkan.cppm @@ -39,7 +39,6 @@ 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 7b27605..f922544 100644 --- a/interfaces/Crafter.Graphics-ShaderBindingTableVulkan.cppm +++ b/interfaces/Crafter.Graphics-ShaderBindingTableVulkan.cppm @@ -26,7 +26,6 @@ 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 4ea2d19..0af5520 100644 --- a/interfaces/Crafter.Graphics-ShaderVulkan.cppm +++ b/interfaces/Crafter.Graphics-ShaderVulkan.cppm @@ -351,17 +351,6 @@ 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 64be449..9675f73 100644 --- a/interfaces/Crafter.Graphics-UI.cppm +++ b/interfaces/Crafter.Graphics-UI.cppm @@ -260,10 +260,9 @@ 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 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 + // 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 // 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 @@ -431,7 +430,7 @@ export namespace Crafter { #ifndef CRAFTER_GRAPHICS_WINDOW_DOM void WriteSwapchainDescriptors(); void WriteFontAtlasDescriptor(); - void WriteBufferDescriptor(std::uint16_t slot, VkBuffer buffer, VkDeviceAddress address, std::uint32_t size); + void WriteBufferDescriptor(std::uint16_t slot, VkDeviceAddress address, std::uint32_t size); void WriteSampledImageDescriptor(std::uint16_t slot, const VkImageViewCreateInfo& viewInfo, VkImageLayout layout); @@ -443,7 +442,7 @@ export namespace Crafter { BufferSlot UIRenderer::RegisterBuffer(GraphicsBuffer& buffer) { auto range = heap_->AllocateBufferSlots(1); #ifndef CRAFTER_GRAPHICS_WINDOW_DOM - WriteBufferDescriptor(range.firstElement, buffer.buffer, buffer.address, buffer.size); + WriteBufferDescriptor(range.firstElement, buffer.address, buffer.size); #else heap_->bufferTable[range.firstElement] = buffer.handle; #endif @@ -455,10 +454,6 @@ 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, @@ -477,7 +472,7 @@ export namespace Crafter { }, }; WriteSampledImageDescriptor(range.firstElement, info, layout); - return slot; + return ImageSlot{heap_, range.firstElement}; } #endif } diff --git a/interfaces/Crafter.Graphics-VulkanTransition.cppm b/interfaces/Crafter.Graphics-VulkanTransition.cppm index f63ce3a..23d3bc5 100644 --- a/interfaces/Crafter.Graphics-VulkanTransition.cppm +++ b/interfaces/Crafter.Graphics-VulkanTransition.cppm @@ -9,7 +9,6 @@ module; export module Crafter.Graphics:VulkanTransition; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM import std; -import :Device; export namespace Crafter { #include "vulkan/vulkan.h" @@ -69,10 +68,7 @@ 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!"); - // 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; + return VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR; default: assert(false); return 0; diff --git a/project.cpp b/project.cpp index 92643c0..7342e2f 100644 --- a/project.cpp +++ b/project.cpp @@ -236,17 +236,11 @@ extern "C" Configuration CrafterBuildProject(std::span a }; cfg.GetInterfacesAndImplementations(ifaces, impls); - // 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.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); cfg.buildFiles.emplace_back(fs::path("shaders/ui-shared.glsl")); // Regression test for issue #18: drive the NVIDIA descriptor-heap @@ -373,40 +367,6 @@ extern "C" Configuration CrafterBuildProject(std::span a setupReuseImpls.emplace_back("tests/SetupCmdBufferReuse/main"); sr.GetInterfacesAndImplementations(ifaces, setupReuseImpls); cfg.tests.push_back(std::move(setupReuseTest)); - - // The UI path has two binding models: VK_EXT_descriptor_heap, and - // a bindless descriptor set for drivers without it (Mesa Turnip). - // The test renders one scene through each — the second by - // re-running itself with CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP — and - // compares the frames, so the fallback cannot drift from the heap - // path unnoticed on a machine that never takes it. Unlike the - // other tests it loads the real UI kernels, so it carries the - // library's shaders (both variants) and a font into its own bin - // dir. Needs the Wayland backend + a real compositor. - Test parityTest; - Configuration& up = parityTest.config; - up.path = cfg.path; - up.name = "UIFallbackParity"; - up.outputName = "UIFallbackParity"; - up.type = ConfigurationType::Executable; - up.target = cfg.target; - up.march = cfg.march; - up.mtune = cfg.mtune; - up.debug = cfg.debug; - up.sysroot = cfg.sysroot; - up.dependencies = cfg.dependencies; - up.externalDependencies = cfg.externalDependencies; - up.compileFlags = cfg.compileFlags; - up.linkFlags = cfg.linkFlags; - up.defines = cfg.defines; - up.cFiles = cfg.cFiles; - up.shaders = cfg.shaders; - up.buildFiles = cfg.buildFiles; - up.files.emplace_back(fs::path("examples/HelloUI/font.ttf")); - std::vector parityImpls(impls.begin(), impls.end()); - parityImpls.emplace_back("tests/UIFallbackParity/main"); - up.GetInterfacesAndImplementations(ifaces, parityImpls); - cfg.tests.push_back(std::move(parityTest)); } // Issue #36: BLAS build options. Drives the real hardware AS-build diff --git a/shaders/ui-shared.glsl b/shaders/ui-shared.glsl index 2157978..b06888b 100644 --- a/shaders/ui-shared.glsl +++ b/shaders/ui-shared.glsl @@ -8,31 +8,7 @@ #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 @@ -40,9 +16,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(UI_HEAP_IMAGES) uniform image2D uiImages[]; -layout(UI_HEAP_TEXTURES) uniform texture2D uiTextures[]; -layout(UI_HEAP_SAMPLERS) uniform sampler uiSamplers[]; +layout(descriptor_heap) uniform image2D uiImages[]; +layout(descriptor_heap) uniform texture2D uiTextures[]; +layout(descriptor_heap) uniform sampler uiSamplers[]; // ─── push-constant header ─────────────────────────────────────────────── // Every UI dispatch's push-constant struct begins with this. User shaders @@ -94,10 +70,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(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[]; +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[]; // ──── Driver workaround: per-member SSBO load ──────────────────────────── diff --git a/tests/UIFallbackParity/main.cpp b/tests/UIFallbackParity/main.cpp deleted file mode 100644 index 0f03bdc..0000000 --- a/tests/UIFallbackParity/main.cpp +++ /dev/null @@ -1,423 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -// The UI path has two binding models. Where the driver has -// VK_EXT_descriptor_heap, shaders index the heap directly; where it does not -// (Mesa Turnip on Adreno), the same slots live in a bindless descriptor set -// and the shaders are a second build of the same source (.sets.spv, -// CRAFTER_GRAPHICS_DESCRIPTOR_SETS). Nothing but the binding model may differ -// between the two, so this test renders one fixed scene through each and -// compares the frames pixel by pixel. -// -// The scene is chosen to reach everything the descriptor-set path implements -// and HelloUI does not exercise: -// -// - the four standalone kernels (DispatchQuads / Circles / Images / Text) -// and the fused kernel with all four categories non-empty; -// - a sampled image and two samplers indexed per item (bindings 1 and 2, -// nonuniform indices), next to the font atlas; -// - slot reuse: an image slot and a sampler slot are each freed and -// registered again before the first frame, so the second write has to -// replace the first one's view / sampler. A stale descriptor shows the -// wrong texture and fails the comparison; -// - a resize, which rewrites the per-frame swapchain descriptors; -// - an image created with ImageVulkan::Create's default consumer stage, -// which has to be resolved to a stage the device actually has; -// - a user-style ui.Dispatch whose push payload is not a whole number of -// words, which the descriptor-set path pads (the heap run pushes the -// whole header and must draw the same quad). -// -// How it runs: with no arguments the test is the driver. It starts itself -// twice with `--render `, once as is and once with -// CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP set, then decodes and compares the two -// PNGs. A child also fails on any Vulkan validation error raised while -// rendering, which only means something when CRAFTER_GRAPHICS_VALIDATION is -// set for the run. -// -// On a driver that has no descriptor heap at all, both children take the -// descriptor-set path. There is then nothing to compare against, and the test -// passes on the strength of the scene rendering cleanly — which is the run -// that matters on such a device. `--render ` can also be used directly to -// produce a frame for inspection. -// -// Needs a live Wayland compositor and a Vulkan device, like FrameLoopSync. - -#include "vulkan/vulkan.h" -#include - -// PNG decode for the comparison. The library only compiles the writer -// (Window::SaveFrame); STB_IMAGE_STATIC keeps the reader private to this file. -#define STB_IMAGE_IMPLEMENTATION -#define STB_IMAGE_STATIC -#define STBI_ONLY_PNG -#include "../../lib/stb_image.h" - -import Crafter.Graphics; -import Crafter.Event; -import std; - -using namespace Crafter; -namespace fs = std::filesystem; - -namespace { - -int failures = 0; - -void Check(bool ok, std::string_view what) { - std::println("{} {}", ok ? "PASS" : "FAIL", what); - if (!ok) ++failures; -} - -// The test binary's own directory: the build places the compiled shaders -// (both variants) and font.ttf next to it, and the test runner's working -// directory is not that directory. -fs::path ExeDir() { - return fs::read_symlink("/proc/self/exe").parent_path(); -} - -int RunCommand(const std::string& cmd) { - int status = std::system(cmd.c_str()); - if (status == -1) return -1; - return (status & 0x7f) == 0 ? ((status >> 8) & 0xff) : 128 + (status & 0x7f); -} - -// RGBA8 texel as a 4-byte word, matching VK_FORMAT_R8G8B8A8_UNORM. -using Texel = std::uint32_t; -constexpr std::uint16_t kTexSize = 8; - -constexpr std::uint32_t kWidth = 640; -constexpr std::uint32_t kHeight = 480; -// The size after the mid-run resize. Smaller than the first one so the scene, -// laid out in absolute pixels, still fits. -constexpr std::uint32_t kResizedWidth = 600; -constexpr std::uint32_t kResizedHeight = 440; - -template -void CreateItemBuffer(GraphicsBuffer& buffer, std::uint32_t count) { - buffer.Create( - VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, count); -} - -void CreateTexture(ImageVulkan& image, VkCommandBuffer cmd, auto&& texelAt) { - image.Create(kTexSize, kTexSize, /*mipLevels*/ 1, cmd, - VK_FORMAT_R8G8B8A8_UNORM, - VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); - for (std::uint32_t y = 0; y < kTexSize; ++y) { - for (std::uint32_t x = 0; x < kTexSize; ++x) { - image.buffer.value[y * kTexSize + x] = texelAt(x, y); - } - } - image.Update(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); -} - -VkSamplerCreateInfo SamplerInfo(VkFilter filter) { - return { - .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, - .magFilter = filter, - .minFilter = filter, - .mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST, - .addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, - .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, - .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, - .maxAnisotropy = 1.0f, - .maxLod = VK_LOD_CLAMP_NONE, - }; -} - -// Renders the scene and writes the last frame to `out`. Also writes -// `.mode` holding "heap" or "sets", so the driver can tell which binding -// model this process actually ran with. -int RenderScene(const fs::path& out) { - const fs::path dir = ExeDir(); - - Device::Initialize(); - Window window(kWidth, kHeight, "UIFallbackParity test"); - VkCommandBuffer init = window.StartInit(); - - GraphicsDescriptorHeap heap; - heap.Initialize(/*images*/ 8, /*buffers*/ 16, /*samplers*/ 4); - window.descriptorHeap = &heap; - - Font font(dir / "font.ttf"); - FontAtlas atlas; - atlas.Initialize(init); - - UIRenderer ui; - ui.fontAtlas = &atlas; - ui.Initialize(window, heap, init, - dir / "ui-quads.comp.spv", dir / "ui-circles.comp.spv", - dir / "ui-images.comp.spv", dir / "ui-text.comp.spv", - dir / "ui-fused.comp.spv"); - window.passes.push_back(&ui); - - // Two textures. `red` is registered first and its slot freed again, so - // `checker` lands on the same slot: its descriptor (in sets mode, its - // view) must replace red's. If the write were lost the image quads below - // would come out red in one model and checkered in the other. - ImageVulkan red; - ImageVulkan checker; - CreateTexture(red, init, [](std::uint32_t, std::uint32_t) { return 0xFF0000FFu; }); - CreateTexture(checker, init, [](std::uint32_t x, std::uint32_t y) { - return ((x + y) & 1u) != 0 ? 0xFFFFC040u : 0xFF204080u; - }); - - // A third image, created with Create's default consumer stage and never - // sampled. That default names the ray tracing stage, which is not a valid - // barrier stage on a device without ray tracing, so Create has to resolve - // it for both the member and the transition it records itself. Only the - // validation layer can see a wrong stage here. - ImageVulkan defaultStage; - defaultStage.Create(kTexSize, kTexSize, /*mipLevels*/ 1, init, - VK_FORMAT_R8G8B8A8_UNORM, - VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); - std::fill_n(defaultStage.buffer.value, kTexSize * kTexSize, Texel{0}); - defaultStage.Update(init, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); - - ImageSlot imageSlot = ui.RegisterImage(red, VK_FORMAT_R8G8B8A8_UNORM); - const std::uint16_t firstImageSlot = imageSlot; - imageSlot.Reset(); - imageSlot = ui.RegisterImage(checker, VK_FORMAT_R8G8B8A8_UNORM); - Check(static_cast(imageSlot) == firstImageSlot, - "the freed image slot is handed out again"); - - // Same for a sampler slot: linear first, then nearest on the reused slot. - // The 8x8 checker is magnified ~12x, so the two filters differ in almost - // every pixel of the quad. - SamplerSlot nearestSlot = ui.RegisterSampler(SamplerInfo(VK_FILTER_LINEAR)); - const std::uint16_t firstSamplerSlot = nearestSlot; - nearestSlot.Reset(); - nearestSlot = ui.RegisterSampler(SamplerInfo(VK_FILTER_NEAREST)); - Check(static_cast(nearestSlot) == firstSamplerSlot, - "the freed sampler slot is handed out again"); - SamplerSlot linearSlot = ui.RegisterLinearClampSampler(); - - // One set of item buffers for the standalone dispatches (left column) and - // one for the fused dispatch (right column). - GraphicsBuffer quadsA, quadsB; - GraphicsBuffer circlesA, circlesB; - GraphicsBuffer imagesA, imagesB; - GraphicsBuffer glyphsA, glyphsB; - CreateItemBuffer(quadsA, 8); CreateItemBuffer(quadsB, 8); - CreateItemBuffer(circlesA, 8); CreateItemBuffer(circlesB, 8); - CreateItemBuffer(imagesA, 8); CreateItemBuffer(imagesB, 8); - CreateItemBuffer(glyphsA, 64); CreateItemBuffer(glyphsB, 64); - BufferSlot quadsASlot = ui.RegisterBuffer(quadsA); - BufferSlot circlesASlot = ui.RegisterBuffer(circlesA); - BufferSlot imagesASlot = ui.RegisterBuffer(imagesA); - BufferSlot glyphsASlot = ui.RegisterBuffer(glyphsA); - BufferSlot quadsBSlot = ui.RegisterBuffer(quadsB); - BufferSlot circlesBSlot = ui.RegisterBuffer(circlesB); - BufferSlot imagesBSlot = ui.RegisterBuffer(imagesB); - BufferSlot glyphsBSlot = ui.RegisterBuffer(glyphsB); - - // A single quad for the odd-sized generic dispatch below. - GraphicsBuffer quadsOdd; - CreateItemBuffer(quadsOdd, 1); - BufferSlot quadsOddSlot = ui.RegisterBuffer(quadsOdd); - - // Everything is laid out in absolute pixels so both runs draw the same - // thing whatever size the compositor settles on. - auto fillColumn = [&](float ox, GraphicsBuffer& quads, - GraphicsBuffer& circles, - GraphicsBuffer& images, - GraphicsBuffer& glyphs, - bool withBackground, std::string_view label, - std::array& counts) { - std::uint32_t q = 0; - if (withBackground) { - quads.value[q++] = QuadItem{ 0, 0, 4096, 4096, 0.08f, 0.09f, 0.12f, 1.0f, 0, 0, 0, 0, 0, 0, 0, 0 }; - } - quads.value[q++] = QuadItem{ ox, 20, 220, 70, 0.20f, 0.45f, 0.85f, 1.0f, 4, 12, 20, 28, 3, 1.0f, 0.8f, 0.2f }; - quads.value[q++] = QuadItem{ ox + 40, 60, 140, 50, 0.90f, 0.30f, 0.30f, 0.6f, 0, 0, 0, 0, 0, 0, 0, 0 }; - - std::uint32_t c = 0; - circles.value[c++] = CircleItem{ ox + 50, 150, 34, 0, 0.30f, 0.85f, 0.45f, 1.0f, 0, 0, 0, 0 }; - circles.value[c++] = CircleItem{ ox + 150, 150, 30, 0, 0.0f, 0.0f, 0.0f, 0.0f, 5, 0.95f, 0.95f, 1.0f }; - - std::uint32_t i = 0; - images.value[i++] = ImageItem{ ox, 200, 96, 96, 0, 0, 1, 1, 1, 1, 1, 1, - imageSlot, nearestSlot, 0, 0 }; - images.value[i++] = ImageItem{ ox + 110, 200, 96, 96, 0, 0, 1, 1, 1.0f, 0.8f, 0.8f, 1.0f, - imageSlot, linearSlot, 0, 0 }; - - std::uint32_t g = ui.ShapeText(font, 22.0f, ox, 340.0f, label, - {1.0f, 1.0f, 1.0f, 1.0f}, glyphs.value, 64); - counts = { q, c, i, g }; - }; - - std::array countsA{}; - std::array countsB{}; - EventListener buildSub(&ui.onBuild, [&](UIBuildArgs a) { - VkCommandBuffer cmd = a.cmd; - fillColumn(30.0f, quadsA, circlesA, imagesA, glyphsA, /*withBackground*/ true, "standalone", countsA); - fillColumn(330.0f, quadsB, circlesB, imagesB, glyphsB, /*withBackground*/ false, "fused", countsB); - - constexpr VkAccessFlags read = VK_ACCESS_SHADER_READ_BIT; - constexpr VkPipelineStageFlags stage = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT; - quadsA.FlushDevice(cmd, read, stage); quadsB.FlushDevice(cmd, read, stage); - circlesA.FlushDevice(cmd, read, stage); circlesB.FlushDevice(cmd, read, stage); - imagesA.FlushDevice(cmd, read, stage); imagesB.FlushDevice(cmd, read, stage); - glyphsA.FlushDevice(cmd, read, stage); glyphsB.FlushDevice(cmd, read, stage); - - ui.DispatchQuads(cmd, quadsASlot, countsA[0]); - ui.DispatchCircles(cmd, circlesASlot, countsA[1]); - ui.DispatchImages(cmd, imagesASlot, countsA[2]); - ui.DispatchText(cmd, glyphsASlot, countsA[3]); - - ui.DispatchFused(cmd, - {quadsBSlot, countsB[0]}, - {circlesBSlot, countsB[1]}, - {imagesBSlot, countsB[2]}, - {glyphsBSlot, countsB[3]}); - - // One more quad through the generic Dispatch a custom shader would - // use. On the descriptor-set path the last three bytes of the - // header's reserved `_pad` are left off, so ComputeShader::Dispatch - // has to pad the payload for vkCmdPushConstants. The heap run pushes - // the whole header (a ragged size is a validation error there and - // nothing pads it), and both must draw the same quad. - quadsOdd.value[0] = QuadItem{ 30, 370, 180, 30, 0.85f, 0.65f, 0.15f, 1.0f, 6, 6, 6, 6, 0, 0, 0, 0 }; - quadsOdd.FlushDevice(cmd, read, stage); - const UIDispatchHeader oddHeader = ui.FillHeader(quadsOddSlot, 1); - const std::uint32_t oddBytes = Device::descriptorHeapSupported - ? static_cast(sizeof(oddHeader)) - : static_cast(sizeof(oddHeader) - 3); - ui.Dispatch(cmd, ui.drawQuads, &oddHeader, oddBytes, - (window.width + 7) / 8, (window.height + 7) / 8); - }); - - window.FinishInit(); - - // More frames than there are in flight, so every per-frame heap copy / - // descriptor set is used; then a resize, which recreates the swapchain - // and rewrites its descriptors; then enough frames for the glyphs shaped - // on the first frame to have reached the atlas texture. - for (int i = 0; i < Window::numFrames * 2; ++i) window.Render(); - window.Resize(kResizedWidth, kResizedHeight); - for (int i = 0; i < Window::numFrames * 3; ++i) window.Render(); - Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); - - // Counted before the capture: SaveFrame reads back an image that has - // already been handed to the presentation engine, which the validation - // layer reports in either binding model. What is under test here is the - // rendering above. - const std::uint32_t renderErrors = Device::validationErrorCount; - Check(renderErrors == 0, - std::format("no Vulkan validation errors while rendering ({} seen)", renderErrors)); - - window.SaveFrame(out); - std::ofstream(fs::path(out).concat(".mode")) << (Device::descriptorHeapSupported ? "heap" : "sets"); - Check(fs::exists(out), "the frame was written"); - - // The buffers, images and heap go out of scope here while the last - // frames may still be presenting; settle the queue first. - Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); - return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; -} - -struct Frame { - int width = 0; - int height = 0; - std::vector rgba; -}; - -Frame LoadFrame(const fs::path& path) { - Frame frame; - int channels = 0; - stbi_uc* pixels = stbi_load(path.string().c_str(), &frame.width, &frame.height, &channels, STBI_rgb_alpha); - if (pixels != nullptr) { - frame.rgba.assign(pixels, pixels + static_cast(frame.width) * frame.height * 4); - stbi_image_free(pixels); - } - return frame; -} - -std::string ReadMode(const fs::path& png) { - std::ifstream in(fs::path(png).concat(".mode")); - std::string mode; - in >> mode; - return mode; -} - -int Drive() { - const fs::path exe = fs::read_symlink("/proc/self/exe"); - const fs::path dir = fs::temp_directory_path() / "crafter-ui-fallback-parity"; - std::error_code ec; - fs::remove_all(dir, ec); - fs::create_directories(dir); - const fs::path heapPng = dir / "heap.png"; - const fs::path setsPng = dir / "sets.png"; - - // `env -u` so a CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP inherited from the - // caller's environment cannot turn the reference run into a second - // fallback run. - const int heapExit = RunCommand(std::format( - "env -u CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP '{}' --render '{}'", exe.string(), heapPng.string())); - const int setsExit = RunCommand(std::format( - "env CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP=1 '{}' --render '{}'", exe.string(), setsPng.string())); - - Check(heapExit == 0, std::format("reference run rendered the scene (exit {})", heapExit)); - Check(setsExit == 0, std::format("descriptor-set run rendered the scene (exit {})", setsExit)); - Check(ReadMode(setsPng) == "sets", "the forced run took the descriptor-set path"); - if (failures != 0) return EXIT_FAILURE; - - const Frame sets = LoadFrame(setsPng); - Check(!sets.rgba.empty(), "descriptor-set frame decodes"); - if (failures != 0) return EXIT_FAILURE; - - // A frame that is one flat colour means nothing was composited, which - // two equally broken runs would agree on. - std::set colours; - for (std::size_t i = 0; i + 3 < sets.rgba.size() && colours.size() < 16; i += 4) { - std::uint32_t texel = 0; - std::memcpy(&texel, &sets.rgba[i], 4); - colours.insert(texel); - } - Check(colours.size() >= 16, std::format("descriptor-set frame has content ({}+ distinct colours)", colours.size())); - - if (ReadMode(heapPng) != "heap") { - std::println("NOTE this driver has no VK_EXT_descriptor_heap: both runs used descriptor sets, " - "so there is no heap frame to compare against"); - return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; - } - - const Frame heap = LoadFrame(heapPng); - Check(!heap.rgba.empty(), "descriptor-heap frame decodes"); - Check(heap.width == sets.width && heap.height == sets.height, - std::format("both frames are the same size ({}x{} vs {}x{})", - heap.width, heap.height, sets.width, sets.height)); - if (failures != 0) return EXIT_FAILURE; - - // One code value of slack per channel: both shaders are the same source, - // but the driver compiles two different SPIR-V modules and may contract - // or reorder float math differently on an anti-aliased edge. - std::size_t differing = 0; - int maxDiff = 0; - for (std::size_t i = 0; i < heap.rgba.size(); ++i) { - const int diff = std::abs(static_cast(heap.rgba[i]) - static_cast(sets.rgba[i])); - if (diff != 0) ++differing; - maxDiff = std::max(maxDiff, diff); - } - Check(maxDiff <= 1, - std::format("heap and descriptor-set frames match (max channel difference {}, {} of {} channel values differ)", - maxDiff, differing, heap.rgba.size())); - if (maxDiff > 1) { - std::println(" frames kept for inspection: {} and {}", heapPng.string(), setsPng.string()); - } - return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; -} - -} // namespace - -int main(int argc, char** argv) { - if (argc == 3 && std::string_view(argv[1]) == "--render") { - return RenderScene(argv[2]); - } - const int result = Drive(); - std::println("{}", result == EXIT_SUCCESS ? "all checks passed" : "check(s) failed"); - return result; -} diff --git a/tests/UIFusedShader/main.cpp b/tests/UIFusedShader/main.cpp index 77fc3c0..9d4f1c3 100644 --- a/tests/UIFusedShader/main.cpp +++ b/tests/UIFusedShader/main.cpp @@ -18,11 +18,7 @@ // UIFusedHeader (four uvec4 headers at 0/16/32/48 then four vec4 clip // rects at 64/80/96/112, 128 bytes total). Any add/remove/reorder of a // member shifts these offsets and fails here, catching a GLSL/C++ drift -// that the C++-side static_assert(sizeof==128) alone cannot; -// 5. repeats steps 1-4 for the descriptor-set build of the same source -// (-DCRAFTER_GRAPHICS_DESCRIPTOR_SETS, shipped as ui-fused.comp.sets.spv -// for drivers without VK_EXT_descriptor_heap). Both builds are fed the -// same UIFusedHeader bytes, so both must keep the same block layout. +// that the C++-side static_assert(sizeof==128) alone cannot. // // glslang and spirv-val are invoked at runtime, so the test declares them as // required tools (see project.cpp). It needs no GPU device. @@ -140,61 +136,52 @@ int main() { const fs::path dir = fs::temp_directory_path() / "crafter-uifused-test"; std::error_code ec; fs::create_directories(dir, ec); + const fs::path spv = dir / "ui-fused.comp.spv"; - // The heap build first, then the descriptor-set build of the same source. - struct Variant { std::string_view label; std::string_view define; std::string_view file; }; - constexpr Variant variants[] = { - { "descriptor-heap", "", "ui-fused.comp.spv" }, - { "descriptor-set", "-DCRAFTER_GRAPHICS_DESCRIPTOR_SETS ", "ui-fused.comp.sets.spv" }, - }; - for (const Variant& variant : variants) { - const fs::path spv = dir / variant.file; - - // 1. Compile the real uber-kernel. GL_GOOGLE_include resolves - // ui-shared.glsl relative to the source directory. - std::string compile = "glslang --target-env vulkan1.4 " + std::string(variant.define) + "-V -S comp \"" - + src.string() + "\" -o \"" + spv.string() + "\" > /dev/null"; - if (RunCommand(compile) != 0) { - std::println(std::cerr, "glslang failed to compile ui-fused.comp.glsl ({} build)", variant.label); - return 1; - } - - std::vector words = ReadSpirv(spv); - if (words.size() < 5) { - std::println(std::cerr, "could not read compiled SPIR-V"); - return 1; - } - - // 2. spirv-val under the engine's relaxed block-layout flags. - std::string validate = "spirv-val \"" + spv.string() - + "\" --relax-block-layout --scalar-block-layout --target-env vulkan1.4"; - if (RunCommand(validate) != 0) { - std::println(std::cerr, "spirv-val rejected {}", variant.file); - return 1; - } - - // 3. Exactly one push-constant block. - int pcVars = CountPushConstantVariables(words); - if (pcVars != 1) { - std::println(std::cerr, "expected exactly 1 push-constant variable, found {}", pcVars); - return 1; - } - - // 4. Member offsets must match UIFusedHeader's layout exactly. - const std::vector expected = {0, 16, 32, 48, 64, 80, 96, 112}; - std::vector offsets = PushConstantMemberOffsets(words); - if (offsets != expected) { - std::print(std::cerr, "push-constant member offsets mismatch — expected {{"); - for (auto o : expected) std::print(std::cerr, "{} ", o); - std::print(std::cerr, "}}, got {{"); - for (auto o : offsets) std::print(std::cerr, "{} ", o); - std::println(std::cerr, "}}"); - return 1; - } - - std::println(std::cout, - "ui-fused shader ok, {} build (push-constant vars: {}, member offsets pinned to UIFusedHeader, 128 bytes)", - variant.label, pcVars); + // 1. Compile the real uber-kernel. GL_GOOGLE_include resolves + // ui-shared.glsl relative to the source directory. + std::string compile = "glslang --target-env vulkan1.4 -V -S comp \"" + + src.string() + "\" -o \"" + spv.string() + "\" > /dev/null"; + if (RunCommand(compile) != 0) { + std::println(std::cerr, "glslang failed to compile ui-fused.comp.glsl"); + return 1; } + + std::vector words = ReadSpirv(spv); + if (words.size() < 5) { + std::println(std::cerr, "could not read compiled SPIR-V"); + return 1; + } + + // 2. spirv-val under the engine's relaxed block-layout flags. + std::string validate = "spirv-val \"" + spv.string() + + "\" --relax-block-layout --scalar-block-layout --target-env vulkan1.4"; + if (RunCommand(validate) != 0) { + std::println(std::cerr, "spirv-val rejected ui-fused.comp.spv"); + return 1; + } + + // 3. Exactly one push-constant block. + int pcVars = CountPushConstantVariables(words); + if (pcVars != 1) { + std::println(std::cerr, "expected exactly 1 push-constant variable, found {}", pcVars); + return 1; + } + + // 4. Member offsets must match UIFusedHeader's layout exactly. + const std::vector expected = {0, 16, 32, 48, 64, 80, 96, 112}; + std::vector offsets = PushConstantMemberOffsets(words); + if (offsets != expected) { + std::print(std::cerr, "push-constant member offsets mismatch — expected {{"); + for (auto o : expected) std::print(std::cerr, "{} ", o); + std::print(std::cerr, "}}, got {{"); + for (auto o : offsets) std::print(std::cerr, "{} ", o); + std::println(std::cerr, "}}"); + return 1; + } + + std::println(std::cout, + "ui-fused shader ok (push-constant vars: {}, member offsets pinned to UIFusedHeader, 128 bytes)", + pcVars); return 0; }