From 07aca0170c0f61f34efdafcce055bdf1ec3dfdcb Mon Sep 17 00:00:00 2001 From: chrg6 Date: Fri, 13 Nov 2026 17:15:35 -0500 Subject: [PATCH] Tie RivaTuner Statistics Server's hold to the want it was decided for, so a stale store no longer keeps the pacer aside. 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 --- runtime/rtss_limit.c | 40 +++++++++++++++++++++------------------ tests/rtss_worker_check.c | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/runtime/rtss_limit.c b/runtime/rtss_limit.c index c03cf59d..83833330 100644 --- a/runtime/rtss_limit.c +++ b/runtime/rtss_limit.c @@ -52,7 +52,7 @@ enum { FAIL_NONE, FAIL_LIBRARY, FAIL_READONLY, FAIL_NOT_LIMITING }; static PlatMutex lock = PLAT_MUTEX_INIT; // the hooks calls and the state below static _Atomic int64_t want = RTSS_UNSET; // millihertz from the pacer: >0 a limit, 0 none; RTSS_OUT_* kept out -static _Atomic int active; // RTSS limits this process now +static _Atomic int64_t active_for = RTSS_UNSET; // the want RTSS holds the game to now; RTSS_UNSET none static _Atomic int failed; // FAIL_*: RTSS is out of it for the rest of the run static _Atomic int closed; // rtss_limit_restore ran: nothing reaches the profile any more static _Atomic int two; // kept out, and RTSS limits the game by a setting of its own (watch) @@ -229,10 +229,12 @@ static void put_back(void) { update_profiles(); } -// Logs which limiter is in use: the first decision, then each change. -static void set_active(int on, const char *why) { +// Whether RTSS holds the game to the want w this poll read, and the log of which limiter is in use: the first decision, +// then each change. The want may have changed since the poll read it: rtss_limit_active compares the two, so a stale +// store never lets the game's limiter stand aside for a newer want. +static void set_active(int64_t w, int on, const char *why) { static int logged = -1; - atomic_store(&active, on); + atomic_store(&active_for, on ? w : RTSS_UNSET); if (logged == on) return; logged = on; if (on) rt_log("frame rate: RivaTuner Statistics Server does the limiting; the game's own limiter stands aside"); @@ -333,24 +335,24 @@ static void worker(void *arg) { if (dll) put_back(); char why[96]; snprintf(why, sizeof why, "RivaTuner Statistics Server kept out: %s", rtss_out_text(w)); - set_active(0, why); + set_active(w, 0, why); } else if (f) { - set_active(0, fail_reason(f)); + set_active(w, 0, fail_reason(f)); } else if (!run) { - set_active(0, "RivaTuner Statistics Server is not running"); + set_active(w, 0, "RivaTuner Statistics Server is not running"); } else if (!load_hooks()) { atomic_store(&failed, FAIL_LIBRARY); - set_active(0, fail_reason(FAIL_LIBRARY)); + set_active(w, 0, fail_reason(FAIL_LIBRARY)); } else if (w == refused) { - set_active(0, "RivaTuner Statistics Server refused this limit"); + set_active(w, 0, "RivaTuner Statistics Server refused this limit"); } else if (w != applied) { int r = apply(w); over = 0; - if (r < 0) atomic_store(&failed, FAIL_READONLY), set_active(0, fail_reason(FAIL_READONLY)); - else if (r == 0) set_active(0, "RivaTuner Statistics Server refused this limit"); - else if (atomic_load(&want) == w) set_active(hooked, "RivaTuner Statistics Server has not hooked the game yet"); - } else if (atomic_load(&want) == w) { // a newer limit waits for the next poll (rtss_limit_want) - set_active(hooked, "RivaTuner Statistics Server has not hooked the game yet"); + if (r < 0) atomic_store(&failed, FAIL_READONLY), set_active(w, 0, fail_reason(FAIL_READONLY)); + else if (r == 0) set_active(w, 0, "RivaTuner Statistics Server refused this limit"); + else set_active(w, hooked, "RivaTuner Statistics Server has not hooked the game yet"); + } else if (atomic_load(&want) == w) { // a newer want waits for the next poll: these frames are not its + set_active(w, hooked, "RivaTuner Statistics Server has not hooked the game yet"); // RTSS's own count of the game's frames while the game's limiter stands aside: well above the limit for // three seconds means RTSS does not hold the game to it, and the game takes the limiting back for the run if (!hooked || w == 0 || fps <= w / 1000.0 * CHECK_SLACK) over = 0; @@ -359,7 +361,7 @@ static void worker(void *arg) { w / 1000.0); atomic_store(&failed, FAIL_NOT_LIMITING); put_back(); - set_active(0, fail_reason(FAIL_NOT_LIMITING)); + set_active(w, 0, fail_reason(FAIL_NOT_LIMITING)); } } watch(w, run && hooked); @@ -401,12 +403,14 @@ void rtss_limit_want(int64_t mhz) { if (atomic_load_explicit(&closed, memory_order_relaxed)) return; // the exit put the profile back if (!started) start(mhz); // a first want that keeps RTSS out starts the worker too, which then only reads (watch) // a new limit: the game's own limiter holds it until the worker had RTSS apply it, up to POLL_MS later (a movie's - // cap that waited for that ran the movie's 60 Hz count fast for half a second) - if (atomic_exchange(&want, mhz) != mhz) atomic_store(&active, 0); + // cap that waited for that ran the movie's 60 Hz count fast for half a second): active_for no longer matches + atomic_store(&want, mhz); } int rtss_limit_active(void) { - return !atomic_load_explicit(&closed, memory_order_relaxed) && atomic_load_explicit(&active, memory_order_relaxed); + if (atomic_load_explicit(&closed, memory_order_relaxed)) return 0; + int64_t a = atomic_load_explicit(&active_for, memory_order_relaxed); + return a >= 0 && a == atomic_load_explicit(&want, memory_order_relaxed); } int rtss_limit_state(void) { diff --git a/tests/rtss_worker_check.c b/tests/rtss_worker_check.c index 0d3da219..b8a2e6d5 100644 --- a/tests/rtss_worker_check.c +++ b/tests/rtss_worker_check.c @@ -190,7 +190,9 @@ DWORD stub_GetCurrentProcessId(void) { return PID; } // ---- host.h and platform.h, as far as rtss_limit.c uses them static char logged_lines[1 << 16]; static size_t logged_at; +static int quiet; // the stress scenario: no lines kept or printed void rt_log(const char *fmt, ...) { + if (quiet) return; char line[512]; va_list ap; va_start(ap, fmt); @@ -221,6 +223,8 @@ int plat_mutex_trylock(PlatMutex *m) { return TryAcquireSRWLockExclusive((PSRWLO static PlatThreadFn worker_fn; static volatile LONG spawned; +static volatile LONG free_run; // the worker polls without waiting +static volatile LONG64 polls; static DWORD WINAPI worker_main(void *arg) { worker_tid = GetCurrentThreadId(); worker_fn(arg); @@ -237,6 +241,8 @@ void plat_sleep_ms(uint32_t ms) { Sleep(ms); return; } + InterlockedIncrement64(&polls); + if (free_run) return; SetEvent(done); // a poll ended WaitForSingleObject(tick, INFINITE); // the next one starts when the scenario says } @@ -389,6 +395,35 @@ static void kept_framegen(void) { CHECK(!loads && !saves, "kept out, the hooks library is never loaded"); } +// Frame generation switched on while the worker polls without pause: from the switch until the worker sees it, the +// game's limiter paces, never RTSS's hold of the limit before (the worker's store for a want that changed after it +// read it must not count for the new one). +static void active_race(void) { + app_set(1, 0xA, 1000, 2000, 60); + ask(60000); + poll(); + CHECK(rtss_limit_active(), "RTSS limits at 60"); + quiet = 1, free_run = 1; + SetEvent(tick); + long hits = 0, n = 400000, stuck = 0; + srand(7); + for (long i = 0; i < n && !stuck; i++) { + for (volatile int k = rand() % 200; k > 0; k--) {} + rtss_limit_want(RTSS_OUT_FRAMEGEN); + LONG64 p0 = polls; + int seen = 0; + while (polls < p0 + 2) seen |= rtss_limit_active(); + hits += seen; + rtss_limit_want(60000); + ULONGLONG t0 = GetTickCount64(); + while (!rtss_limit_active() && !stuck) stuck = GetTickCount64() - t0 > 5000; + } + quiet = 0; + printf(" %ld of %ld switches read RTSS's hold afterwards (worker polls %lld)\n", hits, n, (long long)polls); + CHECK(!stuck, "RTSS did not take the limit back"); + CHECK(!hits, "the game's limiter stood aside for a kept-out want %ld times", hits); +} + static const struct { const char *name; void (*run)(void); } scenarios[] = { {"restore_race", restore_race}, {"restore_player", restore_player}, @@ -398,6 +433,7 @@ static const struct { const char *name; void (*run)(void); } scenarios[] = { {"kept_two", kept_two}, {"kept_lowlat_then_limit", kept_lowlat_then_limit}, {"kept_framegen", kept_framegen}, + {"active_race", active_race}, }; int main(int argc, char **argv) {