Look for RivaTuner Statistics Server on its own thread, so the first present no longer reads the registry and files.

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>
This commit is contained in:
chrg6
2026-11-13 17:18:06 -05:00
parent b2b37a3e5d
commit 6be25cc978
2 changed files with 90 additions and 35 deletions

View File

@@ -56,7 +56,8 @@ static _Atomic int64_t active_for = RTSS_UNSET; // the want RTSS holds the gam
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)
static int started;
static _Atomic int started; // rtss_limit_want spawned the worker
static _Atomic int ready; // the worker found RTSS: dll_path, rtss_dir and profile hold their values
static char dll_path[MAX_PATH], profile[MAX_PATH], rtss_dir[MAX_PATH];
static HMODULE dll;
static EnumProfilesFn enum_profiles;
@@ -315,8 +316,42 @@ static void watch(int64_t w, int listed) {
atomic_store(&two, on);
}
// RTSS's folder (InstallDir under its registry key), its hooks library and this executable's profile name, on the
// worker before its first poll. 0 when RTSS is not installed: the game's own limiter for the run.
static int find_rtss(void) {
char dir[MAX_PATH];
DWORD size = sizeof dir;
if (RegGetValueA(HKEY_LOCAL_MACHINE, "SOFTWARE\\WOW6432Node\\Unwinder\\RTSS", "InstallDir", RRF_RT_REG_SZ, NULL, dir,
&size) != ERROR_SUCCESS) {
rt_log("frame rate: RivaTuner Statistics Server is not installed; the game's own limiter");
return 0;
}
snprintf(dll_path, sizeof dll_path, "%s\\RTSSHooks64.dll", dir);
snprintf(rtss_dir, sizeof rtss_dir, "%s", dir);
if (GetFileAttributesA(dll_path) == INVALID_FILE_ATTRIBUTES) {
rt_log("frame rate: RivaTuner Statistics Server's folder %s has no RTSSHooks64.dll; the game's own limiter", dir);
return 0;
}
char exe[MAX_PATH];
DWORD n = GetModuleFileNameA(NULL, exe, sizeof exe);
if (!n || n >= sizeof exe) return 0;
const char *base = exe;
for (const char *s = exe; *s; s++)
if (*s == '\\' || *s == '/') base = s + 1;
snprintf(profile, sizeof profile, "%s", base); // RTSS names a profile after the executable, as ocarina.exe
int64_t first = atomic_load(&want);
if (first >= 0)
rt_log("frame rate: RivaTuner Statistics Server found in %s; it takes the frame limit while it runs", dir);
else
rt_log("frame rate: RivaTuner Statistics Server found in %s; kept out (%s), so its thread only reads RTSS's own "
"settings for the game, to tell when RTSS limits it a second time", dir, rtss_out_text(first));
return 1;
}
static void worker(void *arg) {
(void)arg;
if (!find_rtss()) return;
atomic_store_explicit(&ready, 1, memory_order_release); // rtss_limit_restore may read the names now
int was_running = -1, was_hooked = 0, over = 0;
for (;; plat_sleep_ms(POLL_MS)) {
int64_t w = atomic_load(&want);
@@ -373,42 +408,16 @@ static void worker(void *arg) {
}
}
static void start(int64_t first) {
started = 1;
char dir[MAX_PATH];
DWORD size = sizeof dir;
if (RegGetValueA(HKEY_LOCAL_MACHINE, "SOFTWARE\\WOW6432Node\\Unwinder\\RTSS", "InstallDir", RRF_RT_REG_SZ, NULL, dir,
&size) != ERROR_SUCCESS) {
rt_log("frame rate: RivaTuner Statistics Server is not installed; the game's own limiter");
return;
}
snprintf(dll_path, sizeof dll_path, "%s\\RTSSHooks64.dll", dir);
snprintf(rtss_dir, sizeof rtss_dir, "%s", dir);
if (GetFileAttributesA(dll_path) == INVALID_FILE_ATTRIBUTES) {
rt_log("frame rate: RivaTuner Statistics Server's folder %s has no RTSSHooks64.dll; the game's own limiter", dir);
return;
}
char exe[MAX_PATH];
DWORD n = GetModuleFileNameA(NULL, exe, sizeof exe);
if (!n || n >= sizeof exe) return;
const char *base = exe;
for (const char *s = exe; *s; s++)
if (*s == '\\' || *s == '/') base = s + 1;
snprintf(profile, sizeof profile, "%s", base); // RTSS names a profile after the executable, as ocarina.exe
if (first >= 0)
rt_log("frame rate: RivaTuner Statistics Server found in %s; it takes the frame limit while it runs", dir);
else
rt_log("frame rate: RivaTuner Statistics Server found in %s; kept out (%s), so its thread only reads RTSS's own "
"settings for the game, to tell when RTSS limits it a second time", dir, rtss_out_text(first));
if (!plat_thread_spawn(worker, NULL, "rtss", 0)) rt_log("frame rate: the RivaTuner Statistics Server thread did not start");
}
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): active_for no longer matches
atomic_store(&want, mhz);
// The first want starts the worker, a kept-out one too (the worker then only reads, watch). The worker looks for
// RTSS itself (find_rtss), so the present that gets here waits on no registry key or file.
if (!atomic_load_explicit(&started, memory_order_relaxed) && !atomic_exchange(&started, 1) &&
!plat_thread_spawn(worker, NULL, "rtss", 0))
rt_log("frame rate: the RivaTuner Statistics Server thread did not start");
}
int rtss_limit_active(void) {
@@ -432,7 +441,7 @@ void rtss_limit_restore(void) {
// the profile just put back, and the next run would take it for the player's own. Set before the check below, so
// that an exit before the first limit, or while RTSS was kept out, also stops a later want from starting the worker.
atomic_store(&closed, 1);
if (!started || !*profile) return;
if (!atomic_load_explicit(&ready, memory_order_acquire)) return;
// the exit may come from a crash on any thread: give up after a second rather than wait on a lock held there
for (int i = 0; i < 100; i++) {
if (plat_mutex_trylock(&lock)) {

View File

@@ -114,8 +114,19 @@ static void app_set(int listed, DWORD flags, DWORD t0, DWORD t1, DWORD frames) {
memcpy(e + 4 + MAX_PATH + 4, t, sizeof t);
}
// The registry and file calls the thread that asks for a limit makes itself: a present must make none.
static DWORD present_tid;
static volatile LONG present_calls, reg_calls;
static int installed = 1;
static void on_call(void) {
if (GetCurrentThreadId() == present_tid) present_calls++;
}
LSTATUS stub_RegGetValueA(HKEY k, LPCSTR sub, LPCSTR val, DWORD fl, LPDWORD ty, PVOID data, LPDWORD size) {
(void)k, (void)sub, (void)val, (void)fl, (void)ty;
on_call();
reg_calls++;
if (!installed) return ERROR_FILE_NOT_FOUND;
snprintf(data, *size, "C:\\FakeRTSS");
return ERROR_SUCCESS;
}
@@ -153,11 +164,13 @@ SIZE_T stub_VirtualQuery(LPCVOID p, PMEMORY_BASIC_INFORMATION mi, SIZE_T n) {
}
BOOL stub_CloseHandle(HANDLE h) { (void)h; return TRUE; }
DWORD stub_GetFileAttributesA(LPCSTR p) {
on_call();
if (strstr(p, "RTSSHooks64.dll")) return FILE_ATTRIBUTE_NORMAL;
return file_of(p) ? FILE_ATTRIBUTE_NORMAL : INVALID_FILE_ATTRIBUTES;
}
BOOL stub_GetFileAttributesExA(LPCSTR p, GET_FILEEX_INFO_LEVELS level, LPVOID out) {
(void)level;
on_call();
const Prof *f = file_of(p);
if (!f) return FALSE;
WIN32_FILE_ATTRIBUTE_DATA *a = out;
@@ -166,6 +179,7 @@ BOOL stub_GetFileAttributesExA(LPCSTR p, GET_FILEEX_INFO_LEVELS level, LPVOID ou
return TRUE;
}
DWORD stub_GetPrivateProfileStringA(LPCSTR sec, LPCSTR key, LPCSTR def, LPSTR out, DWORD n, LPCSTR path) {
on_call();
const Prof *f = file_of(path);
long v = 0;
int have = f && !strcmp(sec, "Framerate");
@@ -183,6 +197,7 @@ DWORD stub_GetPrivateProfileStringA(LPCSTR sec, LPCSTR key, LPCSTR def, LPSTR ou
}
DWORD stub_GetModuleFileNameA(HMODULE m, LPSTR out, DWORD n) {
(void)m;
on_call();
return (DWORD)snprintf(out, n, "C:\\game\\ocarina.exe");
}
DWORD stub_GetCurrentProcessId(void) { return PID; }
@@ -222,6 +237,7 @@ void plat_mutex_unlock(PlatMutex *m) { ReleaseSRWLockExclusive((PSRWLOCK)m); }
int plat_mutex_trylock(PlatMutex *m) { return TryAcquireSRWLockExclusive((PSRWLOCK)m) != 0; }
static PlatThreadFn worker_fn;
static HANDLE worker_thread;
static volatile LONG spawned;
static volatile LONG free_run; // the worker polls without waiting
static volatile LONG64 polls;
@@ -234,7 +250,8 @@ int plat_thread_spawn(PlatThreadFn fn, void *arg, const char *name, size_t stack
(void)name, (void)stack;
worker_fn = fn;
spawned = 1;
return CreateThread(NULL, 0, worker_main, arg, 0, NULL) != NULL;
worker_thread = CreateThread(NULL, 0, worker_main, arg, 0, NULL);
return worker_thread != NULL;
}
void plat_sleep_ms(uint32_t ms) {
if (GetCurrentThreadId() != worker_tid) {
@@ -256,7 +273,10 @@ static void poll(void) { // one more poll of the worker, waited for
static void ask(int64_t mhz) { // a want at a present; the first one that starts the worker waits for its first poll
int was = spawned;
rtss_limit_want(mhz);
if (spawned && !was) CHECK(wait_done(), "the worker did not finish its first poll");
if (spawned && !was) {
HANDLE h[2] = {done, worker_thread}; // or for its end, when it finds no RTSS
CHECK(WaitForMultipleObjects(2, h, FALSE, 5000) != WAIT_TIMEOUT, "the worker did not finish its first poll");
}
}
static void show(const char *what) {
printf(" %s: profile %s %lu/%lu; active %d; state %d; saves %ld deletes %ld updates %ld loads %ld\n", what,
@@ -450,6 +470,30 @@ static void round_logged_whole(void) {
CHECK(logged("profile ocarina.exe limits to 60.000 fps\n"), "a whole limit as it is");
}
// The first want comes at a present: the worker, not the present, reads RTSS's registry key and looks for its files.
static void start_off_present(void) {
present_tid = GetCurrentThreadId();
app_set(1, 0xA, 1000, 2000, 60);
ask(60000);
poll();
show("playing at 60");
CHECK(own.exists && own.limit == 60, "the limit went into the profile");
CHECK(reg_calls == 1, "RTSS's folder was looked up once (%ld)", reg_calls);
CHECK(!present_calls, "the presenting thread made %ld registry or file calls", present_calls);
}
// No RTSS: the worker says so and ends, the game's limiter paces, and the exit has nothing to put back.
static void not_installed(void) {
installed = 0;
ask(60000);
CHECK(spawned && WaitForSingleObject(worker_thread, 5000) == WAIT_OBJECT_0, "the worker ended");
CHECK(logged("RivaTuner Statistics Server is not installed; the game's own limiter"), "logged");
CHECK(!rtss_limit_active() && !rtss_limit_state(), "the game's limiter paces");
ask(144000);
rtss_limit_restore();
CHECK(!loads && !saves && !own.exists, "nothing loaded or written");
}
static const struct { const char *name; void (*run)(void); } scenarios[] = {
{"restore_race", restore_race},
{"restore_player", restore_player},
@@ -462,6 +506,8 @@ static const struct { const char *name; void (*run)(void); } scenarios[] = {
{"active_race", active_race},
{"round_logged", round_logged},
{"round_logged_whole", round_logged_whole},
{"start_off_present", start_off_present},
{"not_installed", not_installed},
};
int main(int argc, char **argv) {