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 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 17:15:35 -05:00
parent b0488796a1
commit 07aca0170c
2 changed files with 58 additions and 18 deletions

View File

@@ -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) {

View File

@@ -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) {