//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 #include #include #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 #include #include #include #include #include #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 addressInfos(faultCounts.addressInfoCount); std::vector vendorInfos(faultCounts.vendorInfoCount); std::vector 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(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 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 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(size)); file.seekg(0); file.read(reinterpret_cast(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 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(data.data()), static_cast(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_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_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_registry_bind(registry, name, &wl_compositor_interface, 3)); } else if (strcmp(interface, xdg_wm_base_interface.name) == 0) { xdgWmBase = reinterpret_cast(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(wl_registry_bind(registry, name, &zxdg_decoration_manager_v1_interface, 1)); } else if (strcmp(interface, wp_viewporter_interface.name) == 0) { wpViewporter = reinterpret_cast(wl_registry_bind(registry, name, &wp_viewporter_interface, 1)); } else if (strcmp(interface, wp_fractional_scale_manager_v1_interface.name) == 0) { fractionalScaleManager = reinterpret_cast(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_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(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(window->textInputRect_[0] / s), static_cast(window->textInputRect_[1] / s), std::max(1, static_cast(window->textInputRect_[2] / s)), std::max(1, static_cast(window->textInputRect_[3] / s))); } else { if (!window->textInputEnabled_) return; window->textInputEnabled_ = false; zwp_text_input_v3_disable(textInput); } zwp_text_input_v3_commit(textInput); wl_display_flush(display); } 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 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(static_cast(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(now - keyRepeat.pressTime).count(); if (sincePress < keyRepeat.delay) return; auto period = std::chrono::milliseconds(1000 / keyRepeat.rate); auto sinceLastFire = std::chrono::duration_cast(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; } } 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 pos = Vector(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 pos = window->currentMousePos; if (TouchSlot* slot = SlotFor(id)) { if (slot->window != nullptr) window = slot->window; pos = Vector(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 pos = Vector(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 void Device::Initialize() { #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND display = wl_display_connect(NULL); if (display == nullptr) { throw std::runtime_error("Could not connect to wayland display"); } wl_registry* registry = wl_display_get_registry(display); wl_registry_add_listener(registry, ®istry_listener, nullptr); if (wl_display_roundtrip(display) == -1) { exit(EXIT_FAILURE); } if (shm == NULL || compositor == NULL || xdgWmBase == NULL) { throw std::runtime_error("No wl_shm, wl_compositor or xdg_wm_base support"); } // 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 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; // 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)); uint32_t physDeviceCount; vkEnumeratePhysicalDevices(instance, &physDeviceCount, NULL); std::vector physDevices(physDeviceCount); vkEnumeratePhysicalDevices(instance, &physDeviceCount, physDevices.data()); uint32_t bestScore = 0; // CRAFTER_GRAPHICS_DEVICE picks a GPU by its number among the distinct // ones in this list: with two of the same GPU nothing else tells them // apart, and the first may be too busy to use. A GPU two driver manifests // both list (a Flatpak sees the runtime's and the GL extension's) counts // once, so the number means the same GPU inside a sandbox and out. const char* chosenDevice = std::getenv("CRAFTER_GRAPHICS_DEVICE"); if (chosenDevice != nullptr) { std::vector distinct; std::vector> 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 uuid; std::ranges::copy(id.deviceUUID, uuid.begin()); if (std::ranges::find(seen, uuid) == seen.end()) { seen.push_back(uuid); distinct.push_back(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 >= distinct.size()) { throw std::runtime_error(std::format("CRAFTER_GRAPHICS_DEVICE={} isn't the number of a GPU: Vulkan lists {} GPU(s), 0 to {}", chosenDevice, distinct.size(), distinct.size() - 1)); } physDevice = distinct[index]; } for (uint32_t i = 0; chosenDevice == nullptr && 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 availableExtensions; { std::uint32_t extCount = 0; vkEnumerateDeviceExtensionProperties(physDevice, nullptr, &extCount, nullptr); std::vector exts(extCount); vkEnumerateDeviceExtensionProperties(physDevice, nullptr, &extCount, exts.data()); for (const VkExtensionProperties& e : exts) { availableExtensions.emplace(e.extensionName); } } const auto hasAll = [&availableExtensions](std::span 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. 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). workaroundDescriptorHeapAS = descriptorHeapSupported && driverProperties.driverID == VK_DRIVER_ID_NVIDIA_PROPRIETARY && std::getenv("CRAFTER_GRAPHICS_NO_AS_WORKAROUND") == nullptr; // 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 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 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(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(vkGetInstanceProcAddr(instance, "vkGetAccelerationStructureBuildSizesKHR")); vkCreateAccelerationStructureKHR = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkCreateAccelerationStructureKHR")); vkDestroyAccelerationStructureKHR = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkDestroyAccelerationStructureKHR")); vkCmdBuildAccelerationStructuresKHR = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkCmdBuildAccelerationStructuresKHR")); vkGetAccelerationStructureDeviceAddressKHR = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkGetAccelerationStructureDeviceAddressKHR")); vkCreateRayTracingPipelinesKHR = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkCreateRayTracingPipelinesKHR")); vkGetRayTracingShaderGroupHandlesKHR = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkGetRayTracingShaderGroupHandlesKHR")); vkCmdTraceRaysKHR = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkCmdTraceRaysKHR")); vkCmdBindResourceHeapEXT = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkCmdBindResourceHeapEXT")); vkCmdBindSamplerHeapEXT = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkCmdBindSamplerHeapEXT")); vkWriteResourceDescriptorsEXT = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkWriteResourceDescriptorsEXT")); vkWriteSamplerDescriptorsEXT = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkWriteSamplerDescriptorsEXT")); vkCmdPushDataEXT = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkCmdPushDataEXT")); vkGetPhysicalDeviceDescriptorSizeEXT = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceDescriptorSizeEXT")); vkGetDeviceFaultInfoEXT = reinterpret_cast(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( 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::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: 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) { 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; }