mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
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:
@@ -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)) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user