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 <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));
|
2026-06-16 18:23:25 +00: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
|
|
|
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.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 18:23:25 +00:00
|
|
|
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): ");
|
2026-06-16 13:37:46 +00:00
|
|
|
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);
|
|
|
|
|
}
|
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);
|
|
|
|
|
}
|
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) {
|
2026-06-12 15:16:57 +00:00
|
|
|
// 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) {
|
2026-06-12 15:16:57 +00:00
|
|
|
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*) {
|
2026-06-12 15:16:57 +00:00
|
|
|
// 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;
|
|
|
|
|
}
|
2026-06-12 15:02:28 +00:00
|
|
|
// 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
|
|
|
}
|
|
|
|
|
|
2026-06-12 15:02:28 +00: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) {
|
2026-10-17 23:48:01 +02:00
|
|
|
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) {
|
2026-10-17 23:48:01 +02:00
|
|
|
// 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
|
|
|
|
2026-10-17 23:48:01 +02: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) {
|
2026-10-17 23:48:01 +02:00
|
|
|
if (keyboardWindow->heldKeys.contains(code)) {
|
|
|
|
|
keyboardWindow->onRawKeyHold.Invoke(code);
|
2026-04-16 23:03:24 +02:00
|
|
|
} else {
|
2026-10-17 23:48:01 +02:00
|
|
|
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;
|
2026-10-17 23:48:01 +02:00
|
|
|
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 {
|
2026-10-17 23:48:01 +02:00
|
|
|
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() {
|
2026-10-17 23:48:01 +02:00
|
|
|
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) {
|
2026-10-17 23:48:01 +02:00
|
|
|
keyboardWindow->onRawKeyDown.Invoke(keyRepeat.key);
|
|
|
|
|
keyboardWindow->onRawKeyHold.Invoke(keyRepeat.key);
|
2026-05-02 00:03:24 +02:00
|
|
|
if (!keyRepeat.utf8.empty()) {
|
2026-10-17 23:48:01 +02:00
|
|
|
keyboardWindow->onTextInput.Invoke(keyRepeat.utf8);
|
2026-05-02 00:03:24 +02:00
|
|
|
}
|
|
|
|
|
keyRepeat.lastFireTime += period;
|
|
|
|
|
}
|
2026-03-09 20:10:19 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-18 01:48:30 +02:00
|
|
|
// 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;
|
|
|
|
|
touchPositions[id] = { pos.x, pos.y };
|
|
|
|
|
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;
|
|
|
|
|
const auto at = touchPositions.find(id);
|
|
|
|
|
const Vector<float, 2> pos = at == touchPositions.end() ? window->currentMousePos : Vector<float, 2>(at->second.first, at->second.second);
|
|
|
|
|
touchPositions.erase(id);
|
|
|
|
|
window->onTouchUp.Invoke({ id, pos });
|
|
|
|
|
if (id == window->primaryTouchId) {
|
|
|
|
|
window->primaryTouchId = -1;
|
|
|
|
|
window->mouseLeftHeld = false;
|
|
|
|
|
window->onMouseLeftRelease.Invoke();
|
|
|
|
|
}
|
|
|
|
|
if (touchPositions.empty()) 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;
|
|
|
|
|
const Vector<float, 2> pos = Vector<float, 2>(wl_fixed_to_double(x), wl_fixed_to_double(y)) * window->scale;
|
|
|
|
|
touchPositions[id] = { pos.x, 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;
|
|
|
|
|
touchPositions.clear();
|
|
|
|
|
touchWindow = nullptr;
|
|
|
|
|
if (window->primaryTouchId >= 0) {
|
|
|
|
|
window->primaryTouchId = -1;
|
|
|
|
|
window->mouseLeftHeld = false;
|
|
|
|
|
window->onMouseLeftRelease.Invoke();
|
|
|
|
|
}
|
|
|
|
|
window->onTouchCancel.Invoke();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 20:10:19 +01:00
|
|
|
void Device::seat_handle_capabilities(void* data, wl_seat* seat, uint32_t capabilities) {
|
|
|
|
|
seat = seat;
|
|
|
|
|
if (capabilities & WL_SEAT_CAPABILITY_POINTER) {
|
|
|
|
|
wl_pointer* pointer = wl_seat_get_pointer(seat);
|
|
|
|
|
wl_pointer_add_listener(pointer, &pointer_listener, nullptr);
|
|
|
|
|
}
|
2026-10-18 01:48:30 +02:00
|
|
|
if ((capabilities & WL_SEAT_CAPABILITY_TOUCH) && wlTouch == nullptr) {
|
|
|
|
|
wlTouch = wl_seat_get_touch(seat);
|
|
|
|
|
wl_touch_add_listener(wlTouch, &touch_listener, nullptr);
|
|
|
|
|
}
|
2026-03-09 20:10:19 +01:00
|
|
|
if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) {
|
|
|
|
|
wl_keyboard* keyboard = wl_seat_get_keyboard(seat);
|
|
|
|
|
wl_keyboard_add_listener(keyboard, &keyboard_listener, nullptr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#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, ®istry_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);
|
|
|
|
|
|
2026-06-03 02:10:23 +00:00
|
|
|
// 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
|
|
|
//
|
2026-06-03 02:10:23 +00: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
|
2026-06-03 02:10:23 +00:00
|
|
|
// 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;
|
2026-06-03 02:10:23 +00:00
|
|
|
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,
|
2026-06-03 02:10:23 +00:00
|
|
|
.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);
|
2026-06-16 18:23:25 +00:00
|
|
|
// 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
|
|
|
|
2026-06-16 18:29:24 +00:00
|
|
|
// Cache the flush/invalidate alignment for ranged host-memory flushes
|
|
|
|
|
// (VulkanBuffer::FlushDevice(offset,bytes)).
|
|
|
|
|
nonCoherentAtomSize = properties2.properties.limits.nonCoherentAtomSize;
|
|
|
|
|
|
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;
|
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,
|
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;
|
|
|
|
|
|
2026-05-31 20:59:10 +00:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-16 18:23:25 +00:00
|
|
|
|
|
|
|
|
// 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
|
|
|
}
|
|
|
|
|
|
2026-06-16 16:03:50 +00: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
|
|
|
{
|
2026-06-16 16:03:50 +00:00
|
|
|
uint32_t bits = typeBits;
|
|
|
|
|
for (uint32_t i = 0; i < memoryProperties.memoryTypeCount; i++)
|
2026-03-09 20:10:19 +01:00
|
|
|
{
|
2026-06-16 16:03:50 +00:00
|
|
|
if ((bits & 1) == 1)
|
2026-03-09 20:10:19 +01:00
|
|
|
{
|
2026-06-16 16:03:50 +00:00
|
|
|
if ((memoryProperties.memoryTypes[i].propertyFlags & wanted) == wanted)
|
|
|
|
|
{
|
|
|
|
|
return i;
|
|
|
|
|
}
|
2026-03-09 20:10:19 +01:00
|
|
|
}
|
2026-06-16 16:03:50 +00: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 17:23:27 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-16 20:10:23 +02:00
|
|
|
void Device::CacheUploadStrategy() {
|
2026-06-16 17:23:27 +00:00
|
|
|
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;
|
2026-06-16 17:23:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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.
|
2026-06-16 17:23:27 +00:00
|
|
|
// 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;
|
2026-06-16 17:23:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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;
|
fix(device): fence-keyed deferred resource-deletion queue (#101)
Since #40 dropped the per-frame vkQueueWaitIdle, frames are pipelined:
a resource the CPU is done with may still be read by the GPU for up to
numFrames-1 more frames. VulkanBuffer::Resize's destroy-and-recreate
path was therefore a live use-after-free (#63), no longer masked by the
wait-idle.
Device gains a monotonic, frame-counter-keyed deletion queue:
EnqueueDeletion tags {buffer, memory} with the current frameCounter;
ReclaimDeletions (called per frame after the fence wait) frees entries
once framesInFlight frames have elapsed; DrainDeletions frees everything
after a wait-idle. VulkanBuffer::DeferredClear hands handles to the queue
and nulls the handle, and Resize uses it instead of immediate Clear().
Window::Render sets framesInFlight at init, reclaims after the per-image
fence wait, bumps Device::frameCounter once per frame, and drains on the
resize / OUT_OF_DATE wait-idle paths. The destructor keeps immediate
Clear() (callers destroying mid-flight remain responsible, unchanged).
Adds the DeferredDeletion test: drives the retire timing on a real
headless device with real buffers, stepping Device::frameCounter to pin
the exact reclaim frame, asserting validation stays silent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 13:21:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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() {
|
2026-06-17 19:37:58 +00:00
|
|
|
// 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();
|
fix(device): fence-keyed deferred resource-deletion queue (#101)
Since #40 dropped the per-frame vkQueueWaitIdle, frames are pipelined:
a resource the CPU is done with may still be read by the GPU for up to
numFrames-1 more frames. VulkanBuffer::Resize's destroy-and-recreate
path was therefore a live use-after-free (#63), no longer masked by the
wait-idle.
Device gains a monotonic, frame-counter-keyed deletion queue:
EnqueueDeletion tags {buffer, memory} with the current frameCounter;
ReclaimDeletions (called per frame after the fence wait) frees entries
once framesInFlight frames have elapsed; DrainDeletions frees everything
after a wait-idle. VulkanBuffer::DeferredClear hands handles to the queue
and nulls the handle, and Resize uses it instead of immediate Clear().
Window::Render sets framesInFlight at init, reclaims after the per-image
fence wait, bumps Device::frameCounter once per frame, and drains on the
resize / OUT_OF_DATE wait-idle paths. The destructor keeps immediate
Clear() (callers destroying mid-flight remain responsible, unchanged).
Adds the DeferredDeletion test: drives the retire timing on a real
headless device with real buffers, stepping Device::frameCounter to pin
the exact reclaim frame, asserting validation stays silent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 13:21:12 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
}
|