Files
NX2navi/runtime/vk_device.c
chrg6 7a8bbad47e Remove the low-end GPU mode from the settings.
The user asked for it: the mode no longer gains frame rate, and the mesh shader fallback covers the cards it was written for. Its switch in the settings menu's Game tab is gone with the rows under it, and so are their 57 text ids and strings.

low_end_gpu itself does nothing any more. It is still a known key, so an older nx2navi.ini gives no unknown-key warning, and while it is on the log says once that the mode was removed in 0.10.2. The preset that gave nine keys other defaults is gone, and so is the rule that read the mode's keys only while it was on: each of them (mesh_emulation, tex_clobber, foliage, vram_heap_limit, vram_reserve_mb, upload_batch_mb, mirror_stage, mirror_hot, submit_finish_gpu and the others) is an ordinary setting with the default of its row or of its reader, so a line a player wrote by hand keeps counting and a file with only the switch runs the game as it runs for everyone else. guest_opt in nx2navi.ini counts for the next rebuild without the switch too (tools/make_game.py).

The recorder thread read the switch itself: on an AMD card without mesh shaders the mode turned it on. It is off on every AMD card now unless vk_recorder = 1 is set.

nx2navi.ini.example lists the former mode's settings as single settings, the 0.10.2 notes have a Removed entry and name the foliage fix of the commit before this one, and the comments that said what the mode sets say so in the past tense. tests/cfg_parse_check has the new reading of the switch and of the keys without it; the test matrix is at 90 ok with the same two not ok as before (installer_test, worktree_test), and a clean build compiles 169 runtime files and links. Not run in the game.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-14 06:13:18 -05:00

980 lines
61 KiB
C

// Vulkan instance, physical device choice, logical device, VMA, timeline semaphore and the validation messenger.
// Contains code Copyright (c) 2026 Peiikai (MIT; see THIRD_PARTY_NOTICES.md).
// The VMA implementation is runtime/vk_vma.cpp (VMA is C++ only).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef __linux__
#include <errno.h>
#include <unistd.h>
#endif
#include "cfg.h"
#include "host.h"
#include "mem_budget.h"
#include "vk.h"
#include "platform/platform.h"
#include "vk_dlaa.h"
#include "vk_gpa.h"
#include "vk_fg.h"
#include "vk_dlssg.h"
#include "vk_dlssg_device.h"
#include "vk_lowlat.h"
#include "vk_mesh_path.h"
// The loader's vkGetDeviceProcAddr until Streamline is on the device (see vk.h).
PFN_vkGetDeviceProcAddr vk_dpa = vkGetDeviceProcAddr;
static uint32_t g_gfx_queues = 1; // queues the graphics family offers (pick_device)
#define QFAM_MAX 16
static uint32_t g_nqfam; // the device's queue families (pick_device): their flags and queue counts
static VkQueueFlags g_qfam_flags[QFAM_MAX];
static uint32_t g_qfam_count[QFAM_MAX];
static VkCtx g_vk;
static int g_ready;
static PlatMutex g_lock = PLAT_MUTEX_INIT;
// The fence vk_device_lost_poll asks about. Nothing submits with it, so its status is "not ready" for as long as the
// device lives.
static VkFence lost_fence;
static PlatMutex lost_fence_lock = PLAT_MUTEX_INIT;
static VKAPI_ATTR VkBool32 VKAPI_CALL debug_cb(VkDebugUtilsMessageSeverityFlagBitsEXT sev, VkDebugUtilsMessageTypeFlagsEXT type,
const VkDebugUtilsMessengerCallbackDataEXT *d, void *user) {
(void)type, (void)user;
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.
// That folder is ours alone, so its limit is raised to MESA_CACHE_SIZE unless MESA_SHADER_CACHE_MAX_SIZE is set: the
// stage libraries the library warm builds in play live only in the driver's cache (vk_pipeline.c, "the play cache"),
// and a preparation of every shader variant does not fit in 1 GB, so the rest would be compiled again in play.
#define MESA_CACHE_SIZE "8G"
static void mesa_cache_beside_exe(void) {
char dir[PLAT_PATH_MAX * 2], size[32];
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);
if (!plat_getenv("MESA_SHADER_CACHE_MAX_SIZE", size, sizeof size)) plat_setenv("MESA_SHADER_CACHE_MAX_SIZE", MESA_CACHE_SIZE);
}
#ifdef __linux__
// NVIDIA's Linux driver keeps the pipelines it compiles in a disk cache of its own, by default in ~/.cache/nvidia,
// shared with every program. One process adds at most __GL_SHADER_DISK_CACHE_SIZE to it (1 GiB unless set), and a start
// that finds more than that in several files deletes the later ones. nx2navi.exe --prepare-shaders builds the 42,142
// variants of game 1.1.1 into it in one process: with the default limit it stored about 30,700 of them and the rest
// were compiled again by the first start after it. So unless the environment sets them, the cache goes to nvidia-cache
// beside the exe, which nothing else writes, with a limit of NV_CACHE_BYTES. The driver makes GLCache inside that
// folder but not the folder, so it is made here; pointed at a folder that is missing or not writable it caches nothing
// at all, so when that folder cannot be made or written neither variable is set and the driver keeps its own folder
// and limit. Without NVIDIA's kernel driver (no /proc/driver/nvidia/version, the test the AppImage's build makes for
// DLSS) the variables do nothing and the folder would stay empty, so nothing is done.
#define NV_CACHE_BYTES "8589934592" // 8 GiB
static void nvidia_cache_beside_exe(void) {
if (plat_fs_kind("/proc/driver/nvidia/version") != PLAT_FS_FILE) return;
char dir[PLAT_PATH_MAX * 2], size[32];
int own_dir = !plat_getenv("__GL_SHADER_DISK_CACHE_PATH", dir, sizeof dir);
if (own_dir) {
int len = plat_exe_path(dir, sizeof dir - 16);
char *cut = len ? plat_last_sep(dir) : NULL;
if (!cut) return;
strcpy(cut + 1, "nvidia-cache");
int made = plat_fs_mkdir(dir), there = made == PLAT_FS_OK || made == PLAT_FS_EXISTS;
int err = !there ? plat_fs_os_error() : access(dir, W_OK | X_OK) ? errno : 0;
if (err) {
rt_log("vk: NVIDIA's shader disk cache stays in the driver's own folder with its own limit: %s %s (%s)", dir,
there ? "is not writable" : "cannot be made", strerror(err));
return;
}
plat_setenv("__GL_SHADER_DISK_CACHE_PATH", dir);
}
int own_size = !plat_getenv("__GL_SHADER_DISK_CACHE_SIZE", size, sizeof size);
if (own_size) plat_setenv("__GL_SHADER_DISK_CACHE_SIZE", NV_CACHE_BYTES);
rt_log("vk: NVIDIA's shader disk cache in %s (%s), up to %s bytes (%s)", dir,
own_dir ? "beside the exe" : "__GL_SHADER_DISK_CACHE_PATH", own_size ? NV_CACHE_BYTES : size,
own_size ? "the game's limit" : "__GL_SHADER_DISK_CACHE_SIZE");
}
#endif
// Mesa's Vulkan drivers on Linux (RADV, ANV, NVK) keep timeline semaphores as kernel sync objects, which cannot wait
// for a value not submitted yet. Their queues therefore submit on the calling thread and use a thread of the queue's
// own only for a submission that waits on such a value. Every other vkQueueSubmit2, the command stream's checks and
// the kernel's ioctl, runs on the replaying thread, once for every part the replay hands to the queue early (vk_exec.c
// maybe_split). MESA_VK_ENABLE_SUBMIT_THREAD=1 sends every submission to the queue's thread: in Kokiri Forest on an
// RX 6800 XT (Mesa 26.2) the replay took 13.9 ms a frame with it and 15.0 ms without. NX2NAVI_SUBMIT_THREAD (default
// 1 on Linux, 0 on Windows) sets it unless the environment already sets it; 0 leaves the driver's own choice. Drivers
// that do not read the variable ignore it. vk_driver_cache_env calls this before any thread can load the driver.
static const char *submit_thread_why; // create_device logs it on a Mesa driver
static void mesa_submit_thread(void) {
char cur[16];
if (plat_getenv("MESA_VK_ENABLE_SUBMIT_THREAD", cur, sizeof cur)) {
submit_thread_why = "MESA_VK_ENABLE_SUBMIT_THREAD is set in the environment";
return;
}
if (!cfg_bool("NX2NAVI_SUBMIT_THREAD")) {
submit_thread_why = "NX2NAVI_SUBMIT_THREAD=0: the driver's own choice";
return;
}
plat_setenv("MESA_VK_ENABLE_SUBMIT_THREAD", "1");
submit_thread_why = "every one on the queue's own thread (NX2NAVI_SUBMIT_THREAD=0 leaves the driver's own choice)";
}
// A driver may read these variables when it loads, and a thread can load the Vulkan driver before create_instance:
// widescreen.c's wide_init asks for the monitor's size (fullscreen with a monitor chosen and the picture at the
// window's size), and SDL's load of the Vulkan library there lists the instance extensions, which loads the drivers,
// while the prewarm thread may be in create_instance; setenv beside other threads' getenv is not safe either. main
// therefore calls this for a game run before any thread of the game starts; create_instance calls it for the other
// modes. Both calls happen before other threads exist or under vk_init's lock, so the flag needs no atomics.
void vk_driver_cache_env(void) {
static int done;
if (done) return;
done = 1;
mesa_cache_beside_exe();
mesa_submit_thread();
#ifdef __linux__
nvidia_cache_beside_exe();
#endif
}
static int create_instance(void) {
vk_driver_cache_env();
vk_dlssg_early(); // DLSS Frame Generation: Streamline before any Vulkan call of the process (its slInit asks so)
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("NX2NAVI_VK_VALIDATION");
if (validate && !layer_present(layers[0])) {
rt_log("vk: NX2NAVI_VK_VALIDATION set but %s is not installed; continuing without", layers[0]);
validate = 0;
}
// DLSS Frame Generation: Streamline's plugins call its naming functions even when they say it is off
if (validate || (vk_dlssg_wanted() && instance_ext_present(VK_EXT_DEBUG_UTILS_EXTENSION_NAME)))
ext[n_ext++] = VK_EXT_DEBUG_UTILS_EXTENSION_NAME;
// armed, not wanted: the menu switches HDR on during the run (vk_hdr.c), and the extensions are fixed here
if (vk_hdr_armed() && 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 = "NX2navi",
.pEngineName = "NX2navi", .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 = vk_dlssg_create_instance(&ci, &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) {
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,
.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,
.pfnUserCallback = debug_cb};
if (f) f(g_vk.instance, &mi, NULL, &g_vk.messenger);
}
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 (of one GPU listed by two
// drivers, the newer driver's listing); NX2NAVI_GPU_INDEX (index) or NX2NAVI_GPU_NAME (case-insensitive part of the
// name) overrides.
static int pick_device(void) {
uint32_t n = 0;
vk_dlssg_enumerate(g_vk.instance, &n, NULL);
if (!n) { rt_log("vk: no Vulkan device"); return 0; }
VkPhysicalDevice *pd = calloc(n, sizeof *pd);
vk_dlssg_enumerate(g_vk.instance, &n, pd);
const char *e = cfg_get("NX2NAVI_GPU_INDEX"); // no fixed default: the best device when unset
int want = e && *e ? atoi(e) : -1;
const char *name = cfg_get("NX2NAVI_GPU_NAME");
int best = -1, best_score = -1, listed = 0;
uint32_t best_driver = 0; // the driver version of the best one so far
retry:
for (uint32_t i = 0; i < n; i++) {
VkPhysicalDeviceProperties p;
vkGetPhysicalDeviceProperties(pd[i], &p);
if (!listed)
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;
// A chosen card of another vendor while DLSS Frame Generation is on: the interposer cannot make its device
// (vk_dlssg_create_device), and the settings ask for what only an NVIDIA card runs. Not usable, so the best
// usable one is taken below, as for a card that is gone; where no NVIDIA card is usable either, the pass
// without a choice takes what there is and the device is made without frame generation.
if (dlssg_card_skipped(vk_dlssg_interposed(), want >= 0 || (name && *name), p.vendorID)) {
rt_log("vk: gpu %u skipped: DLSS Frame Generation is on (framegen = dlss), which only an NVIDIA card runs", i);
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; best_driver = p.driverVersion; continue; }
// The same GPU listed again by another installed driver (a laptop with the vendor's processor graphics and its
// card has both drivers list the card): the newer driver's listing. The older one, there to serve the processor
// graphics, was reported to crash in its shader compiler with the card.
if (score == best_score && best >= 0) {
VkPhysicalDeviceProperties b;
vkGetPhysicalDeviceProperties(pd[best], &b);
if (b.vendorID == p.vendorID && b.deviceID == p.deviceID && p.driverVersion > best_driver) {
if (!listed) rt_log("vk: gpu %u is gpu %d listed by a newer driver (%#x over %#x): gpu %u", i, best,
p.driverVersion, best_driver, i);
best = (int)i, best_driver = p.driverVersion;
}
}
}
// A chosen GPU that is not usable or no longer there (the menu's GPU row, an unplugged card, a driver that lists the
// devices in another order): the best usable one instead, so the game still starts and the choice can be changed
if (best < 0 && (want >= 0 || (name && *name))) {
if (want >= 0) rt_log("vk: NX2NAVI_GPU_INDEX=%d is not usable (see above): the best usable gpu instead", want);
else rt_log("vk: no usable gpu matches NX2NAVI_GPU_NAME=%s (see above): the best usable gpu instead", name);
want = -1, name = NULL, listed = 1;
goto retry;
}
if (best < 0) {
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_gfx_queues = qp[g_vk.gfx_family].queueCount;
g_nqfam = nq < QFAM_MAX ? nq : QFAM_MAX;
for (uint32_t i = 0; i < g_nqfam; i++) g_qfam_flags[i] = qp[i].queueFlags, g_qfam_count[i] = qp[i].queueCount;
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_caps_driver(g_vk.phys); // before vk_mem_class_log, which chooses the pools' storage by driver
{ // and by whether the device imports dma-bufs (vk_memory.c "dma-buf imports")
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);
vk_mem_dmabuf_caps(has_ext(ep, ne, VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME) &&
has_ext(ep, ne, VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME));
free(ep);
}
vk_mem_class_log(&g_vk.props, &g_vk.mem_props);
return 1;
}
// Heap limits for VMA where the GPU has more than one device-local heap. An RX 580 (8 GiB, AMD 23.10) shows a 6 GiB
// heap and a 2 GiB host-visible one; Windows granted the game 5.1 GiB of the first. VMA, asked for device-local memory,
// takes the first heap whatever its budget, and a save load put 5.6 GiB there while the second held 69 MiB: the next
// submit failed with VK_ERROR_OUT_OF_DEVICE_MEMORY. With a limit VMA counts a heap full at it and places what follows in
// the next memory type that allows it (the other device-local heap first), for every allocation: textures, render
// targets, VRAM mirrors, pools. The limit is each device-local heap's budget when the device is made (VK_EXT_memory_budget,
// what other programs leave) less a margin for memory VMA does not see (swapchain, FidelityFX): a tenth, at least
// 256 MiB. NX2NAVI_VRAM_HEAP_LIMIT: auto, 0 (no limits, the default) or a number of MiB for the largest device-local
// heap. The low-end GPU mode set auto up to 0.10.1.
static int vram_heap_limits(VkDeviceSize *limit) {
const char *e = cfg_get("NX2NAVI_VRAM_HEAP_LIMIT");
long mb = !e || !*e ? 0 : strcmp(e, "auto") ? atol(e) : -1; // unset: no limits
if (!mb) return 0;
VkPhysicalDeviceMemoryBudgetPropertiesEXT b = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT};
VkPhysicalDeviceMemoryProperties2 p2 = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2,
.pNext = g_vk.has_memory_budget ? &b : NULL};
vkGetPhysicalDeviceMemoryProperties2(g_vk.phys, &p2);
const VkPhysicalDeviceMemoryProperties *mp = &p2.memoryProperties;
int nlocal = 0;
uint32_t largest = 0;
for (uint32_t h = 0; h < mp->memoryHeapCount && h < VK_MAX_MEMORY_HEAPS; h++)
if (mp->memoryHeaps[h].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) {
if (!nlocal++ || mp->memoryHeaps[h].size > mp->memoryHeaps[largest].size) largest = h;
}
if (mb < 0 && (nlocal < 2 || !g_vk.has_memory_budget)) return 0; // auto: only where another heap can take the rest
for (uint32_t h = 0; h < VK_MAX_MEMORY_HEAPS; h++) limit[h] = VK_WHOLE_SIZE;
for (uint32_t h = 0; h < mp->memoryHeapCount && h < VK_MAX_MEMORY_HEAPS; h++) {
if (!(mp->memoryHeaps[h].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT)) continue;
VkDeviceSize lim;
if (mb > 0) {
if (h != largest) continue;
lim = (VkDeviceSize)mb << 20;
} else {
VkDeviceSize budget = b.heapBudget[h] && b.heapBudget[h] < mp->memoryHeaps[h].size ? b.heapBudget[h]
: mp->memoryHeaps[h].size;
VkDeviceSize margin = budget / 10 > (256ull << 20) ? budget / 10 : (256ull << 20);
if (budget <= margin * 2) continue; // too small to limit
lim = budget - margin;
}
if (lim >= mp->memoryHeaps[h].size) continue;
limit[h] = lim;
rt_log("vk: memory heap %u (%llu MiB, budget %llu MiB): the allocator uses at most %llu MiB, then the next heap "
"(NX2NAVI_VRAM_HEAP_LIMIT)", h, (unsigned long long)(mp->memoryHeaps[h].size >> 20),
(unsigned long long)(b.heapBudget[h] >> 20), (unsigned long long)(lim >> 20));
}
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;
VkPhysicalDeviceMeshShaderFeaturesEXT mesh;
VkPhysicalDeviceFragmentShadingRateFeaturesKHR vrs;
VkPhysicalDeviceShaderMaximalReconvergenceFeaturesKHR mrc;
VkPhysicalDeviceMaintenance5FeaturesKHR m5;
VkPhysicalDevicePipelineBinaryFeaturesKHR pbin;
VkPhysicalDevicePresentIdFeaturesKHR pid;
VkPhysicalDeviceAntiLagFeaturesAMD antilag;
} FeatureChain;
// One line for a fatal error's report (dispatch.h rt_fatal_note_add): the card, its driver, and the settings in effect
// that change how the game's memory behaves. A report often arrives as the end of a log alone, or as a picture of it,
// and the lines that say these things are at its start. No lock is taken: the failing thread may hold any.
// How the game's task and mesh programs are drawn (create_device decides it): natively, through the mesh fallback, the
// mesh emulation, or not at all. Every RX 6000 loss whose marks named a pass was in one that starts with a mesh draw.
static const char *g_mesh_path = "not decided yet";
static void fatal_note(void) {
char mem[512];
vk_mem_fatal_note(mem, sizeof mem);
rt_log("fatal: gpu %s; %s; texture detail %s; mesh draws %s", vk_caps_summary(), mem, vk_texture_detail_name(),
g_mesh_path);
}
// NX2NAVI_TRACE=perf: every device memory allocation VMA makes, timed as the "device memory allocations" work mark
// (perf.c mark_text). An area's first visit made 78 of them, 4.4 GB, in a frame of 421 ms of which about 180 ms
// showed in no mark; vk_headroom.c measured 8-19 ms per 256 MiB block. VMA takes this one function from the
// allocator's create info and the others from its static table (vk_vma.cpp VMA_STATIC_VULKAN_FUNCTIONS). A failed
// attempt counts too, with its time and 0 bytes: VMA may then try again with a smaller block, and the failed call held
// the frame as well, so the mark's count can pass the number of allocations made.
static VkResult VKAPI_PTR timed_alloc(VkDevice d, const VkMemoryAllocateInfo *ai, const VkAllocationCallbacks *cb,
VkDeviceMemory *m) {
uint64_t t0 = perf_mark_begin();
VkResult r = vkAllocateMemory(d, ai, cb, m);
perf_mark_end(PERF_M_DEVMEM, t0, r == VK_SUCCESS ? ai->allocationSize : 0);
return r;
}
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[64];
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);
// The translator'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);
// NVN2 task and mesh programs (gameplay geometry) draw through VK_EXT_mesh_shader (vk_exec_draw.c op_draw_mesh)
OPTIONAL_EXT(has_mesh_shader, VK_EXT_MESH_SHADER_EXTENSION_NAME, mesh, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT);
// The foliage's far shadow cascades shade one fragment per 2x2 pixels (vk_exec_draw.c mesh_shading_rate)
OPTIONAL_EXT(has_vrs, VK_KHR_FRAGMENT_SHADING_RATE_EXTENSION_NAME, vrs, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADING_RATE_FEATURES_KHR);
// Guest shaders that use warp-wide operations after divergent code run with their threads reconverged
// (vk_pipeline.c spirv_reconverge)
OPTIONAL_EXT(has_maximal_reconvergence, VK_KHR_SHADER_MAXIMAL_RECONVERGENCE_EXTENSION_NAME, mrc,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_MAXIMAL_RECONVERGENCE_FEATURES_KHR);
// Compiled pipelines kept under the runtime's own keys (vk_pipeline.c pipe_create): VK_KHR_pipeline_binary, which
// needs VK_KHR_maintenance5 (core only from Vulkan 1.4; the instance asks for 1.3). NX2NAVI_PIPE_BINARIES=0 leaves
// both off.
if (cfg_bool("NX2NAVI_PIPE_BINARIES") && has_ext(ep, ne, VK_KHR_PIPELINE_BINARY_EXTENSION_NAME) &&
has_ext(ep, ne, VK_KHR_MAINTENANCE_5_EXTENSION_NAME)) {
g_vk.has_pipeline_binary = 1;
ext[n_ext++] = VK_KHR_MAINTENANCE_5_EXTENSION_NAME;
ext[n_ext++] = VK_KHR_PIPELINE_BINARY_EXTENSION_NAME;
CHAIN(m5, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES_KHR);
CHAIN(pbin, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_BINARY_FEATURES_KHR);
}
// 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 eight below
// FSR 3.1 (vk_ffx.cpp): its Vulkan back end looks vkGetBufferMemoryRequirements2KHR up by that name, which resolves
// only with the extension enabled (the core function has no suffix); without it the back end called address 0.
// Always asked for: FSR 3.1 can be switched on in the menu during a run
if (has_ext(ep, ne, VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME))
ext[n_ext++] = VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME;
// DLSS Frame Generation (vk_dlssg.c): Streamline's plugins look functions of extensions core in Vulkan 1.3 up by
// their suffixed names too, which resolve only with the extension itself enabled
if (vk_dlssg_wanted()) {
static const char *const promoted[] = {VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME, VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME,
VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME, VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME,
VK_KHR_BIND_MEMORY_2_EXTENSION_NAME, VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME,
VK_KHR_COPY_COMMANDS_2_EXTENSION_NAME, VK_KHR_MAINTENANCE_1_EXTENSION_NAME,
VK_KHR_MAINTENANCE_2_EXTENSION_NAME, VK_KHR_MAINTENANCE_3_EXTENSION_NAME};
for (size_t i = 0; i < sizeof promoted / sizeof *promoted && n_ext < 56; i++)
if (has_ext(ep, ne, promoted[i])) ext[n_ext++] = promoted[i];
}
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_armed() && 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 NX2NAVI_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;
}
// Guest memory imported as dma-bufs (vk_memory.c "dma-buf imports"; pick_device checked the extensions)
if (vk_mem_dmabuf_on() && n_ext + 4 <= sizeof ext / sizeof *ext) {
ext[n_ext++] = VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME;
ext[n_ext++] = VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME;
}
// Low latency without frame generation (vk_lowlat.c): NVIDIA's extension, which needs VK_KHR_present_id enabled
// with it, or AMD's, whose calls need its feature. Each only where the driver lists it; a driver lists one of them
// at most. Not on a device the DLSS Frame Generation proxies make (vk_dlssg_create_device): that path's own
// low-latency plugin asks for what it needs there, and the direct path stays off for the run. And only in a run
// that starts with NX2NAVI_LOW_LATENCY on: with it off the device is made exactly as before the mode existed, and
// a driver that lists one of the two is only remembered (lists_low_latency), so the menu can say that turning the
// setting on takes a restart.
g_vk.lists_low_latency = 0;
if (!vk_dlssg_dpa() && n_ext + 2 <= sizeof ext / sizeof *ext) {
int nv = has_ext(ep, ne, VK_NV_LOW_LATENCY_2_EXTENSION_NAME) && has_ext(ep, ne, VK_KHR_PRESENT_ID_EXTENSION_NAME);
int amd = !nv && has_ext(ep, ne, VK_AMD_ANTI_LAG_EXTENSION_NAME);
if (cfg_int("NX2NAVI_LOW_LATENCY") <= 0) {
g_vk.lists_low_latency = nv || amd;
} else if (nv) {
g_vk.has_low_latency2 = 1;
ext[n_ext++] = VK_KHR_PRESENT_ID_EXTENSION_NAME;
ext[n_ext++] = VK_NV_LOW_LATENCY_2_EXTENSION_NAME;
CHAIN(pid, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_FEATURES_KHR);
} else if (amd) {
g_vk.has_anti_lag = 1;
ext[n_ext++] = VK_AMD_ANTI_LAG_EXTENSION_NAME;
CHAIN(antilag, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ANTI_LAG_FEATURES_AMD);
}
}
// 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_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;
if (g_vk.has_vrs) { // the rate is set per draw; no rate attachment and no per-primitive rates
c->vrs.attachmentFragmentShadingRate = c->vrs.primitiveFragmentShadingRate = VK_FALSE;
if (!c->vrs.pipelineFragmentShadingRate) g_vk.has_vrs = 0;
rt_log("vk: fragment shading rate: %s", g_vk.has_vrs ? "per draw" : "not supported");
}
if (g_vk.has_mesh_shader) {
// these two need features that are not enabled (multiview, primitive fragment shading rate)
c->mesh.multiviewMeshShader = c->mesh.primitiveFragmentShadingRateMeshShader = VK_FALSE;
// Mesh queries for two diagnoses: NX2NAVI_GPU_PASSES (vk_crumbs.c) and the dump's primitive counts
// (vk_dump.c vk_dump_mesh_begin), which also need pipeline statistics and host query reset.
const char *gp = cfg_get("NX2NAVI_GPU_PASSES"), *dmp = cfg_get("NX2NAVI_DUMP");
int want = (gp && *gp && strcmp(gp, "0")) || (dmp && *dmp);
g_vk.has_mesh_queries = want && c->mesh.meshShaderQueries && h->pipelineStatisticsQuery && c->f12.hostQueryReset;
c->mesh.meshShaderQueries = g_vk.has_mesh_queries ? VK_TRUE : VK_FALSE;
if (!c->mesh.meshShader || !c->mesh.taskShader) g_vk.has_mesh_shader = 0;
rt_log("vk: mesh shaders: %s", g_vk.has_mesh_shader ? "VK_EXT_mesh_shader" : "the extension is there, its task or mesh feature is not");
} // vkGetDeviceFaultInfoEXT needs the feature, not just the extension
{ // mesh_fallback: auto, 1 (on every device) or 0 (vk_mesh_path.h says when auto takes it, and why on AMD's driver
// for RDNA 1 and 2). Without mesh shaders the mesh emulation (mesh_emulation = 1) draws them instead.
// has_mesh_shader keeps saying what the hardware has: the recorder thread (vk_rec.c) goes by it, not by how
// mesh programs are drawn.
const char *mf = cfg_value("NX2NAVI_MESH_FALLBACK"), *me = cfg_value("NX2NAVI_MESH_EMULATION"), *why = "";
int path = mesh_path_decide(g_vk.has_mesh_shader, vk_caps.amd_sc, g_vk.props.vendorID == 0x10de, mf, me, &why);
g_vk.mesh_fallback = path == MESH_PATH_FALLBACK;
g_vk.mesh_emulation = path == MESH_PATH_EMULATION;
g_mesh_path = path == MESH_PATH_EMULATION ? "emulated (mesh_emulation)"
: path == MESH_PATH_FALLBACK ? (g_vk.has_mesh_shader ? "through the mesh fallback on a device with mesh "
"shaders (compute passes)"
: "through the mesh fallback (compute passes)")
: path == MESH_PATH_NATIVE ? "native (VK_EXT_mesh_shader)" : "skipped (no mesh shaders, mesh_fallback = 0)";
if (path == MESH_PATH_EMULATION) rt_log("vk: mesh shaders: emulated with compute (mesh_emulation = 1); mesh_fallback is not used");
else if (path == MESH_PATH_FALLBACK)
rt_log("vk: mesh fallback: task and mesh programs run as compute passes and are drawn from their buffers (%s)",
why);
else if (path == MESH_PATH_NONE)
rt_log("vk: mesh shaders: not supported and mesh_fallback = 0 (task/mesh draws are skipped)");
}
if (g_vk.has_maximal_reconvergence && !c->mrc.shaderMaximalReconvergence) g_vk.has_maximal_reconvergence = 0;
if (g_vk.has_pipeline_binary && !(c->pbin.pipelineBinaries && c->m5.maintenance5)) g_vk.has_pipeline_binary = 0;
// The chain enables what the driver reported: a present carries a frame's number only with presentId, and
// vkAntiLagUpdateAMD may be called only with antiLag (the extension alone stays enabled and unused without it).
g_vk.has_present_id = g_vk.has_low_latency2 && c->pid.presentId;
if (g_vk.has_anti_lag && !c->antilag.antiLag) g_vk.has_anti_lag = 0;
g_vk.has_buffer_device_address = c->f12.bufferDeviceAddress;
g_vk.has_astc_ldr = h->textureCompressionASTC_LDR;
{
const char *e = cfg_get("NX2NAVI_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 (NX2NAVI_FEEDBACK=0)",
has_fbl && c->fbl.attachmentFeedbackLoopLayout, has_lr && c->lr.dynamicRenderingLocalRead);
}
// Frame generation (vk_fg.cpp) presents on a second queue of the graphics family and acquires on a third. AMD's
// swapchain needs a present queue nothing else submits to (FidelityFX: the engine must not use it, or it can
// deadlock); the image acquire queue needs no capability. Where the graphics family has one queue (AMD's Windows
// driver, an RX 580), both come from another family: a queue of a family that can present for the present, and
// a second queue of it, or of any other family, for the acquire (the present queue itself when there is none).
// Without them vk_fg_create_swapchain gave up without a word: frame generation set in the menu did nothing.
static const float prio[4] = {1.0f, 1.0f, 1.0f, 1.0f};
uint32_t nq_gfx = vk_fg_queue_count() < g_gfx_queues ? vk_fg_queue_count() : g_gfx_queues;
uint32_t used[QFAM_MAX] = {0};
if (g_vk.gfx_family < QFAM_MAX) used[g_vk.gfx_family] = nq_gfx;
if (g_vk.xfer_family != g_vk.gfx_family && g_vk.xfer_family < QFAM_MAX) used[g_vk.xfer_family] = 1;
uint32_t fg_pf = UINT32_MAX, fg_pi = 0, fg_af = UINT32_MAX, fg_ai = 0;
if (nq_gfx >= 2) {
fg_pf = fg_af = g_vk.gfx_family, fg_pi = 1, fg_ai = nq_gfx - 1;
} else {
for (int pass = 0; pass < 2 && fg_pf == UINT32_MAX; pass++) // compute families first, then the others
for (uint32_t f = 0; f < g_nqfam && fg_pf == UINT32_MAX; f++) {
if (f == g_vk.gfx_family || used[f] >= g_qfam_count[f]) continue;
if (!(g_qfam_flags[f] & (VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT | VK_QUEUE_TRANSFER_BIT))) continue;
if (!pass != !!(g_qfam_flags[f] & VK_QUEUE_COMPUTE_BIT)) continue;
if (!gfx_window_vk_presentable(g_vk.instance, g_vk.phys, f)) continue;
fg_pf = f, fg_pi = used[f]++;
}
if (fg_pf != UINT32_MAX) {
if (used[fg_pf] < g_qfam_count[fg_pf] && used[fg_pf] < 4) fg_af = fg_pf, fg_ai = used[fg_pf]++;
for (uint32_t f = 0; f < g_nqfam && fg_af == UINT32_MAX; f++)
if (f != g_vk.gfx_family && used[f] < g_qfam_count[f] && used[f] < 4) fg_af = f, fg_ai = used[f]++;
if (fg_af == UINT32_MAX) fg_af = fg_pf, fg_ai = fg_pi;
}
}
VkDeviceQueueCreateInfo qi[QFAM_MAX];
uint32_t nqi = 0;
qi[nqi++] = (VkDeviceQueueCreateInfo){.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
.queueFamilyIndex = g_vk.gfx_family, .queueCount = nq_gfx, .pQueuePriorities = prio};
for (uint32_t f = 0; f < g_nqfam; f++)
if (f != g_vk.gfx_family && used[f])
qi[nqi++] = (VkDeviceQueueCreateInfo){.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
.queueFamilyIndex = f, .queueCount = used[f], .pQueuePriorities = prio};
VkDeviceCreateInfo di = {.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, .pNext = &c->f2, .queueCreateInfoCount = nqi,
.pQueueCreateInfos = qi, .enabledExtensionCount = n_ext, .ppEnabledExtensionNames = ext};
VkResult r = vk_dlssg_create_device(g_vk.instance, g_vk.phys, &di, &g_vk.device);
free(c);
if (r != VK_SUCCESS) { rt_log("vk: vkCreateDevice failed (%d)", (int)r); return 0; }
if (submit_thread_why && vk_caps.mesa) rt_log("vk: queue submissions: %s", submit_thread_why);
vk_dlssg_device_ready(g_vk.phys, g_vk.device);
if (vk_dlssg_dpa()) { // Streamline's, so its plugins hook every entry point the replay fetches
vk_dpa = vk_dlssg_dpa();
rt_log("vk: device entry points come through Streamline's interposer (DLSS Frame Generation)");
}
vkGetDeviceQueue(g_vk.device, g_vk.gfx_family, 0, &g_vk.gfx_queue);
if (fg_pf != UINT32_MAX) {
VkQueue present, acquire;
vkGetDeviceQueue(g_vk.device, fg_pf, fg_pi, &present);
vkGetDeviceQueue(g_vk.device, fg_af, fg_ai, &acquire);
vk_fg_queues(present, fg_pf, acquire, fg_af);
if (fg_pf != g_vk.gfx_family)
rt_log("vk: frame generation queues: the graphics family %u has %u queue%s; presents on family %u queue %u, "
"image acquires on family %u queue %u", g_vk.gfx_family, g_gfx_queues, g_gfx_queues == 1 ? "" : "s",
fg_pf, fg_pi, fg_af, fg_ai);
} else {
rt_log("vk: frame generation queues: none (the graphics family %u has %u queue%s and no other family with a free "
"queue can present): AMD FSR 3.1 frame generation cannot run", g_vk.gfx_family, g_gfx_queues,
g_gfx_queues == 1 ? "" : "s");
}
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);
vk_mem_dmabuf_probe(g_vk.device, &g_vk.mem_props, vk_gpa_wanted_ctx(&g_vk)); // may turn shadow storage on
vk_mem_lazy_decide(); // the driver is known: the lazy write-back's default depends on it
rt_fatal_note_add(fatal_note);
static const VmaVulkanFunctions vf = {.vkAllocateMemory = timed_alloc}; // the rest from VMA's static table
VmaAllocatorCreateInfo ai = {.physicalDevice = g_vk.phys, .device = g_vk.device, .instance = g_vk.instance,
.vulkanApiVersion = VK_API_VERSION_1_3,
.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),
.preferredLargeHeapBlockSize = vk_vma_block_size(),
.pDeviceMemoryCallbacks = vk_headroom_callbacks(), .pVulkanFunctions = &vf};
VkDeviceSize heap_limit[VK_MAX_MEMORY_HEAPS];
if (vram_heap_limits(heap_limit)) ai.pHeapSizeLimit = heap_limit;
if ((r = vmaCreateAllocator(&ai, &g_vk.vma)) != VK_SUCCESS) { rt_log("vk: vmaCreateAllocator failed (%d)", (int)r); return 0; }
vk_mem_ballast(g_vk.vma);
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);
vk_lowlat_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_dlaa_release(); // the DLSS features and NGX: the allocator and the device still live
if (g_vk.device) vk_astc_release();
if (g_vk.device) vk_crumbs_release();
if (g_vk.device) vk_lowlat_release(); // the sleep's semaphore: the device still lives
if (g_vk.timeline) vkDestroySemaphore(g_vk.device, g_vk.timeline, NULL);
if (lost_fence) vkDestroyFence(g_vk.device, lost_fence, NULL); // vk_device_lost_poll
lost_fence = VK_NULL_HANDLE;
vk_headroom_stop();
if (g_vk.vma) vk_mem_ballast_release(g_vk.vma);
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_headroom_start(), vk_dlaa_init(), mem_budget_vram(1);
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) { mem_budget_vram(0); 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 report of a lost device: the breadcrumbs, the pass marks, the video memory and the driver's fault information.
// Once per process, on the thread that saw the loss first (vk_device_lost_check).
static void device_fault_report(void) {
vk_sync_lost_report();
vk_crumbs_report();
vk_pass_marks_report();
vk_mem_lost_report();
VkCtx *c = vk_ctx();
PFN_vkGetDeviceFaultInfoEXT get =
c && c->has_device_fault ? (PFN_vkGetDeviceFaultInfoEXT)vk_dpa(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");
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);
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);
}
// ---- the end of a run whose device is lost (vk.h vk_device_lost_check)
// A lost device stays lost: every later call on it fails or, worse, answers as if it had worked, and a driver that is
// called on from several threads after the loss can fault inside itself. So the first thread that sees the loss ends
// the run, and nothing else touches the device meanwhile: the paths that would read vk_lost and return, and a thread
// that gets the loss as a result of its own stops here for good.
// LOST_EXIT_MS bounds the end. The first thread writes the report and waits for a save commit (fs_host.c
// fs_save_crash_wait, 2 s at most); should it hang on a lock that a stopped thread holds, the watchdog (arg non-NULL)
// says so in one line and ends the process with the same code, and the lines already written are in the log
// (rt_fatal_code wrote them itself). The end is plat_terminate: ExitProcess would stop the watchdog too and then run
// every library's detach, and a driver's detach that hangs on a lost device would hold the process for good. A thread
// that stops here otherwise ends the process a little later, should the watchdog not have started.
#define LOST_EXIT_MS 10000
volatile int32_t vk_lost;
static void lost_wait_exit(void *arg) {
int watchdog = arg != NULL;
for (int t = 0; t < LOST_EXIT_MS + (watchdog ? 0 : 2000); t += 50) plat_sleep_ms(50);
if (watchdog) {
rt_log_ending(); // its line reaches the log file past its cap (main.c log_sink)
rt_log_sync(); // the queue's locks only tried (bounded): their holder may be the thread that hangs
rt_log("vk: the end of the run did not finish in %d s; ending it", LOST_EXIT_MS / 1000);
}
plat_terminate(RT_LOST_EXIT);
}
void vk_device_lost_check(VkResult r, const char *where) {
if (r != VK_ERROR_DEVICE_LOST) return;
if (plat_xchg32(&vk_lost, 1)) lost_wait_exit(NULL); // another thread is ending the run: this one stops here
plat_thread_spawn(lost_wait_exit, (void *)1, "vk lost exit", 0);
device_fault_report();
rt_fatal_code(RT_LOST_EXIT, "vk: the GPU device was lost (seen at: %s). The run ends here; the lines above name what "
"the GPU was doing", where);
}
void vk_device_lost_poll(void) {
VkCtx *c = vk_ctx();
if (!c || vk_device_is_lost()) return;
VkFence f = __atomic_load_n(&lost_fence, __ATOMIC_ACQUIRE);
if (!f) {
plat_mutex_lock(&lost_fence_lock);
if (!lost_fence) {
VkFenceCreateInfo fi = {.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
VkFence n = VK_NULL_HANDLE;
VkResult r = vkCreateFence(c->device, &fi, NULL, &n);
if (r == VK_SUCCESS) __atomic_store_n(&lost_fence, n, __ATOMIC_RELEASE);
else vk_device_lost_check(r, "the fence that asks whether the device lives");
}
f = lost_fence;
plat_mutex_unlock(&lost_fence_lock);
if (!f) return;
}
vk_device_lost_check(vkGetFenceStatus(c->device, f), "a poll of the device");
}
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};
vk_sync_signal_note(1); // a host signal of the timeline is noted first (vk_sync.c timeline_beyond)
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);
fatal_note(); // the line a fatal error or a crash would add to the log, to read it without one
vk_shutdown();
printf("probe-vkdevice: %s (validation errors %ld)\n", g_errors_total ? "FAILED" : "passed", g_errors_total);
return g_errors_total ? 1 : 0;
}
// The GPU has mesh shaders (vk_exec.c vk_exec_tail_begin: the early tail runs only on such a card).
int vk_mesh_shader_native(void) { return g_vk.has_mesh_shader; }