Crafter.Graphics/implementations/Crafter.Graphics-Device.cpp

1438 lines
63 KiB
C++
Raw Normal View History

2026-07-22 18:09:06 +02:00
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
2026-03-09 20:10:19 +01:00
module;
#include "vulkan/vulkan.h"
#include "vulkan/vk_enum_string_helper.h"
#define GET_EXTENSION_FUNCTION(_id) ((PFN_##_id)(vkGetInstanceProcAddr(instance, #_id)))
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
#include <linux/input-event-codes.h>
#include <xkbcommon/xkbcommon.h>
#include <string.h>
#include "../lib/xdg-shell-client-protocol.h"
#include "../lib/wayland-xdg-decoration-unstable-v1-client-protocol.h"
#include "../lib/fractional-scale-v1.h"
#include "../lib/viewporter.h"
#include "../lib/text-input-unstable-v3-client-protocol.h"
2026-03-09 20:10:19 +01:00
#include <wayland-client.h>
#include <wayland-client-protocol.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#endif
module Crafter.Graphics:Device_impl;
import :Device;
import :Window;
import :Types;
2026-05-19 00:45:22 +02:00
import :Clipboard;
2026-03-09 20:10:19 +01:00
import std;
using namespace Crafter;
const char* const instanceExtensionNames[] = {
"VK_EXT_debug_utils",
"VK_KHR_surface",
2026-03-09 22:53:26 +01:00
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
2026-03-09 20:10:19 +01:00
"VK_KHR_win32_surface"
2026-03-09 22:53:26 +01:00
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
2026-03-09 20:10:19 +01:00
"VK_KHR_wayland_surface"
#endif
};
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
// 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[] = {
2026-03-09 20:10:19 +01:00
"VK_KHR_swapchain",
"VK_KHR_spirv_1_4",
"VK_KHR_shader_float_controls",
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
"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[] = {
2026-03-09 20:10:19 +01:00
"VK_KHR_acceleration_structure",
"VK_KHR_ray_tracing_pipeline",
2026-05-05 23:49:29 +02:00
"VK_KHR_ray_query",
2026-04-05 22:53:59 +02:00
"VK_KHR_deferred_host_operations",
2026-03-09 20:10:19 +01:00
};
const char* const layerNames[] = {
"VK_LAYER_KHRONOS_validation"
};
void Device::CheckVkResult(VkResult result) {
if (result != VK_SUCCESS)
{
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
// 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) {
2026-04-09 00:15:09 +02:00
VkDeviceFaultCountsEXT faultCounts = {
.sType = VK_STRUCTURE_TYPE_DEVICE_FAULT_COUNTS_EXT,
.pNext = NULL,
};
Device::vkGetDeviceFaultInfoEXT(device, &faultCounts, NULL);
std::vector<VkDeviceFaultAddressInfoEXT> addressInfos(faultCounts.addressInfoCount);
std::vector<VkDeviceFaultVendorInfoEXT> vendorInfos(faultCounts.vendorInfoCount);
std::vector<char> vendorBinaryData(faultCounts.vendorBinarySize);
VkDeviceFaultInfoEXT faultInfo = {
.sType = VK_STRUCTURE_TYPE_DEVICE_FAULT_INFO_EXT,
.pNext = NULL,
.pAddressInfos = addressInfos.data(),
.pVendorInfos = vendorInfos.data(),
.pVendorBinaryData = vendorBinaryData.data(),
};
Device::vkGetDeviceFaultInfoEXT(device, &faultCounts, &faultInfo);
std::println("{}", faultInfo.description);
std::println("{} AddressInfos:", addressInfos.size());
for(const VkDeviceFaultAddressInfoEXT& info : addressInfos) {
std::println("\t{} {}", static_cast<uint32_t>(info.addressType), info.reportedAddress);
}
std::println("{} vendorInfos:", vendorInfos.size());
for(const VkDeviceFaultVendorInfoEXT& info : vendorInfos) {
std::println("\t{} {} {}", info.description, info.vendorFaultCode, info.vendorFaultData);
}
if(!vendorBinaryData.empty()) {
std::string ext = ".bin";
if(vendorBinaryData.size() >= sizeof(VkDeviceFaultVendorBinaryHeaderVersionOneEXT)) {
VkDeviceFaultVendorBinaryHeaderVersionOneEXT header;
std::memcpy(&header, vendorBinaryData.data(), sizeof(header));
if(header.vendorID == 0x10DE) { // NVIDIA
ext = ".nv-gpudmp";
}
}
const auto now = std::chrono::system_clock::now();
const std::string dumpPath = std::format("gpu_crash_dump-{:%Y%m%d-%H%M%S}{}", now, ext);
std::ofstream file(dumpPath, std::ios::binary);
if(file.write(vendorBinaryData.data(), vendorBinaryData.size())) {
std::println("Vendor binary saved to: {}", std::filesystem::canonical(dumpPath).string());
} else {
std::println(stderr, "Failed to write vendor binary to: {}", dumpPath);
}
}
}
2026-03-09 20:10:19 +01:00
throw std::runtime_error(string_VkResult(result));
}
}
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
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,
// u32 deviceID, then VK_UUID_SIZE bytes of pipelineCacheUUID. Parse the
// fields by offset rather than reinterpret_cast'ing the struct so the
// check is independent of the C struct's alignment/padding.
constexpr std::size_t headerBytes = 16 + VK_UUID_SIZE;
if (data.size() < headerBytes) {
return false;
}
auto readU32 = [&](std::size_t offset) {
std::uint32_t value;
std::memcpy(&value, data.data() + offset, sizeof(value));
return value;
};
const std::uint32_t headerSize = readU32(0);
const std::uint32_t headerVersion = readU32(4);
const std::uint32_t vendorID = readU32(8);
const std::uint32_t deviceID = readU32(12);
if (headerSize < headerBytes) {
return false;
}
if (headerVersion != VK_PIPELINE_CACHE_HEADER_VERSION_ONE) {
return false;
}
if (vendorID != deviceProperties.vendorID || deviceID != deviceProperties.deviceID) {
return false;
}
return std::memcmp(data.data() + 16, deviceProperties.pipelineCacheUUID, VK_UUID_SIZE) == 0;
}
void Device::LoadPipelineCache() {
std::vector<std::byte> initialData;
std::error_code ec;
if (std::filesystem::exists(pipelineCachePath, ec)) {
std::ifstream file(pipelineCachePath, std::ios::binary | std::ios::ate);
if (file) {
const std::streamoff size = file.tellg();
if (size > 0) {
initialData.resize(static_cast<std::size_t>(size));
file.seekg(0);
file.read(reinterpret_cast<char*>(initialData.data()), size);
if (!file) {
initialData.clear(); // partial/short read — start cold
}
}
}
}
// Drop a blob the current driver/device wouldn't accept: a header from a
// different GPU (or a corrupt/empty file) would at best be ignored and at
// worst rejected. Starting empty just costs a one-time cold compile.
if (!PipelineCacheDataCompatible(initialData)) {
initialData.clear();
}
VkPipelineCacheCreateInfo info {
.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO,
.initialDataSize = initialData.size(),
.pInitialData = initialData.empty() ? nullptr : initialData.data(),
};
CheckVkResult(vkCreatePipelineCache(device, &info, nullptr, &pipelineCache));
}
void Device::SavePipelineCache() {
if (pipelineCache == VK_NULL_HANDLE) {
return;
}
// Two-call idiom: query size, then fetch. Best-effort — a failure here only
// forfeits the next run's warm start, so it must never throw out of an
// atexit handler.
std::size_t size = 0;
if (vkGetPipelineCacheData(device, pipelineCache, &size, nullptr) != VK_SUCCESS || size == 0) {
return;
}
std::vector<std::byte> data(size);
if (vkGetPipelineCacheData(device, pipelineCache, &size, data.data()) != VK_SUCCESS) {
return;
}
std::ofstream file(pipelineCachePath, std::ios::binary | std::ios::trunc);
if (file) {
file.write(reinterpret_cast<const char*>(data.data()), static_cast<std::streamsize>(size));
}
}
2026-03-09 20:10:19 +01:00
VkBool32 onError(VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMessageTypeFlagsEXT type, const VkDebugUtilsMessengerCallbackDataEXT* callbackData, void* userData)
{
printf("Vulkan ");
switch (type)
{
case VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT :
printf("general ");
break;
case VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT :
printf("validation ");
break;
case VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT :
printf("performance ");
break;
}
switch (severity)
{
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT :
printf("(verbose): ");
break;
default :
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT :
printf("(info): ");
break;
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT :
printf("(warning): ");
break;
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT :
printf("(error): ");
Device::validationErrorCount++;
2026-03-09 20:10:19 +01:00
break;
}
printf("%s\n", callbackData->pMessage);
return 0;
}
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
void Device::xdg_wm_base_handle_ping(void* data, xdg_wm_base* xdg_wm_base, std::uint32_t serial) {
xdg_wm_base_pong(xdg_wm_base, serial);
}
void Device::handle_global(void *data, wl_registry *registry, std::uint32_t name, const char *interface, std::uint32_t version) {
if (strcmp(interface, wl_shm_interface.name) == 0) {
shm = reinterpret_cast<wl_shm*>(wl_registry_bind(registry, name, &wl_shm_interface, 1));
} else if (strcmp(interface, wl_seat_interface.name) == 0) {
2026-05-12 00:24:48 +02:00
// Assign to Device::seat (not a fresh local) so SetClipboardText
// and any other code that needs the seat post-init can find it.
seat = reinterpret_cast<wl_seat*>(wl_registry_bind(registry, name, &wl_seat_interface, 1));
2026-03-09 20:10:19 +01:00
wl_seat_add_listener(seat, &seat_listener, nullptr);
2026-05-12 00:24:48 +02:00
// If the manager came in first, the data device couldn't be
// created yet — do it now that we have the seat.
if (dataDeviceManager != nullptr && dataDevice == nullptr) {
dataDevice = wl_data_device_manager_get_data_device(dataDeviceManager, seat);
}
if (textInputManager != nullptr && textInput == nullptr) {
textInput = zwp_text_input_manager_v3_get_text_input(textInputManager, seat);
zwp_text_input_v3_add_listener(textInput, &text_input_listener, nullptr);
}
2026-03-09 20:10:19 +01:00
} else if (compositor == nullptr && strcmp(interface, wl_compositor_interface.name) == 0) {
compositor = reinterpret_cast<wl_compositor*>(wl_registry_bind(registry, name, &wl_compositor_interface, 3));
} else if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
xdgWmBase = reinterpret_cast<xdg_wm_base*>(wl_registry_bind(registry, name, &xdg_wm_base_interface, 1));
xdg_wm_base_add_listener(xdgWmBase, &xdgWmBaseListener, nullptr);
} else if (strcmp(interface, zxdg_decoration_manager_v1_interface.name) == 0) {
manager = reinterpret_cast<zxdg_decoration_manager_v1*>(wl_registry_bind(registry, name, &zxdg_decoration_manager_v1_interface, 1));
} else if (strcmp(interface, wp_viewporter_interface.name) == 0) {
wpViewporter = reinterpret_cast<wp_viewporter*>(wl_registry_bind(registry, name, &wp_viewporter_interface, 1));
} else if (strcmp(interface, wp_fractional_scale_manager_v1_interface.name) == 0) {
fractionalScaleManager = reinterpret_cast<wp_fractional_scale_manager_v1*>(wl_registry_bind(registry, name, &wp_fractional_scale_manager_v1_interface, 1));
2026-05-12 00:24:48 +02:00
} else if (strcmp(interface, wl_data_device_manager_interface.name) == 0) {
// v3 gives us the full set of data_source events (target / send /
// cancelled / dnd_*). Universally supported by the compositors
// we target — fall back path is the per-source listener simply
// not getting the v3-only callbacks.
dataDeviceManager = reinterpret_cast<wl_data_device_manager*>(
wl_registry_bind(registry, name, &wl_data_device_manager_interface, 3));
if (seat != nullptr && dataDevice == nullptr) {
dataDevice = wl_data_device_manager_get_data_device(dataDeviceManager, seat);
}
} else if (strcmp(interface, zwp_text_input_manager_v3_interface.name) == 0) {
textInputManager = reinterpret_cast<zwp_text_input_manager_v3*>(wl_registry_bind(registry, name, &zwp_text_input_manager_v3_interface, 1));
if (seat != nullptr && textInput == nullptr) {
textInput = zwp_text_input_manager_v3_get_text_input(textInputManager, seat);
zwp_text_input_v3_add_listener(textInput, &text_input_listener, nullptr);
}
2026-03-09 20:10:19 +01:00
}
}
namespace {
// A finger's slot by id, or a free one for a new finger; nullptr with more
// than ten fingers down.
Crafter::Device::TouchSlot* SlotFor(std::int32_t id) {
Crafter::Device::TouchSlot* free = nullptr;
for (Crafter::Device::TouchSlot& slot : Crafter::Device::touchSlots) {
if (slot.used && slot.id == id) return &slot;
if (!slot.used && free == nullptr) free = &slot;
}
if (free != nullptr) {
free->used = true;
free->id = id;
free->window = nullptr;
}
return free;
}
bool AnyTouch() {
for (const Crafter::Device::TouchSlot& slot : Crafter::Device::touchSlots) {
if (slot.used) return true;
}
return false;
}
// The text input's pending strings live here, as function-local statics:
// an inline static std::string in the interface made clang 23.1 crash
// in ThinLTO for aarch64-alpine-linux-musl.
struct TextInputStrings {
std::string preedit;
std::string shown; // what the window was last told
std::string commit;
};
TextInputStrings& TextStrings() {
static TextInputStrings strings;
return strings;
}
}
void Device::text_input_enter(void*, zwp_text_input_v3*, wl_surface* surface) {
textInputWindow = nullptr;
for (Window* window : windows) {
if (window->surface == surface) textInputWindow = window;
}
if (textInputWindow != nullptr && textInputWindow->textInputActive_) ApplyTextInput(textInputWindow);
}
// The compositor's leave ends the enable and the composition: the window
// hears that its composing text is gone, and the next enter enables anew.
void Device::text_input_leave(void*, zwp_text_input_v3*, wl_surface*) {
Window* window = textInputWindow;
textInputWindow = nullptr;
textInputHasPreedit = textInputHasCommit = textInputHasDelete = false;
TextStrings().preedit.clear();
if (window != nullptr) {
window->textInputEnabled_ = false;
if (!TextStrings().shown.empty()) window->onPreedit.Invoke(std::string_view());
}
TextStrings().shown.clear();
}
void Device::text_input_preedit(void*, zwp_text_input_v3*, const char* text, std::int32_t, std::int32_t) {
TextStrings().preedit = text == nullptr ? "" : text;
textInputHasPreedit = true;
}
void Device::text_input_commit(void*, zwp_text_input_v3*, const char* text) {
TextStrings().commit = text == nullptr ? "" : text;
textInputHasCommit = true;
}
void Device::text_input_delete(void*, zwp_text_input_v3*, std::uint32_t before, std::uint32_t after) {
textInputDeleteBefore = before;
textInputDeleteAfter = after;
textInputHasDelete = true;
}
// The protocol's order: the deletion around the cursor, then the committed
// text at the cursor, then the composing text after it. The pending values
// reset with every done, so a round that set no preedit means an empty one;
// the window hears about it only when that differs from what it shows.
void Device::text_input_done(void*, zwp_text_input_v3*, std::uint32_t) {
Window* window = textInputWindow;
TextInputStrings& strings = TextStrings();
if (!textInputHasPreedit) strings.preedit.clear();
if (window != nullptr) {
if (textInputHasDelete && (textInputDeleteBefore > 0 || textInputDeleteAfter > 0)) window->onTextInputDelete.Invoke({ textInputDeleteBefore, textInputDeleteAfter });
if (textInputHasCommit && !strings.commit.empty()) window->onTextInput.Invoke(strings.commit);
if (strings.preedit != strings.shown || (textInputHasCommit && !strings.commit.empty())) {
strings.shown = strings.preedit;
window->onPreedit.Invoke(strings.preedit);
}
}
textInputHasPreedit = textInputHasCommit = textInputHasDelete = false;
strings.commit.clear();
}
// enable resets the input method's state, so it goes once per focus; a
// caret that moved sends its rectangle alone.
void Device::ApplyTextInput(Window* window) {
if (textInput == nullptr || window == nullptr || textInputWindow != window) return;
if (window->textInputActive_) {
if (!window->textInputEnabled_) zwp_text_input_v3_enable(textInput);
window->textInputEnabled_ = true;
const bool phone = window->textInputPurpose_ == TextInputPurpose::Phone;
zwp_text_input_v3_set_content_type(textInput,
phone ? ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE : (ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION | ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK | ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION),
phone ? ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE : ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL);
const float s = window->scale > 0.0f ? window->scale : 1.0f;
zwp_text_input_v3_set_cursor_rectangle(textInput,
static_cast<std::int32_t>(window->textInputRect_[0] / s), static_cast<std::int32_t>(window->textInputRect_[1] / s),
std::max(1, static_cast<std::int32_t>(window->textInputRect_[2] / s)), std::max(1, static_cast<std::int32_t>(window->textInputRect_[3] / s)));
} else {
if (!window->textInputEnabled_) return;
window->textInputEnabled_ = false;
zwp_text_input_v3_disable(textInput);
}
zwp_text_input_v3_commit(textInput);
wl_display_flush(display);
}
2026-03-09 20:10:19 +01:00
void Device::handle_global_remove(void* data, wl_registry* registry, uint32_t name) {
}
void Device::pointer_handle_button(void* data, wl_pointer* pointer, std::uint32_t serial, std::uint32_t time, std::uint32_t button, std::uint32_t state) {
// Defensive: compositors can deliver pointer events without (or after)
// a matching enter — observed on sway during input-device hot-plug.
if (focusedWindow == nullptr) {
return;
}
2026-03-09 20:10:19 +01:00
if (button == BTN_LEFT) {
if(state == WL_POINTER_BUTTON_STATE_PRESSED) {
Device::focusedWindow->mouseLeftHeld = true;
Device::focusedWindow->onMouseLeftClick.Invoke();
} else {
Device::focusedWindow->mouseLeftHeld = false;
Device::focusedWindow->onMouseLeftRelease.Invoke();
}
} else if(button == BTN_RIGHT){
if(state == WL_POINTER_BUTTON_STATE_PRESSED) {
Device::focusedWindow->mouseRightHeld = true;
Device::focusedWindow->onMouseRightClick.Invoke();
} else {
2026-03-09 21:59:27 +01:00
Device::focusedWindow->mouseRightHeld = false;
2026-05-01 23:35:37 +02:00
Device::focusedWindow->onMouseRightRelease.Invoke();
2026-03-09 20:10:19 +01:00
}
}
}
void Device::PointerListenerHandleMotion(void* data, wl_pointer* wl_pointer, std::uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) {
if (focusedWindow == nullptr) {
return;
}
2026-03-09 20:10:19 +01:00
Vector<float, 2> pos(wl_fixed_to_double(surface_x), wl_fixed_to_double(surface_y));
2026-03-09 21:59:27 +01:00
//Device::focusedWindow->lastMousePos = Device::focusedWindow->currentMousePos;
2026-03-09 20:10:19 +01:00
Device::focusedWindow->currentMousePos = pos * Device::focusedWindow->scale;
2026-03-09 21:59:27 +01:00
//Device::focusedWindow->mouseDelta = {Device::focusedWindow->currentMousePos.x-Device::focusedWindow->lastMousePos.x, Device::focusedWindow->currentMousePos.y-Device::focusedWindow->lastMousePos.y};
2026-03-09 20:10:19 +01:00
Device::focusedWindow->onMouseMove.Invoke();
}
void Device::PointerListenerHandleEnter(void* data, wl_pointer* wl_pointer, std::uint32_t serial, wl_surface* surface, wl_fixed_t surface_x, wl_fixed_t surface_y) {
2026-03-12 01:07:46 +01:00
Device::wlPointer = wl_pointer;
2026-03-09 20:10:19 +01:00
for(Window* window : windows) {
if(window->surface == surface) {
2026-05-02 21:08:20 +02:00
window->lastPointerSerial_ = serial;
2026-03-12 01:07:46 +01:00
if(window->cursorSurface != nullptr) {
2026-05-02 21:08:20 +02:00
wl_pointer_set_cursor(wl_pointer, serial, window->cursorSurface,
window->cursorHotspotX_, window->cursorHotspotY_);
2026-03-12 01:07:46 +01:00
}
2026-03-09 20:10:19 +01:00
focusedWindow = window;
window->onMouseEnter.Invoke();
return;
}
}
}
void Device::PointerListenerHandleLeave(void* data, wl_pointer*, std::uint32_t, wl_surface*) {
// Sway has been observed to send a second leave after focus is already
// gone (input-device hot-plug) — don't crash on it.
if (focusedWindow != nullptr) {
focusedWindow->onMouseLeave.Invoke();
focusedWindow = nullptr;
}
// Drop any sub-detent scroll remainder so it can't leak into whatever
// window the pointer enters next.
scrollDetentRemainder = 0.0;
2026-03-09 20:10:19 +01:00
}
void Device::PointerListenerHandleAxis(void*, wl_pointer*, std::uint32_t, std::uint32_t axis, wl_fixed_t value) {
// Vertical wheel only — Window has no horizontal-scroll event (yet).
if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL || focusedWindow == nullptr) {
return;
}
// libinput convention (followed by wlroots, Mutter and KWin): one wheel
// detent = 15 axis units, positive = wheel down / toward the user — the
// same sign as DOM deltaY. Window::onMouseScroll speaks whole detents
// (±1 per notch on every backend), so divide and accumulate the
// remainder: smooth-scroll devices (touchpads) deliver many sub-detent
// events that must add up instead of each truncating to zero.
constexpr double axisUnitsPerDetent = 15.0;
scrollDetentRemainder += wl_fixed_to_double(value) / axisUnitsPerDetent;
const double whole = std::trunc(scrollDetentRemainder);
if (whole != 0.0) {
scrollDetentRemainder -= whole;
// Event payload is uint32 — preserve sign via two's complement.
focusedWindow->onMouseScroll.Invoke(
static_cast<std::uint32_t>(static_cast<std::int32_t>(whole)));
}
2026-03-09 20:10:19 +01:00
}
void Device::keyboard_keymap(void *data, wl_keyboard *keyboard, uint32_t format, int fd, uint32_t size) {
if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
close(fd);
fprintf(stderr, "Unsupported keymap format\n");
return;
}
void *map = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
close(fd);
perror("mmap");
return;
}
xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
xkb_keymap = xkb_keymap_new_from_string(xkb_context, (const char *)map, XKB_KEYMAP_FORMAT_TEXT_V1,XKB_KEYMAP_COMPILE_NO_FLAGS);
munmap(map, size);
close(fd);
xkb_state = xkb_state_new(xkb_keymap);
}
void Device::keyboard_enter(void *data, wl_keyboard *keyboard, uint32_t serial, wl_surface *surface, wl_array *keys) {
for (Window* window : windows) {
if (window->surface == surface) {
keyboardWindow = window;
return;
}
}
keyboardWindow = nullptr;
2026-03-09 20:10:19 +01:00
}
void Device::keyboard_leave(void *data, wl_keyboard *keyboard, uint32_t serial, wl_surface *surface) {
// Releases happen elsewhere while the focus is away: nothing we still
// hold is held, and a repeat must not keep firing into a window that
// lost the keyboard.
if (keyboardWindow != nullptr) keyboardWindow->heldKeys.clear();
keyboardWindow = nullptr;
keyRepeat.active = false;
keyRepeat.utf8.clear();
2026-03-09 20:10:19 +01:00
}
void Device::keyboard_key(void *data, wl_keyboard *keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) {
2026-05-12 00:24:48 +02:00
// `key` is the kernel input-event-code (KEY_*). That is exactly what
// :Keys returns for Wayland builds, so we store it verbatim with no
// translation. The +8 X11 offset is only needed for the XKB layer,
// which we still consult to produce UTF-8 text.
KeyCode code = key;
xkb_keycode_t xkbKeycode = key + 8;
2026-03-09 20:10:19 +01:00
// No enter for any of our surfaces (a compositor quirk, or a key for a
// window already gone): nowhere to deliver it.
if (keyboardWindow == nullptr) return;
2026-04-16 23:03:24 +02:00
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
if (keyboardWindow->heldKeys.contains(code)) {
keyboardWindow->onRawKeyHold.Invoke(code);
2026-04-16 23:03:24 +02:00
} else {
keyboardWindow->heldKeys.insert(code);
keyboardWindow->onRawKeyDown.Invoke(code);
2026-03-09 20:10:19 +01:00
}
2026-04-16 23:03:24 +02:00
std::string buf;
buf.resize(16);
2026-05-12 00:24:48 +02:00
int n = xkb_state_key_get_utf8(xkb_state, xkbKeycode, buf.data(), 16);
2026-05-02 00:03:24 +02:00
std::string utf8;
if (n > 0 && (unsigned char)buf[0] >= 0x20 && buf[0] != 0x7f) {
buf.resize(n);
utf8 = buf;
keyboardWindow->onTextInput.Invoke(utf8);
2026-04-16 23:03:24 +02:00
}
2026-05-02 00:03:24 +02:00
// Replace the active repeat with this key — most recent press wins,
// matching xkbcommon's typical behaviour and most desktop apps.
keyRepeat.active = (keyRepeat.rate > 0);
2026-05-12 00:24:48 +02:00
keyRepeat.key = code;
2026-05-02 00:03:24 +02:00
keyRepeat.utf8 = std::move(utf8);
keyRepeat.pressTime = std::chrono::steady_clock::now();
keyRepeat.lastFireTime = keyRepeat.pressTime;
2026-04-16 23:03:24 +02:00
} else {
keyboardWindow->heldKeys.erase(code);
keyboardWindow->onRawKeyUp.Invoke(code);
2026-05-02 00:03:24 +02:00
// If the released key was the one repeating, stop. Otherwise leave
// the existing repeat alone (user pressed/released a modifier
// mid-repeat etc.).
2026-05-12 00:24:48 +02:00
if (keyRepeat.active && keyRepeat.key == code) {
2026-05-02 00:03:24 +02:00
keyRepeat.active = false;
keyRepeat.utf8.clear();
}
2026-03-09 20:10:19 +01:00
}
}
void Device::keyboard_modifiers(void *data, wl_keyboard *keyboard, uint32_t serial, uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, uint32_t group) {
2026-04-16 23:03:24 +02:00
xkb_state_update_mask(xkb_state, mods_depressed, mods_latched, mods_locked, 0, 0, group);
2026-03-09 20:10:19 +01:00
}
void Device::keyboard_repeat_info(void *data, wl_keyboard *keyboard, int32_t rate, int32_t delay) {
2026-05-02 00:03:24 +02:00
keyRepeat.rate = rate;
keyRepeat.delay = delay;
if (rate <= 0) keyRepeat.active = false; // compositor disabled repeat
}
2026-03-09 20:10:19 +01:00
2026-05-02 00:03:24 +02:00
void Device::TickKeyRepeats() {
if (!keyRepeat.active || !keyboardWindow) return;
2026-05-02 00:03:24 +02:00
if (keyRepeat.rate <= 0) return;
auto now = std::chrono::steady_clock::now();
using ms = std::chrono::milliseconds;
auto sincePress = std::chrono::duration_cast<ms>(now - keyRepeat.pressTime).count();
if (sincePress < keyRepeat.delay) return;
auto period = std::chrono::milliseconds(1000 / keyRepeat.rate);
auto sinceLastFire = std::chrono::duration_cast<ms>(now - keyRepeat.lastFireTime).count();
if (sinceLastFire < period.count()) return;
// Catch up — emit one event per missed period so a paused frame doesn't
// make the repeat permanently lag behind.
while (now - keyRepeat.lastFireTime >= period) {
keyboardWindow->onRawKeyDown.Invoke(keyRepeat.key);
keyboardWindow->onRawKeyHold.Invoke(keyRepeat.key);
2026-05-02 00:03:24 +02:00
if (!keyRepeat.utf8.empty()) {
keyboardWindow->onTextInput.Invoke(keyRepeat.utf8);
2026-05-02 00:03:24 +02:00
}
keyRepeat.lastFireTime += period;
}
2026-03-09 20:10:19 +01:00
}
void Device::ForgetTouches() {
for (TouchSlot& slot : touchSlots) slot = TouchSlot{};
}
// The first finger stands in for the pointer: it enters the window (so the
// clipboard has a serial and Device::focusedWindow a window), presses the
// left button, moves it and releases it. Every finger is also reported as
// such, in physical pixels.
void Device::touch_down(void*, wl_touch*, std::uint32_t serial, std::uint32_t, wl_surface* surface, std::int32_t id, wl_fixed_t x, wl_fixed_t y) {
Window* window = nullptr;
for (Window* candidate : windows) {
if (candidate->surface == surface) window = candidate;
}
if (window == nullptr) return;
touchWindow = window;
window->lastPointerSerial_ = serial;
const Vector<float, 2> pos = Vector<float, 2>(wl_fixed_to_double(x), wl_fixed_to_double(y)) * window->scale;
if (TouchSlot* slot = SlotFor(id)) {
slot->x = pos.x;
slot->y = pos.y;
slot->window = window;
}
window->onTouchDown.Invoke({ id, pos });
if (window->primaryTouchId < 0) {
window->primaryTouchId = id;
focusedWindow = window;
window->currentMousePos = pos;
window->mouseLeftHeld = true;
window->onMouseLeftClick.Invoke();
}
}
void Device::touch_up(void*, wl_touch*, std::uint32_t, std::uint32_t, std::int32_t id) {
if (touchWindow == nullptr) return;
Window* window = touchWindow;
Vector<float, 2> pos = window->currentMousePos;
if (TouchSlot* slot = SlotFor(id)) {
if (slot->window != nullptr) window = slot->window;
pos = Vector<float, 2>(slot->x, slot->y);
*slot = TouchSlot{};
}
window->onTouchUp.Invoke({ id, pos });
if (id == window->primaryTouchId) {
window->primaryTouchId = -1;
window->mouseLeftHeld = false;
window->onMouseLeftRelease.Invoke();
}
if (!AnyTouch()) touchWindow = nullptr;
}
void Device::touch_motion(void*, wl_touch*, std::uint32_t, std::int32_t id, wl_fixed_t x, wl_fixed_t y) {
if (touchWindow == nullptr) return;
Window* window = touchWindow;
TouchSlot* slot = SlotFor(id);
if (slot != nullptr && slot->window != nullptr) window = slot->window;
const Vector<float, 2> pos = Vector<float, 2>(wl_fixed_to_double(x), wl_fixed_to_double(y)) * window->scale;
if (slot != nullptr) {
slot->x = pos.x;
slot->y = pos.y;
}
window->onTouchMove.Invoke({ id, pos });
if (id == window->primaryTouchId) {
window->currentMousePos = pos;
window->onMouseMove.Invoke();
}
}
void Device::touch_frame(void*, wl_touch*) {}
void Device::touch_cancel(void*, wl_touch*) {
if (touchWindow == nullptr) return;
Window* window = touchWindow;
ForgetTouches();
touchWindow = nullptr;
if (window->primaryTouchId >= 0) {
window->primaryTouchId = -1;
window->mouseLeftHeld = false;
window->onMouseLeftRelease.Invoke();
}
window->onTouchCancel.Invoke();
}
// Capabilities come again when a device appears or goes; each object is
// created once, or every event would arrive once per copy.
void Device::seat_handle_capabilities(void*, wl_seat* seat, uint32_t capabilities) {
if ((capabilities & WL_SEAT_CAPABILITY_POINTER) && seatPointer == nullptr) {
seatPointer = wl_seat_get_pointer(seat);
wl_pointer_add_listener(seatPointer, &pointer_listener, nullptr);
2026-03-09 20:10:19 +01:00
}
if ((capabilities & WL_SEAT_CAPABILITY_TOUCH) && wlTouch == nullptr) {
wlTouch = wl_seat_get_touch(seat);
wl_touch_add_listener(wlTouch, &touch_listener, nullptr);
}
if ((capabilities & WL_SEAT_CAPABILITY_KEYBOARD) && seatKeyboard == nullptr) {
seatKeyboard = wl_seat_get_keyboard(seat);
wl_keyboard_add_listener(seatKeyboard, &keyboard_listener, nullptr);
2026-03-09 20:10:19 +01:00
}
}
#endif
void Device::Initialize() {
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
display = wl_display_connect(NULL);
if (display == nullptr) {
throw std::runtime_error("Could not connect to wayland display");
}
wl_registry* registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, &registry_listener, nullptr);
if (wl_display_roundtrip(display) == -1) {
exit(EXIT_FAILURE);
}
if (shm == NULL || compositor == NULL || xdgWmBase == NULL) {
throw std::runtime_error("No wl_shm, wl_compositor or xdg_wm_base support");
}
2026-05-19 00:45:22 +02:00
// After the registry roundtrip the data_device (if the compositor
// exposes one) is bound. Clipboard::Initialize attaches the
// selection listener that Clipboard::GetText reads from; doing it
// before the first wl_data_device.selection arrives is what lets
// GetText work the instant a frame is rendered.
Clipboard::Initialize();
2026-03-09 20:10:19 +01:00
#endif
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
app.pApplicationName = "";
app.pEngineName = "Crafter.Graphics";
2026-04-05 22:53:59 +02:00
app.apiVersion = VK_MAKE_VERSION(1, 4, 0);
// GPU-Assisted Validation: shader-execution-time instrumentation that
// catches out-of-bounds descriptor / buffer access the standard layer
// can't see. Opt-in via the enable list below.
2026-05-05 23:49:29 +02:00
//
// This was previously disabled to work around a bug in SDK 1.4.341's
// GPU-AV, which null-deref'd on descriptor_heap pipelines
// (VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT with layout =
// VK_NULL_HANDLE) in `PipelineSubState::GetPipelineLayoutUnion`:
2026-05-05 23:49:29 +02:00
// https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/12103
// Per spencer-lunarg (LunarG) that was fixed in the next SDK release.
// The validation layer is now 1.4.350 (> 1.4.341), so re-enable it.
2026-07-19 01:38:25 +02:00
// Validation (incl. GPU-Assisted Validation) is a DEVELOPER tool and is OFF
// by default: GPU-AV instruments shaders and has crashed vendor SPIR-V
// compilers (observed: NVIDIA libnvidia-glvkspirv segfault on the
// descriptor-heap compute pipelines). Opt in by setting the environment
// variable CRAFTER_GRAPHICS_VALIDATION.
bool enableValidation = std::getenv("CRAFTER_GRAPHICS_VALIDATION") != nullptr;
VkValidationFeatureEnableEXT enabledValidationFeatures[] = {
VK_VALIDATION_FEATURE_ENABLE_GPU_ASSISTED_EXT,
};
2026-04-05 22:53:59 +02:00
VkValidationFeaturesEXT validationFeatures = {
.sType = VK_STRUCTURE_TYPE_VALIDATION_FEATURES_EXT,
.enabledValidationFeatureCount = sizeof(enabledValidationFeatures) / sizeof(enabledValidationFeatures[0]),
.pEnabledValidationFeatures = enabledValidationFeatures,
2026-04-05 22:53:59 +02:00
};
2026-03-09 20:10:19 +01:00
VkInstanceCreateInfo instanceCreateInfo = {};
instanceCreateInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
2026-07-19 01:38:25 +02:00
instanceCreateInfo.pNext = enableValidation ? (const void*)&validationFeatures : nullptr;
2026-03-09 20:10:19 +01:00
instanceCreateInfo.pApplicationInfo = &app;
instanceCreateInfo.enabledExtensionCount = sizeof(instanceExtensionNames) / sizeof(const char*);
instanceCreateInfo.ppEnabledExtensionNames = instanceExtensionNames;
size_t foundInstanceLayers = 0;
std::uint32_t instanceLayerCount;
CheckVkResult(vkEnumerateInstanceLayerProperties(&instanceLayerCount, NULL));
std::vector<VkLayerProperties> instanceLayerProperties(instanceLayerCount);
CheckVkResult(vkEnumerateInstanceLayerProperties(&instanceLayerCount, instanceLayerProperties.data()));
for (uint32_t i = 0; i < instanceLayerCount; i++)
{
for (size_t j = 0; j < sizeof(layerNames) / sizeof(const char*); j++)
{
if (std::strcmp(instanceLayerProperties[i].layerName, layerNames[j]) == 0)
{
foundInstanceLayers++;
}
}
}
2026-07-19 01:38:25 +02:00
if (enableValidation && foundInstanceLayers >= sizeof(layerNames) / sizeof(const char*))
2026-03-09 20:10:19 +01:00
{
instanceCreateInfo.enabledLayerCount = sizeof(layerNames) / sizeof(const char*);
instanceCreateInfo.ppEnabledLayerNames = layerNames;
}
CheckVkResult(vkCreateInstance(&instanceCreateInfo, NULL, &instance));
VkDebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfo = {};
debugUtilsMessengerCreateInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
debugUtilsMessengerCreateInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
debugUtilsMessengerCreateInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
debugUtilsMessengerCreateInfo.pfnUserCallback = onError;
CheckVkResult(GET_EXTENSION_FUNCTION(vkCreateDebugUtilsMessengerEXT)(instance, &debugUtilsMessengerCreateInfo, NULL, &debugMessenger));
uint32_t physDeviceCount;
vkEnumeratePhysicalDevices(instance, &physDeviceCount, NULL);
std::vector<VkPhysicalDevice> physDevices(physDeviceCount);
vkEnumeratePhysicalDevices(instance, &physDeviceCount, physDevices.data());
uint32_t bestScore = 0;
for (uint32_t i = 0; i < physDeviceCount; i++)
{
VkPhysicalDevice device = physDevices[i];
2026-04-03 03:29:51 +02:00
uint32_t score;
2026-03-09 20:10:19 +01:00
VkPhysicalDeviceProperties properties;
vkGetPhysicalDeviceProperties(device, &properties);
switch (properties.deviceType)
{
default :
continue;
case VK_PHYSICAL_DEVICE_TYPE_OTHER :
score = 1;
break;
case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU :
score = 4;
break;
case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU :
score = 5;
break;
case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU :
score = 3;
break;
case VK_PHYSICAL_DEVICE_TYPE_CPU :
score = 2;
break;
}
if (score > bestScore)
{
physDevice = device;
bestScore = score;
}
}
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
// 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;
2026-05-12 00:24:48 +02:00
{
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) {
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
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;
2026-05-12 00:24:48 +02:00
}
}
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
if (!missing.empty()) {
throw std::runtime_error(std::format("Crafter.Graphics: the Vulkan driver lacks required device extension(s):{}", missing));
}
2026-05-12 00:24:48 +02:00
}
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
// 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;
}
2026-05-12 00:24:48 +02:00
if (memoryDecompressionSupported) {
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
memoryDecompressionProperties.pNext = propertiesChain;
propertiesChain = &memoryDecompressionProperties;
2026-05-12 00:24:48 +02:00
}
2026-04-05 22:53:59 +02:00
VkPhysicalDeviceProperties2 properties2 {
2026-04-03 03:29:51 +02:00
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
.pNext = propertiesChain
2026-04-03 03:29:51 +02:00
};
vkGetPhysicalDeviceProperties2(physDevice, &properties2);
// Keep the core properties around for the pipeline-cache identity check
// (vendorID / deviceID / pipelineCacheUUID).
deviceProperties = properties2.properties;
2026-05-12 00:24:48 +02:00
// Cache the flush/invalidate alignment for ranged host-memory flushes
// (VulkanBuffer::FlushDevice(offset,bytes)).
nonCoherentAtomSize = properties2.properties.limits.nonCoherentAtomSize;
fix(vulkan-rt): work around NVIDIA descriptor-heap AS-read device-loss (#15) Reading an acceleration structure through VK_EXT_descriptor_heap aborts with VK_ERROR_DEVICE_LOST on NVIDIA 610.43.02 — a brand-new-extension driver fault isolated in #7 (engine setup is correct and validation-clean; images/buffers through the same heap work, and both traceRayEXT and inline rayQuery fault identically on the AS read). An acceleration structure can equally be reached by its device address via OpConvertUToAccelerationStructureKHR, which reads no descriptor and so never touches the faulting heap path. glslang has no GLSL spelling for that conversion, so VulkanShader rewrites the compiled SPIR-V at module-load time: every `OpLoad %accelStruct <heap-ptr>` becomes a load of the TLAS device address from a synthesized push-constant block followed by the convert. RTPass pushes the active frame's TLAS address into that push constant. User GLSL and example code are unchanged; acceleration structures still bind into the heap normally. The workaround is gated on Device::workaroundDescriptorHeapAS (true only on the NVIDIA proprietary driver) and confined to one fenced block in Crafter.Graphics-ShaderVulkan.cppm plus the RTPass push and the shaderInt64 feature toggle — delete those once a fixed NVIDIA driver ships and the heap AS read becomes the direct path again. Verified: VulkanTriangle ray-traces correctly on native NVIDIA (RTX 4090), validation-layer-clean, no device loss. The SPIR-V rewrite was independently validated with spirv-val on both the VulkanTriangle and Sponza raygen modules. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 01:59:54 +00:00
// 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)
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
// 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;
fix(vulkan-rt): work around NVIDIA descriptor-heap AS-read device-loss (#15) Reading an acceleration structure through VK_EXT_descriptor_heap aborts with VK_ERROR_DEVICE_LOST on NVIDIA 610.43.02 — a brand-new-extension driver fault isolated in #7 (engine setup is correct and validation-clean; images/buffers through the same heap work, and both traceRayEXT and inline rayQuery fault identically on the AS read). An acceleration structure can equally be reached by its device address via OpConvertUToAccelerationStructureKHR, which reads no descriptor and so never touches the faulting heap path. glslang has no GLSL spelling for that conversion, so VulkanShader rewrites the compiled SPIR-V at module-load time: every `OpLoad %accelStruct <heap-ptr>` becomes a load of the TLAS device address from a synthesized push-constant block followed by the convert. RTPass pushes the active frame's TLAS address into that push constant. User GLSL and example code are unchanged; acceleration structures still bind into the heap normally. The workaround is gated on Device::workaroundDescriptorHeapAS (true only on the NVIDIA proprietary driver) and confined to one fenced block in Crafter.Graphics-ShaderVulkan.cppm plus the RTPass push and the shaderInt64 feature toggle — delete those once a fixed NVIDIA driver ships and the heap AS read becomes the direct path again. Verified: VulkanTriangle ray-traces correctly on native NVIDIA (RTX 4090), validation-layer-clean, no device loss. The SPIR-V rewrite was independently validated with spirv-val on both the VulkanTriangle and Sponza raygen modules. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 01:59:54 +00:00
2026-05-12 00:24:48 +02:00
// Sanity-gate: GDeflate 1.0 must actually be in the supported method set.
if (memoryDecompressionSupported &&
(memoryDecompressionProperties.decompressionMethods & VK_MEMORY_DECOMPRESSION_METHOD_GDEFLATE_1_0_BIT_EXT) == 0) {
memoryDecompressionSupported = false;
}
2026-03-09 20:10:19 +01:00
uint32_t queueFamilyCount;
vkGetPhysicalDeviceQueueFamilyProperties(physDevice, &queueFamilyCount, NULL);
std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
vkGetPhysicalDeviceQueueFamilyProperties(physDevice, &queueFamilyCount, queueFamilies.data());
for (uint32_t i = 0; i < queueFamilyCount; i++)
{
if (queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)
{
queueFamilyIndex = i;
break;
}
}
float priority = 1;
VkDeviceQueueCreateInfo queueCreateInfo = {};
queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueCreateInfo.queueFamilyIndex = queueFamilyIndex;
queueCreateInfo.queueCount = 1;
queueCreateInfo.pQueuePriorities = &priority;
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
// 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.
2026-04-09 00:15:09 +02:00
VkPhysicalDeviceFaultFeaturesEXT faultFeatures = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT,
.deviceFault = VK_TRUE,
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
.deviceFaultVendorBinary = faultQuery.deviceFaultVendorBinary,
2026-04-09 00:15:09 +02:00
};
2026-04-05 22:53:59 +02:00
VkPhysicalDeviceShaderUntypedPointersFeaturesKHR untypedPointersFeatures {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_UNTYPED_POINTERS_FEATURES_KHR,
.shaderUntypedPointers = VK_TRUE,
};
2026-05-12 00:24:48 +02:00
// Enables synchronization2 sentinels (VkMemoryBarrier2, VK_PIPELINE_STAGE_2_*,
// VK_ACCESS_2_*) — required for VK_EXT_memory_decompression's sync tokens.
VkPhysicalDeviceVulkan13Features features13 {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES,
.synchronization2 = VK_TRUE,
};
VkPhysicalDeviceMemoryDecompressionFeaturesEXT memoryDecompressionFeatures {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_DECOMPRESSION_FEATURES_EXT,
.memoryDecompression = VK_TRUE,
};
2026-04-05 22:53:59 +02:00
VkPhysicalDeviceDescriptorHeapFeaturesEXT desciptorHeapFeatures {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_HEAP_FEATURES_EXT,
.descriptorHeap = VK_TRUE,
};
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
// 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,
};
2026-04-01 18:43:18 +02:00
VkPhysicalDevice16BitStorageFeatures bit16 {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES,
.storageBuffer16BitAccess = VK_TRUE,
};
2026-03-09 20:10:19 +01:00
VkPhysicalDeviceVulkan12Features features12 {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES,
2026-04-09 00:15:09 +02:00
.shaderFloat16 = VK_TRUE,
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
// Bindless / runtime descriptor array indexing — needed by both
// binding models (heap and descriptor sets).
2026-05-01 23:35:37 +02:00
.shaderUniformBufferArrayNonUniformIndexing = VK_TRUE,
.shaderSampledImageArrayNonUniformIndexing = VK_TRUE,
.shaderStorageBufferArrayNonUniformIndexing = VK_TRUE,
.shaderStorageImageArrayNonUniformIndexing = VK_TRUE,
2026-03-09 20:10:19 +01:00
.runtimeDescriptorArray = VK_TRUE,
2026-05-01 23:35:37 +02:00
.scalarBlockLayout = VK_TRUE,
.bufferDeviceAddress = VK_TRUE
2026-03-09 20:10:19 +01:00
};
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
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;
}
2026-03-09 20:10:19 +01:00
2026-05-05 23:49:29 +02:00
VkPhysicalDeviceRayQueryFeaturesKHR physicalDeviceRayQueryFeatures{
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_QUERY_FEATURES_KHR,
.rayQuery = VK_TRUE
};
2026-03-09 20:10:19 +01:00
VkPhysicalDeviceRayTracingPipelineFeaturesKHR physicalDeviceRayTracingPipelineFeatures{
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_PIPELINE_FEATURES_KHR,
.rayTracingPipeline = VK_TRUE
};
VkPhysicalDeviceAccelerationStructureFeaturesKHR deviceAccelerationStructureFeature = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_FEATURES_KHR,
.accelerationStructure = VK_TRUE
};
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
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);
}
2026-03-09 20:10:19 +01:00
VkPhysicalDeviceFeatures2 physical_features2 = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
.pNext = featureChain,
2026-03-09 20:10:19 +01:00
.features = {
2026-05-01 23:35:37 +02:00
// Order matches VkPhysicalDeviceFeatures declaration so the
// designated-initializer-order warning stays quiet.
.samplerAnisotropy = VK_TRUE,
.shaderStorageImageReadWithoutFormat = VK_TRUE,
.shaderStorageImageWriteWithoutFormat = VK_TRUE,
// Bindless dynamic indexing — required to index `images[]`,
// `textures[]`, `samplers[]`, `itemHeap[]` with a runtime value.
.shaderSampledImageArrayDynamicIndexing = VK_TRUE,
.shaderStorageBufferArrayDynamicIndexing = VK_TRUE,
.shaderStorageImageArrayDynamicIndexing = VK_TRUE,
fix(vulkan-rt): work around NVIDIA descriptor-heap AS-read device-loss (#15) Reading an acceleration structure through VK_EXT_descriptor_heap aborts with VK_ERROR_DEVICE_LOST on NVIDIA 610.43.02 — a brand-new-extension driver fault isolated in #7 (engine setup is correct and validation-clean; images/buffers through the same heap work, and both traceRayEXT and inline rayQuery fault identically on the AS read). An acceleration structure can equally be reached by its device address via OpConvertUToAccelerationStructureKHR, which reads no descriptor and so never touches the faulting heap path. glslang has no GLSL spelling for that conversion, so VulkanShader rewrites the compiled SPIR-V at module-load time: every `OpLoad %accelStruct <heap-ptr>` becomes a load of the TLAS device address from a synthesized push-constant block followed by the convert. RTPass pushes the active frame's TLAS address into that push constant. User GLSL and example code are unchanged; acceleration structures still bind into the heap normally. The workaround is gated on Device::workaroundDescriptorHeapAS (true only on the NVIDIA proprietary driver) and confined to one fenced block in Crafter.Graphics-ShaderVulkan.cppm plus the RTPass push and the shaderInt64 feature toggle — delete those once a fixed NVIDIA driver ships and the heap AS read becomes the direct path again. Verified: VulkanTriangle ray-traces correctly on native NVIDIA (RTX 4090), validation-layer-clean, no device loss. The SPIR-V rewrite was independently validated with spirv-val on both the VulkanTriangle and Sponza raygen modules. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 01:59:54 +00:00
// shaderInt64: only needed for the NVIDIA descriptor-heap AS-read
// workaround (issue #15 / #7), which loads the TLAS device address
// as a 64-bit push constant. Gated so it isn't required on drivers
// that don't take the workaround path. Remove with the workaround.
.shaderInt64 = workaroundDescriptorHeapAS ? VK_TRUE : VK_FALSE,
2026-05-01 23:35:37 +02:00
.shaderInt16 = VK_TRUE
2026-03-09 20:10:19 +01:00
}
};
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
// The enabled-extension list is the required set plus each optional group
// the driver advertises in full (decided above from the enumeration).
2026-05-12 00:24:48 +02:00
std::vector<const char*> enabledDeviceExtensions(
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap Device::Initialize required twelve device extensions with no capability check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap, VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray tracing extensions. vkCreateDevice failed there, so not even a UI-only program could start, although the UI path is compute-only and needs nothing else that Turnip is missing. The descriptor heap, ray tracing and device fault are now optional groups, detected at start-up from the extension list and the feature bits (Device::descriptorHeapSupported, rayTracingSupported, deviceFaultSupported). Ray tracing needs the heap; on a device without it the ray tracing entry points throw a message naming the cause. Without the heap the same bindless slots live in one update-after-bind descriptor set per frame, owned by DescriptorHeapVulkan: binding 0 storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot allocation, the slot handles, UIDispatchHeader and the UIRenderer API are unchanged; buffer slots simply stop being offset. Window::Render binds that frame's set where it binds the heap, every compute pipeline shares the heap object's pipeline layout, and push data travels as push constants. Each UI kernel is built twice from one source. ui-shared.glsl declares its resources through UI_HEAP_* macros that expand to descriptor_heap or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding. <name>.comp.spv and <name>.comp.sets.spv ship side by side and ComputeShader::Load picks one. The heap build of every shader is byte-identical to before. Breaking: - ComputeShader::Load takes the heap as a second argument. - DescriptorHeapVulkan is no longer copyable or movable. - Custom UI shaders declare heap resources with the UI_HEAP_* macros and are registered twice in project.cpp (see the README). - project.cpp uses Shader::defines and Shader::variant, so it needs a crafter-build that has them (Crafter.Build master, fe38cc1 or later). Also: - maintenance5 is enabled as a feature, not only as an extension: every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo. - Barrier stage masks drop RAY_TRACING_SHADER on devices without ray tracing, and ImageVulkan::Create resolves its default consumer stage to COMPUTE_SHADER there. - Window refuses a swapchain whose image count is not numFrames instead of writing past images[]. The limitation itself is #160. - CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver that has the heap, so it can be exercised on a desktop GPU. Verified on an RTX 4090 in both modes under the validation layer, and on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback is the only path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
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);
}
2026-05-12 00:24:48 +02:00
if (memoryDecompressionSupported) {
enabledDeviceExtensions.push_back(VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME);
}
2026-03-09 20:10:19 +01:00
VkDeviceCreateInfo deviceCreateInfo = {};
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
deviceCreateInfo.queueCreateInfoCount = 1;
deviceCreateInfo.pQueueCreateInfos = &queueCreateInfo;
2026-05-12 00:24:48 +02:00
deviceCreateInfo.enabledExtensionCount = static_cast<std::uint32_t>(enabledDeviceExtensions.size());
deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data();
2026-03-09 20:10:19 +01:00
deviceCreateInfo.pNext = &physical_features2;
// Device layers are deprecated and have been ignored since Vulkan 1.0;
// enabling them is a validation error. Layers are enabled at instance
// creation only, so leave enabledLayerCount at 0.
deviceCreateInfo.enabledLayerCount = 0;
deviceCreateInfo.ppEnabledLayerNames = nullptr;
2026-03-09 20:10:19 +01:00
CheckVkResult(vkCreateDevice(physDevice, &deviceCreateInfo, NULL, &device));
vkGetDeviceQueue(device, queueFamilyIndex, 0, &queue);
VkCommandPoolCreateInfo commandPoolcreateInfo = {};
commandPoolcreateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
commandPoolcreateInfo.queueFamilyIndex = queueFamilyIndex;
commandPoolcreateInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
CheckVkResult(vkCreateCommandPool(device, &commandPoolcreateInfo, NULL, &commandPool));
vkGetPhysicalDeviceMemoryProperties(physDevice, &memoryProperties);
2026-06-16 20:10:23 +02:00
CacheUploadStrategy();
2026-03-09 20:10:19 +01:00
vkGetAccelerationStructureBuildSizesKHR = reinterpret_cast<PFN_vkGetAccelerationStructureBuildSizesKHR>(vkGetInstanceProcAddr(instance, "vkGetAccelerationStructureBuildSizesKHR"));
vkCreateAccelerationStructureKHR = reinterpret_cast<PFN_vkCreateAccelerationStructureKHR>(vkGetInstanceProcAddr(instance, "vkCreateAccelerationStructureKHR"));
2026-05-05 23:49:29 +02:00
vkDestroyAccelerationStructureKHR = reinterpret_cast<PFN_vkDestroyAccelerationStructureKHR>(vkGetInstanceProcAddr(instance, "vkDestroyAccelerationStructureKHR"));
2026-03-09 20:10:19 +01:00
vkCmdBuildAccelerationStructuresKHR = reinterpret_cast<PFN_vkCmdBuildAccelerationStructuresKHR>(vkGetInstanceProcAddr(instance, "vkCmdBuildAccelerationStructuresKHR"));
vkGetAccelerationStructureDeviceAddressKHR = reinterpret_cast<PFN_vkGetAccelerationStructureDeviceAddressKHR>(vkGetInstanceProcAddr(instance, "vkGetAccelerationStructureDeviceAddressKHR"));
vkCreateRayTracingPipelinesKHR = reinterpret_cast<PFN_vkCreateRayTracingPipelinesKHR>(vkGetInstanceProcAddr(instance, "vkCreateRayTracingPipelinesKHR"));
vkGetRayTracingShaderGroupHandlesKHR = reinterpret_cast<PFN_vkGetRayTracingShaderGroupHandlesKHR>(vkGetInstanceProcAddr(instance, "vkGetRayTracingShaderGroupHandlesKHR"));
vkCmdTraceRaysKHR = reinterpret_cast<PFN_vkCmdTraceRaysKHR>(vkGetInstanceProcAddr(instance, "vkCmdTraceRaysKHR"));
2026-04-05 22:53:59 +02:00
vkCmdBindResourceHeapEXT = reinterpret_cast<PFN_vkCmdBindResourceHeapEXT>(vkGetInstanceProcAddr(instance, "vkCmdBindResourceHeapEXT"));
vkCmdBindSamplerHeapEXT = reinterpret_cast<PFN_vkCmdBindSamplerHeapEXT>(vkGetInstanceProcAddr(instance, "vkCmdBindSamplerHeapEXT"));
vkWriteResourceDescriptorsEXT = reinterpret_cast<PFN_vkWriteResourceDescriptorsEXT>(vkGetInstanceProcAddr(instance, "vkWriteResourceDescriptorsEXT"));
2026-05-01 23:35:37 +02:00
vkWriteSamplerDescriptorsEXT = reinterpret_cast<PFN_vkWriteSamplerDescriptorsEXT>(vkGetInstanceProcAddr(instance, "vkWriteSamplerDescriptorsEXT"));
vkCmdPushDataEXT = reinterpret_cast<PFN_vkCmdPushDataEXT>(vkGetInstanceProcAddr(instance, "vkCmdPushDataEXT"));
2026-04-05 22:53:59 +02:00
vkGetPhysicalDeviceDescriptorSizeEXT = reinterpret_cast<PFN_vkGetPhysicalDeviceDescriptorSizeEXT>(vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceDescriptorSizeEXT"));
2026-04-09 00:15:09 +02:00
vkGetDeviceFaultInfoEXT = reinterpret_cast<PFN_vkGetDeviceFaultInfoEXT>(vkGetInstanceProcAddr(instance, "vkGetDeviceFaultInfoEXT"));
2026-05-12 00:24:48 +02:00
if (memoryDecompressionSupported) {
// vkGetDeviceProcAddr skips the loader trampoline that vkGetInstanceProcAddr
// requires for device-level functions. The other PFNs above predate this
// realization; opportunistic adoption for new entry points only.
vkCmdDecompressMemoryEXT = reinterpret_cast<PFN_vkCmdDecompressMemoryEXT>(
vkGetDeviceProcAddr(device, "vkCmdDecompressMemoryEXT"));
if (vkCmdDecompressMemoryEXT == nullptr) {
// Driver advertised the extension but didn't expose the entry
// point — defensively fall back to CPU decode.
memoryDecompressionSupported = false;
}
}
// Create the shared pipeline cache (seeded from disk when a compatible
// blob exists) and arrange for it to be written back at process exit. The
// device handle is a never-destroyed static, so it is still valid when the
// atexit handler runs. Registered once — Initialize is the single device
// bring-up.
LoadPipelineCache();
std::atexit(SavePipelineCache);
2026-03-09 20:10:19 +01:00
}
std::uint32_t Device::GetMemoryType(uint32_t typeBits, VkMemoryPropertyFlags required, VkMemoryPropertyFlags preferred) {
// First pass: honour the preferred bits on top of the required ones.
// Second pass (when nothing matched, or no preferences were given): drop
// the preferences and match the required bits alone. Vulkan orders the
// memory types so first-superset-match is generally optimal, so a plain
// linear scan suffices for each pass. We only throw when even `required`
// cannot be satisfied — i.e. no valid memory exists for the allocation.
for (VkMemoryPropertyFlags wanted : {required | preferred, required})
2026-03-09 20:10:19 +01:00
{
uint32_t bits = typeBits;
for (uint32_t i = 0; i < memoryProperties.memoryTypeCount; i++)
2026-03-09 20:10:19 +01:00
{
if ((bits & 1) == 1)
2026-03-09 20:10:19 +01:00
{
if ((memoryProperties.memoryTypes[i].propertyFlags & wanted) == wanted)
{
return i;
}
2026-03-09 20:10:19 +01:00
}
bits >>= 1;
}
// No preferences to fall back from — the second pass would be identical.
if (preferred == 0) {
break;
2026-03-09 20:10:19 +01:00
}
}
throw std::runtime_error("Could not find a matching memory type");
}
2026-06-16 20:10:23 +02:00
void Device::CacheUploadStrategy() {
constexpr VkMemoryPropertyFlags directFlags =
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT;
// 1. Find a DEVICE_LOCAL | HOST_VISIBLE type and remember its heap. Without
// one (no resizable BAR), direct writes are impossible -> must stage.
bool haveDirectType = false;
std::uint32_t barHeapIndex = 0;
for (std::uint32_t i = 0; i < memoryProperties.memoryTypeCount; ++i) {
if ((memoryProperties.memoryTypes[i].propertyFlags & directFlags) == directFlags) {
haveDirectType = true;
barHeapIndex = memoryProperties.memoryTypes[i].heapIndex;
break;
}
}
if (!haveDirectType) {
2026-06-16 20:10:23 +02:00
directWriteBudget = 0;
return;
}
// 2. Compare the host-visible device-local heap against the largest plain
// DEVICE_LOCAL heap (the true VRAM size).
VkDeviceSize barHeap = memoryProperties.memoryHeaps[barHeapIndex].size;
VkDeviceSize vram = 0;
for (std::uint32_t i = 0; i < memoryProperties.memoryHeapCount; ++i) {
if (memoryProperties.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) {
vram = std::max(vram, memoryProperties.memoryHeaps[i].size);
}
}
2026-06-16 20:10:23 +02:00
// 3. ReBAR / UMA: the BAR heap covers (almost) all of VRAM -> map any size.
// barHeap >= 0.9 * vram, written to dodge floating point / overflow.
if (barHeap >= vram - vram / 10) {
2026-06-16 20:10:23 +02:00
directWriteBudget = std::numeric_limits<VkDeviceSize>::max();
return;
}
// 4. Small BAR window: direct only for buffers small enough that several
// still fit the window — large buffers stage so they don't exhaust it.
// The budget is tied to the actual window size rather than a magic
// constant so it adapts across devices (a fixed cap would be wrong for
// both a 256 MiB and a 4 GiB window). VK_EXT_memory_budget would refine
// this to the window's *remaining* space; see the header note.
constexpr VkDeviceSize barWindowFraction = 8;
2026-06-16 20:10:23 +02:00
directWriteBudget = barHeap / barWindowFraction;
}
bool Device::PreferDirectDeviceWrite(VkDeviceSize size) {
// directWriteBudget caches the size-independent decision (CacheUploadStrategy):
// 0 -> never direct, max -> ReBAR/UMA so any size, else the small-window cap.
return directWriteBudget != 0 && size <= directWriteBudget;
}
void Device::EnqueueDeletion(VkBuffer buffer, VkDeviceMemory memory) {
// Nothing to free for an already-null handle — avoids parking dead
// entries that ReclaimDeletions would have to skip over.
if (buffer == VK_NULL_HANDLE) return;
deletionQueue.push_back({ frameCounter, buffer, memory });
}
void Device::ReclaimDeletions() {
// Free every entry whose retire frame has been reached. An entry tagged at
// frame F retires at F + framesInFlight: by the time the CPU has begun that
// many later frames (each gated by a per-image fence wait), single-queue
// submission order guarantees all GPU work from frame F is complete.
//
// frameCounter is monotonic, so retireAfter is non-decreasing down the
// queue and the ready entries are a contiguous prefix: pop them off the
// front and stop at the first one still in flight (everything behind it is
// newer, hence also in flight). O(ready), not O(queue).
while (!deletionQueue.empty() &&
deletionQueue.front().retireAfter + framesInFlight <= frameCounter) {
const PendingDeletion& entry = deletionQueue.front();
vkDestroyBuffer(device, entry.buffer, nullptr);
vkFreeMemory(device, entry.memory, nullptr);
deletionQueue.pop_front();
}
}
void Device::DrainDeletions() {
// Unconditional: the caller has just wait-idled, so no in-flight GPU work
// can still reference any queued resource.
for (PendingDeletion& entry : deletionQueue) {
vkDestroyBuffer(device, entry.buffer, nullptr);
vkFreeMemory(device, entry.memory, nullptr);
}
deletionQueue.clear();
2026-05-01 23:35:37 +02:00
}