The window's record of the composing text it was last told (TextStrings().shown) was cleared only by the compositor's leave. An app that disables and enables the text input to reset the input method — after it committed the composing word itself — then never heard of a later composition equal to that word: the done handler reports a preedit only when it differs from the record. The disable path clears the record and the pending flags now, as the leave does, so the first composition after an enable is reported whatever it is.
1637 lines
No EOL
76 KiB
C++
1637 lines
No EOL
76 KiB
C++
//SPDX-License-Identifier: LGPL-3.0-only
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
module;
|
|
|
|
#include "vulkan/vulkan.h"
|
|
#include "vulkan/vk_enum_string_helper.h"
|
|
#define GET_EXTENSION_FUNCTION(_id) ((PFN_##_id)(vkGetInstanceProcAddr(instance, #_id)))
|
|
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
|
#include <linux/input-event-codes.h>
|
|
#include <xkbcommon/xkbcommon.h>
|
|
#include <string.h>
|
|
#include "../lib/xdg-shell-client-protocol.h"
|
|
#include "../lib/wayland-xdg-decoration-unstable-v1-client-protocol.h"
|
|
#include "../lib/fractional-scale-v1.h"
|
|
#include "../lib/viewporter.h"
|
|
#include "../lib/text-input-unstable-v3-client-protocol.h"
|
|
#include <wayland-client.h>
|
|
#include <wayland-client-protocol.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <sys/mman.h>
|
|
#endif
|
|
|
|
module Crafter.Graphics:Device_impl;
|
|
import :Device;
|
|
import :Window;
|
|
import :Types;
|
|
import :Clipboard;
|
|
import std;
|
|
using namespace Crafter;
|
|
|
|
const char* const instanceExtensionNames[] = {
|
|
"VK_EXT_debug_utils",
|
|
"VK_KHR_surface",
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
|
"VK_KHR_win32_surface"
|
|
#endif
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
|
"VK_KHR_wayland_surface"
|
|
#endif
|
|
};
|
|
// Device extensions are split by what breaks without them. Only the first
|
|
// group is needed to put UI pixels on screen, 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;
|
|
} else {
|
|
keyboardWindow->heldKeys.erase(code);
|
|
keyboardWindow->onRawKeyUp.Invoke(code);
|
|
|
|
// If the released key was the one repeating, stop. Otherwise leave
|
|
// the existing repeat alone (user pressed/released a modifier
|
|
// mid-repeat etc.).
|
|
if (keyRepeat.active && keyRepeat.key == code) {
|
|
keyRepeat.active = false;
|
|
keyRepeat.utf8.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Device::keyboard_modifiers(void *data, wl_keyboard *keyboard, uint32_t serial, uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, uint32_t group) {
|
|
xkb_state_update_mask(xkb_state, mods_depressed, mods_latched, mods_locked, 0, 0, group);
|
|
}
|
|
|
|
void Device::keyboard_repeat_info(void *data, wl_keyboard *keyboard, int32_t rate, int32_t delay) {
|
|
keyRepeat.rate = rate;
|
|
keyRepeat.delay = delay;
|
|
if (rate <= 0) keyRepeat.active = false; // compositor disabled repeat
|
|
}
|
|
|
|
void Device::TickKeyRepeats() {
|
|
if (!keyRepeat.active || !keyboardWindow) return;
|
|
if (keyRepeat.rate <= 0) return;
|
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
using ms = std::chrono::milliseconds;
|
|
auto sincePress = std::chrono::duration_cast<ms>(now - keyRepeat.pressTime).count();
|
|
if (sincePress < keyRepeat.delay) return;
|
|
|
|
auto period = std::chrono::milliseconds(1000 / keyRepeat.rate);
|
|
auto sinceLastFire = std::chrono::duration_cast<ms>(now - keyRepeat.lastFireTime).count();
|
|
if (sinceLastFire < period.count()) return;
|
|
|
|
// Catch up — emit one event per missed period so a paused frame doesn't
|
|
// make the repeat permanently lag behind.
|
|
while (now - keyRepeat.lastFireTime >= period) {
|
|
keyboardWindow->onRawKeyDown.Invoke(keyRepeat.key);
|
|
keyboardWindow->onRawKeyHold.Invoke(keyRepeat.key);
|
|
if (!keyRepeat.utf8.empty()) {
|
|
keyboardWindow->onTextInput.Invoke(keyRepeat.utf8);
|
|
}
|
|
keyRepeat.lastFireTime += period;
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
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;
|
|
// 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);
|
|
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,
|
|
};
|
|
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);
|
|
}
|
|
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;
|
|
// 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) {
|
|
std::uint32_t physDeviceCount = 0;
|
|
Device::CheckVkResult(vkEnumeratePhysicalDevices(instance, &physDeviceCount, nullptr));
|
|
std::vector<VkPhysicalDevice> physDevices(physDeviceCount);
|
|
Device::CheckVkResult(vkEnumeratePhysicalDevices(instance, &physDeviceCount, physDevices.data()));
|
|
|
|
// The handles, grouped by GPU: a GPU two drivers both list (a Flatpak
|
|
// sees the runtime's and the GL extension's manifests; RADV beside
|
|
// AMDVLK) has one device UUID and two handles. The numbers below count
|
|
// GPUs, so one means the same GPU inside a sandbox and out.
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
// 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;
|
|
instanceCreateInfo.enabledExtensionCount = sizeof(instanceExtensionNames) / sizeof(const char*);
|
|
instanceCreateInfo.ppEnabledExtensionNames = instanceExtensionNames;
|
|
|
|
size_t foundInstanceLayers = 0;
|
|
|
|
std::uint32_t instanceLayerCount;
|
|
CheckVkResult(vkEnumerateInstanceLayerProperties(&instanceLayerCount, NULL));
|
|
|
|
std::vector<VkLayerProperties> instanceLayerProperties(instanceLayerCount);
|
|
CheckVkResult(vkEnumerateInstanceLayerProperties(&instanceLayerCount, instanceLayerProperties.data()));
|
|
|
|
for (uint32_t i = 0; i < instanceLayerCount; i++)
|
|
{
|
|
for (size_t j = 0; j < sizeof(layerNames) / sizeof(const char*); j++)
|
|
{
|
|
if (std::strcmp(instanceLayerProperties[i].layerName, layerNames[j]) == 0)
|
|
{
|
|
foundInstanceLayers++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (enableValidation && foundInstanceLayers >= sizeof(layerNames) / sizeof(const char*))
|
|
{
|
|
instanceCreateInfo.enabledLayerCount = sizeof(layerNames) / sizeof(const char*);
|
|
instanceCreateInfo.ppEnabledLayerNames = layerNames;
|
|
}
|
|
|
|
CheckVkResult(vkCreateInstance(&instanceCreateInfo, NULL, &instance));
|
|
|
|
VkDebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfo = {};
|
|
debugUtilsMessengerCreateInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
|
|
// 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, ®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
|
|
|
|
physDevice = chosen.device;
|
|
apiVersion = chosen.apiVersion;
|
|
supportedFeatures = chosen.supported;
|
|
descriptorHeapSupported = chosen.heap;
|
|
rayTracingSupported = chosen.rayTracing;
|
|
deviceFaultSupported = chosen.fault;
|
|
memoryDecompressionSupported = chosen.decompression;
|
|
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,
|
|
};
|
|
|
|
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 (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);
|
|
}
|
|
|
|
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: 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;
|
|
} |