The SPIR-V rewrite for NVIDIA's descriptor-heap acceleration-structure read fault (#7, #15) was keyed on the driver ID alone, so nothing could check whether a newer driver still needs it. With CRAFTER_GRAPHICS_NO_AS_WORKAROUND set, to any value like the other CRAFTER_GRAPHICS_* switches, Device leaves it off. On 615.78.08 it is still needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1441 lines
No EOL
63 KiB
C++
1441 lines
No EOL
63 KiB
C++
//SPDX-License-Identifier: LGPL-3.0-only
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
module;
|
|
|
|
#include "vulkan/vulkan.h"
|
|
#include "vulkan/vk_enum_string_helper.h"
|
|
#define GET_EXTENSION_FUNCTION(_id) ((PFN_##_id)(vkGetInstanceProcAddr(instance, #_id)))
|
|
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
|
#include <linux/input-event-codes.h>
|
|
#include <xkbcommon/xkbcommon.h>
|
|
#include <string.h>
|
|
#include "../lib/xdg-shell-client-protocol.h"
|
|
#include "../lib/wayland-xdg-decoration-unstable-v1-client-protocol.h"
|
|
#include "../lib/fractional-scale-v1.h"
|
|
#include "../lib/viewporter.h"
|
|
#include "../lib/text-input-unstable-v3-client-protocol.h"
|
|
#include <wayland-client.h>
|
|
#include <wayland-client-protocol.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <sys/mman.h>
|
|
#endif
|
|
|
|
module Crafter.Graphics:Device_impl;
|
|
import :Device;
|
|
import :Window;
|
|
import :Types;
|
|
import :Clipboard;
|
|
import std;
|
|
using namespace Crafter;
|
|
|
|
const char* const instanceExtensionNames[] = {
|
|
"VK_EXT_debug_utils",
|
|
"VK_KHR_surface",
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
|
"VK_KHR_win32_surface"
|
|
#endif
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
|
"VK_KHR_wayland_surface"
|
|
#endif
|
|
};
|
|
// Device extensions are split by what breaks without them. Only the first
|
|
// group is needed to put UI pixels on screen; Device::Initialize enables each
|
|
// of the others only when the driver advertises all of its members, and
|
|
// records the outcome in Device::descriptorHeapSupported / rayTracingSupported
|
|
// / deviceFaultSupported. Mesa Turnip (Adreno), for one, has none of the heap,
|
|
// ray tracing or fault extensions.
|
|
const char* const requiredDeviceExtensionNames[] = {
|
|
"VK_KHR_swapchain",
|
|
"VK_KHR_spirv_1_4",
|
|
"VK_KHR_shader_float_controls",
|
|
"VK_KHR_maintenance5",
|
|
};
|
|
// Without these, descriptors come from a bindless descriptor set instead
|
|
// (DescriptorHeapVulkan's sets mode) and shaders load their .sets.spv build.
|
|
const char* const descriptorHeapExtensionNames[] = {
|
|
"VK_EXT_descriptor_heap",
|
|
"VK_KHR_shader_untyped_pointers",
|
|
};
|
|
// Ray tracing additionally needs the descriptor heap: every RT pipeline and
|
|
// RT shader in this library is heap-only.
|
|
const char* const rayTracingExtensionNames[] = {
|
|
"VK_KHR_acceleration_structure",
|
|
"VK_KHR_ray_tracing_pipeline",
|
|
"VK_KHR_ray_query",
|
|
"VK_KHR_deferred_host_operations",
|
|
};
|
|
const char* const layerNames[] = {
|
|
"VK_LAYER_KHRONOS_validation"
|
|
};
|
|
|
|
|
|
void Device::CheckVkResult(VkResult result) {
|
|
if (result != VK_SUCCESS)
|
|
{
|
|
// The fault report needs VK_EXT_device_fault; without it the entry
|
|
// point below is not callable and the bare VkResult is all there is.
|
|
if(result == VK_ERROR_DEVICE_LOST && deviceFaultSupported) {
|
|
VkDeviceFaultCountsEXT faultCounts = {
|
|
.sType = VK_STRUCTURE_TYPE_DEVICE_FAULT_COUNTS_EXT,
|
|
.pNext = NULL,
|
|
};
|
|
Device::vkGetDeviceFaultInfoEXT(device, &faultCounts, NULL);
|
|
|
|
std::vector<VkDeviceFaultAddressInfoEXT> addressInfos(faultCounts.addressInfoCount);
|
|
std::vector<VkDeviceFaultVendorInfoEXT> vendorInfos(faultCounts.vendorInfoCount);
|
|
std::vector<char> vendorBinaryData(faultCounts.vendorBinarySize);
|
|
|
|
VkDeviceFaultInfoEXT faultInfo = {
|
|
.sType = VK_STRUCTURE_TYPE_DEVICE_FAULT_INFO_EXT,
|
|
.pNext = NULL,
|
|
.pAddressInfos = addressInfos.data(),
|
|
.pVendorInfos = vendorInfos.data(),
|
|
.pVendorBinaryData = vendorBinaryData.data(),
|
|
};
|
|
Device::vkGetDeviceFaultInfoEXT(device, &faultCounts, &faultInfo);
|
|
|
|
std::println("{}", faultInfo.description);
|
|
|
|
std::println("{} AddressInfos:", addressInfos.size());
|
|
for(const VkDeviceFaultAddressInfoEXT& info : addressInfos) {
|
|
std::println("\t{} {}", static_cast<uint32_t>(info.addressType), info.reportedAddress);
|
|
}
|
|
|
|
std::println("{} vendorInfos:", vendorInfos.size());
|
|
for(const VkDeviceFaultVendorInfoEXT& info : vendorInfos) {
|
|
std::println("\t{} {} {}", info.description, info.vendorFaultCode, info.vendorFaultData);
|
|
}
|
|
|
|
if(!vendorBinaryData.empty()) {
|
|
std::string ext = ".bin";
|
|
if(vendorBinaryData.size() >= sizeof(VkDeviceFaultVendorBinaryHeaderVersionOneEXT)) {
|
|
VkDeviceFaultVendorBinaryHeaderVersionOneEXT header;
|
|
std::memcpy(&header, vendorBinaryData.data(), sizeof(header));
|
|
if(header.vendorID == 0x10DE) { // NVIDIA
|
|
ext = ".nv-gpudmp";
|
|
}
|
|
}
|
|
const auto now = std::chrono::system_clock::now();
|
|
const std::string dumpPath = std::format("gpu_crash_dump-{:%Y%m%d-%H%M%S}{}", now, ext);
|
|
std::ofstream file(dumpPath, std::ios::binary);
|
|
if(file.write(vendorBinaryData.data(), vendorBinaryData.size())) {
|
|
std::println("Vendor binary saved to: {}", std::filesystem::canonical(dumpPath).string());
|
|
} else {
|
|
std::println(stderr, "Failed to write vendor binary to: {}", dumpPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
throw std::runtime_error(string_VkResult(result));
|
|
}
|
|
}
|
|
|
|
void Device::RequireRayTracing() {
|
|
if (rayTracingSupported) {
|
|
return;
|
|
}
|
|
// Thrown at the first ray tracing entry point (an RT-stage shader module,
|
|
// a BLAS/TLAS build, the SBT, the pipeline) so the failure names the
|
|
// cause. Letting the call through would hand heap-only SPIR-V or an
|
|
// unloaded entry point to a driver that never enabled the feature.
|
|
if (descriptorHeapSupported) {
|
|
throw std::runtime_error(
|
|
"Crafter.Graphics: ray tracing is unavailable: the Vulkan driver lacks one of "
|
|
"VK_KHR_acceleration_structure, VK_KHR_ray_tracing_pipeline, VK_KHR_ray_query, "
|
|
"VK_KHR_deferred_host_operations");
|
|
}
|
|
throw std::runtime_error(
|
|
"Crafter.Graphics: ray tracing is unavailable: it needs VK_EXT_descriptor_heap and "
|
|
"VK_KHR_shader_untyped_pointers, which this driver lacks (or "
|
|
"CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP is set). Only the compute/UI path runs here.");
|
|
}
|
|
|
|
bool Device::PipelineCacheDataCompatible(std::span<const std::byte> data) {
|
|
// VkPipelineCacheHeaderVersionOne is a fixed 32-byte little-endian header
|
|
// (Vulkan spec 16.5.2): u32 headerSize, u32 headerVersion, u32 vendorID,
|
|
// u32 deviceID, then VK_UUID_SIZE bytes of pipelineCacheUUID. Parse the
|
|
// fields by offset rather than reinterpret_cast'ing the struct so the
|
|
// check is independent of the C struct's alignment/padding.
|
|
constexpr std::size_t headerBytes = 16 + VK_UUID_SIZE;
|
|
if (data.size() < headerBytes) {
|
|
return false;
|
|
}
|
|
|
|
auto readU32 = [&](std::size_t offset) {
|
|
std::uint32_t value;
|
|
std::memcpy(&value, data.data() + offset, sizeof(value));
|
|
return value;
|
|
};
|
|
|
|
const std::uint32_t headerSize = readU32(0);
|
|
const std::uint32_t headerVersion = readU32(4);
|
|
const std::uint32_t vendorID = readU32(8);
|
|
const std::uint32_t deviceID = readU32(12);
|
|
|
|
if (headerSize < headerBytes) {
|
|
return false;
|
|
}
|
|
if (headerVersion != VK_PIPELINE_CACHE_HEADER_VERSION_ONE) {
|
|
return false;
|
|
}
|
|
if (vendorID != deviceProperties.vendorID || deviceID != deviceProperties.deviceID) {
|
|
return false;
|
|
}
|
|
return std::memcmp(data.data() + 16, deviceProperties.pipelineCacheUUID, VK_UUID_SIZE) == 0;
|
|
}
|
|
|
|
void Device::LoadPipelineCache() {
|
|
std::vector<std::byte> initialData;
|
|
|
|
std::error_code ec;
|
|
if (std::filesystem::exists(pipelineCachePath, ec)) {
|
|
std::ifstream file(pipelineCachePath, std::ios::binary | std::ios::ate);
|
|
if (file) {
|
|
const std::streamoff size = file.tellg();
|
|
if (size > 0) {
|
|
initialData.resize(static_cast<std::size_t>(size));
|
|
file.seekg(0);
|
|
file.read(reinterpret_cast<char*>(initialData.data()), size);
|
|
if (!file) {
|
|
initialData.clear(); // partial/short read — start cold
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Drop a blob the current driver/device wouldn't accept: a header from a
|
|
// different GPU (or a corrupt/empty file) would at best be ignored and at
|
|
// worst rejected. Starting empty just costs a one-time cold compile.
|
|
if (!PipelineCacheDataCompatible(initialData)) {
|
|
initialData.clear();
|
|
}
|
|
|
|
VkPipelineCacheCreateInfo info {
|
|
.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO,
|
|
.initialDataSize = initialData.size(),
|
|
.pInitialData = initialData.empty() ? nullptr : initialData.data(),
|
|
};
|
|
CheckVkResult(vkCreatePipelineCache(device, &info, nullptr, &pipelineCache));
|
|
}
|
|
|
|
void Device::SavePipelineCache() {
|
|
if (pipelineCache == VK_NULL_HANDLE) {
|
|
return;
|
|
}
|
|
|
|
// Two-call idiom: query size, then fetch. Best-effort — a failure here only
|
|
// forfeits the next run's warm start, so it must never throw out of an
|
|
// atexit handler.
|
|
std::size_t size = 0;
|
|
if (vkGetPipelineCacheData(device, pipelineCache, &size, nullptr) != VK_SUCCESS || size == 0) {
|
|
return;
|
|
}
|
|
std::vector<std::byte> data(size);
|
|
if (vkGetPipelineCacheData(device, pipelineCache, &size, data.data()) != VK_SUCCESS) {
|
|
return;
|
|
}
|
|
|
|
std::ofstream file(pipelineCachePath, std::ios::binary | std::ios::trunc);
|
|
if (file) {
|
|
file.write(reinterpret_cast<const char*>(data.data()), static_cast<std::streamsize>(size));
|
|
}
|
|
}
|
|
|
|
VkBool32 onError(VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMessageTypeFlagsEXT type, const VkDebugUtilsMessengerCallbackDataEXT* callbackData, void* userData)
|
|
{
|
|
printf("Vulkan ");
|
|
|
|
switch (type)
|
|
{
|
|
case VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT :
|
|
printf("general ");
|
|
break;
|
|
case VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT :
|
|
printf("validation ");
|
|
break;
|
|
case VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT :
|
|
printf("performance ");
|
|
break;
|
|
}
|
|
|
|
switch (severity)
|
|
{
|
|
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT :
|
|
printf("(verbose): ");
|
|
break;
|
|
default :
|
|
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT :
|
|
printf("(info): ");
|
|
break;
|
|
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT :
|
|
printf("(warning): ");
|
|
break;
|
|
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT :
|
|
printf("(error): ");
|
|
Device::validationErrorCount++;
|
|
break;
|
|
}
|
|
|
|
printf("%s\n", callbackData->pMessage);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
|
void Device::xdg_wm_base_handle_ping(void* data, xdg_wm_base* xdg_wm_base, std::uint32_t serial) {
|
|
xdg_wm_base_pong(xdg_wm_base, serial);
|
|
}
|
|
|
|
void Device::handle_global(void *data, wl_registry *registry, std::uint32_t name, const char *interface, std::uint32_t version) {
|
|
if (strcmp(interface, wl_shm_interface.name) == 0) {
|
|
shm = reinterpret_cast<wl_shm*>(wl_registry_bind(registry, name, &wl_shm_interface, 1));
|
|
} else if (strcmp(interface, wl_seat_interface.name) == 0) {
|
|
// Assign to Device::seat (not a fresh local) so SetClipboardText
|
|
// and any other code that needs the seat post-init can find it.
|
|
seat = reinterpret_cast<wl_seat*>(wl_registry_bind(registry, name, &wl_seat_interface, 1));
|
|
wl_seat_add_listener(seat, &seat_listener, nullptr);
|
|
// If the manager came in first, the data device couldn't be
|
|
// created yet — do it now that we have the seat.
|
|
if (dataDeviceManager != nullptr && dataDevice == nullptr) {
|
|
dataDevice = wl_data_device_manager_get_data_device(dataDeviceManager, seat);
|
|
}
|
|
if (textInputManager != nullptr && textInput == nullptr) {
|
|
textInput = zwp_text_input_manager_v3_get_text_input(textInputManager, seat);
|
|
zwp_text_input_v3_add_listener(textInput, &text_input_listener, nullptr);
|
|
}
|
|
} else if (compositor == nullptr && strcmp(interface, wl_compositor_interface.name) == 0) {
|
|
compositor = reinterpret_cast<wl_compositor*>(wl_registry_bind(registry, name, &wl_compositor_interface, 3));
|
|
} else if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
|
|
xdgWmBase = reinterpret_cast<xdg_wm_base*>(wl_registry_bind(registry, name, &xdg_wm_base_interface, 1));
|
|
xdg_wm_base_add_listener(xdgWmBase, &xdgWmBaseListener, nullptr);
|
|
} else if (strcmp(interface, zxdg_decoration_manager_v1_interface.name) == 0) {
|
|
manager = reinterpret_cast<zxdg_decoration_manager_v1*>(wl_registry_bind(registry, name, &zxdg_decoration_manager_v1_interface, 1));
|
|
} else if (strcmp(interface, wp_viewporter_interface.name) == 0) {
|
|
wpViewporter = reinterpret_cast<wp_viewporter*>(wl_registry_bind(registry, name, &wp_viewporter_interface, 1));
|
|
} else if (strcmp(interface, wp_fractional_scale_manager_v1_interface.name) == 0) {
|
|
fractionalScaleManager = reinterpret_cast<wp_fractional_scale_manager_v1*>(wl_registry_bind(registry, name, &wp_fractional_scale_manager_v1_interface, 1));
|
|
} else if (strcmp(interface, wl_data_device_manager_interface.name) == 0) {
|
|
// v3 gives us the full set of data_source events (target / send /
|
|
// cancelled / dnd_*). Universally supported by the compositors
|
|
// we target — fall back path is the per-source listener simply
|
|
// not getting the v3-only callbacks.
|
|
dataDeviceManager = reinterpret_cast<wl_data_device_manager*>(
|
|
wl_registry_bind(registry, name, &wl_data_device_manager_interface, 3));
|
|
if (seat != nullptr && dataDevice == nullptr) {
|
|
dataDevice = wl_data_device_manager_get_data_device(dataDeviceManager, seat);
|
|
}
|
|
} else if (strcmp(interface, zwp_text_input_manager_v3_interface.name) == 0) {
|
|
textInputManager = reinterpret_cast<zwp_text_input_manager_v3*>(wl_registry_bind(registry, name, &zwp_text_input_manager_v3_interface, 1));
|
|
if (seat != nullptr && textInput == nullptr) {
|
|
textInput = zwp_text_input_manager_v3_get_text_input(textInputManager, seat);
|
|
zwp_text_input_v3_add_listener(textInput, &text_input_listener, nullptr);
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
zwp_text_input_v3_commit(textInput);
|
|
wl_display_flush(display);
|
|
}
|
|
|
|
void Device::handle_global_remove(void* data, wl_registry* registry, uint32_t name) {
|
|
|
|
}
|
|
|
|
void Device::pointer_handle_button(void* data, wl_pointer* pointer, std::uint32_t serial, std::uint32_t time, std::uint32_t button, std::uint32_t state) {
|
|
// Defensive: compositors can deliver pointer events without (or after)
|
|
// a matching enter — observed on sway during input-device hot-plug.
|
|
if (focusedWindow == nullptr) {
|
|
return;
|
|
}
|
|
if (button == BTN_LEFT) {
|
|
if(state == WL_POINTER_BUTTON_STATE_PRESSED) {
|
|
Device::focusedWindow->mouseLeftHeld = true;
|
|
Device::focusedWindow->onMouseLeftClick.Invoke();
|
|
} else {
|
|
Device::focusedWindow->mouseLeftHeld = false;
|
|
Device::focusedWindow->onMouseLeftRelease.Invoke();
|
|
}
|
|
} else if(button == BTN_RIGHT){
|
|
if(state == WL_POINTER_BUTTON_STATE_PRESSED) {
|
|
Device::focusedWindow->mouseRightHeld = true;
|
|
Device::focusedWindow->onMouseRightClick.Invoke();
|
|
} else {
|
|
Device::focusedWindow->mouseRightHeld = false;
|
|
Device::focusedWindow->onMouseRightRelease.Invoke();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Device::PointerListenerHandleMotion(void* data, wl_pointer* wl_pointer, std::uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) {
|
|
if (focusedWindow == nullptr) {
|
|
return;
|
|
}
|
|
Vector<float, 2> pos(wl_fixed_to_double(surface_x), wl_fixed_to_double(surface_y));
|
|
//Device::focusedWindow->lastMousePos = Device::focusedWindow->currentMousePos;
|
|
Device::focusedWindow->currentMousePos = pos * Device::focusedWindow->scale;
|
|
//Device::focusedWindow->mouseDelta = {Device::focusedWindow->currentMousePos.x-Device::focusedWindow->lastMousePos.x, Device::focusedWindow->currentMousePos.y-Device::focusedWindow->lastMousePos.y};
|
|
Device::focusedWindow->onMouseMove.Invoke();
|
|
}
|
|
|
|
void Device::PointerListenerHandleEnter(void* data, wl_pointer* wl_pointer, std::uint32_t serial, wl_surface* surface, wl_fixed_t surface_x, wl_fixed_t surface_y) {
|
|
Device::wlPointer = wl_pointer;
|
|
for(Window* window : windows) {
|
|
if(window->surface == surface) {
|
|
window->lastPointerSerial_ = serial;
|
|
if(window->cursorSurface != nullptr) {
|
|
wl_pointer_set_cursor(wl_pointer, serial, window->cursorSurface,
|
|
window->cursorHotspotX_, window->cursorHotspotY_);
|
|
}
|
|
focusedWindow = window;
|
|
window->onMouseEnter.Invoke();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Device::PointerListenerHandleLeave(void* data, wl_pointer*, std::uint32_t, wl_surface*) {
|
|
// Sway has been observed to send a second leave after focus is already
|
|
// gone (input-device hot-plug) — don't crash on it.
|
|
if (focusedWindow != nullptr) {
|
|
focusedWindow->onMouseLeave.Invoke();
|
|
focusedWindow = nullptr;
|
|
}
|
|
// Drop any sub-detent scroll remainder so it can't leak into whatever
|
|
// window the pointer enters next.
|
|
scrollDetentRemainder = 0.0;
|
|
}
|
|
|
|
void Device::PointerListenerHandleAxis(void*, wl_pointer*, std::uint32_t, std::uint32_t axis, wl_fixed_t value) {
|
|
// Vertical wheel only — Window has no horizontal-scroll event (yet).
|
|
if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL || focusedWindow == nullptr) {
|
|
return;
|
|
}
|
|
// libinput convention (followed by wlroots, Mutter and KWin): one wheel
|
|
// detent = 15 axis units, positive = wheel down / toward the user — the
|
|
// same sign as DOM deltaY. Window::onMouseScroll speaks whole detents
|
|
// (±1 per notch on every backend), so divide and accumulate the
|
|
// remainder: smooth-scroll devices (touchpads) deliver many sub-detent
|
|
// events that must add up instead of each truncating to zero.
|
|
constexpr double axisUnitsPerDetent = 15.0;
|
|
scrollDetentRemainder += wl_fixed_to_double(value) / axisUnitsPerDetent;
|
|
const double whole = std::trunc(scrollDetentRemainder);
|
|
if (whole != 0.0) {
|
|
scrollDetentRemainder -= whole;
|
|
// Event payload is uint32 — preserve sign via two's complement.
|
|
focusedWindow->onMouseScroll.Invoke(
|
|
static_cast<std::uint32_t>(static_cast<std::int32_t>(whole)));
|
|
}
|
|
}
|
|
|
|
|
|
void Device::keyboard_keymap(void *data, wl_keyboard *keyboard, uint32_t format, int fd, uint32_t size) {
|
|
if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
|
|
close(fd);
|
|
fprintf(stderr, "Unsupported keymap format\n");
|
|
return;
|
|
}
|
|
|
|
void *map = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
|
|
if (map == MAP_FAILED) {
|
|
close(fd);
|
|
perror("mmap");
|
|
return;
|
|
}
|
|
|
|
xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
|
xkb_keymap = xkb_keymap_new_from_string(xkb_context, (const char *)map, XKB_KEYMAP_FORMAT_TEXT_V1,XKB_KEYMAP_COMPILE_NO_FLAGS);
|
|
munmap(map, size);
|
|
close(fd);
|
|
|
|
xkb_state = xkb_state_new(xkb_keymap);
|
|
}
|
|
|
|
void Device::keyboard_enter(void *data, wl_keyboard *keyboard, uint32_t serial, wl_surface *surface, wl_array *keys) {
|
|
for (Window* window : windows) {
|
|
if (window->surface == surface) {
|
|
keyboardWindow = window;
|
|
return;
|
|
}
|
|
}
|
|
keyboardWindow = nullptr;
|
|
}
|
|
|
|
void Device::keyboard_leave(void *data, wl_keyboard *keyboard, uint32_t serial, wl_surface *surface) {
|
|
// Releases happen elsewhere while the focus is away: nothing we still
|
|
// hold is held, and a repeat must not keep firing into a window that
|
|
// lost the keyboard.
|
|
if (keyboardWindow != nullptr) keyboardWindow->heldKeys.clear();
|
|
keyboardWindow = nullptr;
|
|
keyRepeat.active = false;
|
|
keyRepeat.utf8.clear();
|
|
}
|
|
|
|
void Device::keyboard_key(void *data, wl_keyboard *keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) {
|
|
// `key` is the kernel input-event-code (KEY_*). That is exactly what
|
|
// :Keys returns for Wayland builds, so we store it verbatim with no
|
|
// translation. The +8 X11 offset is only needed for the XKB layer,
|
|
// which we still consult to produce UTF-8 text.
|
|
KeyCode code = key;
|
|
xkb_keycode_t xkbKeycode = key + 8;
|
|
|
|
// No enter for any of our surfaces (a compositor quirk, or a key for a
|
|
// window already gone): nowhere to deliver it.
|
|
if (keyboardWindow == nullptr) return;
|
|
|
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
if (keyboardWindow->heldKeys.contains(code)) {
|
|
keyboardWindow->onRawKeyHold.Invoke(code);
|
|
} else {
|
|
keyboardWindow->heldKeys.insert(code);
|
|
keyboardWindow->onRawKeyDown.Invoke(code);
|
|
}
|
|
|
|
std::string buf;
|
|
buf.resize(16);
|
|
int n = xkb_state_key_get_utf8(xkb_state, xkbKeycode, buf.data(), 16);
|
|
std::string utf8;
|
|
if (n > 0 && (unsigned char)buf[0] >= 0x20 && buf[0] != 0x7f) {
|
|
buf.resize(n);
|
|
utf8 = buf;
|
|
keyboardWindow->onTextInput.Invoke(utf8);
|
|
}
|
|
|
|
// Replace the active repeat with this key — most recent press wins,
|
|
// matching xkbcommon's typical behaviour and most desktop apps.
|
|
keyRepeat.active = (keyRepeat.rate > 0);
|
|
keyRepeat.key = code;
|
|
keyRepeat.utf8 = std::move(utf8);
|
|
keyRepeat.pressTime = std::chrono::steady_clock::now();
|
|
keyRepeat.lastFireTime = keyRepeat.pressTime;
|
|
} else {
|
|
keyboardWindow->heldKeys.erase(code);
|
|
keyboardWindow->onRawKeyUp.Invoke(code);
|
|
|
|
// If the released key was the one repeating, stop. Otherwise leave
|
|
// the existing repeat alone (user pressed/released a modifier
|
|
// mid-repeat etc.).
|
|
if (keyRepeat.active && keyRepeat.key == code) {
|
|
keyRepeat.active = false;
|
|
keyRepeat.utf8.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Device::keyboard_modifiers(void *data, wl_keyboard *keyboard, uint32_t serial, uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, uint32_t group) {
|
|
xkb_state_update_mask(xkb_state, mods_depressed, mods_latched, mods_locked, 0, 0, group);
|
|
}
|
|
|
|
void Device::keyboard_repeat_info(void *data, wl_keyboard *keyboard, int32_t rate, int32_t delay) {
|
|
keyRepeat.rate = rate;
|
|
keyRepeat.delay = delay;
|
|
if (rate <= 0) keyRepeat.active = false; // compositor disabled repeat
|
|
}
|
|
|
|
void Device::TickKeyRepeats() {
|
|
if (!keyRepeat.active || !keyboardWindow) return;
|
|
if (keyRepeat.rate <= 0) return;
|
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
using ms = std::chrono::milliseconds;
|
|
auto sincePress = std::chrono::duration_cast<ms>(now - keyRepeat.pressTime).count();
|
|
if (sincePress < keyRepeat.delay) return;
|
|
|
|
auto period = std::chrono::milliseconds(1000 / keyRepeat.rate);
|
|
auto sinceLastFire = std::chrono::duration_cast<ms>(now - keyRepeat.lastFireTime).count();
|
|
if (sinceLastFire < period.count()) return;
|
|
|
|
// Catch up — emit one event per missed period so a paused frame doesn't
|
|
// make the repeat permanently lag behind.
|
|
while (now - keyRepeat.lastFireTime >= period) {
|
|
keyboardWindow->onRawKeyDown.Invoke(keyRepeat.key);
|
|
keyboardWindow->onRawKeyHold.Invoke(keyRepeat.key);
|
|
if (!keyRepeat.utf8.empty()) {
|
|
keyboardWindow->onTextInput.Invoke(keyRepeat.utf8);
|
|
}
|
|
keyRepeat.lastFireTime += period;
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
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<VkLayerProperties> instanceLayerProperties(instanceLayerCount);
|
|
CheckVkResult(vkEnumerateInstanceLayerProperties(&instanceLayerCount, instanceLayerProperties.data()));
|
|
|
|
for (uint32_t i = 0; i < instanceLayerCount; i++)
|
|
{
|
|
for (size_t j = 0; j < sizeof(layerNames) / sizeof(const char*); j++)
|
|
{
|
|
if (std::strcmp(instanceLayerProperties[i].layerName, layerNames[j]) == 0)
|
|
{
|
|
foundInstanceLayers++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (enableValidation && foundInstanceLayers >= sizeof(layerNames) / sizeof(const char*))
|
|
{
|
|
instanceCreateInfo.enabledLayerCount = sizeof(layerNames) / sizeof(const char*);
|
|
instanceCreateInfo.ppEnabledLayerNames = layerNames;
|
|
}
|
|
|
|
CheckVkResult(vkCreateInstance(&instanceCreateInfo, NULL, &instance));
|
|
|
|
VkDebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfo = {};
|
|
debugUtilsMessengerCreateInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
|
|
debugUtilsMessengerCreateInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
|
|
debugUtilsMessengerCreateInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
|
|
debugUtilsMessengerCreateInfo.pfnUserCallback = onError;
|
|
CheckVkResult(GET_EXTENSION_FUNCTION(vkCreateDebugUtilsMessengerEXT)(instance, &debugUtilsMessengerCreateInfo, NULL, &debugMessenger));
|
|
|
|
uint32_t physDeviceCount;
|
|
vkEnumeratePhysicalDevices(instance, &physDeviceCount, NULL);
|
|
|
|
std::vector<VkPhysicalDevice> physDevices(physDeviceCount);
|
|
vkEnumeratePhysicalDevices(instance, &physDeviceCount, physDevices.data());
|
|
|
|
uint32_t bestScore = 0;
|
|
|
|
for (uint32_t i = 0; i < physDeviceCount; i++)
|
|
{
|
|
VkPhysicalDevice device = physDevices[i];
|
|
|
|
uint32_t score;
|
|
|
|
VkPhysicalDeviceProperties properties;
|
|
vkGetPhysicalDeviceProperties(device, &properties);
|
|
switch (properties.deviceType)
|
|
{
|
|
default :
|
|
continue;
|
|
case VK_PHYSICAL_DEVICE_TYPE_OTHER :
|
|
score = 1;
|
|
break;
|
|
case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU :
|
|
score = 4;
|
|
break;
|
|
case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU :
|
|
score = 5;
|
|
break;
|
|
case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU :
|
|
score = 3;
|
|
break;
|
|
case VK_PHYSICAL_DEVICE_TYPE_CPU :
|
|
score = 2;
|
|
break;
|
|
}
|
|
|
|
if (score > bestScore)
|
|
{
|
|
physDevice = device;
|
|
bestScore = score;
|
|
}
|
|
}
|
|
|
|
// Enumerate the driver's device extensions once; every optional capability
|
|
// below is decided from this set. Drivers without
|
|
// VK_EXT_memory_decompression (AMD, Intel as of early 2026) get the
|
|
// CPU-decode fallback; drivers without the descriptor heap (Mesa Turnip)
|
|
// get the descriptor-set path and no ray tracing.
|
|
std::unordered_set<std::string> availableExtensions;
|
|
{
|
|
std::uint32_t extCount = 0;
|
|
vkEnumerateDeviceExtensionProperties(physDevice, nullptr, &extCount, nullptr);
|
|
std::vector<VkExtensionProperties> exts(extCount);
|
|
vkEnumerateDeviceExtensionProperties(physDevice, nullptr, &extCount, exts.data());
|
|
for (const VkExtensionProperties& e : exts) {
|
|
availableExtensions.emplace(e.extensionName);
|
|
}
|
|
}
|
|
const auto hasAll = [&availableExtensions](std::span<const char* const> names) {
|
|
return std::ranges::all_of(names, [&](const char* name) { return availableExtensions.contains(name); });
|
|
};
|
|
|
|
// Fail here, naming the gap, rather than in vkCreateDevice with a bare
|
|
// VK_ERROR_EXTENSION_NOT_PRESENT.
|
|
{
|
|
std::string missing;
|
|
for (const char* name : requiredDeviceExtensionNames) {
|
|
if (!availableExtensions.contains(name)) {
|
|
missing += ' ';
|
|
missing += name;
|
|
}
|
|
}
|
|
if (!missing.empty()) {
|
|
throw std::runtime_error(std::format("Crafter.Graphics: the Vulkan driver lacks required device extension(s):{}", missing));
|
|
}
|
|
}
|
|
|
|
// CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the descriptor-set path on a
|
|
// driver that does have the heap, so the fallback can be exercised on a
|
|
// desktop GPU. It takes ray tracing down with it, exactly as on a driver
|
|
// that really lacks the extension.
|
|
//
|
|
// A listed extension is not the whole answer: a driver may list one whose
|
|
// feature bit is still off. Query the feature bits of the optional groups
|
|
// whose extensions are present (only those structs are chained) and
|
|
// require both, so such a driver takes the fallback instead of failing
|
|
// in vkCreateDevice.
|
|
const bool heapExtensions = hasAll(descriptorHeapExtensionNames);
|
|
const bool rayTracingExtensions = hasAll(rayTracingExtensionNames);
|
|
const bool faultExtension = availableExtensions.contains(VK_EXT_DEVICE_FAULT_EXTENSION_NAME);
|
|
VkPhysicalDeviceDescriptorHeapFeaturesEXT heapQuery {
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_HEAP_FEATURES_EXT,
|
|
};
|
|
VkPhysicalDeviceShaderUntypedPointersFeaturesKHR untypedPointersQuery {
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_UNTYPED_POINTERS_FEATURES_KHR,
|
|
};
|
|
VkPhysicalDeviceAccelerationStructureFeaturesKHR accelerationStructureQuery {
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_FEATURES_KHR,
|
|
};
|
|
VkPhysicalDeviceRayTracingPipelineFeaturesKHR rayTracingPipelineQuery {
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_PIPELINE_FEATURES_KHR,
|
|
};
|
|
VkPhysicalDeviceRayQueryFeaturesKHR rayQueryQuery {
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_QUERY_FEATURES_KHR,
|
|
};
|
|
VkPhysicalDeviceFaultFeaturesEXT faultQuery {
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT,
|
|
};
|
|
VkPhysicalDeviceFeatures2 featureQuery {
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
|
|
};
|
|
const auto ask = [&featureQuery](auto& features) {
|
|
features.pNext = featureQuery.pNext;
|
|
featureQuery.pNext = &features;
|
|
};
|
|
if (heapExtensions) {
|
|
ask(heapQuery);
|
|
ask(untypedPointersQuery);
|
|
}
|
|
if (rayTracingExtensions) {
|
|
ask(accelerationStructureQuery);
|
|
ask(rayTracingPipelineQuery);
|
|
ask(rayQueryQuery);
|
|
}
|
|
if (faultExtension) {
|
|
ask(faultQuery);
|
|
}
|
|
vkGetPhysicalDeviceFeatures2(physDevice, &featureQuery);
|
|
|
|
descriptorHeapSupported = heapExtensions
|
|
&& heapQuery.descriptorHeap && untypedPointersQuery.shaderUntypedPointers
|
|
&& std::getenv("CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP") == nullptr;
|
|
rayTracingSupported = descriptorHeapSupported && rayTracingExtensions
|
|
&& accelerationStructureQuery.accelerationStructure
|
|
&& rayTracingPipelineQuery.rayTracingPipeline
|
|
&& rayQueryQuery.rayQuery;
|
|
deviceFaultSupported = faultExtension && faultQuery.deviceFault;
|
|
memoryDecompressionSupported = availableExtensions.contains(VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME);
|
|
const bool atomicFloatSupported = availableExtensions.contains(VK_EXT_SHADER_ATOMIC_FLOAT_EXTENSION_NAME);
|
|
// RAY_TRACING_SHADER is not a valid pipeline stage unless the ray tracing
|
|
// pipeline feature is enabled; barrier code masks its stage flags with this.
|
|
stageMask = rayTracingSupported
|
|
? ~VkPipelineStageFlags{0}
|
|
: ~VkPipelineStageFlags{VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR};
|
|
|
|
// Properties query, built back to front. A struct is chained only when its
|
|
// extension is being used: an unrecognized sType is a validation error on
|
|
// drivers that don't expose the extension, and the heap properties must
|
|
// stay zero when the heap is forced off. Driver properties are core and
|
|
// always queried; they identify the NVIDIA proprietary driver for the
|
|
// descriptor-heap AS-read workaround (issue #15 / #7).
|
|
void* propertiesChain = &driverProperties;
|
|
if (descriptorHeapSupported) {
|
|
descriptorHeapProperties.pNext = propertiesChain;
|
|
propertiesChain = &descriptorHeapProperties;
|
|
}
|
|
if (rayTracingSupported) {
|
|
rayTracingProperties.pNext = propertiesChain;
|
|
propertiesChain = &rayTracingProperties;
|
|
}
|
|
if (memoryDecompressionSupported) {
|
|
memoryDecompressionProperties.pNext = propertiesChain;
|
|
propertiesChain = &memoryDecompressionProperties;
|
|
}
|
|
VkPhysicalDeviceProperties2 properties2 {
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
|
|
.pNext = propertiesChain
|
|
};
|
|
vkGetPhysicalDeviceProperties2(physDevice, &properties2);
|
|
// Keep the core properties around for the pipeline-cache identity check
|
|
// (vendorID / deviceID / pipelineCacheUUID).
|
|
deviceProperties = properties2.properties;
|
|
|
|
// Cache the flush/invalidate alignment for ranged host-memory flushes
|
|
// (VulkanBuffer::FlushDevice(offset,bytes)).
|
|
nonCoherentAtomSize = properties2.properties.limits.nonCoherentAtomSize;
|
|
|
|
// NVIDIA's brand-new VK_EXT_descriptor_heap acceleration-structure read
|
|
// path faults (see #7); enable the SPIR-V rewrite workaround there. Other
|
|
// drivers (and any future fixed NVIDIA driver, once this gate is removed)
|
|
// take the normal heap-bound AS path unchanged. The workaround is about
|
|
// the heap, so it is off whenever the heap is: otherwise a forced-fallback
|
|
// run on NVIDIA would still rewrite SPIR-V, enable shaderInt64 and push the
|
|
// TLAS address through vkCmdPushDataEXT, none of which a real heap-less
|
|
// driver does. 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<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
|
|
vkGetPhysicalDeviceQueueFamilyProperties(physDevice, &queueFamilyCount, queueFamilies.data());
|
|
|
|
for (uint32_t i = 0; i < queueFamilyCount; i++)
|
|
{
|
|
if (queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)
|
|
{
|
|
queueFamilyIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
float priority = 1;
|
|
|
|
VkDeviceQueueCreateInfo queueCreateInfo = {};
|
|
queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
|
|
queueCreateInfo.queueFamilyIndex = queueFamilyIndex;
|
|
queueCreateInfo.queueCount = 1;
|
|
queueCreateInfo.pQueuePriorities = &priority;
|
|
|
|
// Feature structs below are declared without a pNext and linked into one
|
|
// chain afterwards, each only when its extension is being enabled: a
|
|
// feature struct for an extension that is not enabled is invalid, and
|
|
// which ones are enabled now depends on the driver.
|
|
VkPhysicalDeviceFaultFeaturesEXT faultFeatures = {
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT,
|
|
.deviceFault = VK_TRUE,
|
|
.deviceFaultVendorBinary = faultQuery.deviceFaultVendorBinary,
|
|
};
|
|
|
|
VkPhysicalDeviceShaderUntypedPointersFeaturesKHR untypedPointersFeatures {
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_UNTYPED_POINTERS_FEATURES_KHR,
|
|
.shaderUntypedPointers = VK_TRUE,
|
|
};
|
|
|
|
// Enables synchronization2 sentinels (VkMemoryBarrier2, VK_PIPELINE_STAGE_2_*,
|
|
// VK_ACCESS_2_*) — required for VK_EXT_memory_decompression's sync tokens.
|
|
VkPhysicalDeviceVulkan13Features features13 {
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES,
|
|
.synchronization2 = VK_TRUE,
|
|
};
|
|
|
|
VkPhysicalDeviceMemoryDecompressionFeaturesEXT memoryDecompressionFeatures {
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_DECOMPRESSION_FEATURES_EXT,
|
|
.memoryDecompression = VK_TRUE,
|
|
};
|
|
|
|
VkPhysicalDeviceDescriptorHeapFeaturesEXT desciptorHeapFeatures {
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_HEAP_FEATURES_EXT,
|
|
.descriptorHeap = VK_TRUE,
|
|
};
|
|
|
|
// Every VulkanBuffer is created with usage = 0 plus a
|
|
// VkBufferUsageFlags2CreateInfo, and heap pipelines carry a
|
|
// VkPipelineCreateFlags2CreateInfo. Both are maintenance5 functionality,
|
|
// which needs the feature bit as well as the extension name.
|
|
VkPhysicalDeviceMaintenance5Features maintenance5Features {
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES,
|
|
.maintenance5 = VK_TRUE,
|
|
};
|
|
|
|
VkPhysicalDevice16BitStorageFeatures bit16 {
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES,
|
|
.storageBuffer16BitAccess = VK_TRUE,
|
|
};
|
|
|
|
VkPhysicalDeviceVulkan12Features features12 {
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES,
|
|
.shaderFloat16 = VK_TRUE,
|
|
// Bindless / runtime descriptor array indexing — needed by both
|
|
// binding models (heap and descriptor sets).
|
|
.shaderUniformBufferArrayNonUniformIndexing = VK_TRUE,
|
|
.shaderSampledImageArrayNonUniformIndexing = VK_TRUE,
|
|
.shaderStorageBufferArrayNonUniformIndexing = VK_TRUE,
|
|
.shaderStorageImageArrayNonUniformIndexing = VK_TRUE,
|
|
.runtimeDescriptorArray = VK_TRUE,
|
|
.scalarBlockLayout = VK_TRUE,
|
|
.bufferDeviceAddress = VK_TRUE
|
|
};
|
|
if (!descriptorHeapSupported) {
|
|
// Descriptor-set path (DescriptorHeapVulkan sets mode): one bindless
|
|
// set whose slots are written while earlier frames may still be
|
|
// executing with it bound, and where most slots are never written.
|
|
// The sampled-image bit also covers SAMPLER descriptors.
|
|
features12.descriptorIndexing = VK_TRUE;
|
|
features12.descriptorBindingSampledImageUpdateAfterBind = VK_TRUE;
|
|
features12.descriptorBindingStorageImageUpdateAfterBind = VK_TRUE;
|
|
features12.descriptorBindingStorageBufferUpdateAfterBind = VK_TRUE;
|
|
features12.descriptorBindingUpdateUnusedWhilePending = VK_TRUE;
|
|
features12.descriptorBindingPartiallyBound = VK_TRUE;
|
|
}
|
|
|
|
VkPhysicalDeviceRayQueryFeaturesKHR physicalDeviceRayQueryFeatures{
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_QUERY_FEATURES_KHR,
|
|
.rayQuery = VK_TRUE
|
|
};
|
|
|
|
VkPhysicalDeviceRayTracingPipelineFeaturesKHR physicalDeviceRayTracingPipelineFeatures{
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_PIPELINE_FEATURES_KHR,
|
|
.rayTracingPipeline = VK_TRUE
|
|
};
|
|
|
|
VkPhysicalDeviceAccelerationStructureFeaturesKHR deviceAccelerationStructureFeature = {
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_FEATURES_KHR,
|
|
.accelerationStructure = VK_TRUE
|
|
};
|
|
|
|
void* featureChain = nullptr;
|
|
const auto link = [&featureChain](auto& features) {
|
|
features.pNext = featureChain;
|
|
featureChain = &features;
|
|
};
|
|
if (deviceFaultSupported) {
|
|
link(faultFeatures);
|
|
}
|
|
if (descriptorHeapSupported) {
|
|
link(untypedPointersFeatures);
|
|
}
|
|
link(features13);
|
|
if (memoryDecompressionSupported) {
|
|
link(memoryDecompressionFeatures);
|
|
}
|
|
if (descriptorHeapSupported) {
|
|
link(desciptorHeapFeatures);
|
|
}
|
|
link(maintenance5Features);
|
|
link(bit16);
|
|
link(features12);
|
|
if (rayTracingSupported) {
|
|
link(physicalDeviceRayQueryFeatures);
|
|
link(physicalDeviceRayTracingPipelineFeatures);
|
|
link(deviceAccelerationStructureFeature);
|
|
}
|
|
|
|
VkPhysicalDeviceFeatures2 physical_features2 = {
|
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
|
|
.pNext = featureChain,
|
|
.features = {
|
|
// Order matches VkPhysicalDeviceFeatures declaration so the
|
|
// designated-initializer-order warning stays quiet.
|
|
.samplerAnisotropy = VK_TRUE,
|
|
.shaderStorageImageReadWithoutFormat = VK_TRUE,
|
|
.shaderStorageImageWriteWithoutFormat = VK_TRUE,
|
|
// Bindless dynamic indexing — required to index `images[]`,
|
|
// `textures[]`, `samplers[]`, `itemHeap[]` with a runtime value.
|
|
.shaderSampledImageArrayDynamicIndexing = VK_TRUE,
|
|
.shaderStorageBufferArrayDynamicIndexing = VK_TRUE,
|
|
.shaderStorageImageArrayDynamicIndexing = VK_TRUE,
|
|
// shaderInt64: only needed for the NVIDIA descriptor-heap AS-read
|
|
// workaround (issue #15 / #7), which loads the TLAS device address
|
|
// as a 64-bit push constant. Gated so it isn't required on drivers
|
|
// that don't take the workaround path. Remove with the workaround.
|
|
.shaderInt64 = workaroundDescriptorHeapAS ? VK_TRUE : VK_FALSE,
|
|
.shaderInt16 = VK_TRUE
|
|
}
|
|
};
|
|
|
|
// The enabled-extension list is the required set plus each optional group
|
|
// the driver advertises in full (decided above from the enumeration).
|
|
std::vector<const char*> enabledDeviceExtensions(
|
|
std::begin(requiredDeviceExtensionNames),
|
|
std::end(requiredDeviceExtensionNames));
|
|
if (descriptorHeapSupported) {
|
|
enabledDeviceExtensions.append_range(descriptorHeapExtensionNames);
|
|
}
|
|
if (rayTracingSupported) {
|
|
enabledDeviceExtensions.append_range(rayTracingExtensionNames);
|
|
}
|
|
if (deviceFaultSupported) {
|
|
enabledDeviceExtensions.push_back(VK_EXT_DEVICE_FAULT_EXTENSION_NAME);
|
|
}
|
|
if (atomicFloatSupported) {
|
|
enabledDeviceExtensions.push_back(VK_EXT_SHADER_ATOMIC_FLOAT_EXTENSION_NAME);
|
|
}
|
|
if (memoryDecompressionSupported) {
|
|
enabledDeviceExtensions.push_back(VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME);
|
|
}
|
|
|
|
VkDeviceCreateInfo deviceCreateInfo = {};
|
|
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
|
|
deviceCreateInfo.queueCreateInfoCount = 1;
|
|
deviceCreateInfo.pQueueCreateInfos = &queueCreateInfo;
|
|
deviceCreateInfo.enabledExtensionCount = static_cast<std::uint32_t>(enabledDeviceExtensions.size());
|
|
deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data();
|
|
deviceCreateInfo.pNext = &physical_features2;
|
|
|
|
// Device layers are deprecated and have been ignored since Vulkan 1.0;
|
|
// enabling them is a validation error. Layers are enabled at instance
|
|
// creation only, so leave enabledLayerCount at 0.
|
|
deviceCreateInfo.enabledLayerCount = 0;
|
|
deviceCreateInfo.ppEnabledLayerNames = nullptr;
|
|
|
|
CheckVkResult(vkCreateDevice(physDevice, &deviceCreateInfo, NULL, &device));
|
|
vkGetDeviceQueue(device, queueFamilyIndex, 0, &queue);
|
|
|
|
VkCommandPoolCreateInfo commandPoolcreateInfo = {};
|
|
commandPoolcreateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
|
|
commandPoolcreateInfo.queueFamilyIndex = queueFamilyIndex;
|
|
commandPoolcreateInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
|
CheckVkResult(vkCreateCommandPool(device, &commandPoolcreateInfo, NULL, &commandPool));
|
|
|
|
vkGetPhysicalDeviceMemoryProperties(physDevice, &memoryProperties);
|
|
CacheUploadStrategy();
|
|
|
|
vkGetAccelerationStructureBuildSizesKHR = reinterpret_cast<PFN_vkGetAccelerationStructureBuildSizesKHR>(vkGetInstanceProcAddr(instance, "vkGetAccelerationStructureBuildSizesKHR"));
|
|
vkCreateAccelerationStructureKHR = reinterpret_cast<PFN_vkCreateAccelerationStructureKHR>(vkGetInstanceProcAddr(instance, "vkCreateAccelerationStructureKHR"));
|
|
vkDestroyAccelerationStructureKHR = reinterpret_cast<PFN_vkDestroyAccelerationStructureKHR>(vkGetInstanceProcAddr(instance, "vkDestroyAccelerationStructureKHR"));
|
|
vkCmdBuildAccelerationStructuresKHR = reinterpret_cast<PFN_vkCmdBuildAccelerationStructuresKHR>(vkGetInstanceProcAddr(instance, "vkCmdBuildAccelerationStructuresKHR"));
|
|
vkGetAccelerationStructureDeviceAddressKHR = reinterpret_cast<PFN_vkGetAccelerationStructureDeviceAddressKHR>(vkGetInstanceProcAddr(instance, "vkGetAccelerationStructureDeviceAddressKHR"));
|
|
vkCreateRayTracingPipelinesKHR = reinterpret_cast<PFN_vkCreateRayTracingPipelinesKHR>(vkGetInstanceProcAddr(instance, "vkCreateRayTracingPipelinesKHR"));
|
|
vkGetRayTracingShaderGroupHandlesKHR = reinterpret_cast<PFN_vkGetRayTracingShaderGroupHandlesKHR>(vkGetInstanceProcAddr(instance, "vkGetRayTracingShaderGroupHandlesKHR"));
|
|
vkCmdTraceRaysKHR = reinterpret_cast<PFN_vkCmdTraceRaysKHR>(vkGetInstanceProcAddr(instance, "vkCmdTraceRaysKHR"));
|
|
vkCmdBindResourceHeapEXT = reinterpret_cast<PFN_vkCmdBindResourceHeapEXT>(vkGetInstanceProcAddr(instance, "vkCmdBindResourceHeapEXT"));
|
|
vkCmdBindSamplerHeapEXT = reinterpret_cast<PFN_vkCmdBindSamplerHeapEXT>(vkGetInstanceProcAddr(instance, "vkCmdBindSamplerHeapEXT"));
|
|
vkWriteResourceDescriptorsEXT = reinterpret_cast<PFN_vkWriteResourceDescriptorsEXT>(vkGetInstanceProcAddr(instance, "vkWriteResourceDescriptorsEXT"));
|
|
vkWriteSamplerDescriptorsEXT = reinterpret_cast<PFN_vkWriteSamplerDescriptorsEXT>(vkGetInstanceProcAddr(instance, "vkWriteSamplerDescriptorsEXT"));
|
|
vkCmdPushDataEXT = reinterpret_cast<PFN_vkCmdPushDataEXT>(vkGetInstanceProcAddr(instance, "vkCmdPushDataEXT"));
|
|
vkGetPhysicalDeviceDescriptorSizeEXT = reinterpret_cast<PFN_vkGetPhysicalDeviceDescriptorSizeEXT>(vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceDescriptorSizeEXT"));
|
|
vkGetDeviceFaultInfoEXT = reinterpret_cast<PFN_vkGetDeviceFaultInfoEXT>(vkGetInstanceProcAddr(instance, "vkGetDeviceFaultInfoEXT"));
|
|
|
|
if (memoryDecompressionSupported) {
|
|
// vkGetDeviceProcAddr skips the loader trampoline that vkGetInstanceProcAddr
|
|
// requires for device-level functions. The other PFNs above predate this
|
|
// realization; opportunistic adoption for new entry points only.
|
|
vkCmdDecompressMemoryEXT = reinterpret_cast<PFN_vkCmdDecompressMemoryEXT>(
|
|
vkGetDeviceProcAddr(device, "vkCmdDecompressMemoryEXT"));
|
|
if (vkCmdDecompressMemoryEXT == nullptr) {
|
|
// Driver advertised the extension but didn't expose the entry
|
|
// point — defensively fall back to CPU decode.
|
|
memoryDecompressionSupported = false;
|
|
}
|
|
}
|
|
|
|
// Create the shared pipeline cache (seeded from disk when a compatible
|
|
// blob exists) and arrange for it to be written back at process exit. The
|
|
// device handle is a never-destroyed static, so it is still valid when the
|
|
// atexit handler runs. Registered once — Initialize is the single device
|
|
// bring-up.
|
|
LoadPipelineCache();
|
|
std::atexit(SavePipelineCache);
|
|
}
|
|
|
|
std::uint32_t Device::GetMemoryType(uint32_t typeBits, VkMemoryPropertyFlags required, VkMemoryPropertyFlags preferred) {
|
|
// First pass: honour the preferred bits on top of the required ones.
|
|
// Second pass (when nothing matched, or no preferences were given): drop
|
|
// the preferences and match the required bits alone. Vulkan orders the
|
|
// memory types so first-superset-match is generally optimal, so a plain
|
|
// linear scan suffices for each pass. We only throw when even `required`
|
|
// cannot be satisfied — i.e. no valid memory exists for the allocation.
|
|
for (VkMemoryPropertyFlags wanted : {required | preferred, required})
|
|
{
|
|
uint32_t bits = typeBits;
|
|
for (uint32_t i = 0; i < memoryProperties.memoryTypeCount; i++)
|
|
{
|
|
if ((bits & 1) == 1)
|
|
{
|
|
if ((memoryProperties.memoryTypes[i].propertyFlags & wanted) == wanted)
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
bits >>= 1;
|
|
}
|
|
|
|
// No preferences to fall back from — the second pass would be identical.
|
|
if (preferred == 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
throw std::runtime_error("Could not find a matching memory type");
|
|
}
|
|
|
|
void Device::CacheUploadStrategy() {
|
|
constexpr VkMemoryPropertyFlags directFlags =
|
|
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT;
|
|
|
|
// 1. Find a DEVICE_LOCAL | HOST_VISIBLE type and remember its heap. Without
|
|
// one (no resizable BAR), direct writes are impossible -> must stage.
|
|
bool haveDirectType = false;
|
|
std::uint32_t barHeapIndex = 0;
|
|
for (std::uint32_t i = 0; i < memoryProperties.memoryTypeCount; ++i) {
|
|
if ((memoryProperties.memoryTypes[i].propertyFlags & directFlags) == directFlags) {
|
|
haveDirectType = true;
|
|
barHeapIndex = memoryProperties.memoryTypes[i].heapIndex;
|
|
break;
|
|
}
|
|
}
|
|
if (!haveDirectType) {
|
|
directWriteBudget = 0;
|
|
return;
|
|
}
|
|
|
|
// 2. Compare the host-visible device-local heap against the largest plain
|
|
// DEVICE_LOCAL heap (the true VRAM size).
|
|
VkDeviceSize barHeap = memoryProperties.memoryHeaps[barHeapIndex].size;
|
|
VkDeviceSize vram = 0;
|
|
for (std::uint32_t i = 0; i < memoryProperties.memoryHeapCount; ++i) {
|
|
if (memoryProperties.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) {
|
|
vram = std::max(vram, memoryProperties.memoryHeaps[i].size);
|
|
}
|
|
}
|
|
|
|
// 3. ReBAR / UMA: the BAR heap covers (almost) all of VRAM -> map any size.
|
|
// barHeap >= 0.9 * vram, written to dodge floating point / overflow.
|
|
if (barHeap >= vram - vram / 10) {
|
|
directWriteBudget = std::numeric_limits<VkDeviceSize>::max();
|
|
return;
|
|
}
|
|
|
|
// 4. Small BAR window: direct only for buffers small enough that several
|
|
// still fit the window — large buffers stage so they don't exhaust it.
|
|
// The budget is tied to the actual window size rather than a magic
|
|
// constant so it adapts across devices (a fixed cap would be wrong for
|
|
// both a 256 MiB and a 4 GiB window). VK_EXT_memory_budget would refine
|
|
// this to the window's *remaining* space; see the header note.
|
|
constexpr VkDeviceSize barWindowFraction = 8;
|
|
directWriteBudget = barHeap / barWindowFraction;
|
|
}
|
|
|
|
bool Device::PreferDirectDeviceWrite(VkDeviceSize size) {
|
|
// directWriteBudget caches the size-independent decision (CacheUploadStrategy):
|
|
// 0 -> never direct, max -> ReBAR/UMA so any size, else the small-window cap.
|
|
return directWriteBudget != 0 && size <= directWriteBudget;
|
|
}
|
|
|
|
void Device::EnqueueDeletion(VkBuffer buffer, VkDeviceMemory memory) {
|
|
// Nothing to free for an already-null handle — avoids parking dead
|
|
// entries that ReclaimDeletions would have to skip over.
|
|
if (buffer == VK_NULL_HANDLE) return;
|
|
deletionQueue.push_back({ frameCounter, buffer, memory });
|
|
}
|
|
|
|
void Device::ReclaimDeletions() {
|
|
// Free every entry whose retire frame has been reached. An entry tagged at
|
|
// frame F retires at F + framesInFlight: by the time the CPU has begun that
|
|
// many later frames (each gated by a per-image fence wait), single-queue
|
|
// submission order guarantees all GPU work from frame F is complete.
|
|
//
|
|
// frameCounter is monotonic, so retireAfter is non-decreasing down the
|
|
// queue and the ready entries are a contiguous prefix: pop them off the
|
|
// front and stop at the first one still in flight (everything behind it is
|
|
// newer, hence also in flight). O(ready), not O(queue).
|
|
while (!deletionQueue.empty() &&
|
|
deletionQueue.front().retireAfter + framesInFlight <= frameCounter) {
|
|
const PendingDeletion& entry = deletionQueue.front();
|
|
vkDestroyBuffer(device, entry.buffer, nullptr);
|
|
vkFreeMemory(device, entry.memory, nullptr);
|
|
deletionQueue.pop_front();
|
|
}
|
|
}
|
|
|
|
void Device::DrainDeletions() {
|
|
// Unconditional: the caller has just wait-idled, so no in-flight GPU work
|
|
// can still reference any queued resource.
|
|
for (PendingDeletion& entry : deletionQueue) {
|
|
vkDestroyBuffer(device, entry.buffer, nullptr);
|
|
vkFreeMemory(device, entry.memory, nullptr);
|
|
}
|
|
deletionQueue.clear();
|
|
} |