Crafter.Graphics/implementations/Crafter.Graphics-Device.cpp
Jorijn van der Graaf 3f646ec42b
fix(wayland): the first key repeat lands at the delay, not as a burst of the periods since the press
keyboard_key set lastFireTime to the press and TickKeyRepeats caught up on
every period since it, so the first tick past the compositor's delay fired
floor(delay / period) repeats at once: with rate 25 and delay 600 a held
Backspace deleted sixteen characters at the delay mark and a held letter
put fifteen copies in the field, from a hardware keyboard and from an
on-screen keyboard that sends Backspace as key events. A rate above 1000
made the period zero and the loop endless.

The first repeat now lands at the delay, as wl_keyboard.repeat_info
promises (one after `delay`, then `rate` a second): the first tick past
the delay sets lastFireTime to pressTime + delay - period. The period is
at least a millisecond, and the catch-up after a stalled frame is capped
(kKeyRepeatCatchUp, three) with the rest of the stall dropped, so a held
key is never a burst of edits. TickKeyRepeats takes the clock as a
parameter for the tests; the frame path's call is unchanged.

tests/KeyRepeat drives TickKeyRepeats with an injected clock on a
default-constructed Window: nothing before the delay, one at it, one per
period after, a capped catch-up, a key without text, a rate above 1000, a
released key, rate 0.
2026-11-01 00:53:56 +01:00

1729 lines
No EOL
80 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
};
namespace {
// The surface_maintenance1 Initialize enabled on the instance, the KHR
// form or the EXT one, or null. A device's swapchain_maintenance1 has to
// be the same form: each requires its own.
const char* instanceSurfaceMaintenance1 = nullptr;
}
// Device extensions are split by what breaks without them. Only the first
// group is needed to put UI pixels on screen, and a GPU without it is skipped,
// as is one below Vulkan 1.4 without VK_KHR_maintenance5 (core from 1.4).
// VK_KHR_spirv_1_4 and VK_KHR_shader_float_controls, which ray tracing needs
// on Vulkan 1.1, are core since 1.2. Device::Initialize enables each of the
// other groups only when the driver advertises all of its members and their
// feature bits, and records the outcome in Device::descriptorHeapSupported /
// rayTracingSupported / deviceFaultSupported / memoryDecompressionSupported.
// Mesa Turnip (Adreno), for one, has none of the heap, ray tracing or fault
// extensions.
const char* const requiredDeviceExtensionNames[] = {
"VK_KHR_swapchain",
};
// 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);
}
}
}
namespace {
// A finger's slot by id, or a free one for a new finger; nullptr with more
// than ten fingers down.
Crafter::Device::TouchSlot* SlotFor(std::int32_t id) {
Crafter::Device::TouchSlot* free = nullptr;
for (Crafter::Device::TouchSlot& slot : Crafter::Device::touchSlots) {
if (slot.used && slot.id == id) return &slot;
if (!slot.used && free == nullptr) free = &slot;
}
if (free != nullptr) {
free->used = true;
free->id = id;
free->window = nullptr;
}
return free;
}
bool AnyTouch() {
for (const Crafter::Device::TouchSlot& slot : Crafter::Device::touchSlots) {
if (slot.used) return true;
}
return false;
}
// The text input's pending strings live here, as function-local statics:
// an inline static std::string in the interface made clang 23.1 crash
// in ThinLTO for aarch64-alpine-linux-musl.
struct TextInputStrings {
std::string preedit;
std::string shown; // what the window was last told
std::string commit;
};
TextInputStrings& TextStrings() {
static TextInputStrings strings;
return strings;
}
}
void Device::text_input_enter(void*, zwp_text_input_v3*, wl_surface* surface) {
textInputWindow = nullptr;
for (Window* window : windows) {
if (window->surface == surface) textInputWindow = window;
}
if (textInputWindow != nullptr && textInputWindow->textInputActive_) ApplyTextInput(textInputWindow);
}
// The compositor's leave ends the enable and the composition: the window
// hears that its composing text is gone, and the next enter enables anew.
void Device::text_input_leave(void*, zwp_text_input_v3*, wl_surface*) {
Window* window = textInputWindow;
textInputWindow = nullptr;
textInputHasPreedit = textInputHasCommit = textInputHasDelete = false;
TextStrings().preedit.clear();
if (window != nullptr) {
window->textInputEnabled_ = false;
if (!TextStrings().shown.empty()) window->onPreedit.Invoke(std::string_view());
}
TextStrings().shown.clear();
}
void Device::text_input_preedit(void*, zwp_text_input_v3*, const char* text, std::int32_t, std::int32_t) {
TextStrings().preedit = text == nullptr ? "" : text;
textInputHasPreedit = true;
}
void Device::text_input_commit(void*, zwp_text_input_v3*, const char* text) {
TextStrings().commit = text == nullptr ? "" : text;
textInputHasCommit = true;
}
void Device::text_input_delete(void*, zwp_text_input_v3*, std::uint32_t before, std::uint32_t after) {
textInputDeleteBefore = before;
textInputDeleteAfter = after;
textInputHasDelete = true;
}
// The protocol's order: the deletion around the cursor, then the committed
// text at the cursor, then the composing text after it. The pending values
// reset with every done, so a round that set no preedit means an empty one;
// the window hears about it only when that differs from what it shows.
void Device::text_input_done(void*, zwp_text_input_v3*, std::uint32_t) {
Window* window = textInputWindow;
TextInputStrings& strings = TextStrings();
if (!textInputHasPreedit) strings.preedit.clear();
if (window != nullptr) {
if (textInputHasDelete && (textInputDeleteBefore > 0 || textInputDeleteAfter > 0)) window->onTextInputDelete.Invoke({ textInputDeleteBefore, textInputDeleteAfter });
if (textInputHasCommit && !strings.commit.empty()) window->onTextInput.Invoke(strings.commit);
if (strings.preedit != strings.shown || (textInputHasCommit && !strings.commit.empty())) {
strings.shown = strings.preedit;
window->onPreedit.Invoke(strings.preedit);
}
}
textInputHasPreedit = textInputHasCommit = textInputHasDelete = false;
strings.commit.clear();
}
// enable resets the input method's state, so it goes once per focus; a
// caret that moved sends its rectangle alone.
void Device::ApplyTextInput(Window* window) {
if (textInput == nullptr || window == nullptr || textInputWindow != window) return;
if (window->textInputActive_) {
if (!window->textInputEnabled_) zwp_text_input_v3_enable(textInput);
window->textInputEnabled_ = true;
const bool phone = window->textInputPurpose_ == TextInputPurpose::Phone;
zwp_text_input_v3_set_content_type(textInput,
phone ? ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE : (ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION | ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK | ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION),
phone ? ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE : ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL);
const float s = window->scale > 0.0f ? window->scale : 1.0f;
zwp_text_input_v3_set_cursor_rectangle(textInput,
static_cast<std::int32_t>(window->textInputRect_[0] / s), static_cast<std::int32_t>(window->textInputRect_[1] / s),
std::max(1, static_cast<std::int32_t>(window->textInputRect_[2] / s)), std::max(1, static_cast<std::int32_t>(window->textInputRect_[3] / s)));
} else {
if (!window->textInputEnabled_) return;
window->textInputEnabled_ = false;
zwp_text_input_v3_disable(textInput);
// The disable ends the composition as a leave does: the next enable
// starts afresh, so the record of what the window was last told goes
// with it (a composition equal to it after the enable is new).
textInputHasPreedit = textInputHasCommit = textInputHasDelete = false;
TextStrings().preedit.clear();
TextStrings().shown.clear();
}
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->PointerMove(pos * Device::focusedWindow->scale);
}
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;
const Vector<float, 2> pos = Vector<float, 2>(wl_fixed_to_double(surface_x), wl_fixed_to_double(surface_y)) * window->scale;
window->PointerEnter(pos);
// ~Window clears focusedWindow, should a listener destroy it.
if (focusedWindow == window) {
window->PointerMove(pos);
}
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->PointerLeave();
focusedWindow = nullptr;
}
}
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; smooth-scroll devices
// (touchpads) deliver many sub-detent events, which PointerScroll adds
// up instead of each truncating to zero.
constexpr double axisUnitsPerDetent = 15.0;
focusedWindow->PointerScroll(wl_fixed_to_double(value) / axisUnitsPerDetent);
}
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;
keyRepeat.fired = false;
} 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() {
TickKeyRepeats(std::chrono::steady_clock::now());
}
void Device::TickKeyRepeats(std::chrono::steady_clock::time_point now) {
if (!keyRepeat.active || !keyboardWindow) return;
if (keyRepeat.rate <= 0) return;
using ms = std::chrono::milliseconds;
const auto delay = ms(keyRepeat.delay);
if (now - keyRepeat.pressTime < delay) return;
// At least a millisecond: a compositor's rate above 1000 would make the
// period zero and the loop below endless.
const auto period = ms(std::max(1, 1000 / keyRepeat.rate));
// The first repeat lands at the delay — wl_keyboard.repeat_info's
// contract: one after `delay`, then `rate` a second — so the delay is not
// counted as periods missed.
if (!keyRepeat.fired) {
keyRepeat.lastFireTime = keyRepeat.pressTime + delay - period;
keyRepeat.fired = true;
}
if (now - keyRepeat.lastFireTime < period) return;
// Catch up — one event per missed period, so a paused frame doesn't make
// the repeat permanently lag behind — but a few at most: a long stall is
// not a burst of edits, the rest of it is dropped.
int fires = 0;
while (now - keyRepeat.lastFireTime >= period && fires < kKeyRepeatCatchUp) {
keyboardWindow->onRawKeyDown.Invoke(keyRepeat.key);
keyboardWindow->onRawKeyHold.Invoke(keyRepeat.key);
if (!keyRepeat.utf8.empty()) {
keyboardWindow->onTextInput.Invoke(keyRepeat.utf8);
}
keyRepeat.lastFireTime += period;
++fires;
}
if (now - keyRepeat.lastFireTime >= period) keyRepeat.lastFireTime = now;
}
void Device::ForgetTouches() {
for (TouchSlot& slot : touchSlots) slot = TouchSlot{};
}
// The first finger stands in for the pointer: it enters the window (so the
// clipboard has a serial and Device::focusedWindow a window), presses the
// left button, moves it and releases it. Every finger is also reported as
// such, in physical pixels.
void Device::touch_down(void*, wl_touch*, std::uint32_t serial, std::uint32_t, wl_surface* surface, std::int32_t id, wl_fixed_t x, wl_fixed_t y) {
Window* window = nullptr;
for (Window* candidate : windows) {
if (candidate->surface == surface) window = candidate;
}
if (window == nullptr) return;
touchWindow = window;
window->lastPointerSerial_ = serial;
const Vector<float, 2> pos = Vector<float, 2>(wl_fixed_to_double(x), wl_fixed_to_double(y)) * window->scale;
if (TouchSlot* slot = SlotFor(id)) {
slot->x = pos.x;
slot->y = pos.y;
slot->window = window;
}
window->onTouchDown.Invoke({ id, pos });
if (window->primaryTouchId < 0) {
window->primaryTouchId = id;
focusedWindow = window;
window->currentMousePos = pos;
window->mouseLeftHeld = true;
window->onMouseLeftClick.Invoke();
}
}
void Device::touch_up(void*, wl_touch*, std::uint32_t, std::uint32_t, std::int32_t id) {
if (touchWindow == nullptr) return;
Window* window = touchWindow;
Vector<float, 2> pos = window->currentMousePos;
if (TouchSlot* slot = SlotFor(id)) {
if (slot->window != nullptr) window = slot->window;
pos = Vector<float, 2>(slot->x, slot->y);
*slot = TouchSlot{};
}
window->onTouchUp.Invoke({ id, pos });
if (id == window->primaryTouchId) {
window->primaryTouchId = -1;
window->mouseLeftHeld = false;
window->onMouseLeftRelease.Invoke();
}
if (!AnyTouch()) touchWindow = nullptr;
}
void Device::touch_motion(void*, wl_touch*, std::uint32_t, std::int32_t id, wl_fixed_t x, wl_fixed_t y) {
if (touchWindow == nullptr) return;
Window* window = touchWindow;
TouchSlot* slot = SlotFor(id);
if (slot != nullptr && slot->window != nullptr) window = slot->window;
const Vector<float, 2> pos = Vector<float, 2>(wl_fixed_to_double(x), wl_fixed_to_double(y)) * window->scale;
if (slot != nullptr) {
slot->x = pos.x;
slot->y = pos.y;
}
window->onTouchMove.Invoke({ id, pos });
if (id == window->primaryTouchId) {
window->currentMousePos = pos;
window->onMouseMove.Invoke();
}
}
void Device::touch_frame(void*, wl_touch*) {}
void Device::touch_cancel(void*, wl_touch*) {
if (touchWindow == nullptr) return;
Window* window = touchWindow;
ForgetTouches();
touchWindow = nullptr;
if (window->primaryTouchId >= 0) {
window->primaryTouchId = -1;
window->mouseLeftHeld = false;
window->onMouseLeftRelease.Invoke();
}
window->onTouchCancel.Invoke();
}
// Capabilities come again when a device appears or goes; each object is
// created once, or every event would arrive once per copy.
void Device::seat_handle_capabilities(void*, wl_seat* seat, uint32_t capabilities) {
if ((capabilities & WL_SEAT_CAPABILITY_POINTER) && seatPointer == nullptr) {
seatPointer = wl_seat_get_pointer(seat);
wl_pointer_add_listener(seatPointer, &pointer_listener, nullptr);
}
if ((capabilities & WL_SEAT_CAPABILITY_TOUCH) && wlTouch == nullptr) {
wlTouch = wl_seat_get_touch(seat);
wl_touch_add_listener(wlTouch, &touch_listener, nullptr);
}
if ((capabilities & WL_SEAT_CAPABILITY_KEYBOARD) && seatKeyboard == nullptr) {
seatKeyboard = wl_seat_get_keyboard(seat);
wl_keyboard_add_listener(seatKeyboard, &keyboard_listener, nullptr);
}
}
#endif
std::vector<std::vector<VkPhysicalDevice>> Device::DistinctGpus(VkInstance instance) {
std::uint32_t physDeviceCount = 0;
CheckVkResult(vkEnumeratePhysicalDevices(instance, &physDeviceCount, nullptr));
std::vector<VkPhysicalDevice> physDevices(physDeviceCount);
CheckVkResult(vkEnumeratePhysicalDevices(instance, &physDeviceCount, physDevices.data()));
std::vector<std::vector<VkPhysicalDevice>> gpus;
std::vector<std::array<std::uint8_t, VK_UUID_SIZE>> seen;
for (VkPhysicalDevice device : physDevices) {
VkPhysicalDeviceIDProperties id { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES };
VkPhysicalDeviceProperties2 properties { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &id };
vkGetPhysicalDeviceProperties2(device, &properties);
std::array<std::uint8_t, VK_UUID_SIZE> uuid;
std::ranges::copy(id.deviceUUID, uuid.begin());
const auto at = std::ranges::find(seen, uuid);
if (at == seen.end()) {
seen.push_back(uuid);
gpus.push_back({ device });
} else {
gpus[at - seen.begin()].push_back(device);
}
}
return gpus;
}
namespace {
// What Initialize learns about one GPU before choosing it.
struct Candidate {
VkPhysicalDevice device = VK_NULL_HANDLE;
VkPhysicalDeviceProperties properties {};
// min(the instance's apiVersion, the GPU's): the version the
// application may use on it.
std::uint32_t apiVersion = 0;
// "NVIDIA GeForce RTX 4090 (NVIDIA, Vulkan 1.4)", as messages name it.
// The driver tells apart one GPU that two drivers both list.
std::string label;
VkPhysicalDeviceVulkan11Properties vulkan11 { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_PROPERTIES };
// The capability groups: heap, rayTracing, fault and decompression
// become descriptorHeapSupported, rayTracingSupported,
// deviceFaultSupported and memoryDecompressionSupported, and
// workaroundAS workaroundDescriptorHeapAS.
bool heap = false;
bool rayTracing = false;
bool fault = false;
bool faultVendorBinary = false;
bool decompression = false;
bool workaroundAS = false;
// VK_KHR_swapchain_maintenance1 or VK_EXT_swapchain_maintenance1,
// whichever matches the instance's surface_maintenance1, with its
// feature bit; null without. Becomes swapchainMaintenance1Supported.
const char* swapchainMaintenance1 = nullptr;
// The extensions every device needs, maintenance5 among them below
// Vulkan 1.4. Initialize adds the groups'.
std::vector<const char*> requiredExtensions;
DeviceFeatures supported;
DeviceFeatures enabled;
// Why it can't run the application. Empty when it can.
std::vector<std::string> gaps;
};
Candidate EvaluateDevice(VkPhysicalDevice physical, std::uint32_t instanceVersion,
const DeviceFeatures& required, const DeviceFeatures& optional) {
Candidate candidate { .device = physical };
vkGetPhysicalDeviceProperties(physical, &candidate.properties);
candidate.apiVersion = std::min(instanceVersion, candidate.properties.apiVersion);
const std::uint32_t major = VK_API_VERSION_MAJOR(candidate.apiVersion);
const std::uint32_t minor = VK_API_VERSION_MINOR(candidate.apiVersion);
// Below 1.3 there is nothing more to ask: a core feature struct is not
// legal in a query below its version.
if (candidate.apiVersion < VK_API_VERSION_1_3) {
candidate.label = std::format("{} (Vulkan {}.{})", candidate.properties.deviceName, major, minor);
candidate.gaps.push_back("Crafter.Graphics needs Vulkan 1.3");
return candidate;
}
const bool vulkan14 = candidate.apiVersion >= VK_API_VERSION_1_4;
// The driver identifies NVIDIA's, for the descriptor-heap AS-read
// workaround; Vulkan 1.1's properties hold the subgroup operations.
VkPhysicalDeviceDriverProperties driver {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES,
.pNext = &candidate.vulkan11,
};
VkPhysicalDeviceProperties2 properties2 { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &driver };
vkGetPhysicalDeviceProperties2(physical, &properties2);
candidate.label = std::format("{} ({}, Vulkan {}.{})", candidate.properties.deviceName, driver.driverName, major, minor);
// 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(physical, nullptr, &extCount, nullptr);
std::vector<VkExtensionProperties> exts(extCount);
vkEnumerateDeviceExtensionProperties(physical, 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); });
};
// Name the gap here rather than fail in vkCreateDevice with a bare
// VK_ERROR_EXTENSION_NOT_PRESENT. maintenance5 is core from 1.4.
candidate.requiredExtensions.assign(std::begin(requiredDeviceExtensionNames), std::end(requiredDeviceExtensionNames));
if (!vulkan14) {
candidate.requiredExtensions.push_back(VK_KHR_MAINTENANCE_5_EXTENSION_NAME);
}
std::vector<std::string> missing;
for (const char* name : candidate.requiredExtensions) {
if (!availableExtensions.contains(name)) {
missing.emplace_back(name);
}
}
if (!missing.empty()) {
candidate.gaps.push_back(std::format("lacks {}", DeviceDetail::Join(missing, ", ")));
return candidate;
}
// One features query: the core structs for what the library and the
// application need, and the feature bits of the optional groups whose
// extensions are present. A listed extension is not the whole answer:
// a driver may list one whose feature bit is still off, and 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);
const bool decompressionExtension = availableExtensions.contains(VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME);
const char* swapchainMaintenance1Extension =
instanceSurfaceMaintenance1 == nullptr ? nullptr
: std::string_view(instanceSurfaceMaintenance1) == VK_KHR_SURFACE_MAINTENANCE_1_EXTENSION_NAME
? VK_KHR_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME
: VK_EXT_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME;
if (swapchainMaintenance1Extension != nullptr && !availableExtensions.contains(swapchainMaintenance1Extension)) {
swapchainMaintenance1Extension = nullptr;
}
DeviceFeatures& supported = candidate.supported;
VkPhysicalDeviceMaintenance5Features maintenance5Query {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES,
};
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,
};
VkPhysicalDeviceMemoryDecompressionFeaturesEXT decompressionQuery {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_DECOMPRESSION_FEATURES_EXT,
};
// The EXT form's struct is the same one under another name.
VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR swapchainMaintenance1Query {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_KHR,
};
VkPhysicalDeviceFeatures2 featureQuery {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
};
const auto ask = [&featureQuery](auto& features) {
features.pNext = featureQuery.pNext;
featureQuery.pNext = &features;
};
ask(supported.vulkan11);
ask(supported.vulkan12);
ask(supported.vulkan13);
if (vulkan14) {
ask(supported.vulkan14);
} else {
ask(maintenance5Query);
}
if (heapExtensions) {
ask(heapQuery);
ask(untypedPointersQuery);
}
if (rayTracingExtensions) {
ask(accelerationStructureQuery);
ask(rayTracingPipelineQuery);
ask(rayQueryQuery);
}
if (faultExtension) {
ask(faultQuery);
}
if (decompressionExtension) {
ask(decompressionQuery);
}
if (swapchainMaintenance1Extension != nullptr) {
ask(swapchainMaintenance1Query);
}
vkGetPhysicalDeviceFeatures2(physical, &featureQuery);
supported.vulkan10 = featureQuery.features;
// The chain pointed at this function's locals.
supported.vulkan11.pNext = supported.vulkan12.pNext = supported.vulkan13.pNext = supported.vulkan14.pNext = nullptr;
// Below 1.4 maintenance5 is VK_KHR_maintenance5's, and counts as the
// vulkan14 feature it became.
if (!vulkan14) {
supported.vulkan14.maintenance5 = maintenance5Query.maintenance5;
}
// 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.
candidate.heap = heapExtensions
&& heapQuery.descriptorHeap && untypedPointersQuery.shaderUntypedPointers
&& std::getenv("CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP") == nullptr;
candidate.rayTracing = candidate.heap && rayTracingExtensions
&& accelerationStructureQuery.accelerationStructure
&& rayTracingPipelineQuery.rayTracingPipeline
&& rayQueryQuery.rayQuery;
candidate.fault = faultExtension && faultQuery.deviceFault;
candidate.faultVendorBinary = faultQuery.deviceFaultVendorBinary;
candidate.decompression = decompressionExtension && decompressionQuery.memoryDecompression;
candidate.swapchainMaintenance1 = swapchainMaintenance1Query.swapchainMaintenance1 ? swapchainMaintenance1Extension : nullptr;
// 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. Still needed on 615.78.08.
// CRAFTER_GRAPHICS_NO_AS_WORKAROUND, set to any value like the other
// CRAFTER_GRAPHICS_* switches, turns it off, which is how a new driver
// gets checked (tests/HeapASRead --no-workaround).
candidate.workaroundAS = candidate.heap
&& driver.driverID == VK_DRIVER_ID_NVIDIA_PROPRIETARY
&& std::getenv("CRAFTER_GRAPHICS_NO_AS_WORKAROUND") == nullptr;
// What the library's own shaders and code use on this GPU. Everything
// else is the application's to ask for: shaderInt16 for a uint16_t spec
// constant carrying a heap slot, scalarBlockLayout, fp16, ...
DeviceFeatures library;
// Enabled where the GPU has them, never a reason to skip it.
DeviceFeatures wanted = optional;
// SamplerVulkan turns anisotropic filtering on.
library.vulkan10.samplerAnisotropy = VK_TRUE;
// Every UI kernel reads and writes its storage images without a
// format. From Vulkan 1.3 those capabilities need no feature: each
// format says whether it allows it
// (VK_FORMAT_FEATURE_2_STORAGE_READ/WRITE_WITHOUT_FORMAT_BIT), and
// Window picks a swapchain format that does. The 1.0 bits, which only
// say every format does, are a bonus where the GPU has them.
wanted.vulkan10.shaderStorageImageReadWithoutFormat = VK_TRUE;
wanted.vulkan10.shaderStorageImageWriteWithoutFormat = VK_TRUE;
// Bindless indexing: `images[]`, `textures[]`, `samplers[]` and
// `itemHeap[]` are indexed with a runtime value, in both binding models.
library.vulkan10.shaderSampledImageArrayDynamicIndexing = VK_TRUE;
library.vulkan10.shaderStorageBufferArrayDynamicIndexing = VK_TRUE;
library.vulkan10.shaderStorageImageArrayDynamicIndexing = VK_TRUE;
library.vulkan12.shaderSampledImageArrayNonUniformIndexing = VK_TRUE;
library.vulkan12.runtimeDescriptorArray = VK_TRUE;
// shaderInt64 and scalarBlockLayout: only for the NVIDIA descriptor-heap
// AS-read workaround (issue #15 / #7), which loads the TLAS device
// address as a 64-bit push constant placed right after the shader's
// own push members, inside a struct member's std430 tail padding where
// there is any, which only the scalar layout allows. Remove with the
// workaround.
library.vulkan10.shaderInt64 = candidate.workaroundAS ? VK_TRUE : VK_FALSE;
library.vulkan12.scalarBlockLayout = candidate.workaroundAS ? VK_TRUE : VK_FALSE;
// VulkanBuffer addresses, the descriptor heap and ray tracing.
library.vulkan12.bufferDeviceAddress = VK_TRUE;
// The VkMemoryBarrier2 around vkCmdDecompressMemoryEXT.
library.vulkan13.synchronization2 = VK_TRUE;
// Every VulkanBuffer is created with usage = 0 plus a
// VkBufferUsageFlags2CreateInfo, and heap pipelines carry a
// VkPipelineCreateFlags2CreateInfo: maintenance5 functionality.
library.vulkan14.maintenance5 = VK_TRUE;
if (!candidate.heap) {
// 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.
library.vulkan12.descriptorIndexing = VK_TRUE;
library.vulkan12.descriptorBindingSampledImageUpdateAfterBind = VK_TRUE;
library.vulkan12.descriptorBindingStorageImageUpdateAfterBind = VK_TRUE;
library.vulkan12.descriptorBindingStorageBufferUpdateAfterBind = VK_TRUE;
library.vulkan12.descriptorBindingUpdateUnusedWhilePending = VK_TRUE;
library.vulkan12.descriptorBindingPartiallyBound = VK_TRUE;
}
// The UI kernels' subgroup arithmetic is not asked for here: only an
// application with a UIRenderer runs them, and VulkanShader refuses
// them, naming the operation, on a GPU without it.
FeatureGaps featureGaps;
candidate.enabled = Device::ResolveFeatures(library, required, wanted, supported, candidate.apiVersion, featureGaps);
if (!featureGaps.library.empty()) {
candidate.gaps.push_back(std::format("lacks {}, which Crafter.Graphics needs", DeviceDetail::Join(featureGaps.library, ", ")));
}
if (!featureGaps.application.empty()) {
candidate.gaps.push_back(std::format("lacks {}, which the application requires", DeviceDetail::Join(featureGaps.application, ", ")));
}
return candidate;
}
// The GPU Initialize uses, or a throw naming why there is none.
Candidate ChooseGpu(VkInstance instance, std::uint32_t instanceVersion,
const DeviceFeatures& required, const DeviceFeatures& optional) {
const std::vector<std::vector<VkPhysicalDevice>> gpus = Device::DistinctGpus(instance);
// A GPU's first handle that can run the application, else its first
// handle, gaps and all.
const auto evaluate = [&](const std::vector<VkPhysicalDevice>& handles) {
Candidate first = EvaluateDevice(handles.front(), instanceVersion, required, optional);
for (std::size_t i = 1; i < handles.size() && !first.gaps.empty(); ++i) {
Candidate other = EvaluateDevice(handles[i], instanceVersion, required, optional);
if (other.gaps.empty()) {
return other;
}
}
return first;
};
// CRAFTER_GRAPHICS_DEVICE picks a GPU by its number: with two of the
// same GPU nothing else tells them apart, and the first may be too busy
// to use. A GPU picked that way is never swapped for another; when it
// can't run the application, that is the error. Without it every GPU is
// weighed and ChooseDevice picks among those that can.
if (const char* chosenDevice = std::getenv("CRAFTER_GRAPHICS_DEVICE")) {
const std::string_view text = chosenDevice;
std::size_t index = 0;
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), index);
if (error != std::errc() || end != text.data() + text.size() || index >= gpus.size()) {
throw std::runtime_error(gpus.empty()
? std::format("CRAFTER_GRAPHICS_DEVICE={} isn't the number of a GPU: Vulkan lists none", chosenDevice)
: std::format("CRAFTER_GRAPHICS_DEVICE={} isn't the number of a GPU: Vulkan lists {} GPU(s), 0 to {}",
chosenDevice, gpus.size(), gpus.size() - 1));
}
Candidate chosen = evaluate(gpus[index]);
if (!chosen.gaps.empty()) {
throw std::runtime_error(std::format("Crafter.Graphics: CRAFTER_GRAPHICS_DEVICE={} is {}, which can't run this application: {}",
chosenDevice, chosen.label, DeviceDetail::Join(chosen.gaps, "; ")));
}
return chosen;
}
std::vector<Candidate> candidates;
std::vector<DeviceChoice> choices;
for (const std::vector<VkPhysicalDevice>& handles : gpus) {
const Candidate& candidate = candidates.emplace_back(evaluate(handles));
choices.push_back({ candidate.properties.deviceType, candidate.label, candidate.gaps });
}
return std::move(candidates[Device::ChooseDevice(choices)]);
}
}
void Device::Initialize(const DeviceFeatures& required, const DeviceFeatures& optional) {
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
app.pApplicationName = "";
app.pEngineName = "Crafter.Graphics";
// CRAFTER_GRAPHICS_NO_VULKAN_1_4 runs as a 1.3 application, so the 1.3
// path (maintenance5 through its extension) can be exercised on a 1.4 GPU.
app.apiVersion = std::getenv("CRAFTER_GRAPHICS_NO_VULKAN_1_4") != nullptr ? VK_API_VERSION_1_3 : VK_API_VERSION_1_4;
// 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;
// The extensions above, plus optional ones where the loader has them:
// surface_maintenance1 answers a surface's capabilities for one present
// mode, through vkGetPhysicalDeviceSurfaceCapabilities2KHR, so it comes
// with get_surface_capabilities2 or not at all. The KHR form is the EXT
// one promoted; either will do.
std::vector<const char*> enabledInstanceExtensions(std::begin(instanceExtensionNames), std::end(instanceExtensionNames));
{
std::uint32_t count = 0;
CheckVkResult(vkEnumerateInstanceExtensionProperties(nullptr, &count, nullptr));
std::vector<VkExtensionProperties> available(count);
CheckVkResult(vkEnumerateInstanceExtensionProperties(nullptr, &count, available.data()));
const auto offered = [&](const char* name) {
return std::ranges::any_of(available, [&](const VkExtensionProperties& extension) {
return std::strcmp(extension.extensionName, name) == 0;
});
};
const char* surfaceMaintenance1 =
offered(VK_KHR_SURFACE_MAINTENANCE_1_EXTENSION_NAME) ? VK_KHR_SURFACE_MAINTENANCE_1_EXTENSION_NAME
: offered(VK_EXT_SURFACE_MAINTENANCE_1_EXTENSION_NAME) ? VK_EXT_SURFACE_MAINTENANCE_1_EXTENSION_NAME
: nullptr;
surfaceMaintenance1Supported = surfaceMaintenance1 != nullptr && offered(VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME);
instanceSurfaceMaintenance1 = surfaceMaintenance1Supported ? surfaceMaintenance1 : nullptr;
if (surfaceMaintenance1Supported) {
enabledInstanceExtensions.push_back(VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME);
enabledInstanceExtensions.push_back(surfaceMaintenance1);
}
}
instanceCreateInfo.enabledExtensionCount = static_cast<std::uint32_t>(enabledInstanceExtensions.size());
instanceCreateInfo.ppEnabledExtensionNames = enabledInstanceExtensions.data();
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));
if (surfaceMaintenance1Supported) {
vkGetPhysicalDeviceSurfaceCapabilities2KHR = reinterpret_cast<PFN_vkGetPhysicalDeviceSurfaceCapabilities2KHR>(
vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceSurfaceCapabilities2KHR"));
surfaceMaintenance1Supported = vkGetPhysicalDeviceSurfaceCapabilities2KHR != nullptr;
if (!surfaceMaintenance1Supported) {
instanceSurfaceMaintenance1 = nullptr;
}
}
VkDebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfo = {};
debugUtilsMessengerCreateInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
// Info messages (the loader reports its device sorting, the layers their
// settings) are for whoever is debugging with validation on; without it
// they would start every application's output.
debugUtilsMessengerCreateInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT
| (enableValidation ? VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT : 0);
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));
// Pick the GPU before anything this call can't take back. When none can
// run the application this throws, and takes the instance with it, so
// the application may call Initialize again asking for less.
Candidate chosen;
try {
chosen = ChooseGpu(instance, app.apiVersion, required, optional);
} catch (...) {
GET_EXTENSION_FUNCTION(vkDestroyDebugUtilsMessengerEXT)(instance, debugMessenger, nullptr);
vkDestroyInstance(instance, nullptr);
debugMessenger = VK_NULL_HANDLE;
instance = VK_NULL_HANDLE;
throw;
}
#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
physDevice = chosen.device;
apiVersion = chosen.apiVersion;
supportedFeatures = chosen.supported;
descriptorHeapSupported = chosen.heap;
rayTracingSupported = chosen.rayTracing;
deviceFaultSupported = chosen.fault;
memoryDecompressionSupported = chosen.decompression;
swapchainMaintenance1Supported = chosen.swapchainMaintenance1 != nullptr;
workaroundDescriptorHeapAS = chosen.workaroundAS;
shaderInt64ForWorkaroundOnly = chosen.workaroundAS
&& required.vulkan10.shaderInt64 == VK_FALSE && optional.vulkan10.shaderInt64 == VK_FALSE;
vulkan11Properties = chosen.vulkan11;
// 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, for Device::driverProperties: the driver's ID and
// version, which callers and tests read (EvaluateDevice took its own copy
// to decide the AS-read workaround).
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;
// 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;
// The core features to enable, as EvaluateDevice resolved them: the
// library's own on this GPU, the required ones, and the optional ones it
// has. On a 1.4 GPU maintenance5 is enabled through vulkan14; below 1.4
// through VK_KHR_maintenance5's struct instead, since Vulkan14Features
// isn't legal there and a feature may not be named through both structs
// (VUID-VkDeviceCreateInfo-pNext-10360).
const bool vulkan14 = chosen.apiVersion >= VK_API_VERSION_1_4;
DeviceFeatures enabled = chosen.enabled;
VkPhysicalDeviceMaintenance5Features maintenance5Features {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES,
.maintenance5 = enabled.vulkan14.maintenance5,
};
// The extension groups' feature structs, linked into the same chain only
// when their group is being enabled: a feature struct for an extension
// that is not enabled is invalid.
VkPhysicalDeviceFaultFeaturesEXT faultFeatures = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT,
.deviceFault = VK_TRUE,
.deviceFaultVendorBinary = chosen.faultVendorBinary ? VK_TRUE : VK_FALSE,
};
VkPhysicalDeviceShaderUntypedPointersFeaturesKHR untypedPointersFeatures {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_UNTYPED_POINTERS_FEATURES_KHR,
.shaderUntypedPointers = VK_TRUE,
};
VkPhysicalDeviceMemoryDecompressionFeaturesEXT memoryDecompressionFeatures {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_DECOMPRESSION_FEATURES_EXT,
.memoryDecompression = VK_TRUE,
};
VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR swapchainMaintenance1Features {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_KHR,
.swapchainMaintenance1 = VK_TRUE,
};
VkPhysicalDeviceDescriptorHeapFeaturesEXT desciptorHeapFeatures {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_HEAP_FEATURES_EXT,
.descriptorHeap = 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(desciptorHeapFeatures);
}
if (memoryDecompressionSupported) {
link(memoryDecompressionFeatures);
}
if (swapchainMaintenance1Supported) {
link(swapchainMaintenance1Features);
}
if (rayTracingSupported) {
link(physicalDeviceRayQueryFeatures);
link(physicalDeviceRayTracingPipelineFeatures);
link(deviceAccelerationStructureFeature);
}
link(enabled.vulkan11);
link(enabled.vulkan12);
link(enabled.vulkan13);
if (vulkan14) {
link(enabled.vulkan14);
} else {
link(maintenance5Features);
}
VkPhysicalDeviceFeatures2 physical_features2 = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
.pNext = featureChain,
.features = enabled.vulkan10,
};
// What the application can read back, without the chain above.
enabledFeatures = chosen.enabled;
// The enabled-extension list is the required set (maintenance5 among it
// below 1.4) plus each optional group the driver advertises in full.
std::vector<const char*> enabledDeviceExtensions = chosen.requiredExtensions;
if (descriptorHeapSupported) {
enabledDeviceExtensions.append_range(descriptorHeapExtensionNames);
}
if (rayTracingSupported) {
enabledDeviceExtensions.append_range(rayTracingExtensionNames);
}
if (deviceFaultSupported) {
enabledDeviceExtensions.push_back(VK_EXT_DEVICE_FAULT_EXTENSION_NAME);
}
if (memoryDecompressionSupported) {
enabledDeviceExtensions.push_back(VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME);
}
if (swapchainMaintenance1Supported) {
enabledDeviceExtensions.push_back(chosen.swapchainMaintenance1);
}
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;
std::lock_guard lock(deletionMutex);
deletionQueue.push_back({ .retireAfter = frameCounter, .buffer = buffer, .memory = memory });
}
void Device::EnqueueDeletion(VkAccelerationStructureKHR accelerationStructure) {
if (accelerationStructure == VK_NULL_HANDLE) return;
std::lock_guard lock(deletionMutex);
deletionQueue.push_back({ .retireAfter = frameCounter, .accelerationStructure = accelerationStructure });
}
void Device::EnqueueDeletion(VkShaderModule shaderModule) {
if (shaderModule == VK_NULL_HANDLE) return;
std::lock_guard lock(deletionMutex);
deletionQueue.push_back({ .retireAfter = frameCounter, .shaderModule = shaderModule });
}
void Device::EnqueueDeletion(VkPipeline pipeline) {
if (pipeline == VK_NULL_HANDLE) return;
std::lock_guard lock(deletionMutex);
deletionQueue.push_back({ .retireAfter = frameCounter, .pipeline = pipeline });
}
namespace {
// Free whatever one queued entry holds. The destroy and free calls accept
// VK_NULL_HANDLE for the kinds it doesn't hold; the acceleration-structure
// entry point is only loaded with ray tracing, so that one is guarded.
void DestroyPending(const Device::PendingDeletion& entry) {
if (entry.accelerationStructure != VK_NULL_HANDLE) {
Device::vkDestroyAccelerationStructureKHR(Device::device, entry.accelerationStructure, nullptr);
}
vkDestroyShaderModule(Device::device, entry.shaderModule, nullptr);
vkDestroyPipeline(Device::device, entry.pipeline, nullptr);
vkDestroyBuffer(Device::device, entry.buffer, nullptr);
vkFreeMemory(Device::device, entry.memory, nullptr);
}
}
void Device::ReclaimDeletions() {
std::lock_guard lock(deletionMutex);
// Free every entry whose retire frame has been reached. An entry tagged at
// frame F retires at F + framesInFlight: Window::Render calls this after
// waiting for its frame slot's fence, and that slot's last submission is
// frame F itself once framesInFlight frames have passed, so all GPU work
// from frame F is complete (and, by single-queue submission order,
// everything before it).
//
// 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) {
DestroyPending(deletionQueue.front());
deletionQueue.pop_front();
}
}
void Device::DrainDeletions() {
std::lock_guard lock(deletionMutex);
// Unconditional: the caller has just wait-idled, so no in-flight GPU work
// can still reference any queued resource.
for (const PendingDeletion& entry : deletionQueue) {
DestroyPending(entry);
}
deletionQueue.clear();
++drains;
}