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 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 18:58:40 -05:00
parent dc3f424fde
commit e0843439a7
4 changed files with 29 additions and 11 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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},

View File

@@ -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 <windows.h>
#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"