Add the low latency setting and the module that drives the drivers' own modes.

NX2NAVI_LOW_LATENCY (0 off, 1 on, 2 on with boost; live) is for low latency without frame generation: NVIDIA's mode
through VK_NV_low_latency2 and AMD's through VK_AMD_anti_lag, each enabled on the device only where the driver lists
it, NVIDIA's with VK_KHR_present_id, which it needs, and AMD's with its feature. A device made by the DLSS Frame
Generation proxies gets neither: that path keeps its own low latency. vk_lowlat.c holds the calls, the frame numbering
and the statistics line; nothing calls into it yet beyond the start and the end of the device.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-12 22:28:22 -05:00
parent d9bc002d9a
commit 2f30f1a2e5
6 changed files with 623 additions and 1 deletions

View File

@@ -635,6 +635,15 @@
# Each step adds a little latency, and a display shows no more than its refresh rate. Changes while the game runs
# take effect at once.
# framegen_multiplier = 2
# Low latency without frame generation: 0 off, 1 on, 2 on with boost (NVIDIA's: the GPU keeps a high clock; on AMD 2
# is 1). The graphics driver holds each frame's start, so the game reads the controller as late as the picture allows
# and no finished picture waits for its turn: NVIDIA's low-latency mode (VK_NV_low_latency2) or AMD's Anti-Lag
# (VK_AMD_anti_lag), whichever the driver has. On a driver with neither the setting does nothing. While it is on the
# driver holds the game to fps_limit, in place of the game's own limiter and of RivaTuner Statistics Server (rtss).
# With framegen = dlss on Windows, DLSS Frame Generation's own low latency is used and this setting is not; with
# framegen = fsr3 it runs on AMD GPUs and is off on NVIDIA ones while frames are generated. Changes while the game runs
# take effect at once.
# low_latency = 0
# HDR output, two kinds (both need HDR on in Windows, "Use HDR", or a Wayland compositor with colour management;
# without it the picture stays SDR, the game's own): hdr = 1 sends the game's scene tone mapped to your display, one

View File

@@ -65,7 +65,7 @@ static const char *const known[] = {
"POOL_FAST", "POOL_GROUP", "POOL_GROUP_CHECK", "POOL_PRESNAP", "POOL_PRESNAP_RUNS",
"SUBMIT_FINISH_GPU", "SVC_WAITS", "SNAP_WIDEN", "SUBMIT_FINISH_RESULTS", "SUBMIT_FINISH_KINDS", "FOLIAGE", "MIRROR_STAGE", "FLUSH_SKIP_CLEAN", "MIRROR_HOT", "MESH_EMULATION", "MESH_EMU_MB", "MESH_EMU_TASKS", "MESH_EMU_BATCH", "GUEST_OPT", "LOG_TEX_NEW", "DUMP_KEY", "VEC_INDEX_FIX", "PASS_TIMES",
"PROFILE_MAIN", "PROFILE_RENDER", "PROMPTS", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_END", "REEXEC",
"REFLEX", "RENDER_SCALE", "REPLAY_ASYNC", "REPLAY_PIPE", "REPLAY_RUNS", "REPLAY_WORKERS", "RTSS", "RTSS_ENUM", "SCENE_SHARE",
"REFLEX", "LOW_LATENCY", "RENDER_SCALE", "REPLAY_ASYNC", "REPLAY_PIPE", "REPLAY_RUNS", "REPLAY_WORKERS", "RTSS", "RTSS_ENUM", "SCENE_SHARE",
"PRESNAP_PLACE",
"REPLAY_CHECK", "RESOLVE_CHECK", "RESTART_WAIT", "ROMFS", "ROMFS_INDEX", "ROMFS_MMAP", "ROMFS_MODS", "ROMFS_PATCHES",
"ROMFS_PROOF_DIR", "ROMFS_VERIFY", "RSTICK_DEADZONE", "RUMBLE", "RUMBLE_CURVE", "RUMBLE_HD", "RUMBLE_HD_MS", "RUMBLE_STRENGTH", "RUMBLE_TRIGGERS", "SHADER_CACHE",
@@ -112,6 +112,7 @@ static const struct { const char *key, *section; } sections[] = {
{"HDR_UI_NITS", "display"}, {"SHADOW_RES", "display"},
{"DLAA", "display"}, {"FSR4", "display"}, {"FSR4_FG", "display"}, {"DLSS", "display"}, {"DLSS_PRESET", "display"}, {"DLSS_IDLE_FREE", "display"}, {"DLSS_RANGE", "display"}, {"DLSS_CHAR_MASK", "display"}, {"FXAA", "display"}, {"AA", "display"},
{"FRAMEGEN", "display"}, {"FRAMEGEN_MULTIPLIER", "display"}, {"FG_MULT", "display"}, {"SKIN_MOTION", "display"},
{"LOW_LATENCY", "display"},
{"DEVICE_PROFILE", "display"}, {"ASSUME_FOCUS", "display"},
{"DISCORD_RPC", "discord"}, {"DISCORD_APP_ID", "discord"}, {"DISCORD_IMAGE", "discord"},
{"COOP", "coop"}, {"COOP_BETA", "coop"}, {"COOP_NAME", "coop"}, {"COOP_SERVER", "coop"}, {"COOP_PORT", "coop"},
@@ -180,6 +181,8 @@ static const CfgMeta metas[] = {
// Screen
{"FULLSCREEN", "0", "0|1", LIVE}, {"WINDOW_SIZE", "1920x1080", NULL, LIVE},
{"VK_PRESENT", "mailbox", "mailbox|fifo|immediate", LIVE}, {"GPU_INDEX", "", NULL, RESTART},
// vk_lowlat.c level_now: the driver's low latency without frame generation (0 off, 1 on, 2 on with boost)
{"LOW_LATENCY", "0", "0|1|2", LIVE},
{"MONITOR", "auto", NULL, RESTART}, // auto|primary|<1-based index>|<part of a monitor name> (gfx_window_win.c, gfx_window_sdl.c chosen_monitor)
{"PERF_OVERLAY", "off", "off|fps|full", LIVE},
{"OUTPUT_RES", "1920x1080", NULL, RESTART}, {"RENDER_SCALE", "game", "game|50|67|75|100|125|150|200", RESTART},

View File

@@ -54,6 +54,9 @@ typedef struct VkCtx {
has_mesh_shader, has_vrs, // VK_EXT_mesh_shader with taskShader and meshShader (NVN2 task/mesh stages)
has_maximal_reconvergence, // VK_KHR_shader_maximal_reconvergence (vk_pipeline.c spirv_reconverge)
has_pipeline_binary, // VK_KHR_pipeline_binary with VK_KHR_maintenance5 (vk_pipeline.c pipe_create)
has_low_latency2, // VK_NV_low_latency2 with VK_KHR_present_id, which it needs (vk_lowlat.c)
has_present_id, // ... and the presentId feature: presents may carry a frame's number
has_anti_lag, // VK_AMD_anti_lag with its antiLag feature (vk_lowlat.c)
has_mesh_queries; // meshShaderQueries, kept while NX2NAVI_GPU_PASSES or NX2NAVI_DUMP is set
int mesh_fallback; // task and mesh programs run as compute passes and are drawn from buffers
// (mesh_fallback: auto without has_mesh_shader; vk_exec_draw.c "mesh fallback")

View File

@@ -17,6 +17,7 @@
#include "vk_gpa.h"
#include "vk_fg.h"
#include "vk_dlssg.h"
#include "vk_lowlat.h"
// The loader's vkGetDeviceProcAddr until Streamline is on the device (see vk.h).
PFN_vkGetDeviceProcAddr vk_dpa = vkGetDeviceProcAddr;
@@ -397,6 +398,8 @@ typedef struct {
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
@@ -507,6 +510,22 @@ static int create_device(void) {
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.
if (!vk_dlssg_dpa() && n_ext + 2 <= sizeof ext / sizeof *ext) {
if (has_ext(ep, ne, VK_NV_LOW_LATENCY_2_EXTENSION_NAME) && has_ext(ep, ne, VK_KHR_PRESENT_ID_EXTENSION_NAME)) {
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 (has_ext(ep, ne, VK_AMD_ANTI_LAG_EXTENSION_NAME)) {
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)) {
@@ -574,6 +593,10 @@ static int create_device(void) {
}
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;
{
@@ -682,6 +705,7 @@ static int create_device(void) {
return 0;
}
vk_crumbs_init(&g_vk);
vk_lowlat_init(&g_vk);
return 1;
}
@@ -693,6 +717,7 @@ static void destroy_all(void) {
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;

518
runtime/vk_lowlat.c Normal file
View File

@@ -0,0 +1,518 @@
// Low latency without frame generation (NX2NAVI_LOW_LATENCY: 0 off, 1 on, 2 on with boost; default 0; live).
//
// A frame's delay from the pad to the screen has two parts the runtime can shorten. The first is the time the frame
// waits after it is drawn: the frame limit used to hold a finished picture back until its slot (vk_backend.c
// present_main), so the pad was read up to a whole period before the picture left. The second is the queue in front of
// the GPU: when the GPU is the slower side, the game draws frames faster than they are shown and each waits its turn.
// Both go when the wait moves to the frame's start, before the pad is read: the driver knows how long the GPU took and
// when the last picture left, and holds the next frame's start until its picture can go out as soon as it is drawn.
//
// Two drivers offer that, and the device enables the one it lists (vk_device.c create_device):
// - NVIDIA, VK_NV_low_latency2. The window's swapchain is made with VkSwapchainLatencyCreateInfoNV (gfx_present.c
// create_swapchain) and the sleep mode is set on it: low latency, boost for the value 2, and the frame limit as the
// minimum interval. Each frame asks the driver for its sleep (vkLatencySleepNV, which signals a timeline semaphore
// when the frame may start) and marks its stages with the frame's number: simulation start, input sample,
// simulation end, render submit start and end, present start and end. The frame's queue submissions and its
// present carry the same number (VkLatencySubmissionPresentIdNV, VkPresentIdKHR).
// - AMD, VK_AMD_anti_lag. vkAntiLagUpdateAMD is called with the input stage where the frame starts (the driver waits
// inside the call) and with the present stage before the frame's present, both with the frame's number and the frame
// limit in frames a second.
// Any other driver has neither: the setting does nothing there and the menu's row says so.
//
// Threads. The game's frame is made on guest threads and shown by the present thread, so the calls are split:
// - guest thread, where the frame starts (vk_lowlat_frame_start): the first pad read (ov_input.c npad_states) or
// window image acquire (nvn_api_obj.c nvnWindowAcquireTexture) after a present, whichever the game makes first. The
// sleep, then the simulation-start marker; the pad read sets the input marker. The thread gives its virtual core
// up for the sleep, as around every host wait.
// - guest thread, the frame's first nvnQueueSubmitCommands (vk_lowlat_submit_start): simulation end, render submit
// start. The game's own threads cannot be told apart, so the simulation ends where the first commands are handed over.
// - the thread that replays (the guest's, or the replay thread with NX2NAVI_REPLAY_ASYNC), at the frame's present
// (vk_lowlat_submitted): render submit end. Its queue submissions are tagged in vk_sync.c queue_submit.
// - present thread (gfx_present.c flip_locked): the present stage or the present markers around the host present.
// The frame's number is the count of the game's presents, as vk_backend.c numbers the frame it hands to the present
// thread (PresentJob.frame): a guest thread starts frame n while n - 1 presents were made, and the replay and the
// present thread get n with the frame's work. A frame the present leaves out (vk_backend.c frame_hold) simply has no
// present stage.
//
// One limiter. With the mode on, the driver's sleep holds the frames to the frame limit and the runtime's own pacer
// holds nothing: nvnQueuePresentTexture asks vk_lowlat_pace, and on 1 gives its present the time now and frees the
// window image at once. Both together would wait twice, and the pacer's wait is the one after the picture is drawn,
// which is the delay the mode is there to remove. The pacer keeps the limit where the driver cannot: while a movie
// plays (nvn_api_obj.c pace keeps the movie's slots through a late frame, which a driver's limiter does not), while no
// host present goes out (a minimized window: the driver would have nothing to time its sleep by, so the sleep is left
// out too), and before the first host present. RivaTuner Statistics Server is kept out as with frame generation
// (frame_rate.c frame_step_cap_ns): it would hold the finished picture as the pacer did. NX2NAVI_PACE_SMOOTH and
// NX2NAVI_FRAME_HOLD_MS act on the present thread as before.
//
// Frame generation. DLSS Frame Generation through the vendor's interposer brings its own low latency
// (vk_dlssg.c, NX2NAVI_REFLEX): a device its proxies make does not get these extensions and this path stays off for
// the run. AMD's frame generation puts its own swapchain object in the window swapchain's place, which NVIDIA's calls
// cannot be given, so NVIDIA's path is off while it is there and on again with the plain swapchain; AMD's path has no
// swapchain in its calls and runs beside it. NVIDIA's frame generation with the runtime's own presenter (gfx_present.c
// ng_run) presents several times a rendered frame, which the driver would take for frames: off while it presents.
// Each is decided where the state changes: at device creation, at each swapchain creation, and at the present that
// starts or stops the presenter.
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "cfg.h"
#include "host.h"
#include "nvn_api.h"
#include "platform/platform.h"
#include "vk.h"
#include "vk_dlssg.h"
#include "vk_fg.h"
#include "vk_lowlat.h"
enum { LL_NONE, LL_NV, LL_AMD };
#define RING 64 // frames between a frame's start and its present are a handful at most
#define HOLD_IDLE_NS 200000000ull // no host present for this long: the driver's sleep is left out
#define SLEEP_MAX_NS 500000000ull // the longest wait for the driver to end a sleep
#define SLEEP_FAILS 3 // that many waits in a row running out: the mode is given up for the run
#define REPORTS 16 // frames of the driver's report read for a statistics line
#define PACE_WINDOW 60 // frames over which the driver's hold of the frame limit is checked
#define MODE_UNSET 0xffffffffu
#define LOAD(p) __atomic_load_n(p, __ATOMIC_ACQUIRE)
#define STORE(p, v) __atomic_store_n(p, v, __ATOMIC_RELEASE)
static struct {
int kind; // LL_*: fixed by vk_lowlat_init
int why; // vk_lowlat_status while kind is LL_NONE
int present_ids; // the presentId feature is on: the present carries the frame's number
VkDevice dev;
PFN_vkSetLatencySleepModeNV set_mode;
PFN_vkLatencySleepNV sleep;
PFN_vkSetLatencyMarkerNV marker;
PFN_vkGetLatencyTimingsNV timings;
PFN_vkAntiLagUpdateAMD antilag;
VkSemaphore sem; // NVIDIA: the timeline the driver signals to end a sleep
// NVIDIA's calls name the swapchain and nothing says they may run at once on two threads: all of them, and the
// swapchain's replacement, hold m. None of them waits (the sleep's wait is on the semaphore, outside m).
PlatMutex m;
VkSwapchainKHR sc; // m: the swapchain made with the latency mode, or none
uint32_t mode; // m to write, atomic to read: the sleep mode as last set on sc (mode_want), or MODE_UNSET
uint64_t sleep_value; // m: the semaphore value of the newest sleep
// atomics
int attached; // sc is set
int on; // the direct path is in effect
int said; // on, as last logged
int failed; // the driver refused the mode or did not end its sleeps: off for the run
int fg_block; // vk_lowlat_fg_presents
int pace_bad; // the driver's sleep did not hold the frame limit: the pacer holds it (vk_lowlat_pace)
int cfg_read, level; // the setting as last parsed (level_now)
unsigned cfg_seen;
uint32_t interval_us; // the period the driver holds frames to, 0 while the pacer does (vk_lowlat_pace)
uint32_t fails; // sleeps in a row whose wait ran out
uint64_t game; // the game's presents so far
uint64_t started; // the newest frame that started
uint64_t input_id; // the newest frame whose pad read was marked
uint64_t submit_id; // the newest frame whose first submission was marked
uint64_t cur; // the frame of the present thread's next blit
uint64_t hosted_ns; // kern_now_ns after the newest host present of a frame this path followed
uint64_t ring[RING]; // [n % RING] == n: frame n started with the path on, so its later stages are marked
uint64_t staged[RING]; // AMD, the same: frame n's input stage was called, so its present stage is too
// statistics since the last line (vk_lowlat_present_end)
uint64_t n_sleep, sleep_ns, n_input, n_unheld, n_timeout;
} L = {.why = VK_LOWLAT_NO_DRIVER, .mode = MODE_UNSET};
// NX2NAVI_LOW_LATENCY as a number, parsed again when a setting changed.
static int level_now(void) {
unsigned v = cfg_version();
if (!LOAD(&L.cfg_read) || v != LOAD(&L.cfg_seen)) {
int n = cfg_int("NX2NAVI_LOW_LATENCY");
STORE(&L.level, n < 0 ? 0 : n > 2 ? 2 : n);
STORE(&L.cfg_seen, v);
STORE(&L.cfg_read, 1);
}
return LOAD(&L.level);
}
static int level_eff(void) { return LOAD(&L.failed) ? 0 : level_now(); }
static int frame_on(uint64_t id) { return id && LOAD(&L.ring[id % RING]) == id; }
static void say(int on, const char *why) {
static int32_t lines;
if (__atomic_exchange_n(&L.said, on, __ATOMIC_ACQ_REL) == on || plat_inc32(&lines) > 40) return;
if (on)
rt_log("lowlat: low latency on%s: the driver holds each frame's start, and the frame limit with it",
L.kind == LL_NV && level_now() == 2 ? " with boost" : "");
else rt_log("lowlat: low latency off (%s): the game's own limiter paces the frames", why);
}
static void give_up(const char *what, int r) {
if (__atomic_exchange_n(&L.failed, 1, __ATOMIC_ACQ_REL)) return;
rt_log("lowlat: %s (%d): low latency is off for the rest of the run", what, r);
}
// ---- NVIDIA
// The sleep mode in one word: bits 0-1 the level (0 off, 1 low latency, 2 with boost), the rest the minimum interval
// in microseconds. 0 while the setting is off or the runtime's frame generation presenter presents.
static uint32_t mode_want(void) {
int lv = LOAD(&L.fg_block) ? 0 : level_eff();
return lv ? (uint32_t)lv | LOAD(&L.interval_us) << 2 : 0;
}
// m held, sc set. vkSetLatencySleepModeNV: sc is a live swapchain of dev (it is replaced only under m), the structure
// has its type; the call returns at once.
static void nv_mode_locked(uint32_t want) {
if (want == L.mode) return;
VkLatencySleepModeInfoNV mi = {.sType = VK_STRUCTURE_TYPE_LATENCY_SLEEP_MODE_INFO_NV,
.lowLatencyMode = (want & 3) != 0, .lowLatencyBoost = (want & 3) == 2,
.minimumIntervalUs = want >> 2};
VkResult r = L.set_mode(L.dev, L.sc, &mi);
if (r != VK_SUCCESS) {
give_up("the driver refused the low-latency sleep mode", (int)r);
want = 0; // taken as off: nothing sleeps on it
}
STORE(&L.mode, want);
STORE(&L.on, (want & 3) != 0);
say((want & 3) != 0, LOAD(&L.failed) ? "the driver refused it" : LOAD(&L.fg_block) ? "frame generation presents the frames"
: "the setting");
}
static void nv_marker(VkLatencyMarkerNV marker, uint64_t id) {
VkSetLatencyMarkerInfoNV mi = {.sType = VK_STRUCTURE_TYPE_SET_LATENCY_MARKER_INFO_NV, .presentID = id, .marker = marker};
plat_mutex_lock(&L.m);
if (L.sc) L.marker(L.dev, L.sc, &mi);
plat_mutex_unlock(&L.m);
}
// The driver's sleep for the frame about to start: vkLatencySleepNV returns at once and signals the timeline semaphore
// with the value given when the frame may start. Each sleep takes the next value, so a wait that ran out leaves a value
// behind that a later signal passes. 1 when the driver ended the sleep.
static int nv_sleep(void) {
VkLatencySleepInfoNV si = {.sType = VK_STRUCTURE_TYPE_LATENCY_SLEEP_INFO_NV, .signalSemaphore = L.sem};
VkResult r = VK_NOT_READY;
plat_mutex_lock(&L.m);
if (L.sc && L.mode != MODE_UNSET && (L.mode & 3)) {
si.value = ++L.sleep_value;
r = L.sleep(L.dev, L.sc, &si);
}
plat_mutex_unlock(&L.m);
if (r == VK_NOT_READY) return 0; // the swapchain went, or the mode was switched off, since the frame started
if (r != VK_SUCCESS) {
vk_device_lost_check(r, "the low-latency sleep");
give_up("the driver refused the low-latency sleep", (int)r);
return 0;
}
VkSemaphoreWaitInfo wi = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO, .semaphoreCount = 1, .pSemaphores = &L.sem,
.pValues = &si.value};
r = vkWaitSemaphores(L.dev, &wi, SLEEP_MAX_NS);
vk_device_lost_check(r, "the wait for the low-latency sleep");
if (r == VK_SUCCESS) {
STORE(&L.fails, 0);
return 1;
}
__atomic_add_fetch(&L.n_timeout, 1, __ATOMIC_RELAXED);
if (__atomic_add_fetch(&L.fails, 1, __ATOMIC_ACQ_REL) >= SLEEP_FAILS)
give_up("the driver did not end its low-latency sleep three times in a row", (int)r);
return 0;
}
// ---- AMD
// vkAntiLagUpdateAMD with a stage of frame id: the antiLag feature is enabled (vk_device.c create_device keeps
// has_anti_lag only with it), both structures have their types, the mode and the stage are values of their enums.
static void amd_stage(VkAntiLagStageAMD stage, uint64_t id) {
uint32_t us = LOAD(&L.interval_us);
VkAntiLagPresentationInfoAMD pi = {.sType = VK_STRUCTURE_TYPE_ANTI_LAG_PRESENTATION_INFO_AMD, .stage = stage,
.frameIndex = id};
VkAntiLagDataAMD d = {.sType = VK_STRUCTURE_TYPE_ANTI_LAG_DATA_AMD, .mode = VK_ANTI_LAG_MODE_ON_AMD,
.maxFPS = us ? (1000000u + us / 2) / us : 0, .pPresentationInfo = &pi};
L.antilag(L.dev, &d);
}
// ---- both
// The guest thread at a frame's start: brings the driver's mode in step with the setting, the frame limit and what
// presents the frames. 1 while the direct path is in effect.
static int refresh(void) {
if (L.kind == LL_AMD) {
int want = level_eff() > 0, was = __atomic_exchange_n(&L.on, want, __ATOMIC_ACQ_REL);
if (was && !want) { // switched off: the driver is told once, without a stage
VkAntiLagDataAMD d = {.sType = VK_STRUCTURE_TYPE_ANTI_LAG_DATA_AMD, .mode = VK_ANTI_LAG_MODE_OFF_AMD};
L.antilag(L.dev, &d);
}
say(want, LOAD(&L.failed) ? "the driver refused it" : "the setting");
return want;
}
if (!LOAD(&L.attached)) return 0; // no swapchain with the mode: on is 0 (vk_lowlat_swapchain)
uint32_t want = mode_want();
if (want != LOAD(&L.mode)) {
plat_mutex_lock(&L.m);
if (L.sc) nv_mode_locked(want);
plat_mutex_unlock(&L.m);
}
return LOAD(&L.on);
}
static void frame_begin(uint64_t id, int at_input) {
uint64_t t0 = kern_now_ns(), hosted = LOAD(&L.hosted_ns);
STORE(&L.ring[id % RING], id);
if (hosted && t0 - hosted < HOLD_IDLE_NS) {
int away = kern_core_away(); // 0 where the caller gave the core up already (the acquire)
uint64_t p0 = perf_begin();
if (L.kind == LL_NV) nv_sleep();
else STORE(&L.staged[id % RING], id), amd_stage(VK_ANTI_LAG_STAGE_INPUT_AMD, id);
perf_end(PERF_PACE, p0);
kern_core_back(&away);
__atomic_add_fetch(&L.sleep_ns, kern_now_ns() - t0, __ATOMIC_RELAXED);
__atomic_add_fetch(&L.n_sleep, 1, __ATOMIC_RELAXED);
if (at_input) __atomic_add_fetch(&L.n_input, 1, __ATOMIC_RELAXED);
} else {
__atomic_add_fetch(&L.n_unheld, 1, __ATOMIC_RELAXED);
}
if (L.kind == LL_NV) nv_marker(VK_LATENCY_MARKER_SIMULATION_START_NV, id);
}
void vk_lowlat_frame_start(int at_input) {
if (L.kind == LL_NONE) return;
if (!refresh() || vk_device_is_lost()) return;
uint64_t id = LOAD(&L.game) + 1, prev = LOAD(&L.started);
// once a frame, whichever guest thread comes first: the game reads several pads and may acquire again
if (prev != id && __atomic_compare_exchange_n(&L.started, &prev, id, 0, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED))
frame_begin(id, at_input);
if (!at_input || L.kind != LL_NV) return;
prev = LOAD(&L.input_id);
if (prev != id && __atomic_compare_exchange_n(&L.input_id, &prev, id, 0, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED) && frame_on(id))
nv_marker(VK_LATENCY_MARKER_INPUT_SAMPLE_NV, id);
}
void vk_lowlat_submit_start(void) {
if (L.kind != LL_NV || !LOAD(&L.on)) return;
uint64_t id = LOAD(&L.game) + 1, prev = LOAD(&L.submit_id);
if (prev == id || !frame_on(id)) return;
if (!__atomic_compare_exchange_n(&L.submit_id, &prev, id, 0, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) return;
nv_marker(VK_LATENCY_MARKER_SIMULATION_END_NV, id);
nv_marker(VK_LATENCY_MARKER_RENDERSUBMIT_START_NV, id);
}
void vk_lowlat_game_present(void) { __atomic_add_fetch(&L.game, 1, __ATOMIC_RELEASE); }
void vk_lowlat_submitted(uint64_t frame) {
if (L.kind == LL_NV && frame_on(frame)) nv_marker(VK_LATENCY_MARKER_RENDERSUBMIT_END_NV, frame);
}
// The pacer gives the frame limit up only while the driver keeps it, and that is checked: of every PACE_WINDOW frames
// the driver was to hold, at most half may come sooner than three quarters of the period after the frame before. More
// means nothing holds the game (a driver that ignores the interval, or a game whose pad read and image acquire are not
// on the thread that makes the frame, so the sleep held another thread), and a game that runs free can outrun what
// its own code survives (frame_rate.c LIMIT_FREE_MAX_HZ). The pacer then holds the limit for the rest of the run and
// the driver's sleep goes on without an interval.
int vk_lowlat_pace(uint64_t period_ns) {
static uint64_t last_ns, last_period; // the presenting guest thread's: the newest present the driver was to hold
static int window, fast;
if (!LOAD(&L.on)) return 0;
uint64_t now = kern_now_ns(), hosted = LOAD(&L.hosted_ns);
if (last_period && last_period == period_ns && !LOAD(&L.pace_bad)) {
if (now - last_ns < period_ns / 4 * 3) fast++;
if (++window >= PACE_WINDOW) {
if (fast > PACE_WINDOW / 2) {
STORE(&L.pace_bad, 1);
rt_log("lowlat: the driver's sleep does not hold the frame limit (%d of %d frames came sooner than three "
"quarters of %.2f ms): the game's own limiter holds it for the rest of the run", fast, PACE_WINDOW,
(double)period_ns / 1e6);
}
window = fast = 0;
}
}
uint64_t us = period_ns && !LOAD(&L.pace_bad) && hosted && now - hosted < HOLD_IDLE_NS ? (period_ns + 500) / 1000 : 0;
if (us > 0x3fffffffu) us = 0x3fffffffu;
STORE(&L.interval_us, (uint32_t)us);
last_ns = now, last_period = us ? period_ns : 0;
return us != 0;
}
// VkLatencySubmissionPresentIdNV extends VkSubmitInfo2 where the extension is enabled, which kind LL_NV says.
static void tag_fill(VkLatencySubmissionPresentIdNV *tag, uint64_t id) {
*tag = (VkLatencySubmissionPresentIdNV){.sType = VK_STRUCTURE_TYPE_LATENCY_SUBMISSION_PRESENT_ID_NV, .presentID = id};
}
// The frame being replayed is the one after the newest present the replaying thread counted (vk_backend.c: the guest
// thread in vk_be_present, the replay thread at its present item).
int vk_lowlat_submit_tag(VkLatencySubmissionPresentIdNV *tag) {
if (L.kind != LL_NV || !LOAD(&L.on)) return 0;
uint64_t id = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED) + 1;
if (!frame_on(id)) return 0;
tag_fill(tag, id);
return 1;
}
void vk_lowlat_present_frame(uint64_t frame) { STORE(&L.cur, frame); }
uint64_t vk_lowlat_blit_frame(VkLatencySubmissionPresentIdNV *tag, int *tagged) {
*tagged = 0;
if (L.kind == LL_NONE) return 0;
uint64_t id = __atomic_exchange_n(&L.cur, 0, __ATOMIC_ACQ_REL);
if (!frame_on(id)) return 0;
if (L.kind == LL_NV) tag_fill(tag, id), *tagged = 1;
return id;
}
// The caller holds the present lock, under which the swapchain is replaced too: sc, when set, is the swapchain this
// present goes to. VkPresentIdKHR: one swapchain as in the present, the presentId feature on (present_ids), and the
// frame numbers of a swapchain's presents only grow (a frame is presented once, in order; a new swapchain starts over).
const void *vk_lowlat_present_begin(uint64_t frame, VkLowlatPresent *p) {
p->id = 0;
if (L.kind == LL_NONE || !frame_on(frame)) return NULL;
p->id = frame;
if (L.kind == LL_AMD) {
if (LOAD(&L.on) && LOAD(&L.staged[frame % RING]) == frame) amd_stage(VK_ANTI_LAG_STAGE_PRESENT_AMD, frame);
return NULL;
}
VkSetLatencyMarkerInfoNV mi = {.sType = VK_STRUCTURE_TYPE_SET_LATENCY_MARKER_INFO_NV, .presentID = frame,
.marker = VK_LATENCY_MARKER_PRESENT_START_NV};
plat_mutex_lock(&L.m);
int have = L.sc != VK_NULL_HANDLE;
if (have) L.marker(L.dev, L.sc, &mi);
plat_mutex_unlock(&L.m);
if (!have || !L.present_ids) return NULL;
p->info = (VkPresentIdKHR){.sType = VK_STRUCTURE_TYPE_PRESENT_ID_KHR, .swapchainCount = 1, .pPresentIds = &p->id};
return &p->info;
}
// The driver's own account of the newest frames (vkGetLatencyTimingsNV; every report has its type, the count says how
// many the array holds and comes back as how many were written), averaged into text for the statistics line.
static void nv_report(char *out, size_t cap) {
static VkLatencyTimingsFrameReportNV rep[REPORTS]; // the present thread's
memset(rep, 0, sizeof rep);
for (int i = 0; i < REPORTS; i++) rep[i].sType = VK_STRUCTURE_TYPE_LATENCY_TIMINGS_FRAME_REPORT_NV;
VkGetLatencyMarkerInfoNV gi = {.sType = VK_STRUCTURE_TYPE_GET_LATENCY_MARKER_INFO_NV, .timingCount = REPORTS, .pTimings = rep};
plat_mutex_lock(&L.m);
if (L.sc) L.timings(L.dev, L.sc, &gi);
else gi.timingCount = 0;
plat_mutex_unlock(&L.m);
uint32_t n = gi.timingCount > REPORTS ? REPORTS : gi.timingCount, used = 0;
double total = 0, sim = 0, sub = 0, pres = 0, drv = 0, osq = 0, gpu = 0;
#define SPAN(a, b) ((b) > (a) ? (double)((b) - (a)) / 1000.0 : 0.0)
for (uint32_t i = 0; i < n; i++) {
const VkLatencyTimingsFrameReportNV *t = &rep[i];
uint64_t from = t->inputSampleTimeUs ? t->inputSampleTimeUs : t->simStartTimeUs;
if (!from || t->gpuRenderEndTimeUs <= from) continue; // a frame the driver has not finished timing
used++;
total += SPAN(from, t->gpuRenderEndTimeUs), sim += SPAN(t->simStartTimeUs, t->simEndTimeUs);
sub += SPAN(t->renderSubmitStartTimeUs, t->renderSubmitEndTimeUs), pres += SPAN(t->presentStartTimeUs, t->presentEndTimeUs);
drv += SPAN(t->driverStartTimeUs, t->driverEndTimeUs), osq += SPAN(t->osRenderQueueStartTimeUs, t->osRenderQueueEndTimeUs);
gpu += SPAN(t->gpuRenderStartTimeUs, t->gpuRenderEndTimeUs);
}
#undef SPAN
if (!used) {
snprintf(out, cap, "; the driver reported no frame timings (%u reports)", n);
return;
}
snprintf(out, cap, "; the driver's report over %u frames: pad read to GPU done %.2f ms (simulation %.2f, render "
"submit %.2f, present call %.2f, driver %.2f, OS queue %.2f, GPU %.2f)", used, total / used, sim / used,
sub / used, pres / used, drv / used, osq / used, gpu / used);
}
// One line after the first 600 frames the path followed and every 3600 after that (every 600 with NX2NAVI_TRACE=perf):
// how many frame starts the driver held and for how long, where they were held, and the driver's report.
static void stats(uint64_t frame) {
static uint64_t shown, next = 600; // the present thread's
if (++shown < next) return;
next = shown + (rt_trace & RT_TRACE_PERF ? 600 : 3600);
uint64_t n = __atomic_exchange_n(&L.n_sleep, 0, __ATOMIC_RELAXED), ns = __atomic_exchange_n(&L.sleep_ns, 0, __ATOMIC_RELAXED);
uint64_t in = __atomic_exchange_n(&L.n_input, 0, __ATOMIC_RELAXED), un = __atomic_exchange_n(&L.n_unheld, 0, __ATOMIC_RELAXED);
uint64_t late = __atomic_exchange_n(&L.n_timeout, 0, __ATOMIC_RELAXED);
uint32_t us = LOAD(&L.interval_us);
char limit[96], drv[320] = "";
if (us) snprintf(limit, sizeof limit, "the driver holds frames to %.3f ms", us / 1000.0);
else snprintf(limit, sizeof limit, "the game's own limiter holds the frame limit");
if (L.kind == LL_NV) nv_report(drv, sizeof drv);
rt_log("lowlat: frame %llu: %s; %llu frame starts held %.2f ms on average, %llu at the pad read and %llu at the "
"image acquire; %llu not held, %llu sleeps not ended in time%s", (unsigned long long)frame, limit,
(unsigned long long)n, n ? (double)ns / (double)n / 1e6 : 0.0, (unsigned long long)in,
(unsigned long long)(n - in), (unsigned long long)un, (unsigned long long)late, drv);
}
void vk_lowlat_present_end(const VkLowlatPresent *p) {
if (!p->id) return;
if (L.kind == LL_NV) nv_marker(VK_LATENCY_MARKER_PRESENT_END_NV, p->id);
STORE(&L.hosted_ns, kern_now_ns());
stats(p->id);
}
// ---- the swapchain and the state
int vk_lowlat_swapchain_wanted(void) { return L.kind == LL_NV && level_eff() > 0; }
void vk_lowlat_swapchain(VkSwapchainKHR sc, int made) {
if (L.kind != LL_NV) return;
plat_mutex_lock(&L.m);
L.sc = sc;
STORE(&L.mode, MODE_UNSET); // a new swapchain has no mode yet; none has none
STORE(&L.on, 0);
if (sc) nv_mode_locked(mode_want());
STORE(&L.attached, sc != VK_NULL_HANDLE);
plat_mutex_unlock(&L.m);
// a swapchain was made and it is not one for this path, with the setting on: a frame generation's (a resize in
// between says nothing: the next swapchain is there a moment later)
if (made && !sc && level_eff() > 0) say(0, "a frame generation's swapchain is in the window swapchain's place");
}
void vk_lowlat_swapchain_refused(int result) { give_up("no swapchain could be made with the latency mode", result); }
void vk_lowlat_fg_presents(int on) {
if (L.kind == LL_NV) STORE(&L.fg_block, on != 0); // taken up at the next frame's start (refresh)
}
int vk_lowlat_on(void) { return LOAD(&L.on); }
int vk_lowlat_status(void) {
if (L.kind == LL_NONE) return L.why;
if (LOAD(&L.failed)) return VK_LOWLAT_FAILED;
if (L.kind == LL_AMD) return VK_LOWLAT_AMD;
return vk_fg_active() || LOAD(&L.fg_block) ? VK_LOWLAT_FG : VK_LOWLAT_NV;
}
void vk_lowlat_init(VkCtx *c) {
int level = level_now();
L.dev = c->device;
L.kind = LL_NONE, L.why = VK_LOWLAT_NO_DRIVER, L.present_ids = 0; // a probe makes the device more than once
STORE(&L.failed, 0);
STORE(&L.pace_bad, 0);
if (vk_dlssg_dpa()) {
L.why = VK_LOWLAT_DLSSG;
rt_log("lowlat: DLSS Frame Generation's interposer owns low latency in this run (NX2NAVI_REFLEX); the direct "
"path is off (NX2NAVI_LOW_LATENCY=%d)", level);
return;
}
if (c->has_low_latency2) {
L.set_mode = (PFN_vkSetLatencySleepModeNV)vk_dpa(c->device, "vkSetLatencySleepModeNV");
L.sleep = (PFN_vkLatencySleepNV)vk_dpa(c->device, "vkLatencySleepNV");
L.marker = (PFN_vkSetLatencyMarkerNV)vk_dpa(c->device, "vkSetLatencyMarkerNV");
L.timings = (PFN_vkGetLatencyTimingsNV)vk_dpa(c->device, "vkGetLatencyTimingsNV");
// the sleep's semaphore has to be a timeline semaphore (the timelineSemaphore feature is one the device needs)
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 (L.set_mode && L.sleep && L.marker && L.timings && vkCreateSemaphore(c->device, &si, NULL, &L.sem) == VK_SUCCESS) {
L.present_ids = c->has_present_id;
L.kind = LL_NV;
rt_log("lowlat: NVIDIA's low latency through VK_NV_low_latency2 (presents %s the frame's number): %s "
"(NX2NAVI_LOW_LATENCY=%d)", L.present_ids ? "carry" : "do not carry",
level == 2 ? "on with boost" : level ? "on" : "off", level);
return;
}
rt_log("lowlat: VK_NV_low_latency2 is enabled but its entry points or the sleep's semaphore are missing");
} else if (c->has_anti_lag) {
L.antilag = (PFN_vkAntiLagUpdateAMD)vk_dpa(c->device, "vkAntiLagUpdateAMD");
if (L.antilag) {
L.kind = LL_AMD;
rt_log("lowlat: AMD's Anti-Lag through VK_AMD_anti_lag: %s (NX2NAVI_LOW_LATENCY=%d)", level ? "on" : "off", level);
return;
}
rt_log("lowlat: VK_AMD_anti_lag is enabled but its entry point is missing");
}
rt_log("lowlat: this driver has neither VK_NV_low_latency2 nor VK_AMD_anti_lag: no low-latency mode%s",
level ? " (NX2NAVI_LOW_LATENCY does nothing here)" : "");
}
void vk_lowlat_release(void) {
L.kind = LL_NONE;
STORE(&L.on, 0);
STORE(&L.attached, 0);
L.sc = VK_NULL_HANDLE;
if (L.sem) vkDestroySemaphore(L.dev, L.sem, NULL);
L.sem = VK_NULL_HANDLE;
}

64
runtime/vk_lowlat.h Normal file
View File

@@ -0,0 +1,64 @@
// Low latency without frame generation (vk_lowlat.c): the driver's own mode, NVIDIA's through VK_NV_low_latency2 or
// AMD's through VK_AMD_anti_lag, used directly. NX2NAVI_LOW_LATENCY: 0 off, 1 on, 2 on with boost (NVIDIA), live.
#pragma once
#include <stdint.h>
#include <vulkan/vulkan.h>
struct VkCtx;
// vk_device.c create_device, once the device exists: takes the entry points of the extension the device enabled
// (VkCtx.has_low_latency2, has_anti_lag) and logs which path this run has. vk_lowlat_release: before the device goes.
void vk_lowlat_init(struct VkCtx *c);
void vk_lowlat_release(void);
// What the menu's row shows (ui/menu.cpp row_low_latency). The first two are the paths that can run; the others say
// why none does: the driver has neither extension, DLSS Frame Generation's interposer owns low latency in this run
// (vk_dlssg.c, NX2NAVI_REFLEX), a frame generation presents the frames in the window swapchain's place, or the driver
// refused the mode or never ended its sleep (the log says which).
enum { VK_LOWLAT_NV, VK_LOWLAT_AMD, VK_LOWLAT_NO_DRIVER, VK_LOWLAT_DLSSG, VK_LOWLAT_FG, VK_LOWLAT_FAILED };
int vk_lowlat_status(void);
// The direct path is in effect now: the setting is on and nothing else owns the presents. Any thread, one atomic load.
int vk_lowlat_on(void);
// ---- the window's swapchain (gfx_present.c, with its present lock held). NVIDIA's mode belongs to a swapchain.
// 1 when the next plain swapchain is to be made with VkSwapchainLatencyCreateInfoNV (NVIDIA's path, the setting on).
int vk_lowlat_swapchain_wanted(void);
// made 1, after a swapchain was created: the plain swapchain made that way, on which the sleep mode is set from here
// on, or VK_NULL_HANDLE for one this path cannot name (a frame generation's, one made without the mode). made 0, with
// VK_NULL_HANDLE: before the window's swapchain is replaced or destroyed.
void vk_lowlat_swapchain(VkSwapchainKHR sc, int made);
// No swapchain could be made with the mode asked for (gfx_present.c create_swapchain, after three tries): the mode is
// given up for the run, so the next swapchain is made without it.
void vk_lowlat_swapchain_refused(int result);
// The frames go out through a frame generation presenter of the runtime's own (gfx_present.c ng_ready), several presents
// a rendered frame: the direct path steps aside while on is 1.
void vk_lowlat_fg_presents(int on);
// ---- the game's frame. Frames are numbered by the game's presents, frame n ending with the n-th, as
// gfx_counters.frames numbers them at the present (vk_backend.c), so the number a guest thread starts a frame with is
// the one the replay and the present thread end it with.
// A frame starts: the first of the game's pad read (at_input 1, ov_input.c) and its acquire of a window image (0,
// nvn_api_obj.c) after a present. The driver's sleep, then the simulation-start marker; the pad read also sets the
// input marker. Returns at once for a frame that already started and while the mode is off.
void vk_lowlat_frame_start(int at_input);
void vk_lowlat_submit_start(void); // the frame's first submission is handed over (vk_backend.c vk_be_submit)
void vk_lowlat_game_present(void); // the game presents it (vk_backend.c vk_be_present): counts it
void vk_lowlat_submitted(uint64_t frame); // its submissions were replayed (where vk_dlssg_submitted is called)
// nvn_api_obj.c nvnQueuePresentTexture: 1 when the driver's sleep holds the next frame to period_ns, so the pacer must
// not wait as well; 0 when the pacer holds it as before (the mode off, period 0, or no host present went out lately).
int vk_lowlat_pace(uint64_t period_ns);
// vk_sync.c queue_submit: 1 with *tag filled when the submission being handed to the queue is to carry the frame's
// number (NVIDIA's path on); the caller chains it into its VkSubmitInfo2.
int vk_lowlat_submit_tag(VkLatencySubmissionPresentIdNV *tag);
// ---- the host present (the present thread)
void vk_lowlat_present_frame(uint64_t frame); // vk_backend.c present_main: the next blit is of this frame
// gfx_present.c present_image, at the blit's submit: the frame set above when the direct path follows it (0 otherwise),
// taken, and *tag filled when *tagged is set to 1 as for vk_lowlat_submit_tag.
uint64_t vk_lowlat_blit_frame(VkLatencySubmissionPresentIdNV *tag, int *tagged);
// Around the present of that frame (gfx_present.c flip_locked; frame 0: nothing). Begin: AMD's present stage or
// NVIDIA's present-start marker; returns what goes into the VkPresentInfoKHR's pNext (the frame's present id) or NULL.
// End: the present-end marker and the statistics.
typedef struct { VkPresentIdKHR info; uint64_t id; } VkLowlatPresent;
const void *vk_lowlat_present_begin(uint64_t frame, VkLowlatPresent *p);
void vk_lowlat_present_end(const VkLowlatPresent *p);