From 3b250adffda4745f12db1ccb21ba785b01c9cde6 Mon Sep 17 00:00:00 2001 From: chrg6 Date: Fri, 13 Nov 2026 17:30:52 -0500 Subject: [PATCH] Describe the frame pacer, the one-limiter rule and RivaTuner Statistics Server as they behave now. docs/architecture.md mentioned the pacer only inside its low latency paragraph and RTSS nowhere. It now has the pacer (the limit counting drawn frames, the 240 ceiling, movie rates, the slots and their catch-up, the present thread's wait and pace_smooth, the acquire's waits, the step snapped to the paced rate), the one-limiter rule as the goal with the exceptions that stand today (pace_smooth under RTSS, DLSS Frame Generation's Reflex beside the pacer, no limit at all from the pacer while RTSS holds it through movies and windows with no present, a limit set in RTSS itself while RTSS is kept out), RTSS's part (the lookup on its thread, the profile write and read-back, when the pacer stands aside and when it keeps the limit, the kept-out runs that only read RTSS's settings, the restore on orderly exits and what a kill leaves), and how to measure pacing (the present and perf traces, perf_summary, PresentMon as the reference, the runtime's own hitches). The changes that wait for approval are not described as done. nx2navi.ini.example's rtss paragraph adds low latency, and the second limiter a limit set in RTSS itself makes with rtss = 0, frame generation or low latency, which the log and the settings menu now point out. README.md's frame generation item ends its sentence on RTSS with what it does and the same note. Docs only; check_content passes. Co-Authored-By: Claude Opus 5.5 --- README.md | 3 ++- docs/architecture.md | 41 +++++++++++++++++++++++++++++++++++++++++ nx2navi.ini.example | 10 +++++++--- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2478798f..5814200d 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,8 @@ Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md). display; in a window of another shape than the picture, frames are generated inside the picture only, so the black bars stay clean. The frame limit counts the frames the game draws, and frame generation adds its frames on top of it. The frame limit can also be - handed to RivaTuner Statistics Server + handed to RivaTuner Statistics Server, which then limits the game in its place; where frame generation or low + latency keeps it out, the game says when a limit set in RivaTuner itself holds the game as well - HDR on HDR displays, two kinds: the game's scene tone mapped to the display's peak, matched to the game's mid grey and tone, with true blacks, or the game's own native HDR; peak, game and HUD brightness in the menu - The game's known shaders prepared when the game is built, and the pipelines met in play built again in the background at each start, so play rarely stops to compile them; a shader or pipeline met for the first time can diff --git a/docs/architecture.md b/docs/architecture.md index ea2a9ea1..a05396b4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -239,6 +239,47 @@ keeps per-core data without locks and relies on that, so the host models it (`ru while the swapchain is SDR, and a change of the setting makes the swapchain again in the other format (`gfx_present.c` `window_settings_live`). Only `native` waits for a restart, as the game asks about the display once. `NX2NAVI_HDR_LIVE=0` leaves the HDR path out of a run that starts with HDR off. +- Frame pacing (`nvn_api_obj.c` `nvnQueuePresentTexture` and `pace`, `frame_rate.c` `frame_step_cap_ns`, + `vk_backend.c` `present_main`): the frame limit (`NX2NAVI_FPS_LIMIT`, live) counts the frames the game draws. + Ocarina steps by the clock, so the pacer holds the frames to the limit alone: at most `LIMIT_FREE_MAX_HZ` (240, also + for "unlimited"), and while a movie plays at the movie's rate (`movie_cap_hz`, `NX2NAVI_MOVIE_FPS`). Each game + present gets a slot one period after the one before: an early frame waits for it, a frame late by less than a period + keeps the phase, a later one starts again from now, and while a movie plays the slots are kept through a lateness of + up to a second (`MOVIE_CATCHUP_NS`). The game's present returns at once; the present thread blits the picture, + waits until the slot (`wait_until`, with `pace_smooth`'s floor of seven eighths of the recent interval, off with + frame generation) and presents under the present and queue locks (`gfx_present.c` `flip_locked`). A window image is + free again one period after its slot, and its acquire also waits for the GPU to finish the image's previous frame + (`NX2NAVI_ACQUIRE_WAIT`); `frame_step_present` gives `ocarina_step` the slot times, snapped to the paced rate within + 3%. +- One limiter is the goal: the driver's low latency (`vk_lowlat.c`), RivaTuner Statistics Server (`rtss_limit.c`) or + the game's pacer holds the frame limit, and the log's "frame rate:" and "lowlat:" lines name the one in charge. + Frame generation keeps RTSS out (it would count the generated frames), and the game's pacer holds the drawn frames + beside it. The exceptions as they stand: `pace_smooth` still waits on the present thread while RTSS holds the limit; + DLSS Frame Generation's Reflex sleeps where the frame starts while the pacer holds the limit; while RTSS holds it the + pacer holds nothing at all, through movies and while no picture goes out (a minimised window, `frame_hold`) too; and + a limit set in RTSS itself still holds the game where the game keeps RTSS out, which the log ("two limiters") and the + frame rate row of the menu point out. +- RivaTuner Statistics Server (`rtss_limit.c`, `NX2NAVI_RTSS`, restart; its decisions in `rtss_decide.h`): the game's + first present starts a thread that looks RTSS up (`InstallDir` under `HKLM\SOFTWARE\WOW6432Node\Unwinder\RTSS`, + `RTSSHooks64.dll`) and reads its shared memory (`RTSSSharedMemoryV2`) every 500 ms. While RTSS runs, the limit goes + into RTSS's profile for the executable (`Profiles\.cfg`, through the hooks library's profile calls, read back + from the saved file; a fractional rate over a denominator where RTSS knows one, else rounded to whole frames), and + once RTSS lists the process the pacer stands aside (`rtss_limit_active`). The pacer keeps the limit while RTSS does + not run or has not hooked the game, for a limit RTSS refuses, and for the rest of the run when the profile cannot be + written (no administrator rights), the hooks library does not load, or RTSS's own frame count of the game stays + above 1.2 times the limit for six polls; the menu names the first and the last of these. Frame generation, the + driver's low latency and `NX2NAVI_RTSS=0` keep RTSS out (`RTSS_OUT_*`): the profile gets back what it held, and a + run kept out from its first present never loads the hooks library, its thread only reading RTSS's own settings for + the game. While RTSS lists the game the log names those settings (limit, frame time limit, scanline sync, display + sync, Reflex sleep, passive wait) once per change. Every orderly exit puts the profile back as it was before the run + (`rtss_limit_restore`), and no want reaches it after that; a kill, the device-lost watchdog or a crash inside the + crash handler leaves the game's limit in the profile, and the next run takes it for the player's own. +- Measuring pacing: `NX2NAVI_TRACE=present` logs a line per picture (the host present's interval, the game's present + interval, the wait for the slot and how far past it the present went), `NX2NAVI_TRACE=perf` each wait's share of the + frame ("bound:"), and `tools/perf_summary.py` condenses both. Both measure before the host present, so a wait RTSS + adds inside it does not show there: PresentMon is the reference for what reached the screen. Compare runs with the + same executable name, cache state and save, and keep `trace` off when counting hitches; `NX2NAVI_MEM_CENSUS` and + the memory report every 3600 frames are the runtime's own hitches. - Low latency (`vk_lowlat.c`, `NX2NAVI_LOW_LATENCY`, off by default): the graphics driver holds each frame's start, so the pad is read as late as the picture's time allows and no finished picture waits. NVIDIA's mode runs through `VK_NV_low_latency2`: the window's swapchain is made with the latency mode, the frame sleeps in the driver before its diff --git a/nx2navi.ini.example b/nx2navi.ini.example index ace664f9..59baa521 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -25,9 +25,13 @@ # fps_limit = 60 # Where RivaTuner Statistics Server is installed and running, the limit goes into its profile for the game's # executable and RTSS does the limiting instead of the game; closing the game puts back what the profile held. -# Changing that profile needs the game run as administrator (RTSS keeps its profiles under Program Files); without -# it, or with frame generation on, the game limits by itself, and also when RTSS's own frame counter shows the game -# well past the limit (RTSS then hooks the game without holding it back). rtss = 0 always leaves it to the game. +# Changing that profile needs the game run as administrator (RTSS keeps its profiles under Program Files). Without it +# the game limits by itself, as it does with frame generation on (RTSS would count the generated frames) and for the +# rest of the run when RTSS's own frame counter shows the game well past the limit (RTSS then hooks the game without +# holding it back); with low latency on, the driver holds the limit. rtss = 0 always leaves it to the game. With +# rtss = 0, frame generation or low latency, a limit set in RTSS itself (its global profile or the game's, a frame time +# limit or scanline sync) limits the game a second time: the log and the settings menu say so, and setting it to 0 in +# RTSS leaves one limiter. # rtss = 1 # 1: asks RTSS's EnumProfiles whether the game's profile exists, instead of looking for its file. EnumProfiles crashes # the game (RTSS keeps a pointer of the game's in 32 bits); only for testing an RTSS that fixed it.