The review found three comments that claimed more than the code does. rtss_decide.h rtss_pace_period and frame_rate.c
(above frame_step_cap_ns) said the frames RTSS lets through always come further apart than the 240 ceiling, so the two
limiters never stack: at a limit of 240, or unlimited, which RTSS gets as 240, the ceiling is RTSS's own period, and
both may hold a frame. rtss_limit.c state_put said it runs on the worker thread only, but rtss_limit_restore's put back
calls it on the exiting thread, under the lock. And rtss_limit.c's header said a crash and a fatal error make no call
into RTSS's library, which holds only while the state file has the run's line. The comments now say so, and the header
names the exception at 240. (The comment above rtss_decide was rewrapped and lost its reference to rtss_fresh with the
unproven-hold rule.)
No behaviour changes. Checks that passed: tests/rtss_limit_check.py, tests/frame_step_check.py, pass_cc_check.py --wall
on rtss_limit.c and frame_rate.c (only frame_rate.c's existing game_interval warning), tools/check_content.py --staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that rtss_limit.c state_put took a state file plat_file_read could not read for a missing one: a file
another program held open (a scan) was rewritten with this profile's line alone, or deleted when the run removed its
line, and the lines of other executables that share the folder (dev or probe builds) were lost. state_put now returns
0 when the read gave nothing but plat_file_size finds the file with bytes in it; the run then goes on as with a file
that cannot be written (one log line, and a crash puts the profile back as an orderly exit does). An empty file is
still rewritten. tests/rtss_worker_check.c state_locked checks a file holding another executable's line that does not
read: the run limits as before, logs it, and the file stays as it was through the exit.
Not reproduced before the change. Checks that passed: tests/rtss_limit_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 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 the worker read rtss-profile.txt only on its first poll with the hooks library, which it loads
only for a limit, so a start with frame generation on, with the driver's low latency on from the first present, or
with NX2NAVI_RTSS=0 never put back a killed run's limit: RTSS then went on limiting the game at that value (counting
the generated frames too under frame generation), and the log and the menu reported it as the player's own limit
beside the game's limiter. rtss_limit.c worker now looks at the state file on its first poll that finds RTSS running
(left_start): a limit puts the profile back with the library it loads anyway, as before; frame generation and low
latency load the library for that one restore (restore_left, which now takes the line found); NX2NAVI_RTSS=0 keeps the
thread read-only and logs that the file holds the game's limit from a run that did not close and that a start with
RTSS let in puts it back. A restore the library could not be loaded for waits for the first poll with the library.
tests/rtss_worker_check.c adds kill_restart_framegen (the next start asks frame generation first: the profile is
removed, no limit written, the line gone) and kill_restart_setting (NX2NAVI_RTSS=0: no library loaded, the profile and
the line stay, the log says so). The rtss_limit.c header, docs/architecture.md, docs/building.md and
nx2navi.ini.example say so, and the ini text no longer says the game alone paces movies under RTSS.
Not reproduced before the change. Checks that passed: tests/rtss_limit_check.py (40 of 40),
pass_cc_check.py --wall on rtss_limit.c, tools/check_content.py --staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/architecture.md said the pacer holds nothing while RTSS holds the limit, through movies and minimised windows,
that the pacer stands aside once RTSS lists the process, that RTSS fails at 1.2 times the limit over six polls, and
that a killed run leaves its limit for the next run to take as the player's. The one-limiter and RTSS paragraphs now
name the pacer's fallbacks (before the first host present, a movie, 200 ms with no host present, a limiter seen
letting frames through, any failure) and its 240 ceiling beside RTSS, the settle rule, a movie's profile, the proof
RTSS must give before the pacer stands aside, the 1.05 rule over three new periods, the rounding down, and the state
file with rtss_limit_crash. nx2navi.ini.example's rtss lines, README.md's frame generation item, docs/building.md's
rtss_limit_check and frame_step_check paragraphs and rtss_limit.c's header follow, and the comments at main.c
crash_handler's and dispatch.c fatal_end's rtss_limit_crash say when RTSS's library is still called there (no line in
the state file, never on RTSS's own thread). No code changes.
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>
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>
Rereading the fixes for the review showed lines past the 120 columns the files keep to: the RTSS paragraph of
docs/architecture.md ran one line to 176 columns after its hooks library sentence was replaced, rtss_limit.c had three
lines just over (the creations and removals counters, the removed line, find_rtss's kept-out line), and
tests/rtss_worker_check.c three. They are wrapped; no text, log line or check 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 A-11's fix stores the hold after every decision but also logs it then: when the want changed
while the worker wrote the profile (frame generation switched on during the first write), rtss_limit.c set_active
logged "RivaTuner Statistics Server does the limiting; the game's own limiter stands aside" while the game's limiter
was pacing the new want (rtss_limit_active compares the hold with the want), and the kept-out line followed one poll
later. set_active still stores the hold every time, so that a want going w, w2 and w again never finds an older hold,
but logs a hold only while the want is still the one decided for; a decision that RTSS does not hold the game is
logged as before. The next poll logs what holds the newer want.
tests/rtss_worker_check.c active_logged switches frame generation on inside the first SaveProfile (a hook in the fake
RTSS): no line may say RTSS does the limiting, the next poll logs the kept-out line, and a later limit is held and
logged. It fails on the code before this change. Log only; rtss_limit_check passes 27 of 27, active_race included.
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 review found docs/architecture.md claiming that a run kept out of RTSS from its first present never loads the hooks
library, its thread only reading RTSS's settings. That holds only for NX2NAVI_RTSS=0: once frame generation or low
latency is switched off, a limit is wanted, rtss_decide.h rtss_wants_hooks lets the poll load the library, and the
limit goes into the profile as usual (tests/rtss_worker_check.c kept_lowlat_then_limit). The paragraph now says that
the thread reads only while RTSS is kept out, that a later limit loads the library, and that NX2NAVI_RTSS=0 keeps the
thread read-only for the run; it also says the profile gets back what it held before the run changed it. The same claim
is corrected in rtss_limit.c's header and read_settings comments, in rtss_decide.h rtss_wants_hooks's comment, and in
find_rtss's start-up line for a kept-out first want, which now says the thread only reads while RTSS is kept out.
Docs, comments and one log line's wording: rtss_limit_check passes 24 of 24, check_content passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that numbering these lines had been set aside as the performance branch's work, while it is the part
of A-10 that may land on its own, and only this branch writes rtss_limit.c. A want that flips between a limit and kept
out (low latency before the first swapchain and during a rebuild, or switched in the menu) creates the profile and
removes it again at each flip; the play-test's live toggles item counts those lines. rtss_limit.c apply now ends the
created line with "(creation N this run)" and put_back the removed line with "(removal N this run)", counted under the
lock both run under. The settle rule that would stop the churn still waits for approval.
tests/rtss_worker_check.c churn_run also checks that the ten flips log creations 1 to 6 and removals 1 to 5 and no
more. rtss_limit_check passes 24 of 24.
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>
vk_dlssg.c said frame_rate.c divides the frame limit by the multiplier, but the limit counts the frames the game draws
and only a limit of display is divided (frame_rate.c limit_game_hz). vk_fg.cpp said FSR 3.1 frame generation applies
after a restart, but FRAMEGEN is live (gfx_present.c window_settings_live makes the swapchain again; only a device made
for DLSS Frame Generation waits for a restart, vk_fg_wanted), and that its two queues come from the graphics family,
while vk_device.c create_device takes them from another family that can present where the graphics family has one
queue. rtss_limit.c's header said the profile is put back "from every path that ends the process" and stays "until the
next run changes it", and named only the setting and frame generation as keeping RTSS out: it now names the exits that
skip the restore (a kill, the device-lost watchdog, a crash inside the crash handler), that the next run takes the
leftover limit for the player's own, low latency, the read-only thread of a kept-out run, and the lookup on the
worker. (rtss_limit_want's "both reasons need a restart" went with the change that starts the worker for a kept-out
first want.)
Comments only: the three files compile, and check_content 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>
rtss_limit_want ran start() on the thread that called it: the registry read of RTSS's folder, a file attribute query
under Program Files, the executable's name and the thread's creation. frame_step_cap_ns calls it from
nvnQueuePresentTexture, so a guest present paid for them once per run (or at the first present after frame
generation or low latency was switched off).
rtss_limit_want now only stores the want and, once, spawns the worker; the worker does the lookup and the "found" or
"not installed" line before its first poll (find_rtss) and ends when RTSS is not installed. rtss_limit_restore,
which may run on another thread, reads the names only once the worker has published them (ready). Pacing does not
change: the worker's first poll comes the time of the lookup later.
Not reproduced before the change (its cost was never measured). tests/rtss_worker_check.c gains start_off_present,
which counts the registry and file calls made on the thread that asked for the limit (3 before the change, 0 after),
and not_installed (the worker ends, the game's limiter paces, the exit has nothing to put back). The other scenarios
and a compile of rtss_limit.c with -Wall pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An RTSS without FramerateLimitDenominator gets a fractional limit rounded to whole frames (apply: 59.94 becomes 60),
but the line after the write named the rate the game asked for: "limits to 59.940 fps" while the profile held 60. A
play-test log would hide that RTSS limits above a 59.94 Hz monitor. When the written limit over its denominator
differs from the asked rate, the line now names both: "limits to 60/1 (60.000 fps; the game asked 59.940)". What is
written does not change.
tests/rtss_worker_check.c gains round_logged (an RTSS with the denominator: 59940/1000, logged as 59.940 fps) and
round_logged_whole (without it: 59.94 and 143.856 Hz logged as 60/1 and 144/1, a whole 60 as before). The second
failed before the change and passes after it; a compile of rtss_limit.c with -Wall passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The worker checked that the want was still the one it had read and then stored active = 1, two steps on two atomics.
A rtss_limit_want between them (frame generation or low latency switched on, a movie, a new limit) stored the new want
and active = 0, and the worker's store then set active back to 1: frame_step_cap_ns returned 0 for the kept-out want
until the next poll, up to 500 ms in which nothing limited the game.
The worker now stores the want RTSS holds the game to (active_for, or RTSS_UNSET for none), and rtss_limit_active is
true only while it equals the newest want, so a change of the want stops matching at once and rtss_limit_want no
longer clears anything. The frame count check against the limit keeps its test that the want has not changed.
tests/rtss_worker_check.c gains active_race: the worker polls without pause while the want switches 400000 times
between 60 fps and frame generation, and after each switch to frame generation rtss_limit_active must read 0 until
the worker has seen it. Before the change 128440 switches read 1; after it none do. The other scenarios and a compile
of rtss_limit.c with -Wall pass.
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>
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>
EnumProfiles walks RTSS's Profiles\*.cfg with _findfirst and _findnext and keeps the find handle in 32 bits. The handle
points into the process heap, which lies above 4 GB here because the guest address space takes the low part (memory.c
mem_init), so _findnext gets a cut pointer and faults in ntdll: the crash report then ends the game with code 71 before
the title screen (issue #231). rtss_limit.c profile_exists now looks for Profiles\<name>.cfg in RTSS's folder, which is
what EnumProfiles reports. NX2NAVI_RTSS_ENUM=1 calls EnumProfiles again, to test an RTSS that keeps the whole handle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The game's event script times the intro movie's narration and fades by counting updates as 60 Hz frames. At 144 fps everything ran 2.4 times early: each line started over the one before, and the closing fade hid the dream. frame_step_cap_ns caps the rate at the movie's 60 fps from the decoder's start to its end of stream (ov_movie_live); with fps_limit = unlimited the cap is the movie's rate. With RTSS limiting, the game's own limiter holds a new rate until RTSS has applied it. NX2NAVI_MOVIE_FPS sets the cap, 0 turns it off. From a tester's fixes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where RTSS is installed and running, the frame limit from the menu goes into RTSS's profile for the game's executable
through RTSS's hooks library, loaded at run time, and the game's own pacer stands aside while RTSS has hooked the game.
Menu changes apply live, unlimited clears the limit, and closing the game (or any exit path) puts back what the profile
held, or removes a profile the game created. Each change is read back from the saved profile: without administrator
rights RTSS's profile folder cannot be written, and the game then keeps its own limiter. While RTSS limits, RTSS's own
frame counter for the game is checked against the limit; well past it for three seconds, the game takes the limiting
back for the run. The log says which limiter is in use and why, and a note under the frame-rate row says when RTSS
limits, when its profile is read-only, and when it does not hold the game to the limit. rtss = 0 and frame generation
keep RTSS out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>