The review found three ways rtss-profile.txt could fall behind the profile. Every write after a run's first one saved
the profile before the line, so a kill between the two left a line whose value the profile no longer held, and the
next start took the game's limit for the player's (rtss_restore_plan LEAVE). A put back whose write did not land kept
the line, but rtss_put_back_done cleared S.changed, so a later limit took the run's first-write path again and read the
game's own limit from the profile as the value to put back, overwriting the true one in the line. And put_back removed
the line after DeleteProfile without checking that the profile went away.
The line now carries the value the run wrote before the newest one (rtss_decide.h RtssSaved prev_limit and prev_den,
rtss1 with two more fields; the file came with this branch and was never released). rtss_limit.c apply writes the new
value into the line before the profile (record_before) and drops the previous one once the profile holds the new one,
or returns to it when RTSS refused it (record_after), and rtss_restore_plan takes a profile holding either value for
the game's. A new owed flag keeps had_profile, old_limit and old_denominator from the run's first write until a put
back lands: a later first write keeps them and the line's values, a failed first write leaves the line alone, and
rtss_limit_restore tries the put back again at the exit. put_back and restore_left check that a deleted profile is
gone before they remove the line, and restore_left's lines name the value the profile held. tests/rtss_limit_check.c
covers the new fields and the plan for either value; tests/rtss_worker_check.c checks the line before and after a
write (state_record), a run killed inside the save of its second limit and the next start removing the profile
(kill_restart_mid_write), a put back that did not land followed by a limit and the exit (put_back_owed), and a removal
that did not land (put_back_undeleted). Lines over 120 columns from the earlier fixes are wrapped.
Not reproduced before the change. Checks that passed: tests/rtss_limit_check.py (44 of 44), tests/exit_order_check.py,
tests/fatal_exit_check.py, pass_cc_check.py --wall on rtss_limit.c, tools/check_content.py --staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that nothing ended an RTSS hold that was never proven: when RTSS listed the game with the limit in
its profile but one sign never came (presents of another API value than Vulkan's, the update flag never cleared, a
period that never moved), the profile kept the limit and the game's pacer held the frames too for the whole run, two
limiters where the pacing rules allow one, with only a reason line in the log and nothing in the menu. Before the
hold had to be proven, RTSS alone held the game once it listed it. rtss_decide.h rtss_decide now counts the polls in a
row where RTSS lists the game, the profile holds a limit of this run (not "none"), a picture went out within 200 ms
and the hold is not shown; at RTSS_UNPROVEN_POLLS (60, 30 s) it puts the profile back and RTSS is out for the run
(RTSS_FAIL_UNPROVEN, "RivaTuner Statistics Server never showed it holds the game"). A minimised window, RTSS not
hooked, a want of no limit, a kept-out want or a poll that shows the hold start the count again. rtss_limit.c logs the
give-up, rtss_limit_state returns 3 for it, so the menu says RivaTuner does not limit the game and the game does, and
rtss_out_of_limit counts it, so a limit set in RTSS itself is reported as a second limiter. The comment above
rtss_decide, rewritten for the rule, is wrapped at 120 columns and no longer cites rtss_fresh, which does not exist.
tests/rtss_limit_check.c checks each sign that never comes (given up at the 60th poll, once, for the run), the polls
that do not count and a shown hold restarting the count; tests/rtss_worker_check.c unproven_run checks it against the
real worker. docs/architecture.md, docs/building.md and nx2navi.ini.example describe the rule and the remaining window
of two limiters before it.
This changes when the game takes the limit back from RTSS, beyond the approved rules: an RTSS whose hold never shows
in its shared memory (a version or setting that differs) is now left out after 30 s and the game's limiter holds
alone, as with rtss = 0, where it held the frames beside the game's limiter before. Not reproduced before the change.
Checks that passed: tests/rtss_limit_check.py (41 of 41), pass_cc_check.py --wall on rtss_limit.c,
tools/check_content.py --staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that rtss_decide.h rtss_target writes no limit only while a movie runs faster than the play limit, so
at the default limit of 60 a 60 fps movie leaves 60 in RTSS's profile and RTSS holds the movie beside the game's pacer.
Its comment said RTSS at the movie's rate would space out the frames that catch up after a late frame, and README.md
("one limiter at a time") and docs/architecture.md read as if every movie under RTSS were the pacer's alone. The
approved rule writes nothing to the profile at movies when the limit is 60 or more, so the code stays: rtss_target's
and rtss_pace_period's comments now say that at the movie's own rate RTSS keeps holding it and that whether its
limiter lets the frames that catch up through is not measured, README.md drops "one limiter at a time", and
docs/architecture.md lists the movie at 60 among the exceptions to one limiter. tests/rtss_limit_check.c pins a movie
at 60 under a limit of 60: no write, the profile keeps 60, held again after it.
No behaviour changes. Checks that passed: tests/rtss_limit_check.py, tools/check_content.py --staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
apply() rounded the millihertz to the nearest whole frame where RTSS has no FramerateLimitDenominator, so "display" on
a 59.94 Hz monitor wrote 60 and on a 143.856 Hz one 144: above the refresh, one frame more than the monitor shows
every 17 s and every 7 s. Only "display" asks a fractional rate under RTSS (fixed limits and movie rates are whole,
frame generation keeps RTSS out), so the new rtss_decide.h rtss_limit_value rounds every fraction down when the
denominator is missing (59/1, 143/1), and keeps a whole rate over 1 and a fraction over 1000 where RTSS knows the
denominator. VRR and present-wait handling stay to be measured.
Not reproduced before the change. rtss_limit_check (T15: rtss_limit_value at 59940, 60000, 143856, 0 and 240000 with
and without the denominator) and rtss_worker_check (round_logged_whole now 59/1 and 143/1, round_logged) pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A want that flips between a limit and kept-out created and removed RTSS's profile at each flip: with a driver's low
latency vk_lowlat_on() is 0 before the first swapchain and during every swapchain rebuild, so a warm start without the
boot screen wrote the profile and removed it a poll later, and a poll that fell into a rebuild did the same (lane A's
probe: 6 creations and 5 removals for a want flipping each poll, 1 and 1 at start before RTSS hooked the game).
rtss_decide now counts the polls in a row a want stayed: a limit goes into the profile and a kept-out want puts it
back only after RTSS_SETTLE_POLLS (2), while standing aside still ends at once (rtss_limit_active compares the want),
and nothing is written before the first host present (RTSS_WHY_NO_PRESENT). A limit change reaches the profile up to a
second later; the game's limiter holds the new limit meanwhile. The created and removed lines were numbered before.
Not reproduced before the change. rtss_limit_check (T8: kept out for one poll and for two, a second reason, a limit
again; T9: 20 flips after a written limit give no write or put back, 20 from the start none, flips every two polls
act; no write while no host present) and rtss_worker_check (churn_run and every scenario that now settles) pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The worker stood aside as soon as RTSS listed the process and the profile read back, before RTSS had seen a Vulkan
present (with InjectionDelay=15000 it lists the game some seconds after start) or read the new profile. Its frame
count check took the newest period whatever its age: a period frozen at 144 fps after the limit went to 60 and the
window was minimised failed RTSS for the run, and 1.15 times the limit was never caught (lane A's probe: listed_no_api,
stale_fps, over115). rtss_decide now parses the entry's flags and stands aside only when the profile held the target
at the poll before, RTSS lists the game with the Vulkan API value (flags & 0xFFFF == 0xA), RTSS_UPDATE_ASKED is clear,
a new period was seen within max(2.5 s, 2.5 times its length), no movie plays and a host present went out within
200 ms. Only a new period after a poll that stood aside is checked: three in a row over 1.05 times the limit put the
profile back for the run, and a stale period counts neither way. The worker reads the poll time and the newest host
present (gfx_window_hosted_ns), and logs each change of why the game's limiter paces, up to 100 lines.
Not reproduced before the change. rtss_limit_check (the flags at +264; T2 injection delay through not listed, no API,
the update flag, no period and holding; T3 quit; T6 the frame count at 144, 69, 63 fps, a frozen period and a newer
want; the idle case) and rtss_worker_check (over_run, over_slack_run, new api_run and idle_run, and every scenario that
now holds from the poll after the write) pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
During a movie frame_step_cap_ns wanted the movie's rate and returned 0 once RTSS held it, so pace() kept none of the
movie's slots while ocarina_step still gave one movie frame per update: each late frame put the picture behind the
sound for good, the fall-behind d3e50328 fixed for the game's pacer. Each movie start and end also rewrote RTSS's
profile (two saves and two UpdateProfiles per movie in lane A's probe). Now frame_step_cap_ns wants the play limit
through the movie and tells RTSS of the movie (rtss_limit_movie); rtss_pace_period keeps the movie's period from its
first frame, and the worker leaves the movie to the pacer (RTSS_WHY_MOVIE) with the profile as it is. Under a limit
below the movie's rate (30, 40, 45) the profile has no limit for the movie's length (rtss_decide.h rtss_target), since
RTSS at the movie's rate would space out the frames the pacer sends at once to catch up; that is still two writes per
movie there, and none at 60 or more.
Not reproduced before the change. rtss_limit_check (the movie case at 144 and 40, rtss_target, rtss_pace_period),
rtss_worker_check (movie_run, new movie_low_run) and frame_step_check (the want and period through a movie under RTSS)
pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frame_step_cap_ns returned 0 once RTSS held the limit, so pace() set each slot to now and the acquire never waited.
RTSS holds frames inside the host present, and a minimised window (gfx_present.c present_image skips the present) or
a frame hold gives it none to hold: the game then ran with no limiter but pace_smooth's floor, which follows the
intervals down, and the quick menu is where 640-700 fps read address 0 (frame_rate.c LIMIT_FREE_MAX_HZ).
gfx_present.c flip_locked now stores the time of each host present that took its picture (gfx_window_hosted_ns), and
rtss_decide.h rtss_pace_period gives the pacer the limit's period before the first host present and once none went
out for 200 ms (as vk_lowlat.c HOLD_IDLE_NS), and the 240 ceiling's period, never 0, while RTSS holds. The frames RTSS
lets through come further apart than the ceiling, so pace() waits for none of them; no new wait site.
Not reproduced before the change. frame_step_check (new cases: holding with a host present 10 and 199 ms ago, 250 ms
ago, none yet, not holding, 144 minimised, unlimited) and rtss_limit_check (rtss_pace_period) pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found RtssIn.closed and its RTSS_WHY_CLOSED rule unused in the game: rtss_limit.c worker builds a poll's
view with the want alone, after it has skipped the poll under the lock once rtss_limit_restore ran, so the pure check's
closed case pinned a rule the worker never reaches. The field, the rule and the reason go; rtss_decide's comment says
that no poll decides after the exit and where that is done. The worker's skip stays as it was, and
tests/rtss_worker_check.c's restore_* scenarios (restore_inflight among them) keep covering it. The pure check's
closed_and_library becomes unset_and_library.
No decision changes: rtss_limit_check passes 27 of 27, check_content passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that docs/architecture.md's list of exceptions to the one-limiter rule leaves one out: when RTSS is
out of it for the run because its profile cannot be written (no administrator rights) or its hooks library does not
load, a limit the player set in RTSS itself (Global or the game's .cfg, a frame time limit, scanline sync) still holds
the game while the game's own limiter paces it. rtss_limit.c watch worked the second limiter out only for a kept-out
want, so neither the "two limiters" line nor anything else said so; only the "settings for the game" line showed the
values.
rtss_decide.h now has rtss_out_of_limit (kept out, or failed with RTSS_FAIL_READONLY or RTSS_FAIL_LIBRARY; a run the
frame count check gave up on is left out, since RTSS was seen not to hold the game), and rtss_two takes the failure.
watch gets S.fail and logs the two-limiter line for those runs too, and logs it again when the other limiter changes
(a failed run that then keeps RTSS out for low latency). The menu is unchanged: its two-limiter note (rtss_limit_state
4) stays for a kept-out want, and a read-only profile keeps its administrator note (state 2). Log only: no pacing
decision changes. The docs' exceptions sentence names the case and what the log and the menu show.
tests/rtss_worker_check.c readonly_two and library_two run it with a limit in Global; both fail on the code before
this change (no two-limiter line). rtss_limit_check.c's two() covers the failures. rtss_limit_check passes 26 of 26,
check_content passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The worker read RTSS's shared memory, decided and wrote the profile in one loop over file statics, so its decisions
could be checked only against a running RTSS. rtss_decide.h now holds the parts that make no system call: rtss_parse
reads the shared memory's header and this process's entry (bounded by the memory's size and 4096 entries, as before),
rtss_decide takes one poll's view (the want, whether it is still current, a closed run, the hooks library, the entry)
and the state between polls (RtssState: the limit applied and the one refused, whether the run changed the profile,
the frame count, the failure) and returns what to do and whether RTSS holds the game, and rtss_poll carries that out
through the profile calls rtss_limit.c hands it (apply and put_back, which keep their I/O and log lines). The worker,
rtss_limit_restore and the menu state read the same state.
No decision changes: the order of the rules, the immediate write before RTSS lists the game, six polls over 1.2 times
the limit, a write per movie and per switch of low latency all stay as they are. The rules that wait for approval are
not in it (the pacer keeping movies and windows with no host present, Vulkan flags, settling and a fresh period
before standing aside, rounding the limit down, the state file).
tests/rtss_worker_check.c gains nine scenarios that pin those decisions against the real worker (a read-only profile,
a refused limit, the frame count rule at 144, 69, 73 fps and unlimited, RTSS quitting, a hooks library that does not
load, low latency switching, a movie); the logs of all 23 scenarios are the same, line for line, before and after this
change. tests/rtss_limit_check.c checks the same decisions on rtss_decide.h alone, with a fake RTSSSharedMemoryV2 for
the reader (a bad header, short entries, an entry past the memory or past the 4096th) and a fake profile behind
RtssOps; mutating the frame count's polls or slack, the refusal, the library failure or the clear of a refused limit
makes it fail. docs/building.md describes the check. Every runtime file compiles; -Wall adds no warning;
frame_step_check passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rtss_limit.c writes and reads back only FramerateLimit and FramerateLimitDenominator, but RTSS's [Framerate] section
holds more that limits or paces the frames: a frame time limit (LimitTime over LimitTimeDenominator), scanline sync
(SyncScanline0/1), display sync, its own Reflex sleep and passive waiting. A player with any of them set in Global gets
an RTSS that holds the game to more than the game's limit, and no log said so.
While RTSS lists the game, the worker's reader (read_settings, the system's ini reader on Profiles\<exe>.cfg, else
Profiles\Global, read again only when either file's write time changes) now also takes SyncDisplay, ReflexSleep and
PassiveWait, and watch logs "RivaTuner Statistics Server's settings for the game (<file>): limit L/D, frame time limit
T/TD, scanline sync S0/S1, display sync X, Reflex sleep R, passive wait P" once per change, 40 lines at most. It reads
only, makes no call into RTSS's hooks library and changes nothing RTSS or the game does; whether the game should keep
RTSS out when one of these is set is left for approval.
Not reproduced before the change: it needs RTSS's settings in a play-test. tests/rtss_limit_check.c checks the text
with and without the modes and that a short buffer cuts it; tests/rtss_worker_check.c gains settings_logged (nothing
read while the game is not listed, then one line for the game's profile with what it took from Global, and one more
after the limit changes). Both pass, with a compile of every runtime file; -Wall adds no warning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>