3 Commits

Author SHA1 Message Date
chrg6
04f4d44747 Keep RivaTuner Statistics Server's profile as it was in rtss-profile.txt, so a start after a kill or crash puts it back, and leave it to that start at a crash.
An exit that skipped rtss_limit_restore (Task Manager, a hang, lost_wait_exit, a kill during the pipeline save) left
the game's limit in Profiles\<exe>.cfg, and the next run's first apply took it for the player's own: that run then
put the stale limit back at its close, and a profile the game had made stayed for good (lane A's probe: kill1, then
kill2 'held a limit of 60/1'). The crash handler and the fatal error's end also called into RTSSHooks64.dll from a
process that had failed (P-13).

rtss_limit.c now keeps one line per profile in rtss-profile.txt next to the executable (rtss_decide.h RtssSaved:
whether the profile existed, its old limit and denominator, the value the run wrote), written atomically before the
run's first change of the profile, updated with each value written, and removed once the profile is back (put_back;
also when the first write did not stick). On the first poll with the hooks library a start that finds a line of its
profile puts the profile back first (restore_left, rtss_restore_plan): the old limit, or no profile, unless the player
changed or removed it since, which the written value tells. main.c crash_handler and dispatch.c fatal_end call the
new rtss_limit_crash, which closes the run and leaves the profile to that start with no call into RTSS's library; only
when the file could not be written does it put the profile back as before, and never from the worker's own thread. A
file that cannot be written leaves the run as it was, with one line in the log. Profiles frozen by earlier versions
cannot be told from the player's own.

Not reproduced before the change. rtss_limit_check (T12: the line's format and parse, the file's rewrite, each plan;
the static check that both crash paths go through rtss_limit_crash), rtss_worker_check (state_record, state_player,
state_unwritable, crash_recorded, and kill_restart with its player and edited variants, each a killed run and the next
start in processes of their own), fatal_exit_check and exit_order_check pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 20:12:25 -05:00
chrg6
b0488796a1 Tell in the log and the menu when RivaTuner Statistics Server's own limit holds the game beside another limiter.
With NX2NAVI_RTSS=0, frame generation or the driver's low latency, the game keeps RTSS out of its frame limit, but a
limit the player set in RTSS itself (its Global profile or the game's own: a frame rate limit, a frame time limit or
scanline sync) still holds the frames, on top of the game's limiter, the driver's or frame generation's. Nothing read
RTSS's settings in such a run: a first want that kept RTSS out never started its thread, and frame_step_cap_ns folded
the reasons into -1, so the log said "NX2NAVI_RTSS=0 or frame generation" for low latency too.

frame_step_cap_ns now passes the reason (rtss_decide.h RTSS_OUT_SETTING, RTSS_OUT_FRAMEGEN, RTSS_OUT_LOWLAT; the
unlimited branch too), and the kept-out line names it. A kept-out first want starts the worker as well. While RTSS is
kept out the worker never loads RTSS's hooks library and only reads: its shared memory, and once RTSS lists the game
the [Framerate] values of Profiles\<exe>.cfg, else Profiles\Global, with the system's ini reader, again only when
either file's write time changes. When one of them limits the game, the log names the file, the values and the other
limiter, once per change, and rtss_limit_state returns 4, which the frame rate row shows as a new note
(UI_MENU_FPS_RTSS_TWO, in English, Russian, Brazilian Portuguese and Simplified Chinese). A later want of a limit
(frame generation or low latency switched off) loads the hooks and hands RTSS the limit as before; NX2NAVI_RTSS=0
stays read-only for the run. Pacing does not change.

Not reproduced before the change: a stacked limit needs RTSS running in a play-test. tests/rtss_worker_check.c gains
three scenarios against the fake RTSS (a Global limit with NX2NAVI_RTSS=0, scanline sync in the game's profile under
low latency and then a limit, frame generation with no limit of RTSS's own), and tests/rtss_limit_check.c builds
rtss_decide.h alone and checks which settings hold the frames and their text. Both pass, with frame_step_check and a
compile of every runtime file; -Wall adds no warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:41:39 -05:00
chrg6
a1153aa079 Keep RivaTuner Statistics Server's profile as the exit put it back, so the game's limit no longer returns to it.
rtss_limit_restore put the profile back and set the want to unset, but the game presents on until the process ends
(the rest of the exit runs after it). The next frame_step_cap_ns called rtss_limit_want with the limit, which replaced
the unset want, and the worker's next poll applied it again: the profile kept the game's limit after the exit, and the
next run took it for the player's own. An exit before the first want, or while RTSS was kept out, found the worker not
started, and a want after it started the worker, whose first poll applied at once. A want the worker read before the
restore was applied after it too, since the worker reads the want before it takes its lock.

rtss_limit_restore now marks the run closed before anything else: rtss_limit_want returns at once, the worker skips
its polls once it holds the lock, and rtss_limit_active and rtss_limit_state read 0, so the game's own limiter paces
the frames left.

tests/rtss_limit_check.py builds tests/rtss_worker_check.c, which compiles the real rtss_limit.c against a fake RTSS
(its registry key, hooks library, profiles and shared memory) and runs each scenario in a process of its own: a want
after the restore, the player's own limit, a want read before the restore, and an exit before the first want or while
RTSS was kept out. All five failed before the change and pass after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:41:39 -05:00