Crafter.Graphics/implementations/Crafter.Graphics-Device.cpp
Jorijn van der Graaf 678f38964f
feat(wayland): text input through zwp_text_input_v3
A window can now say that a text field has the focus and where its caret
is — Window::SetTextInput(active, caret rectangle in physical pixels,
purpose) — which is what opens the on-screen keyboard on a phone (Maliit
under KWin, squeekboard under phoc). What the input method produces comes
back as events: committed text as onTextInput, the composing text as
onPreedit (empty clears it), deletions around the cursor as
onTextInputDelete; done() applies them in the protocol's order. The state
a window wants is remembered and sent again whenever the keyboard focus
returns to it. The protocol's client code is generated with wayland-scanner
1.26.0 from wayland-protocols' text-input-unstable-v3.xml, under lib/ like
the others.
2026-10-18 01:57:09 +02:00

1369 lines
No EOL
60 KiB
C++

//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "vulkan/vulkan.h"
#include "vulkan/vk_enum_string_helper.h"
#define GET_EXTENSION_FUNCTION(_id) ((PFN_##_id)(vkGetInstanceProcAddr(instance, #_id)))
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
#include <linux/input-event-codes.h>
#include <xkbcommon/xkbcommon.h>
#include <string.h>
#include "../lib/xdg-shell-client-protocol.h"
#include "../lib/wayland-xdg-decoration-unstable-v1-client-protocol.h"
#include "../lib/fractional-scale-v1.h"
#include "../lib/viewporter.h"
#include "../lib/text-input-unstable-v3-client-protocol.h"
#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;
import :Clipboard;
import std;
using namespace Crafter;
const char* const instanceExtensionNames[] = {
"VK_EXT_debug_utils",
"VK_KHR_surface",
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
"VK_KHR_win32_surface"
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
"VK_KHR_wayland_surface"
#endif
};
// Device extensions are split by what breaks without them. Only the first
// group is needed to put UI pixels on screen; Device::Initialize enables each
// of the others only when the driver advertises all of its members, and
// records the outcome in Device::descriptorHeapSupported / rayTracingSupported
// / deviceFaultSupported. Mesa Turnip (Adreno), for one, has none of the heap,
// ray tracing or fault extensions.
const char* const requiredDeviceExtensionNames[] = {
"VK_KHR_swapchain",
"VK_KHR_spirv_1_4",
"VK_KHR_shader_float_controls",
"VK_KHR_maintenance5",
};
// Without these, descriptors come from a bindless descriptor set instead
// (DescriptorHeapVulkan's sets mode) and shaders load their .sets.spv build.
const char* const descriptorHeapExtensionNames[] = {
"VK_EXT_descriptor_heap",
"VK_KHR_shader_untyped_pointers",
};
// Ray tracing additionally needs the descriptor heap: every RT pipeline and
// RT shader in this library is heap-only.
const char* const rayTracingExtensionNames[] = {
"VK_KHR_acceleration_structure",
"VK_KHR_ray_tracing_pipeline",
"VK_KHR_ray_query",
"VK_KHR_deferred_host_operations",
};
const char* const layerNames[] = {
"VK_LAYER_KHRONOS_validation"
};
void Device::CheckVkResult(VkResult result) {
if (result != VK_SUCCESS)
{
// The fault report needs VK_EXT_device_fault; without it the entry
// point below is not callable and the bare VkResult is all there is.
if(result == VK_ERROR_DEVICE_LOST && deviceFaultSupported) {
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);
}
}
}
throw std::runtime_error(string_VkResult(result));
}
}
void Device::RequireRayTracing() {
if (rayTracingSupported) {
return;
}
// Thrown at the first ray tracing entry point (an RT-stage shader module,
// a BLAS/TLAS build, the SBT, the pipeline) so the failure names the
// cause. Letting the call through would hand heap-only SPIR-V or an
// unloaded entry point to a driver that never enabled the feature.
if (descriptorHeapSupported) {
throw std::runtime_error(
"Crafter.Graphics: ray tracing is unavailable: the Vulkan driver lacks one of "
"VK_KHR_acceleration_structure, VK_KHR_ray_tracing_pipeline, VK_KHR_ray_query, "
"VK_KHR_deferred_host_operations");
}
throw std::runtime_error(
"Crafter.Graphics: ray tracing is unavailable: it needs VK_EXT_descriptor_heap and "
"VK_KHR_shader_untyped_pointers, which this driver lacks (or "
"CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP is set). Only the compute/UI path runs here.");
}
bool Device::PipelineCacheDataCompatible(std::span<const std::byte> data) {
// VkPipelineCacheHeaderVersionOne is a fixed 32-byte little-endian header
// (Vulkan spec 16.5.2): u32 headerSize, u32 headerVersion, u32 vendorID,
// 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));
}
}
VkBool32 onError(VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMessageTypeFlagsEXT type, const VkDebugUtilsMessengerCallbackDataEXT* callbackData, void* userData)
{
printf("Vulkan ");
switch (type)
{
case VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT :
printf("general ");
break;
case VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT :
printf("validation ");
break;
case VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT :
printf("performance ");
break;
}
switch (severity)
{
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT :
printf("(verbose): ");
break;
default :
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT :
printf("(info): ");
break;
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT :
printf("(warning): ");
break;
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT :
printf("(error): ");
Device::validationErrorCount++;
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) {
// 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));
wl_seat_add_listener(seat, &seat_listener, nullptr);
// If the manager came in first, the data device couldn't be
// created yet — do it now that we have the seat.
if (dataDeviceManager != nullptr && dataDevice == nullptr) {
dataDevice = wl_data_device_manager_get_data_device(dataDeviceManager, seat);
}
if (textInputManager != nullptr && textInput == nullptr) {
textInput = zwp_text_input_manager_v3_get_text_input(textInputManager, seat);
zwp_text_input_v3_add_listener(textInput, &text_input_listener, nullptr);
}
} 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));
} else if (strcmp(interface, wl_data_device_manager_interface.name) == 0) {
// v3 gives us the full set of data_source events (target / send /
// cancelled / dnd_*). Universally supported by the compositors
// we target — fall back path is the per-source listener simply
// not getting the v3-only callbacks.
dataDeviceManager = reinterpret_cast<wl_data_device_manager*>(
wl_registry_bind(registry, name, &wl_data_device_manager_interface, 3));
if (seat != nullptr && dataDevice == nullptr) {
dataDevice = wl_data_device_manager_get_data_device(dataDeviceManager, seat);
}
} else if (strcmp(interface, zwp_text_input_manager_v3_interface.name) == 0) {
textInputManager = reinterpret_cast<zwp_text_input_manager_v3*>(wl_registry_bind(registry, name, &zwp_text_input_manager_v3_interface, 1));
if (seat != nullptr && textInput == nullptr) {
textInput = zwp_text_input_manager_v3_get_text_input(textInputManager, seat);
zwp_text_input_v3_add_listener(textInput, &text_input_listener, nullptr);
}
}
}
void Device::text_input_enter(void*, zwp_text_input_v3*, wl_surface* surface) {
textInputWindow = nullptr;
for (Window* window : windows) {
if (window->surface == surface) textInputWindow = window;
}
if (textInputWindow != nullptr && textInputWindow->textInputActive_) ApplyTextInput(textInputWindow);
}
void Device::text_input_leave(void*, zwp_text_input_v3*, wl_surface*) {
textInputWindow = nullptr;
textInputHasPreedit = textInputHasCommit = textInputHasDelete = false;
textInputPreedit.clear();
textInputShownPreedit.clear();
}
void Device::text_input_preedit(void*, zwp_text_input_v3*, const char* text, std::int32_t, std::int32_t) {
textInputPreedit = text == nullptr ? "" : text;
textInputHasPreedit = true;
}
void Device::text_input_commit(void*, zwp_text_input_v3*, const char* text) {
textInputCommit = text == nullptr ? "" : text;
textInputHasCommit = true;
}
void Device::text_input_delete(void*, zwp_text_input_v3*, std::uint32_t before, std::uint32_t after) {
textInputDeleteBefore = before;
textInputDeleteAfter = after;
textInputHasDelete = true;
}
// The protocol's order: the deletion around the cursor, then the committed
// text at the cursor, then the composing text after it. The pending values
// reset with every done, so a round that set no preedit means an empty one;
// the window hears about it only when that differs from what it shows.
void Device::text_input_done(void*, zwp_text_input_v3*, std::uint32_t) {
Window* window = textInputWindow;
if (!textInputHasPreedit) textInputPreedit.clear();
if (window != nullptr) {
if (textInputHasDelete && (textInputDeleteBefore > 0 || textInputDeleteAfter > 0)) window->onTextInputDelete.Invoke({ textInputDeleteBefore, textInputDeleteAfter });
if (textInputHasCommit && !textInputCommit.empty()) window->onTextInput.Invoke(textInputCommit);
if (textInputPreedit != textInputShownPreedit || (textInputHasCommit && !textInputCommit.empty())) {
textInputShownPreedit = textInputPreedit;
window->onPreedit.Invoke(textInputPreedit);
}
}
textInputHasPreedit = textInputHasCommit = textInputHasDelete = false;
textInputCommit.clear();
}
void Device::ApplyTextInput(Window* window) {
if (textInput == nullptr || window == nullptr || textInputWindow != window) return;
if (window->textInputActive_) {
zwp_text_input_v3_enable(textInput);
const bool phone = window->textInputPurpose_ == TextInputPurpose::Phone;
zwp_text_input_v3_set_content_type(textInput,
phone ? ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE : (ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION | ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK | ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION),
phone ? ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE : ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL);
const float s = window->scale > 0.0f ? window->scale : 1.0f;
zwp_text_input_v3_set_cursor_rectangle(textInput,
static_cast<std::int32_t>(window->textInputRect_[0] / s), static_cast<std::int32_t>(window->textInputRect_[1] / s),
std::max(1, static_cast<std::int32_t>(window->textInputRect_[2] / s)), std::max(1, static_cast<std::int32_t>(window->textInputRect_[3] / s)));
} else {
zwp_text_input_v3_disable(textInput);
}
zwp_text_input_v3_commit(textInput);
wl_display_flush(display);
}
void Device::handle_global_remove(void* data, wl_registry* registry, uint32_t name) {
}
void Device::pointer_handle_button(void* data, wl_pointer* pointer, std::uint32_t serial, std::uint32_t time, std::uint32_t button, std::uint32_t state) {
// Defensive: compositors can deliver pointer events without (or after)
// a matching enter — observed on sway during input-device hot-plug.
if (focusedWindow == nullptr) {
return;
}
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 {
Device::focusedWindow->mouseRightHeld = false;
Device::focusedWindow->onMouseRightRelease.Invoke();
}
}
}
void Device::PointerListenerHandleMotion(void* data, wl_pointer* wl_pointer, std::uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) {
if (focusedWindow == nullptr) {
return;
}
Vector<float, 2> pos(wl_fixed_to_double(surface_x), wl_fixed_to_double(surface_y));
//Device::focusedWindow->lastMousePos = Device::focusedWindow->currentMousePos;
Device::focusedWindow->currentMousePos = pos * Device::focusedWindow->scale;
//Device::focusedWindow->mouseDelta = {Device::focusedWindow->currentMousePos.x-Device::focusedWindow->lastMousePos.x, Device::focusedWindow->currentMousePos.y-Device::focusedWindow->lastMousePos.y};
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) {
Device::wlPointer = wl_pointer;
for(Window* window : windows) {
if(window->surface == surface) {
window->lastPointerSerial_ = serial;
if(window->cursorSurface != nullptr) {
wl_pointer_set_cursor(wl_pointer, serial, window->cursorSurface,
window->cursorHotspotX_, window->cursorHotspotY_);
}
focusedWindow = window;
window->onMouseEnter.Invoke();
return;
}
}
}
void Device::PointerListenerHandleLeave(void* data, wl_pointer*, std::uint32_t, wl_surface*) {
// Sway has been observed to send a second leave after focus is already
// gone (input-device hot-plug) — don't crash on it.
if (focusedWindow != nullptr) {
focusedWindow->onMouseLeave.Invoke();
focusedWindow = nullptr;
}
// Drop any sub-detent scroll remainder so it can't leak into whatever
// window the pointer enters next.
scrollDetentRemainder = 0.0;
}
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)));
}
}
void Device::keyboard_keymap(void *data, wl_keyboard *keyboard, uint32_t format, int fd, uint32_t size) {
if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
close(fd);
fprintf(stderr, "Unsupported keymap format\n");
return;
}
void *map = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
close(fd);
perror("mmap");
return;
}
xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
xkb_keymap = xkb_keymap_new_from_string(xkb_context, (const char *)map, XKB_KEYMAP_FORMAT_TEXT_V1,XKB_KEYMAP_COMPILE_NO_FLAGS);
munmap(map, size);
close(fd);
xkb_state = xkb_state_new(xkb_keymap);
}
void Device::keyboard_enter(void *data, wl_keyboard *keyboard, uint32_t serial, wl_surface *surface, wl_array *keys) {
for (Window* window : windows) {
if (window->surface == surface) {
keyboardWindow = window;
return;
}
}
keyboardWindow = nullptr;
}
void Device::keyboard_leave(void *data, wl_keyboard *keyboard, uint32_t serial, wl_surface *surface) {
// Releases happen elsewhere while the focus is away: nothing we still
// hold is held, and a repeat must not keep firing into a window that
// lost the keyboard.
if (keyboardWindow != nullptr) keyboardWindow->heldKeys.clear();
keyboardWindow = nullptr;
keyRepeat.active = false;
keyRepeat.utf8.clear();
}
void Device::keyboard_key(void *data, wl_keyboard *keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) {
// `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;
// 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;
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
if (keyboardWindow->heldKeys.contains(code)) {
keyboardWindow->onRawKeyHold.Invoke(code);
} else {
keyboardWindow->heldKeys.insert(code);
keyboardWindow->onRawKeyDown.Invoke(code);
}
std::string buf;
buf.resize(16);
int n = xkb_state_key_get_utf8(xkb_state, xkbKeycode, buf.data(), 16);
std::string utf8;
if (n > 0 && (unsigned char)buf[0] >= 0x20 && buf[0] != 0x7f) {
buf.resize(n);
utf8 = buf;
keyboardWindow->onTextInput.Invoke(utf8);
}
// 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);
keyRepeat.key = code;
keyRepeat.utf8 = std::move(utf8);
keyRepeat.pressTime = std::chrono::steady_clock::now();
keyRepeat.lastFireTime = keyRepeat.pressTime;
} else {
keyboardWindow->heldKeys.erase(code);
keyboardWindow->onRawKeyUp.Invoke(code);
// If the released key was the one repeating, stop. Otherwise leave
// the existing repeat alone (user pressed/released a modifier
// mid-repeat etc.).
if (keyRepeat.active && keyRepeat.key == code) {
keyRepeat.active = false;
keyRepeat.utf8.clear();
}
}
}
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) {
xkb_state_update_mask(xkb_state, mods_depressed, mods_latched, mods_locked, 0, 0, group);
}
void Device::keyboard_repeat_info(void *data, wl_keyboard *keyboard, int32_t rate, int32_t delay) {
keyRepeat.rate = rate;
keyRepeat.delay = delay;
if (rate <= 0) keyRepeat.active = false; // compositor disabled repeat
}
void Device::TickKeyRepeats() {
if (!keyRepeat.active || !keyboardWindow) return;
if (keyRepeat.rate <= 0) return;
auto now = std::chrono::steady_clock::now();
using ms = std::chrono::milliseconds;
auto sincePress = std::chrono::duration_cast<ms>(now - keyRepeat.pressTime).count();
if (sincePress < keyRepeat.delay) return;
auto period = std::chrono::milliseconds(1000 / keyRepeat.rate);
auto sinceLastFire = std::chrono::duration_cast<ms>(now - keyRepeat.lastFireTime).count();
if (sinceLastFire < period.count()) return;
// Catch up — emit one event per missed period so a paused frame doesn't
// make the repeat permanently lag behind.
while (now - keyRepeat.lastFireTime >= period) {
keyboardWindow->onRawKeyDown.Invoke(keyRepeat.key);
keyboardWindow->onRawKeyHold.Invoke(keyRepeat.key);
if (!keyRepeat.utf8.empty()) {
keyboardWindow->onTextInput.Invoke(keyRepeat.utf8);
}
keyRepeat.lastFireTime += period;
}
}
// 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();
}
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);
}
if ((capabilities & WL_SEAT_CAPABILITY_TOUCH) && wlTouch == nullptr) {
wlTouch = wl_seat_get_touch(seat);
wl_touch_add_listener(wlTouch, &touch_listener, nullptr);
}
if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) {
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, &registry_listener, nullptr);
if (wl_display_roundtrip(display) == -1) {
exit(EXIT_FAILURE);
}
if (shm == NULL || compositor == NULL || xdgWmBase == NULL) {
throw std::runtime_error("No wl_shm, wl_compositor or xdg_wm_base support");
}
// 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();
#endif
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
app.pApplicationName = "";
app.pEngineName = "Crafter.Graphics";
app.apiVersion = VK_MAKE_VERSION(1, 4, 0);
// GPU-Assisted Validation: shader-execution-time instrumentation that
// catches out-of-bounds descriptor / buffer access the standard layer
// can't see. Opt-in via the enable list below.
//
// 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`:
// https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/12103
// Per spencer-lunarg (LunarG) that was fixed in the next SDK release.
// The validation layer is now 1.4.350 (> 1.4.341), so re-enable it.
// Validation (incl. GPU-Assisted Validation) is a DEVELOPER tool and is OFF
// by default: GPU-AV instruments shaders and has crashed vendor SPIR-V
// compilers (observed: NVIDIA libnvidia-glvkspirv segfault on the
// descriptor-heap compute pipelines). Opt in by setting the environment
// variable CRAFTER_GRAPHICS_VALIDATION.
bool enableValidation = std::getenv("CRAFTER_GRAPHICS_VALIDATION") != nullptr;
VkValidationFeatureEnableEXT enabledValidationFeatures[] = {
VK_VALIDATION_FEATURE_ENABLE_GPU_ASSISTED_EXT,
};
VkValidationFeaturesEXT validationFeatures = {
.sType = VK_STRUCTURE_TYPE_VALIDATION_FEATURES_EXT,
.enabledValidationFeatureCount = sizeof(enabledValidationFeatures) / sizeof(enabledValidationFeatures[0]),
.pEnabledValidationFeatures = enabledValidationFeatures,
};
VkInstanceCreateInfo instanceCreateInfo = {};
instanceCreateInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instanceCreateInfo.pNext = enableValidation ? (const void*)&validationFeatures : nullptr;
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++;
}
}
}
if (enableValidation && foundInstanceLayers >= sizeof(layerNames) / sizeof(const char*))
{
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];
uint32_t score;
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;
}
}
// 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;
{
std::uint32_t extCount = 0;
vkEnumerateDeviceExtensionProperties(physDevice, nullptr, &extCount, nullptr);
std::vector<VkExtensionProperties> exts(extCount);
vkEnumerateDeviceExtensionProperties(physDevice, nullptr, &extCount, exts.data());
for (const VkExtensionProperties& e : exts) {
availableExtensions.emplace(e.extensionName);
}
}
const auto hasAll = [&availableExtensions](std::span<const char* const> names) {
return std::ranges::all_of(names, [&](const char* name) { return availableExtensions.contains(name); });
};
// Fail here, naming the gap, rather than in vkCreateDevice with a bare
// VK_ERROR_EXTENSION_NOT_PRESENT.
{
std::string missing;
for (const char* name : requiredDeviceExtensionNames) {
if (!availableExtensions.contains(name)) {
missing += ' ';
missing += name;
}
}
if (!missing.empty()) {
throw std::runtime_error(std::format("Crafter.Graphics: the Vulkan driver lacks required device extension(s):{}", missing));
}
}
// CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the descriptor-set path on a
// driver that does have the heap, so the fallback can be exercised on a
// desktop GPU. It takes ray tracing down with it, exactly as on a driver
// that really lacks the extension.
//
// A listed extension is not the whole answer: a driver may list one whose
// feature bit is still off. Query the feature bits of the optional groups
// whose extensions are present (only those structs are chained) and
// require both, so such a driver takes the fallback instead of failing
// in vkCreateDevice.
const bool heapExtensions = hasAll(descriptorHeapExtensionNames);
const bool rayTracingExtensions = hasAll(rayTracingExtensionNames);
const bool faultExtension = availableExtensions.contains(VK_EXT_DEVICE_FAULT_EXTENSION_NAME);
VkPhysicalDeviceDescriptorHeapFeaturesEXT heapQuery {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_HEAP_FEATURES_EXT,
};
VkPhysicalDeviceShaderUntypedPointersFeaturesKHR untypedPointersQuery {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_UNTYPED_POINTERS_FEATURES_KHR,
};
VkPhysicalDeviceAccelerationStructureFeaturesKHR accelerationStructureQuery {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_FEATURES_KHR,
};
VkPhysicalDeviceRayTracingPipelineFeaturesKHR rayTracingPipelineQuery {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_PIPELINE_FEATURES_KHR,
};
VkPhysicalDeviceRayQueryFeaturesKHR rayQueryQuery {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_QUERY_FEATURES_KHR,
};
VkPhysicalDeviceFaultFeaturesEXT faultQuery {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT,
};
VkPhysicalDeviceFeatures2 featureQuery {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
};
const auto ask = [&featureQuery](auto& features) {
features.pNext = featureQuery.pNext;
featureQuery.pNext = &features;
};
if (heapExtensions) {
ask(heapQuery);
ask(untypedPointersQuery);
}
if (rayTracingExtensions) {
ask(accelerationStructureQuery);
ask(rayTracingPipelineQuery);
ask(rayQueryQuery);
}
if (faultExtension) {
ask(faultQuery);
}
vkGetPhysicalDeviceFeatures2(physDevice, &featureQuery);
descriptorHeapSupported = heapExtensions
&& heapQuery.descriptorHeap && untypedPointersQuery.shaderUntypedPointers
&& std::getenv("CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP") == nullptr;
rayTracingSupported = descriptorHeapSupported && rayTracingExtensions
&& accelerationStructureQuery.accelerationStructure
&& rayTracingPipelineQuery.rayTracingPipeline
&& rayQueryQuery.rayQuery;
deviceFaultSupported = faultExtension && faultQuery.deviceFault;
memoryDecompressionSupported = availableExtensions.contains(VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME);
const bool atomicFloatSupported = availableExtensions.contains(VK_EXT_SHADER_ATOMIC_FLOAT_EXTENSION_NAME);
// RAY_TRACING_SHADER is not a valid pipeline stage unless the ray tracing
// pipeline feature is enabled; barrier code masks its stage flags with this.
stageMask = rayTracingSupported
? ~VkPipelineStageFlags{0}
: ~VkPipelineStageFlags{VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR};
// Properties query, built back to front. A struct is chained only when its
// extension is being used: an unrecognized sType is a validation error on
// drivers that don't expose the extension, and the heap properties must
// stay zero when the heap is forced off. Driver properties are core and
// always queried; they identify the NVIDIA proprietary driver for the
// descriptor-heap AS-read workaround (issue #15 / #7).
void* propertiesChain = &driverProperties;
if (descriptorHeapSupported) {
descriptorHeapProperties.pNext = propertiesChain;
propertiesChain = &descriptorHeapProperties;
}
if (rayTracingSupported) {
rayTracingProperties.pNext = propertiesChain;
propertiesChain = &rayTracingProperties;
}
if (memoryDecompressionSupported) {
memoryDecompressionProperties.pNext = propertiesChain;
propertiesChain = &memoryDecompressionProperties;
}
VkPhysicalDeviceProperties2 properties2 {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
.pNext = propertiesChain
};
vkGetPhysicalDeviceProperties2(physDevice, &properties2);
// Keep the core properties around for the pipeline-cache identity check
// (vendorID / deviceID / pipelineCacheUUID).
deviceProperties = properties2.properties;
// Cache the flush/invalidate alignment for ranged host-memory flushes
// (VulkanBuffer::FlushDevice(offset,bytes)).
nonCoherentAtomSize = properties2.properties.limits.nonCoherentAtomSize;
// NVIDIA's brand-new VK_EXT_descriptor_heap acceleration-structure read
// path faults (see #7); enable the SPIR-V rewrite workaround there. Other
// drivers (and any future fixed NVIDIA driver, once this gate is removed)
// take the normal heap-bound AS path unchanged. The workaround is about
// the heap, so it is off whenever the heap is: otherwise a forced-fallback
// run on NVIDIA would still rewrite SPIR-V, enable shaderInt64 and push the
// TLAS address through vkCmdPushDataEXT, none of which a real heap-less
// driver does.
workaroundDescriptorHeapAS = descriptorHeapSupported
&& driverProperties.driverID == VK_DRIVER_ID_NVIDIA_PROPRIETARY;
// 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;
}
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;
// Feature structs below are declared without a pNext and linked into one
// chain afterwards, each only when its extension is being enabled: a
// feature struct for an extension that is not enabled is invalid, and
// which ones are enabled now depends on the driver.
VkPhysicalDeviceFaultFeaturesEXT faultFeatures = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT,
.deviceFault = VK_TRUE,
.deviceFaultVendorBinary = faultQuery.deviceFaultVendorBinary,
};
VkPhysicalDeviceShaderUntypedPointersFeaturesKHR untypedPointersFeatures {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_UNTYPED_POINTERS_FEATURES_KHR,
.shaderUntypedPointers = VK_TRUE,
};
// 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,
};
VkPhysicalDeviceDescriptorHeapFeaturesEXT desciptorHeapFeatures {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_HEAP_FEATURES_EXT,
.descriptorHeap = VK_TRUE,
};
// Every VulkanBuffer is created with usage = 0 plus a
// VkBufferUsageFlags2CreateInfo, and heap pipelines carry a
// VkPipelineCreateFlags2CreateInfo. Both are maintenance5 functionality,
// which needs the feature bit as well as the extension name.
VkPhysicalDeviceMaintenance5Features maintenance5Features {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES,
.maintenance5 = VK_TRUE,
};
VkPhysicalDevice16BitStorageFeatures bit16 {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES,
.storageBuffer16BitAccess = VK_TRUE,
};
VkPhysicalDeviceVulkan12Features features12 {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES,
.shaderFloat16 = VK_TRUE,
// Bindless / runtime descriptor array indexing — needed by both
// binding models (heap and descriptor sets).
.shaderUniformBufferArrayNonUniformIndexing = VK_TRUE,
.shaderSampledImageArrayNonUniformIndexing = VK_TRUE,
.shaderStorageBufferArrayNonUniformIndexing = VK_TRUE,
.shaderStorageImageArrayNonUniformIndexing = VK_TRUE,
.runtimeDescriptorArray = VK_TRUE,
.scalarBlockLayout = VK_TRUE,
.bufferDeviceAddress = VK_TRUE
};
if (!descriptorHeapSupported) {
// Descriptor-set path (DescriptorHeapVulkan sets mode): one bindless
// set whose slots are written while earlier frames may still be
// executing with it bound, and where most slots are never written.
// The sampled-image bit also covers SAMPLER descriptors.
features12.descriptorIndexing = VK_TRUE;
features12.descriptorBindingSampledImageUpdateAfterBind = VK_TRUE;
features12.descriptorBindingStorageImageUpdateAfterBind = VK_TRUE;
features12.descriptorBindingStorageBufferUpdateAfterBind = VK_TRUE;
features12.descriptorBindingUpdateUnusedWhilePending = VK_TRUE;
features12.descriptorBindingPartiallyBound = VK_TRUE;
}
VkPhysicalDeviceRayQueryFeaturesKHR physicalDeviceRayQueryFeatures{
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_QUERY_FEATURES_KHR,
.rayQuery = VK_TRUE
};
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
};
void* featureChain = nullptr;
const auto link = [&featureChain](auto& features) {
features.pNext = featureChain;
featureChain = &features;
};
if (deviceFaultSupported) {
link(faultFeatures);
}
if (descriptorHeapSupported) {
link(untypedPointersFeatures);
}
link(features13);
if (memoryDecompressionSupported) {
link(memoryDecompressionFeatures);
}
if (descriptorHeapSupported) {
link(desciptorHeapFeatures);
}
link(maintenance5Features);
link(bit16);
link(features12);
if (rayTracingSupported) {
link(physicalDeviceRayQueryFeatures);
link(physicalDeviceRayTracingPipelineFeatures);
link(deviceAccelerationStructureFeature);
}
VkPhysicalDeviceFeatures2 physical_features2 = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
.pNext = featureChain,
.features = {
// 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,
// 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,
.shaderInt16 = VK_TRUE
}
};
// The enabled-extension list is the required set plus each optional group
// the driver advertises in full (decided above from the enumeration).
std::vector<const char*> enabledDeviceExtensions(
std::begin(requiredDeviceExtensionNames),
std::end(requiredDeviceExtensionNames));
if (descriptorHeapSupported) {
enabledDeviceExtensions.append_range(descriptorHeapExtensionNames);
}
if (rayTracingSupported) {
enabledDeviceExtensions.append_range(rayTracingExtensionNames);
}
if (deviceFaultSupported) {
enabledDeviceExtensions.push_back(VK_EXT_DEVICE_FAULT_EXTENSION_NAME);
}
if (atomicFloatSupported) {
enabledDeviceExtensions.push_back(VK_EXT_SHADER_ATOMIC_FLOAT_EXTENSION_NAME);
}
if (memoryDecompressionSupported) {
enabledDeviceExtensions.push_back(VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME);
}
VkDeviceCreateInfo deviceCreateInfo = {};
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
deviceCreateInfo.queueCreateInfoCount = 1;
deviceCreateInfo.pQueueCreateInfos = &queueCreateInfo;
deviceCreateInfo.enabledExtensionCount = static_cast<std::uint32_t>(enabledDeviceExtensions.size());
deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data();
deviceCreateInfo.pNext = &physical_features2;
// Device layers are deprecated and have been ignored since Vulkan 1.0;
// enabling them is a validation error. Layers are enabled at instance
// creation only, so leave enabledLayerCount at 0.
deviceCreateInfo.enabledLayerCount = 0;
deviceCreateInfo.ppEnabledLayerNames = nullptr;
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);
CacheUploadStrategy();
vkGetAccelerationStructureBuildSizesKHR = reinterpret_cast<PFN_vkGetAccelerationStructureBuildSizesKHR>(vkGetInstanceProcAddr(instance, "vkGetAccelerationStructureBuildSizesKHR"));
vkCreateAccelerationStructureKHR = reinterpret_cast<PFN_vkCreateAccelerationStructureKHR>(vkGetInstanceProcAddr(instance, "vkCreateAccelerationStructureKHR"));
vkDestroyAccelerationStructureKHR = reinterpret_cast<PFN_vkDestroyAccelerationStructureKHR>(vkGetInstanceProcAddr(instance, "vkDestroyAccelerationStructureKHR"));
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"));
vkCmdBindResourceHeapEXT = reinterpret_cast<PFN_vkCmdBindResourceHeapEXT>(vkGetInstanceProcAddr(instance, "vkCmdBindResourceHeapEXT"));
vkCmdBindSamplerHeapEXT = reinterpret_cast<PFN_vkCmdBindSamplerHeapEXT>(vkGetInstanceProcAddr(instance, "vkCmdBindSamplerHeapEXT"));
vkWriteResourceDescriptorsEXT = reinterpret_cast<PFN_vkWriteResourceDescriptorsEXT>(vkGetInstanceProcAddr(instance, "vkWriteResourceDescriptorsEXT"));
vkWriteSamplerDescriptorsEXT = reinterpret_cast<PFN_vkWriteSamplerDescriptorsEXT>(vkGetInstanceProcAddr(instance, "vkWriteSamplerDescriptorsEXT"));
vkCmdPushDataEXT = reinterpret_cast<PFN_vkCmdPushDataEXT>(vkGetInstanceProcAddr(instance, "vkCmdPushDataEXT"));
vkGetPhysicalDeviceDescriptorSizeEXT = reinterpret_cast<PFN_vkGetPhysicalDeviceDescriptorSizeEXT>(vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceDescriptorSizeEXT"));
vkGetDeviceFaultInfoEXT = reinterpret_cast<PFN_vkGetDeviceFaultInfoEXT>(vkGetInstanceProcAddr(instance, "vkGetDeviceFaultInfoEXT"));
if (memoryDecompressionSupported) {
// vkGetDeviceProcAddr skips the loader trampoline that vkGetInstanceProcAddr
// requires for device-level functions. The other PFNs above predate this
// realization; opportunistic adoption for new entry points only.
vkCmdDecompressMemoryEXT = reinterpret_cast<PFN_vkCmdDecompressMemoryEXT>(
vkGetDeviceProcAddr(device, "vkCmdDecompressMemoryEXT"));
if (vkCmdDecompressMemoryEXT == nullptr) {
// Driver advertised the extension but didn't expose the entry
// point — defensively fall back to CPU decode.
memoryDecompressionSupported = false;
}
}
// Create the shared pipeline cache (seeded from disk when a compatible
// blob exists) and arrange for it to be written back at process exit. The
// device handle is a never-destroyed static, so it is still valid when the
// atexit handler runs. Registered once — Initialize is the single device
// bring-up.
LoadPipelineCache();
std::atexit(SavePipelineCache);
}
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})
{
uint32_t bits = typeBits;
for (uint32_t i = 0; i < memoryProperties.memoryTypeCount; i++)
{
if ((bits & 1) == 1)
{
if ((memoryProperties.memoryTypes[i].propertyFlags & wanted) == wanted)
{
return i;
}
}
bits >>= 1;
}
// No preferences to fall back from — the second pass would be identical.
if (preferred == 0) {
break;
}
}
throw std::runtime_error("Could not find a matching memory type");
}
void Device::CacheUploadStrategy() {
constexpr VkMemoryPropertyFlags directFlags =
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT;
// 1. Find a DEVICE_LOCAL | HOST_VISIBLE type and remember its heap. Without
// one (no resizable BAR), direct writes are impossible -> must stage.
bool haveDirectType = false;
std::uint32_t barHeapIndex = 0;
for (std::uint32_t i = 0; i < memoryProperties.memoryTypeCount; ++i) {
if ((memoryProperties.memoryTypes[i].propertyFlags & directFlags) == directFlags) {
haveDirectType = true;
barHeapIndex = memoryProperties.memoryTypes[i].heapIndex;
break;
}
}
if (!haveDirectType) {
directWriteBudget = 0;
return;
}
// 2. Compare the host-visible device-local heap against the largest plain
// DEVICE_LOCAL heap (the true VRAM size).
VkDeviceSize barHeap = memoryProperties.memoryHeaps[barHeapIndex].size;
VkDeviceSize vram = 0;
for (std::uint32_t i = 0; i < memoryProperties.memoryHeapCount; ++i) {
if (memoryProperties.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) {
vram = std::max(vram, memoryProperties.memoryHeaps[i].size);
}
}
// 3. ReBAR / UMA: the BAR heap covers (almost) all of VRAM -> map any size.
// barHeap >= 0.9 * vram, written to dodge floating point / overflow.
if (barHeap >= vram - vram / 10) {
directWriteBudget = std::numeric_limits<VkDeviceSize>::max();
return;
}
// 4. Small BAR window: direct only for buffers small enough that several
// still fit the window — large buffers stage so they don't exhaust it.
// The budget is tied to the actual window size rather than a magic
// constant so it adapts across devices (a fixed cap would be wrong for
// both a 256 MiB and a 4 GiB window). VK_EXT_memory_budget would refine
// this to the window's *remaining* space; see the header note.
constexpr VkDeviceSize barWindowFraction = 8;
directWriteBudget = barHeap / barWindowFraction;
}
bool Device::PreferDirectDeviceWrite(VkDeviceSize size) {
// directWriteBudget caches the size-independent decision (CacheUploadStrategy):
// 0 -> never direct, max -> ReBAR/UMA so any size, else the small-window cap.
return directWriteBudget != 0 && size <= directWriteBudget;
}
void Device::EnqueueDeletion(VkBuffer buffer, VkDeviceMemory memory) {
// Nothing to free for an already-null handle — avoids parking dead
// entries that ReclaimDeletions would have to skip over.
if (buffer == VK_NULL_HANDLE) return;
deletionQueue.push_back({ frameCounter, buffer, memory });
}
void Device::ReclaimDeletions() {
// Free every entry whose retire frame has been reached. An entry tagged at
// frame F retires at F + framesInFlight: by the time the CPU has begun that
// many later frames (each gated by a per-image fence wait), single-queue
// submission order guarantees all GPU work from frame F is complete.
//
// frameCounter is monotonic, so retireAfter is non-decreasing down the
// queue and the ready entries are a contiguous prefix: pop them off the
// front and stop at the first one still in flight (everything behind it is
// newer, hence also in flight). O(ready), not O(queue).
while (!deletionQueue.empty() &&
deletionQueue.front().retireAfter + framesInFlight <= frameCounter) {
const PendingDeletion& entry = deletionQueue.front();
vkDestroyBuffer(device, entry.buffer, nullptr);
vkFreeMemory(device, entry.memory, nullptr);
deletionQueue.pop_front();
}
}
void Device::DrainDeletions() {
// Unconditional: the caller has just wait-idled, so no in-flight GPU work
// can still reference any queued resource.
for (PendingDeletion& entry : deletionQueue) {
vkDestroyBuffer(device, entry.buffer, nullptr);
vkFreeMemory(device, entry.memory, nullptr);
}
deletionQueue.clear();
}