From e0843439a7346ad62a7fb5add9545c4ebbefff7d Mon Sep 17 00:00:00 2001 From: chrg6 Date: Fri, 13 Nov 2026 18:58:40 -0500 Subject: [PATCH] Add the reflex_limit setting (NX2NAVI_REFLEX_LIMIT, default 1, live), which gives the frame limit back to the game's pacer with DLSS Frame Generation. Reflex's hold of the frame limit with DLSS-G is unmeasured here (no RTX 40 or 50), so a player needs a way back to the pacing of 0.10.1 without an older DLL. vk_dlssg.c limit_allowed reads the setting through cfg_bool, again whenever a setting changed (cfg_version), as vk_lowlat.c reads low_latency; with 0 the presenting guest thread asks Reflex for no limit, the present thread hands Streamline 0, and the "frame rate:" line names NX2NAVI_REFLEX_LIMIT=0 as the reason the game's own limiter holds it. The row sits beside FG_MULT in metas[], beside REFLEX in known[] and beside LOW_LATENCY in sections[] (display), and nx2navi.ini.example explains it under framegen_multiplier. cfg_rows_check reports 0 problems; vk_dlssg.c and cfg.c compile clean with the dev build's flags. Co-Authored-By: Claude Opus 5.5 --- docs/architecture.md | 2 +- nx2navi.ini.example | 8 ++++++++ runtime/cfg.c | 7 +++++-- runtime/vk_dlssg.c | 23 +++++++++++++++-------- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 6ce79c75..b5a11d4f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -334,7 +334,7 @@ keeps per-core data without locks and relies on that, so the host models it (`ru (`frame_rate.c` `frame_step_ceiling_ns`). Since the SDK does not say whether the driver's interval counts the frames the game draws or every frame on screen, the frames are checked: about the multiplier periods apart switches Reflex to the period over the multiplier once, and the same again or frames too soon give the limit to the pacer for the - run. + run. `NX2NAVI_REFLEX_LIMIT=0` (live) leaves the limit to the pacer, as before Reflex's frame limit was used. The device gets the drivers' extensions only in a run that starts with the setting on (`vk_device.c` `create_device`): turning it on takes a restart, and with it off the device is made as it was before the mode. - ASTC textures are uploaded as they are where the device samples ASTC, and decoded by a compute shader everywhere diff --git a/nx2navi.ini.example b/nx2navi.ini.example index b9ce5245..a018d020 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -782,6 +782,14 @@ # 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 +# With framegen = dlss, Reflex holds fps_limit where each frame starts, before the controller is read, and the game's +# own limiter keeps only its ceiling of 240 frames a second: a finished picture no longer waits for its turn, which +# takes up to a frame of the game's off the delay from the controller to the screen. The game's own limiter takes the +# limit back while a movie plays, while the window is minimised, and for the rest of the run when the frames show that +# Reflex does not hold it; the log's "frame rate:" lines say which one holds it. 0 leaves the limit to the game's own +# limiter, as before 0.10.2. Needs the nx2navi_dlssg.dll of 0.10.2 or later (an older one leaves the limit to the +# game). Changes while the game runs take effect at once. +# reflex_limit = 1 # 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 diff --git a/runtime/cfg.c b/runtime/cfg.c index 1f835953..870f7282 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -67,7 +67,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", "LOW_LATENCY", "RENDER_SCALE", "REPLAY_ASYNC", "REPLAY_PIPE", "REPLAY_RUNS", "REPLAY_SMALL", "TAIL_EARLY", "REPLAY_WORKERS", "RTSS", "RTSS_ENUM", "SCENE_SHARE", + "REFLEX", "REFLEX_LIMIT", "LOW_LATENCY", "RENDER_SCALE", "REPLAY_ASYNC", "REPLAY_PIPE", "REPLAY_RUNS", "REPLAY_SMALL", "TAIL_EARLY", "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", @@ -127,7 +127,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_PREBLOOM", "display"}, {"DLSS_CHAR_MASK", "display"}, {"FXAA", "display"}, {"AA", "display"}, {"FRAMEGEN", "display"}, {"FRAMEGEN_MULTIPLIER", "display"}, {"FG_MULT", "display"}, {"SKIN_MOTION", "display"}, - {"LOW_LATENCY", "display"}, + {"LOW_LATENCY", "display"}, {"REFLEX_LIMIT", "display"}, {"DEVICE_PROFILE", "display"}, {"ASSUME_FOCUS", "display"}, {"DISCORD_RPC", "discord"}, {"DISCORD_APP_ID", "discord"}, {"DISCORD_IMAGE", "discord"}, {"SHADOW_SCALE", "display"}, {"SHADOW_RANGE", "display"}, {"CONTACT_SHADOWS", "display"}, @@ -221,6 +221,9 @@ static const CfgMeta metas[] = { {"FRAMEGEN_MULTIPLIER", "2", "2|3|4", LIVE}, // vk_dlssg.c: frames on screen per rendered frame with DLSS // the same where DLSS's frame generation runs through NGX itself (gfx_present.c ng_multiplier): 2x to 6x or dynamic {"FG_MULT", "2", "2|3|4|5|6|dynamic", LIVE}, + // vk_dlssg.c limit_allowed: with DLSS Frame Generation, Reflex holds the frame limit where each frame starts; 0 leaves + // it to the game's own limiter + {"REFLEX_LIMIT", "1", "0|1", LIVE}, {"DLSS_CHAR_MASK", "65", "0|35|65|100", LIVE}, {"FXAA", "1", "0|1", LIVE}, // vk_skin_motion.inc skin_enabled: motion vectors of their own for every skinned character, not the player alone {"SKIN_MOTION", "1", "0|1", RESTART}, diff --git a/runtime/vk_dlssg.c b/runtime/vk_dlssg.c index df4d3926..705d209c 100644 --- a/runtime/vk_dlssg.c +++ b/runtime/vk_dlssg.c @@ -24,11 +24,11 @@ // game's own pacer then keeps only the game's ceiling (vk_dlssg_pace, nvn_api_obj.c nvnQueuePresentTexture): the pacer's // wait came after the picture was drawn, so the pad was read up to a period before the picture left, and Reflex's sleep // at the frame's start already waited for the GPU. The pacer keeps the limit while a movie plays, before the first host -// present, while none went out for DLSSG_HOLD_IDLE_NS, with a DLL older than nx2sl_reflex2 or a limit Streamline -// refused, and for the rest of the run once the frames show Reflex does not hold it (dlssg_pace.h). The guest thread -// that presents decides and the present thread hands the limit to Streamline before its present (vk_dlssg_frame, under -// the present lock, which keeps Streamline's options calls on one thread at a time); the pacer stands aside only once -// Streamline took that limit. +// present, while none went out for DLSSG_HOLD_IDLE_NS, with NX2NAVI_REFLEX_LIMIT=0 (live), a DLL older than +// nx2sl_reflex2 or a limit Streamline refused, and for the rest of the run once the frames show Reflex does not hold it +// (dlssg_pace.h). The guest thread that presents decides and the present thread hands the limit to Streamline before +// its present (vk_dlssg_frame, under the present lock, which keeps Streamline's options calls on one thread at a time); +// the pacer stands aside only once Streamline took that limit. #ifdef _WIN32 #include #endif @@ -115,9 +115,15 @@ static void reflex_apply(uint32_t us) { __atomic_store_n(&S.limit_set, ok ? us : 0, __ATOMIC_RELEASE); } -// Reflex may hold the frame limit: a DLL with nx2sl_reflex2 whose limit Streamline did not refuse. +// Reflex may hold the frame limit: NX2NAVI_REFLEX_LIMIT (default 1, live; 0 leaves the limit to the game's own limiter, +// as before Reflex's frame limit was used), and a DLL with nx2sl_reflex2 whose limit Streamline did not refuse. The +// setting is parsed again when a setting changed (cfg_version). Only the presenting guest thread calls it. static int limit_allowed(void) { - return S.reflex2 && !__atomic_load_n(&S.limit_refused, __ATOMIC_ACQUIRE); + static unsigned seen; + static int read, on; + unsigned v = cfg_version(); + if (!read || v != seen) on = cfg_bool("NX2NAVI_REFLEX_LIMIT"), seen = v, read = 1; + return on && S.reflex2 && !__atomic_load_n(&S.limit_refused, __ATOMIC_ACQUIRE); } // With any upscaler or none: vk_dlaa.c makes the inputs either way (vk_dlaa_frame says which, at the start and at each @@ -380,7 +386,8 @@ int vk_dlssg_pace(uint64_t period_ns) { if (held) pace_say(1, want, NULL); else if (!want) // (a limit Streamline has yet to take is no change: the present thread hands it over next) pace_say(0, 0, !allowed ? (!S.reflex2 ? "nx2navi_dlssg.dll is older than Reflex's frame limit" - : "Streamline refused the limit") + : __atomic_load_n(&S.limit_refused, __ATOMIC_ACQUIRE) ? "Streamline refused the limit" + : "NX2NAVI_REFLEX_LIMIT=0") : S.pace.bad ? "Reflex did not hold the limit" : !period_ns ? "a movie plays, or no limit" : hosted ? "no host present for 200 ms"