// 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 #include #include #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; VkPhysicalDevicePresentIdFeaturesKHR pid; } 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[52]; 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 twelve slots after it stay for the nine 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; } // NVIDIA Reflex Low Latency (VK_NV_low_latency2 requires VK_KHR_present_id) const char *rv = cfg_value("TOTK_REFLEX"); if (rv && strcmp(rv, "off") != 0 && strcmp(rv, "0") != 0) { if (has_ext(ep, ne, VK_KHR_PRESENT_ID_EXTENSION_NAME) && has_ext(ep, ne, "VK_NV_low_latency2")) { g_vk.has_nv_reflex = 1; ext[n_ext++] = VK_KHR_PRESENT_ID_EXTENSION_NAME; ext[n_ext++] = "VK_NV_low_latency2"; CHAIN(pid, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_FEATURES_KHR); } else { rt_log("Reflex: VK_NV_low_latency2 not available"); } } // 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; // VkPresentIdKHR on a present needs the presentId feature, not just the extension if (g_vk.has_nv_reflex && !c->pid.presentId) { g_vk.has_nv_reflex = 0; rt_log("Reflex: VK_NV_low_latency2 not available (no presentId feature)"); } 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; }