Files
Valiantsin DzerakhandClaude Opus 5.5 29cb79086f Take 64 MiB blocks from the driver for buffers and other images
Since the textures moved to a pool of their own, the default device-local
blocks hold mirrors and other buffers. With VMA's 256 MiB blocks one block
held 53 MiB of them after 10 minutes in the field, and the headroom thread
kept half a block of each device-local type ahead. TOTK_VMA_BLOCK_MIB
(default 64, 0 = VMA's 256) sets the size; the headroom spare follows it.

On the RX 9070, 10 minutes in the field: VMA blocks 3582 -> 3252 MiB,
process commit 8614 -> 8327 MB, slow frames 13 (2.8 s) -> 9 (1.1 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 10:51:49 +03:00

607 lines
35 KiB
C

// Vulkan instance, physical device choice, logical device, VMA, timeline semaphore and the validation messenger.
// Ported from LAS Recomp, runtime/nvn/vulkan/device.c:33-330
// (https://github.com/peiikai/LASRecomp @ da35b83a9611d046a9a681aae668d7085954d37f, MIT, Copyright (c) 2026 Peiikai),
// without SDL. The VMA implementation is runtime/vk_vma.cpp (VMA is C++ only).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "host.h"
#include "vk.h"
#include "platform/platform.h"
#include "vk_dlaa.h"
static VkCtx g_vk;
static int g_ready;
static PlatMutex g_lock = PLAT_MUTEX_INIT;
// ---- GPU address map for device faults
// VK_EXT_device_address_binding_report: the driver reports each bind and unbind of an object's GPU virtual range through
// the debug messenger. The newest AB_N events stay in a ring written without a lock (one atomic add each), so a device
// fault names the object behind its address and whether it was unbound before the fault, which is memory freed while
// a submission still used it. One event per image, buffer or memory block made or freed, none per draw.
// TOTK_ADDR_REPORT=0 turns it off.
#define AB_N (1u << 15)
typedef struct { uint64_t seq, base, size, handle, issued, done; int32_t type; uint32_t unbind; } AbEvent;
static AbEvent *ab_ring;
static uint64_t ab_head;
static int ab_want; // the instance has the messenger for it (create_instance)
static void ab_record(const VkDebugUtilsMessengerCallbackDataEXT *d) {
const VkDeviceAddressBindingCallbackDataEXT *b = NULL;
for (const VkBaseInStructure *p = d->pNext; p; p = p->pNext)
if (p->sType == VK_STRUCTURE_TYPE_DEVICE_ADDRESS_BINDING_CALLBACK_DATA_EXT) b = (const void *)p;
if (!b || !ab_ring) return;
uint64_t i = __atomic_fetch_add(&ab_head, 1, __ATOMIC_RELAXED);
AbEvent *e = &ab_ring[i & (AB_N - 1)];
__atomic_store_n(&e->seq, 0, __ATOMIC_RELAXED);
e->base = b->baseAddress, e->size = b->size;
e->handle = d->objectCount ? d->pObjects[0].objectHandle : 0;
e->type = d->objectCount ? (int32_t)d->pObjects[0].objectType : 0;
e->unbind = b->bindingType == VK_DEVICE_ADDRESS_BINDING_TYPE_UNBIND_EXT;
e->issued = vk_sync_issued(), e->done = vk_sync_fence_done();
__atomic_store_n(&e->seq, i + 1, __ATOMIC_RELEASE);
}
static const char *ab_type_name(int32_t t, char *buf, size_t n) {
switch (t) {
case VK_OBJECT_TYPE_IMAGE: return "image";
case VK_OBJECT_TYPE_BUFFER: return "buffer";
case VK_OBJECT_TYPE_DEVICE_MEMORY: return "memory";
default: snprintf(buf, n, "object type %d", (int)t); return buf;
}
}
// The newest events whose range meets the precision-aligned range around a fault address, newest first.
static void ab_report(uint64_t addr, uint64_t precision) {
if (!ab_ring) return;
uint64_t lo = precision > 1 ? addr & ~(precision - 1) : addr, hi = lo + (precision > 1 ? precision : 1);
uint64_t head = __atomic_load_n(&ab_head, __ATOMIC_ACQUIRE), n = head < AB_N ? head : AB_N;
int found = 0;
for (uint64_t k = 1; k <= n && found < 6; k++) {
const AbEvent *e = &ab_ring[(head - k) & (AB_N - 1)];
if (__atomic_load_n(&e->seq, __ATOMIC_ACQUIRE) != head - k + 1) continue;
if (e->base >= hi || e->base + e->size <= lo) continue;
found++;
char tn[32];
rt_log("vk: fault address %#llx: %s %#llx [%#llx, +%#llx] %s %llu address events before the fault, "
"submitted %llu, done %llu", (unsigned long long)addr, ab_type_name(e->type, tn, sizeof tn),(unsigned long long)e->handle,
(unsigned long long)e->base, (unsigned long long)e->size, e->unbind ? "unbound" : "bound",
(unsigned long long)k, (unsigned long long)e->issued, (unsigned long long)e->done);
}
if (!found) rt_log("vk: fault address %#llx: no binding in the last %llu address events", (unsigned long long)addr,
(unsigned long long)n);
}
static VKAPI_ATTR VkBool32 VKAPI_CALL debug_cb(VkDebugUtilsMessageSeverityFlagBitsEXT sev, VkDebugUtilsMessageTypeFlagsEXT type,
const VkDebugUtilsMessengerCallbackDataEXT *d, void *user) {
(void)user;
if (type & VK_DEBUG_UTILS_MESSAGE_TYPE_DEVICE_ADDRESS_BINDING_BIT_EXT) { ab_record(d); return VK_FALSE; }
if (sev < VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) return VK_FALSE; // info, asked for the address events
int err = (sev & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) != 0;
if (err) plat_inc32(&g_vk.validation_errors);
static volatile int32_t logged;
if (plat_inc32(&logged) <= 500) rt_log("vk validation %s: %s", err ? "error" : "warning", d->pMessage);
return VK_FALSE;
}
static int has_ext(const VkExtensionProperties *e, uint32_t n, const char *name) {
for (uint32_t i = 0; i < n; i++) if (!strcmp(e[i].extensionName, name)) return 1;
return 0;
}
static int layer_present(const char *name) {
uint32_t n = 0;
vkEnumerateInstanceLayerProperties(&n, NULL);
VkLayerProperties *l = calloc(n ? n : 1, sizeof *l);
vkEnumerateInstanceLayerProperties(&n, l);
int found = 0;
for (uint32_t i = 0; i < n; i++) if (!strcmp(l[i].layerName, name)) found = 1;
free(l);
return found;
}
static int instance_ext_present(const char *name) {
uint32_t n = 0;
vkEnumerateInstanceExtensionProperties(NULL, &n, NULL);
VkExtensionProperties *e = calloc(n ? n : 1, sizeof *e);
vkEnumerateInstanceExtensionProperties(NULL, &n, e);
int found = 0;
for (uint32_t i = 0; i < n; i++) if (!strcmp(e[i].extensionName, name)) found = 1;
free(e);
return found;
}
// Mesa's drivers (RADV on Linux and the Steam Deck) keep a shader cache of their own beside our pipeline cache, by
// default in ~/.cache and up to 1 GB: about 35 MB in a minute of the title screen. A Deck's /home is small and the game
// usually sits on the SD card, so unless MESA_SHADER_CACHE_DIR names a folder, it goes to mesa-cache beside the exe.
static void mesa_cache_beside_exe(void) {
char dir[PLAT_PATH_MAX * 2];
if (plat_getenv("MESA_SHADER_CACHE_DIR", dir, sizeof dir)) return;
int len = plat_exe_path(dir, sizeof dir - 16);
char *cut = len ? plat_last_sep(dir) : NULL;
if (!cut) return;
strcpy(cut + 1, "mesa-cache");
plat_setenv("MESA_SHADER_CACHE_DIR", dir);
}
static int create_instance(void) {
mesa_cache_beside_exe();
const char *ext[12];
uint32_t n_ext = (uint32_t)gfx_window_vk_extensions(ext, 7);
const char *layers[] = {"VK_LAYER_KHRONOS_validation"};
int validate = cfg_bool("TOTK_VK_VALIDATION");
if (validate && !layer_present(layers[0])) {
rt_log("vk: TOTK_VK_VALIDATION set but %s is not installed; continuing without", layers[0]);
validate = 0;
}
const char *ar = cfg_get("TOTK_ADDR_REPORT");
ab_want = !(ar && !strcmp(ar, "0")) && instance_ext_present(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
if (ab_want && !ab_ring && !(ab_ring = calloc(AB_N, sizeof *ab_ring))) ab_want = 0;
if (validate || ab_want) ext[n_ext++] = VK_EXT_DEBUG_UTILS_EXTENSION_NAME;
if (vk_hdr_wanted() && instance_ext_present(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)) // HDR10 surface formats
ext[n_ext++] = VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME;
vk_dlaa_instance_exts(ext, &n_ext, 12);
VkApplicationInfo app = {.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, .pApplicationName = "TotK Recomp",
.pEngineName = "TotK Recomp", .apiVersion = VK_API_VERSION_1_3};
VkInstanceCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, .pApplicationInfo = &app,
.enabledExtensionCount = n_ext, .ppEnabledExtensionNames = ext,
.enabledLayerCount = validate ? 1 : 0, .ppEnabledLayerNames = layers};
rdoc_init();
VkResult r = vkCreateInstance(&ci, NULL, &g_vk.instance);
if (r != VK_SUCCESS) { rt_log("vk: vkCreateInstance failed (%d); is a Vulkan 1.3 driver installed?", (int)r); return 0; }
g_vk.validation = validate;
if (validate || ab_want) {
PFN_vkCreateDebugUtilsMessengerEXT f =
(PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(g_vk.instance, "vkCreateDebugUtilsMessengerEXT");
VkDebugUtilsMessengerCreateInfoEXT mi = {
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT,
.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT |
(ab_want ? VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT : 0),
.messageType = (validate ? VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT : 0) |
(ab_want ? VK_DEBUG_UTILS_MESSAGE_TYPE_DEVICE_ADDRESS_BINDING_BIT_EXT : 0),
.pfnUserCallback = debug_cb};
if (!f || f(g_vk.instance, &mi, NULL, &g_vk.messenger) != VK_SUCCESS) ab_want = 0;
}
return 1;
}
static const char *const k_required_ext[] = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME, VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME, VK_EXT_DEPTH_CLIP_CONTROL_EXTENSION_NAME,
VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME};
// Name of the first missing required extension, or NULL.
static const char *missing_required(VkPhysicalDevice pd) {
uint32_t n = 0;
vkEnumerateDeviceExtensionProperties(pd, NULL, &n, NULL);
VkExtensionProperties *e = calloc(n ? n : 1, sizeof *e);
vkEnumerateDeviceExtensionProperties(pd, NULL, &n, e);
const char *miss = NULL;
for (size_t i = 0; i < sizeof k_required_ext / sizeof *k_required_ext && !miss; i++)
if (!has_ext(e, n, k_required_ext[i])) miss = k_required_ext[i];
free(e);
return miss;
}
// Graphics+compute family that can present to the window.
static uint32_t find_gfx_family(VkPhysicalDevice pd, const VkQueueFamilyProperties *qp, uint32_t nq) {
for (uint32_t i = 0; i < nq; i++)
if ((qp[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && (qp[i].queueFlags & VK_QUEUE_COMPUTE_BIT) &&
gfx_window_vk_presentable(g_vk.instance, pd, i))
return i;
return UINT32_MAX;
}
static int name_has(const char *s, const char *sub) {
for (size_t n = strlen(sub); *s; s++)
if (!strncasecmp(s, sub, n)) return 1;
return 0;
}
// First discrete GPU with Vulkan 1.3, the required extensions and a graphics+present queue; TOTK_GPU_INDEX (index) or
// TOTK_GPU_NAME (case-insensitive part of the name) overrides.
static int pick_device(void) {
uint32_t n = 0;
vkEnumeratePhysicalDevices(g_vk.instance, &n, NULL);
if (!n) { rt_log("vk: no Vulkan device"); return 0; }
VkPhysicalDevice *pd = calloc(n, sizeof *pd);
vkEnumeratePhysicalDevices(g_vk.instance, &n, pd);
const char *e = cfg_get("TOTK_GPU_INDEX"); // no fixed default: the best device when unset
int want = e && *e ? atoi(e) : -1;
const char *name = cfg_get("TOTK_GPU_NAME");
int best = -1, best_score = -1;
for (uint32_t i = 0; i < n; i++) {
VkPhysicalDeviceProperties p;
vkGetPhysicalDeviceProperties(pd[i], &p);
rt_log("vk: gpu %u: %s (type %d, api %u.%u.%u)", i, p.deviceName, (int)p.deviceType, VK_API_VERSION_MAJOR(p.apiVersion),
VK_API_VERSION_MINOR(p.apiVersion), VK_API_VERSION_PATCH(p.apiVersion));
if (want >= 0 && (int)i != want) continue;
if (name && *name && !name_has(p.deviceName, name)) continue;
if (p.apiVersion < VK_API_VERSION_1_3) { rt_log("vk: gpu %u skipped: Vulkan below 1.3", i); continue; }
const char *miss = missing_required(pd[i]);
if (miss) { rt_log("vk: gpu %u skipped: missing extension %s", i, miss); continue; }
uint32_t nq = 0;
vkGetPhysicalDeviceQueueFamilyProperties(pd[i], &nq, NULL);
VkQueueFamilyProperties *qp = calloc(nq ? nq : 1, sizeof *qp);
vkGetPhysicalDeviceQueueFamilyProperties(pd[i], &nq, qp);
uint32_t fam = find_gfx_family(pd[i], qp, nq);
free(qp);
if (fam == UINT32_MAX) { rt_log("vk: gpu %u skipped: no graphics+compute+present queue family", i); continue; }
int score = (int)i == want ? 100 : p.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU ? 3
: p.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU ? 2 : 1;
if (score > best_score) { best_score = score; best = (int)i; }
}
if (best < 0) {
if (want >= 0) rt_log("vk: TOTK_GPU_INDEX=%d is not usable (see above)", want);
else if (name && *name) rt_log("vk: no usable gpu matches TOTK_GPU_NAME=%s (see above)", name);
else rt_log("vk: no device with Vulkan 1.3 and the required extensions (see above)");
free(pd);
return 0;
}
g_vk.phys = pd[best];
g_vk.device_index = best;
free(pd);
vkGetPhysicalDeviceProperties(g_vk.phys, &g_vk.props);
vkGetPhysicalDeviceMemoryProperties(g_vk.phys, &g_vk.mem_props);
uint32_t nq = 0;
vkGetPhysicalDeviceQueueFamilyProperties(g_vk.phys, &nq, NULL);
VkQueueFamilyProperties *qp = calloc(nq ? nq : 1, sizeof *qp);
vkGetPhysicalDeviceQueueFamilyProperties(g_vk.phys, &nq, qp);
g_vk.gfx_family = find_gfx_family(g_vk.phys, qp, nq);
g_vk.xfer_family = g_vk.gfx_family;
for (uint32_t i = 0; i < nq; i++) // a family that does transfer only (async DMA engine)
if ((qp[i].queueFlags & VK_QUEUE_TRANSFER_BIT) && !(qp[i].queueFlags & (VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT))) {
g_vk.xfer_family = i;
break;
}
free(qp);
rt_log("vk: using gpu %d: %s", best, g_vk.props.deviceName);
vk_mem_class_log(&g_vk.props, &g_vk.mem_props);
return 1;
}
typedef struct {
VkPhysicalDeviceFeatures2 f2;
VkPhysicalDeviceVulkan11Features f11;
VkPhysicalDeviceVulkan12Features f12;
VkPhysicalDeviceVulkan13Features f13;
VkPhysicalDeviceDepthClipControlFeaturesEXT clip;
VkPhysicalDeviceCustomBorderColorFeaturesEXT cbc;
VkPhysicalDeviceExtendedDynamicStateFeaturesEXT eds;
VkPhysicalDeviceExtendedDynamicState2FeaturesEXT eds2;
VkPhysicalDeviceExtendedDynamicState3FeaturesEXT eds3;
VkPhysicalDeviceVertexInputDynamicStateFeaturesEXT vids;
VkPhysicalDeviceConditionalRenderingFeaturesEXT cond;
VkPhysicalDeviceTransformFeedbackFeaturesEXT xfb;
VkPhysicalDeviceProvokingVertexFeaturesEXT pv;
VkPhysicalDeviceRobustness2FeaturesEXT rob2;
VkPhysicalDeviceFaultFeaturesEXT fault;
VkPhysicalDeviceFragmentShaderInterlockFeaturesEXT fsi;
VkPhysicalDeviceGraphicsPipelineLibraryFeaturesEXT gpl;
VkPhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesEXT fbl;
VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR lr;
VkPhysicalDeviceAddressBindingReportFeaturesEXT abr;
} FeatureChain;
static int create_device(void) {
uint32_t ne = 0;
vkEnumerateDeviceExtensionProperties(g_vk.phys, NULL, &ne, NULL);
VkExtensionProperties *ep = calloc(ne ? ne : 1, sizeof *ep);
vkEnumerateDeviceExtensionProperties(g_vk.phys, NULL, &ne, ep);
const char *ext[48];
uint32_t n_ext = 0;
for (size_t i = 0; i < sizeof k_required_ext / sizeof *k_required_ext; i++) ext[n_ext++] = k_required_ext[i];
FeatureChain *c = calloc(1, sizeof *c);
void **tail = &c->f2.pNext;
#define CHAIN(member, stype) do { c->member.sType = stype; *tail = &c->member; tail = &c->member.pNext; } while (0)
c->f2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
CHAIN(f11, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES);
CHAIN(f12, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES);
CHAIN(f13, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES);
CHAIN(clip, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEPTH_CLIP_CONTROL_FEATURES_EXT);
#define OPTIONAL_EXT(flag, name, member, stype) do { \
if (has_ext(ep, ne, name)) { g_vk.flag = 1; ext[n_ext++] = name; CHAIN(member, stype); } } while (0)
OPTIONAL_EXT(has_custom_border_color, VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME, cbc, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CUSTOM_BORDER_COLOR_FEATURES_EXT);
OPTIONAL_EXT(has_eds, VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME, eds, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT);
OPTIONAL_EXT(has_eds2, VK_EXT_EXTENDED_DYNAMIC_STATE_2_EXTENSION_NAME, eds2, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_2_FEATURES_EXT);
OPTIONAL_EXT(has_eds3, VK_EXT_EXTENDED_DYNAMIC_STATE_3_EXTENSION_NAME, eds3, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_3_FEATURES_EXT);
OPTIONAL_EXT(has_vertex_input_dynamic_state, VK_EXT_VERTEX_INPUT_DYNAMIC_STATE_EXTENSION_NAME, vids, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VERTEX_INPUT_DYNAMIC_STATE_FEATURES_EXT);
OPTIONAL_EXT(has_conditional_rendering, VK_EXT_CONDITIONAL_RENDERING_EXTENSION_NAME, cond, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CONDITIONAL_RENDERING_FEATURES_EXT);
OPTIONAL_EXT(has_transform_feedback, VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME, xfb, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TRANSFORM_FEEDBACK_FEATURES_EXT);
OPTIONAL_EXT(has_provoking_vertex, VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME, pv, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROVOKING_VERTEX_FEATURES_EXT);
OPTIONAL_EXT(has_robustness2, VK_EXT_ROBUSTNESS_2_EXTENSION_NAME, rob2, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT);
OPTIONAL_EXT(has_device_fault, VK_EXT_DEVICE_FAULT_EXTENSION_NAME, fault, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT);
// Ryujinx's SPIR-V declares fragment shader interlock whenever the host reports it (vk_pipeline.c spirv_fixup).
OPTIONAL_EXT(has_fragment_shader_interlock, VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME, fsi,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_INTERLOCK_FEATURES_EXT);
// The GPU address map for device faults (ab_record), with the messenger made for it
int has_abr = 0;
if (ab_want && has_ext(ep, ne, VK_EXT_DEVICE_ADDRESS_BINDING_REPORT_EXTENSION_NAME)) {
has_abr = 1;
ext[n_ext++] = VK_EXT_DEVICE_ADDRESS_BINDING_REPORT_EXTENSION_NAME;
CHAIN(abr, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ADDRESS_BINDING_REPORT_FEATURES_EXT);
}
// Feedback loops (vk.h VkCtx.feedback): the layout, and barriers inside dynamic rendering
int has_fbl = 0, has_lr = 0;
if (has_ext(ep, ne, VK_EXT_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_EXTENSION_NAME) &&
has_ext(ep, ne, VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME)) {
has_fbl = has_lr = 1;
ext[n_ext++] = VK_EXT_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_EXTENSION_NAME;
ext[n_ext++] = VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME;
CHAIN(fbl, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_FEATURES_EXT);
CHAIN(lr, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES_KHR);
}
vk_dlaa_device_exts(ep, ne, ext, &n_ext, 40); // NGX's; the eight slots after it stay for the seven below
if (has_ext(ep, ne, VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) { g_vk.has_memory_budget = 1; ext[n_ext++] = VK_EXT_MEMORY_BUDGET_EXTENSION_NAME; }
// GPU timestamps on the CPU clock (vk_sync.c): the perf line splits GPU idle time into waiting for a submission or not.
if (has_ext(ep, ne, VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME)) { g_vk.has_calibrated = 2; ext[n_ext++] = VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME; }
else if (has_ext(ep, ne, VK_EXT_CALIBRATED_TIMESTAMPS_EXTENSION_NAME)) { g_vk.has_calibrated = 1; ext[n_ext++] = VK_EXT_CALIBRATED_TIMESTAMPS_EXTENSION_NAME; }
if (vk_hdr_wanted() && has_ext(ep, ne, VK_EXT_HDR_METADATA_EXTENSION_NAME)) { // the display's peak (vk_hdr_metadata)
g_vk.has_hdr_metadata = 1;
ext[n_ext++] = VK_EXT_HDR_METADATA_EXTENSION_NAME;
}
// GPU breadcrumbs for a lost device (vk_crumbs.c): AMD's drivers and RADV report it.
if (has_ext(ep, ne, VK_AMD_BUFFER_MARKER_EXTENSION_NAME)) { g_vk.has_buffer_marker = 1; ext[n_ext++] = VK_AMD_BUFFER_MARKER_EXTENSION_NAME; }
// Pass marks on NVIDIA (vk_crumbs.c): checkpoints only with TOTK_PASS_CRUMBS=1, since NVIDIA documents a cost for
// the extension. NVIDIA's drivers report it, AMD's do not.
if (vk_pass_marks_want_nv() && has_ext(ep, ne, VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME)) {
g_vk.has_nv_checkpoints = 1;
ext[n_ext++] = VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME;
}
// Graphics pipeline library: chained last so it can be dropped again when vk_caps_gpl declines it.
void **gpl_link = NULL;
if (has_ext(ep, ne, VK_KHR_PIPELINE_LIBRARY_EXTENSION_NAME) && has_ext(ep, ne, VK_EXT_GRAPHICS_PIPELINE_LIBRARY_EXTENSION_NAME)) {
ext[n_ext++] = VK_KHR_PIPELINE_LIBRARY_EXTENSION_NAME;
ext[n_ext++] = VK_EXT_GRAPHICS_PIPELINE_LIBRARY_EXTENSION_NAME;
gpl_link = tail;
CHAIN(gpl, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_FEATURES_EXT);
}
free(ep);
// The driver fills the chain with what it supports; the same chain then enables all of it (robustBufferAccess
// stays on: guest shaders index constant buffers freely and out-of-range reads must stay harmless).
vkGetPhysicalDeviceFeatures2(g_vk.phys, &c->f2);
vk_caps_driver(g_vk.phys);
vk_caps_features(&c->f2.features);
if (gpl_link) {
VkPhysicalDeviceGraphicsPipelineLibraryPropertiesEXT gp = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_PROPERTIES_EXT};
VkPhysicalDeviceProperties2 p2 = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &gp};
vkGetPhysicalDeviceProperties2(g_vk.phys, &p2);
if (!vk_caps_gpl(&c->gpl, &gp)) { *gpl_link = NULL; n_ext -= 2; }
}
const VkPhysicalDeviceFeatures *h = &c->f2.features;
const char *miss = NULL;
if (!c->f12.timelineSemaphore) miss = "timelineSemaphore";
else if (!c->f13.dynamicRendering) miss = "dynamicRendering";
else if (!c->f13.synchronization2) miss = "synchronization2";
else if (!h->shaderStorageImageWriteWithoutFormat) miss = "shaderStorageImageWriteWithoutFormat";
else if (!c->clip.depthClipControl) miss = "depthClipControl";
else if (!h->shaderInt64) miss = "shaderInt64";
else if (!h->samplerAnisotropy) miss = "samplerAnisotropy";
else if (!h->independentBlend) miss = "independentBlend";
else if (!h->geometryShader) miss = "geometryShader";
else if (!h->textureCompressionBC) miss = "textureCompressionBC";
if (miss) { rt_log("vk: required feature missing on %s: %s", g_vk.props.deviceName, miss); free(c); return 0; }
if (!c->fault.deviceFault) g_vk.has_device_fault = 0; // vkGetDeviceFaultInfoEXT needs the feature, not just the extension
rt_log("vk: GPU address map for device faults: %s",
has_abr && c->abr.reportAddressBinding ? "on (VK_EXT_device_address_binding_report; TOTK_ADDR_REPORT=0 turns it off)"
: !ab_want ? "off (TOTK_ADDR_REPORT=0 or no VK_EXT_debug_utils)"
: "unavailable (no VK_EXT_device_address_binding_report)");
g_vk.has_buffer_device_address = c->f12.bufferDeviceAddress;
g_vk.has_astc_ldr = h->textureCompressionASTC_LDR;
{
const char *e = cfg_get("TOTK_FEEDBACK");
int layout_ok = has_fbl && has_lr && c->fbl.attachmentFeedbackLoopLayout && c->lr.dynamicRenderingLocalRead;
g_vk.feedback = e && !strcmp(e, "0") ? VK_FEEDBACK_OFF
: layout_ok && !(e && !strcmp(e, "restart")) ? VK_FEEDBACK_LAYOUT : VK_FEEDBACK_RESTART;
rt_log("vk: feedback loops: %s (attachmentFeedbackLoopLayout %d, dynamicRenderingLocalRead %d)",
g_vk.feedback == VK_FEEDBACK_LAYOUT ? "feedback layout + by-region barrier"
: g_vk.feedback == VK_FEEDBACK_RESTART ? "pass restart + full barrier" : "off (TOTK_FEEDBACK=0)",
has_fbl && c->fbl.attachmentFeedbackLoopLayout, has_lr && c->lr.dynamicRenderingLocalRead);
}
float prio = 1.0f;
VkDeviceQueueCreateInfo qi[2] = {
{.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, .queueFamilyIndex = g_vk.gfx_family, .queueCount = 1,
.pQueuePriorities = &prio}};
uint32_t nqi = 1;
if (g_vk.xfer_family != g_vk.gfx_family) {
qi[1] = (VkDeviceQueueCreateInfo){.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
.queueFamilyIndex = g_vk.xfer_family, .queueCount = 1, .pQueuePriorities = &prio};
nqi = 2;
}
VkDeviceCreateInfo di = {.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, .pNext = &c->f2, .queueCreateInfoCount = nqi,
.pQueueCreateInfos = qi, .enabledExtensionCount = n_ext, .ppEnabledExtensionNames = ext};
VkResult r = vkCreateDevice(g_vk.phys, &di, NULL, &g_vk.device);
free(c);
if (r != VK_SUCCESS) { rt_log("vk: vkCreateDevice failed (%d)", (int)r); return 0; }
vkGetDeviceQueue(g_vk.device, g_vk.gfx_family, 0, &g_vk.gfx_queue);
if (g_vk.xfer_family != g_vk.gfx_family) vkGetDeviceQueue(g_vk.device, g_vk.xfer_family, 0, &g_vk.xfer_queue);
else { g_vk.xfer_queue = g_vk.gfx_queue; g_vk.xfer_shared = 1; }
VkPhysicalDeviceExternalMemoryHostPropertiesEXT emh = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_HOST_PROPERTIES_EXT};
VkPhysicalDeviceProperties2 p2 = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &emh};
vkGetPhysicalDeviceProperties2(g_vk.phys, &p2);
g_vk.min_imported_host_alignment = (uint32_t)emh.minImportedHostPointerAlignment;
vk_caps_init(&g_vk);
// Blocks of 64 MiB instead of VMA's 256 (TOTK_VMA_BLOCK_MIB, 0 = VMA's). Since the textures moved to their own pool
// (vk_memory.c vk_texture_image), the default device-local blocks hold mirrors and other buffers: on the RX 9070
// after 10 minutes in the field one 256 MiB block held 53 MiB of them, and the headroom thread kept half a block of
// each device-local type ahead (128 MiB each). Larger buffers than half a block get memory of their own.
g_vk.vma_block = (VkDeviceSize)cfg_int("TOTK_VMA_BLOCK_MIB") << 20;
if (!g_vk.vma_block) g_vk.vma_block = 256ull << 20; // VMA_DEFAULT_LARGE_HEAP_BLOCK_SIZE
VmaAllocatorCreateInfo ai = {.physicalDevice = g_vk.phys, .device = g_vk.device, .instance = g_vk.instance,
.vulkanApiVersion = VK_API_VERSION_1_3, .preferredLargeHeapBlockSize = g_vk.vma_block,
.flags = (g_vk.has_buffer_device_address ? VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT : 0) |
(g_vk.has_memory_budget ? VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT : 0),
.pDeviceMemoryCallbacks = vk_headroom_callbacks()};
if ((r = vmaCreateAllocator(&ai, &g_vk.vma)) != VK_SUCCESS) { rt_log("vk: vmaCreateAllocator failed (%d)", (int)r); return 0; }
VkSemaphoreTypeCreateInfo st = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO,
.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE, .initialValue = 0};
VkSemaphoreCreateInfo si = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, .pNext = &st};
if ((r = vkCreateSemaphore(g_vk.device, &si, NULL, &g_vk.timeline)) != VK_SUCCESS) {
rt_log("vk: timeline semaphore creation failed (%d)", (int)r);
return 0;
}
vk_crumbs_init(&g_vk);
return 1;
}
static long g_errors_total; // outlives the device so the probe can print it after vk_shutdown
static void destroy_all(void) {
gfx_window_release_vk();
if (g_vk.device) vkDeviceWaitIdle(g_vk.device);
if (g_vk.device) vk_astc_release();
if (g_vk.device) vk_deswz_release();
if (g_vk.device) vk_crumbs_release();
if (g_vk.timeline) vkDestroySemaphore(g_vk.device, g_vk.timeline, NULL);
vk_headroom_stop();
if (g_vk.vma) vmaDestroyAllocator(g_vk.vma);
if (g_vk.device) vkDestroyDevice(g_vk.device, NULL);
if (g_vk.messenger) {
PFN_vkDestroyDebugUtilsMessengerEXT f =
(PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(g_vk.instance, "vkDestroyDebugUtilsMessengerEXT");
if (f) f(g_vk.instance, g_vk.messenger, NULL);
}
if (g_vk.instance) vkDestroyInstance(g_vk.instance, NULL);
g_errors_total += g_vk.validation_errors;
memset(&g_vk, 0, sizeof g_vk);
}
long vk_validation_errors_total(void) { return g_errors_total + (g_ready ? g_vk.validation_errors : 0); }
int vk_init(void) {
plat_mutex_lock(&g_lock);
if (!g_ready) {
memset(&g_vk, 0, sizeof g_vk);
if (create_instance() && pick_device() && create_device())
g_ready = 1, vk_mem_host_probe(), vk_upload_pools_init(), vk_headroom_start(), vk_dlaa_init();
else destroy_all();
}
int ok = g_ready;
plat_mutex_unlock(&g_lock);
return ok;
}
void vk_shutdown(void) {
plat_mutex_lock(&g_lock);
if (g_ready) { destroy_all(); g_ready = 0; }
plat_mutex_unlock(&g_lock);
}
VkCtx *vk_ctx(void) { return g_ready ? &g_vk : NULL; }
static const char *fault_address_type(VkDeviceFaultAddressTypeEXT t) {
switch (t) {
case VK_DEVICE_FAULT_ADDRESS_TYPE_NONE_EXT: return "none";
case VK_DEVICE_FAULT_ADDRESS_TYPE_READ_INVALID_EXT: return "read invalid";
case VK_DEVICE_FAULT_ADDRESS_TYPE_WRITE_INVALID_EXT: return "write invalid";
case VK_DEVICE_FAULT_ADDRESS_TYPE_EXECUTE_INVALID_EXT: return "execute invalid";
case VK_DEVICE_FAULT_ADDRESS_TYPE_INSTRUCTION_POINTER_UNKNOWN_EXT: return "instruction pointer (unknown)";
case VK_DEVICE_FAULT_ADDRESS_TYPE_INSTRUCTION_POINTER_INVALID_EXT: return "instruction pointer (invalid)";
case VK_DEVICE_FAULT_ADDRESS_TYPE_INSTRUCTION_POINTER_FAULT_EXT: return "instruction pointer (fault)";
default: return "unknown type";
}
}
// The query runs once per process. Threads that hit the loss at the same time wait on the lock until it is logged, so
// none of them reaches its rt_fatal and ends the process before the report is written.
void vk_device_fault_report(VkResult r) {
static PlatMutex lock = PLAT_MUTEX_INIT;
static int done;
if (r != VK_ERROR_DEVICE_LOST) return;
plat_mutex_lock(&lock);
if (done) { plat_mutex_unlock(&lock); return; }
done = 1;
vk_crumbs_report();
vk_pass_marks_report();
vk_mem_fault_log();
VkCtx *c = vk_ctx();
PFN_vkGetDeviceFaultInfoEXT get =
c && c->has_device_fault ? (PFN_vkGetDeviceFaultInfoEXT)vkGetDeviceProcAddr(c->device, "vkGetDeviceFaultInfoEXT")
: NULL;
if (!get) {
rt_log("vk: device lost; no fault report (VK_EXT_device_fault %s)", c && c->has_device_fault ? "has no entry point"
: "not enabled");
plat_mutex_unlock(&lock);
return;
}
VkDeviceFaultCountsEXT n = {.sType = VK_STRUCTURE_TYPE_DEVICE_FAULT_COUNTS_EXT};
VkResult q = get(c->device, &n, NULL);
VkDeviceFaultAddressInfoEXT *ai = calloc(n.addressInfoCount + 1, sizeof *ai);
VkDeviceFaultVendorInfoEXT *vi = calloc(n.vendorInfoCount + 1, sizeof *vi);
VkDeviceFaultInfoEXT info = {.sType = VK_STRUCTURE_TYPE_DEVICE_FAULT_INFO_EXT, .pAddressInfos = ai, .pVendorInfos = vi};
uint64_t binary = n.vendorBinarySize;
n.vendorBinarySize = 0; // the vendor binary is not fetched; its size alone is logged
if (q == VK_SUCCESS && ai && vi) q = get(c->device, &n, &info);
if (q != VK_SUCCESS && q != VK_INCOMPLETE) {
rt_log("vk: device lost; vkGetDeviceFaultInfoEXT failed (%d)", (int)q);
} else {
rt_log("vk: device fault: \"%s\" (%u address infos, %u vendor infos, vendor binary %llu bytes)", info.description,
n.addressInfoCount, n.vendorInfoCount, (unsigned long long)binary);
// The faulting access lies in the precision-aligned range around the reported address.
for (uint32_t i = 0; i < n.addressInfoCount; i++) {
rt_log("vk: device fault address %u: %s at %#llx, precision %#llx", i, fault_address_type(ai[i].addressType),
(unsigned long long)ai[i].reportedAddress, (unsigned long long)ai[i].addressPrecision);
ab_report(ai[i].reportedAddress, ai[i].addressPrecision);
}
for (uint32_t i = 0; i < n.vendorInfoCount; i++)
rt_log("vk: device fault vendor info %u: \"%s\" code %#llx data %#llx", i, vi[i].description,
(unsigned long long)vi[i].vendorFaultCode, (unsigned long long)vi[i].vendorFaultData);
}
free(ai);
free(vi);
plat_mutex_unlock(&lock);
}
int vk_selftest(void) {
if (!vk_init()) return 1;
VkCtx *c = &g_vk;
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = 64 << 10,
.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT};
VmaAllocationCreateInfo aci = {.usage = VMA_MEMORY_USAGE_AUTO, .flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT};
VkBuffer buf;
VmaAllocation alloc;
VkResult r = vmaCreateBuffer(c->vma, &bi, &aci, &buf, &alloc, NULL);
if (r != VK_SUCCESS) { rt_log("vk selftest: vmaCreateBuffer failed (%d)", (int)r); return 2; }
vmaDestroyBuffer(c->vma, buf, alloc);
VkSemaphoreSignalInfo sig = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO, .semaphore = c->timeline, .value = 1};
if ((r = vkSignalSemaphore(c->device, &sig)) != VK_SUCCESS) { rt_log("vk selftest: vkSignalSemaphore failed (%d)", (int)r); return 3; }
uint64_t value = 1;
VkSemaphoreWaitInfo wi = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO, .semaphoreCount = 1, .pSemaphores = &c->timeline,
.pValues = &value};
if ((r = vkWaitSemaphores(c->device, &wi, 1000000000ull)) != VK_SUCCESS) { rt_log("vk selftest: vkWaitSemaphores failed (%d)", (int)r); return 4; }
uint64_t now = 0;
vkGetSemaphoreCounterValue(c->device, c->timeline, &now);
if (now != 1) { rt_log("vk selftest: timeline value %llu, expected 1", (unsigned long long)now); return 5; }
return 0;
}
// --probe-vkdevice (host.h)
int probe_vkdevice(void) {
int rc = vk_selftest();
VkCtx *c = vk_ctx();
if (rc || !c) { printf("probe-vkdevice: FAILED (selftest %d)\n", rc ? rc : 1); vk_shutdown(); return 1; }
uint32_t v = c->props.apiVersion;
printf("probe-vkdevice: device %d \"%s\" api %u.%u.%u, gfx queue family %u, transfer family %u%s, host import alignment %#x\n",
c->device_index, c->props.deviceName, VK_API_VERSION_MAJOR(v), VK_API_VERSION_MINOR(v), VK_API_VERSION_PATCH(v),
c->gfx_family, c->xfer_family, c->xfer_shared ? " (shared queue)" : "", c->min_imported_host_alignment);
printf("probe-vkdevice: optional: custom_border=%d eds=%d eds2=%d eds3=%d vertex_input=%d cond_render=%d xfb=%d provoking=%d "
"robustness2=%d fault=%d budget=%d bda=%d interlock=%d buffer_marker=%d nv_checkpoints=%d validation=%d\n",
c->has_custom_border_color, c->has_eds, c->has_eds2, c->has_eds3, c->has_vertex_input_dynamic_state,
c->has_conditional_rendering, c->has_transform_feedback, c->has_provoking_vertex, c->has_robustness2,
c->has_device_fault, c->has_memory_budget, c->has_buffer_device_address, c->has_fragment_shader_interlock,
c->has_buffer_marker, c->has_nv_checkpoints, c->validation);
vk_shutdown();
printf("probe-vkdevice: %s (validation errors %ld)\n", g_errors_total ? "FAILED" : "passed", g_errors_total);
return g_errors_total ? 1 : 0;
}