Compare commits
24 changed files with 153 additions and 1425 deletions
63
README.md
63
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:
|
||||
`<name>.spv` for the heap and `<name>.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
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -47,13 +47,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<std::byte> padded(wholeWords);
|
||||
std::memcpy(padded.data(), push, pushBytes);
|
||||
vkCmdPushConstants(cmd, layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, wholeWords, padded.data());
|
||||
}
|
||||
} else {
|
||||
VkPushDataInfoEXT pushInfo {
|
||||
.sType = VK_STRUCTURE_TYPE_PUSH_DATA_INFO_EXT,
|
||||
.offset = 0,
|
||||
.data = { .address = const_cast<void*>(push), .size = pushBytes },
|
||||
};
|
||||
Device::vkCmdPushDataEXT(cmd, &pushInfo);
|
||||
}
|
||||
VkPushDataInfoEXT pushInfo {
|
||||
.sType = VK_STRUCTURE_TYPE_PUSH_DATA_INFO_EXT,
|
||||
.offset = 0,
|
||||
.data = { .address = const_cast<void*>(push), .size = pushBytes },
|
||||
};
|
||||
Device::vkCmdPushDataEXT(cmd, &pushInfo);
|
||||
}
|
||||
// NVIDIA descriptor-heap AS-read workaround (issue #15 / #7): if this shader
|
||||
// ray-queries the TLAS through the heap it was rewritten to read the TLAS
|
||||
|
|
|
|||
|
|
@ -41,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<const std::byte> 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<std::string> 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<VkExtensionProperties> exts(extCount);
|
||||
vkEnumerateDeviceExtensionProperties(physDevice, nullptr, &extCount, exts.data());
|
||||
for (const VkExtensionProperties& e : exts) {
|
||||
availableExtensions.emplace(e.extensionName);
|
||||
}
|
||||
}
|
||||
const auto hasAll = [&availableExtensions](std::span<const char* const> names) {
|
||||
return std::ranges::all_of(names, [&](const char* name) { return availableExtensions.contains(name); });
|
||||
};
|
||||
|
||||
// Fail here, naming the gap, rather than in vkCreateDevice with a bare
|
||||
// VK_ERROR_EXTENSION_NOT_PRESENT.
|
||||
{
|
||||
std::string missing;
|
||||
for (const char* name : requiredDeviceExtensionNames) {
|
||||
if (!availableExtensions.contains(name)) {
|
||||
missing += ' ';
|
||||
missing += name;
|
||||
if (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<void*>(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<void*>(&memoryDecompressionFeatures)
|
||||
: static_cast<void*>(&features13);
|
||||
|
||||
VkPhysicalDeviceDescriptorHeapFeaturesEXT desciptorHeapFeatures {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_HEAP_FEATURES_EXT,
|
||||
.pNext = postDecompressChain,
|
||||
.descriptorHeap = VK_TRUE,
|
||||
};
|
||||
|
||||
// Every VulkanBuffer is created with usage = 0 plus a
|
||||
// VkBufferUsageFlags2CreateInfo, and heap pipelines carry a
|
||||
// VkPipelineCreateFlags2CreateInfo. Both are maintenance5 functionality,
|
||||
// which needs the feature bit as well as the extension name.
|
||||
VkPhysicalDeviceMaintenance5Features maintenance5Features {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES,
|
||||
.maintenance5 = VK_TRUE,
|
||||
};
|
||||
|
||||
VkPhysicalDevice16BitStorageFeatures bit16 {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES,
|
||||
.pNext = &desciptorHeapFeatures,
|
||||
.storageBuffer16BitAccess = VK_TRUE,
|
||||
};
|
||||
|
||||
VkPhysicalDeviceVulkan12Features features12 {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES,
|
||||
.pNext = &bit16,
|
||||
.shaderFloat16 = VK_TRUE,
|
||||
// Bindless / runtime descriptor array indexing — needed 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<const char*> 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,9 +191,6 @@ namespace {
|
|||
}
|
||||
|
||||
void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd, RTBuildOptions options) {
|
||||
// Every Build* records an acceleration-structure build; without ray
|
||||
// tracing those entry points are not loaded. Fail with the reason.
|
||||
Device::RequireRayTracing();
|
||||
// Place both inputs in device-local memory (direct map on ReBAR/UMA,
|
||||
// staged copy otherwise — UploadDeviceLocal decides) and barrier the
|
||||
// upload before the BLAS build reads them. Replaces the previous
|
||||
|
|
@ -208,7 +205,6 @@ void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32
|
|||
}
|
||||
|
||||
void Mesh::Build(const CompressedMeshAsset& asset, VkCommandBuffer cmd, RTBuildOptions options) {
|
||||
Device::RequireRayTracing();
|
||||
if (!Device::memoryDecompressionSupported) {
|
||||
// CPU fallback: decompress into temporary host vectors, then take
|
||||
// the existing uncompressed path. The data region is decompressed
|
||||
|
|
@ -337,7 +333,6 @@ namespace {
|
|||
}
|
||||
|
||||
void Mesh::BuildProcedural(std::span<const RTAabb> aabbs, bool opaque, VkCommandBuffer cmd, RTBuildOptions options) {
|
||||
Device::RequireRayTracing();
|
||||
this->opaque = opaque;
|
||||
allowUpdate = options.allowUpdate;
|
||||
builtInputCount = static_cast<std::uint32_t>(aabbs.size());
|
||||
|
|
@ -346,7 +341,6 @@ void Mesh::BuildProcedural(std::span<const RTAabb> aabbs, bool opaque, VkCommand
|
|||
}
|
||||
|
||||
void Mesh::BuildProcedural(VkDeviceAddress aabbAddress, std::uint32_t count, bool opaque, VkCommandBuffer cmd, RTBuildOptions options, std::uint32_t stride) {
|
||||
Device::RequireRayTracing();
|
||||
// Zero-copy procedural build: the AABBs already live in `aabbAddress`
|
||||
// (a device buffer a GPU compute pass wrote). Nothing touches
|
||||
// self.aabbBuffer — the build input is the producer's buffer directly.
|
||||
|
|
|
|||
|
|
@ -32,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<std::uint32_t>(elements.size());
|
||||
|
||||
|
|
|
|||
|
|
@ -30,11 +30,11 @@ void UIRenderer::Initialize(Window& window, GraphicsDescriptorHeap& heap, Graphi
|
|||
heap_ = &heap;
|
||||
|
||||
// Load the four standard pipelines plus the fused uber-kernel (issue #47).
|
||||
drawQuads.Load(quadsSpv, 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<VkImageDescriptorInfoEXT, Window::numFrames> infos{};
|
||||
std::array<VkResourceDescriptorInfoEXT, Window::numFrames> 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<VkImageDescriptorInfoEXT, Window::numFrames> infos{};
|
||||
std::array<VkResourceDescriptorInfoEXT, Window::numFrames> resources{};
|
||||
std::array<VkHostAddressRangeEXT, Window::numFrames> 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<VkDeviceAddressRangeEXT, Window::numFrames> ranges{};
|
||||
std::array<VkResourceDescriptorInfoEXT, Window::numFrames> resources{};
|
||||
std::array<VkHostAddressRangeEXT, Window::numFrames> 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<VkSamplerCreateInfo, Window::numFrames> infos{};
|
||||
std::array<VkHostAddressRangeEXT, Window::numFrames> destinations{};
|
||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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 <name>.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 (<name>.spv). On a
|
||||
// device without the heap, the <name>.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
|
||||
|
|
|
|||
|
|
@ -39,95 +39,7 @@ export namespace Crafter {
|
|||
std::vector<std::uint16_t> bufferFreelist;
|
||||
std::vector<std::uint16_t> 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<VkImageView> setImageViews[Window::numFrames];
|
||||
std::vector<VkSampler> 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<VkImageView>& 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<std::uint32_t>(images, 1);
|
||||
const std::uint32_t bufferCount = std::max<std::uint32_t>(buffers, 1);
|
||||
const std::uint32_t samplerCount = std::max<std::uint32_t>(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::uint32_t>(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::uint32_t>(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::uint32_t>(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<VkImageView>& 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<VkImageView>& 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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<VkRayTracingShaderGroupCreateInfoKHR> raygenGroups, std::span<VkRayTracingShaderGroupCreateInfoKHR> missGroups, std::span<VkRayTracingShaderGroupCreateInfoKHR> hitGroups, ShaderBindingTableVulkan& shaderTable, std::uint32_t maxRecursionDepth = 1) {
|
||||
Device::RequireRayTracing();
|
||||
workaroundNeedsTlas = shaderTable.workaroundNeedsTlas;
|
||||
workaroundTlasPushOffset = shaderTable.workaroundTlasPushOffset;
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ export namespace Crafter {
|
|||
bool workaroundNeedsTlas = false;
|
||||
std::uint32_t workaroundTlasPushOffset = 0;
|
||||
void Init(const std::span<const VulkanShader> 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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<T, Mapped>& 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<Pixel>& 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
50
project.cpp
50
project.cpp
|
|
@ -236,17 +236,11 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
};
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
|
||||
// Every UI kernel is built twice from one source: <name>.comp.spv
|
||||
// indexes VK_EXT_descriptor_heap, <name>.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<const std::string_view> 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<fs::path> 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
|
||||
|
|
|
|||
|
|
@ -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: <name>.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 ────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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 (<name>.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 <png>`, 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 <png>` 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 <cstdlib>
|
||||
|
||||
// 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<typename T>
|
||||
void CreateItemBuffer(GraphicsBuffer<T, true>& 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<Texel>& 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
|
||||
// `<out>.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<Texel> red;
|
||||
ImageVulkan<Texel> 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<Texel> 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<std::uint16_t>(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<std::uint16_t>(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<QuadItem, true> quadsA, quadsB;
|
||||
GraphicsBuffer<CircleItem, true> circlesA, circlesB;
|
||||
GraphicsBuffer<ImageItem, true> imagesA, imagesB;
|
||||
GraphicsBuffer<GlyphItem, true> 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<QuadItem, true> 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<QuadItem, true>& quads,
|
||||
GraphicsBuffer<CircleItem, true>& circles,
|
||||
GraphicsBuffer<ImageItem, true>& images,
|
||||
GraphicsBuffer<GlyphItem, true>& glyphs,
|
||||
bool withBackground, std::string_view label,
|
||||
std::array<std::uint32_t, 4>& 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<std::uint32_t, 4> countsA{};
|
||||
std::array<std::uint32_t, 4> countsB{};
|
||||
EventListener<UIBuildArgs> 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<std::uint32_t>(sizeof(oddHeader))
|
||||
: static_cast<std::uint32_t>(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<std::uint8_t> 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<std::size_t>(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<std::uint32_t> 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<int>(heap.rgba[i]) - static_cast<int>(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;
|
||||
}
|
||||
|
|
@ -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<std::uint32_t> 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<std::uint32_t> expected = {0, 16, 32, 48, 64, 80, 96, 112};
|
||||
std::vector<std::uint32_t> 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<std::uint32_t> 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<std::uint32_t> expected = {0, 16, 32, 48, 64, 80, 96, 112};
|
||||
std::vector<std::uint32_t> 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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue