mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 17:09:57 -04:00
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>
640 lines
37 KiB
C
640 lines
37 KiB
C
// RivaTuner Statistics Server as the frame limiter (frame_rate.c frame_step_cap_ns): where RTSS is installed and running,
|
|
// the frame limit goes into RTSS's profile for this executable and, while RTSS is seen holding the game to it, the
|
|
// game's own pacer keeps only the 240-frame ceiling (rtss_decide.h rtss_pace_period), so there is one limiter (at a
|
|
// limit of 240 the ceiling is RTSS's own period). Anywhere else, or when any step fails, the game's own limiter works
|
|
// as it always did.
|
|
//
|
|
// RTSS's hooks library (RTSSHooks64.dll in the folder its installer records under
|
|
// HKLM\SOFTWARE\WOW6432Node\Unwinder\RTSS, InstallDir) exports the profile calls, loaded here at run time:
|
|
// LoadProfile(name) reads a profile (the global one for a name without its own), Get/SetProfileProperty read and change
|
|
// the loaded copy, SaveProfile(name) writes it to Profiles\<name>.cfg, UpdateProfiles makes every hooked process
|
|
// read its profile again, EnumProfiles lists the profiles that exist (profile_exists says why it is not called),
|
|
// DeleteProfile removes one. "FramerateLimit" is the limit in frames per second (0: none); "FramerateLimitDenominator"
|
|
// divides it, so 59940 over 1000 limits to 59.94 (a monitor's measured rate). SaveProfile reports nothing: RTSS's
|
|
// Profiles folder sits under Program Files, which a process without administrator rights cannot write, and the save
|
|
// then fails silently. So every change is read back
|
|
// from the saved profile, and a limit that did not stick leaves the game's own limiter in charge for the session.
|
|
//
|
|
// RTSS runs when its shared memory "RTSSSharedMemoryV2" carries the signature 'RTSS'. The game's own limiter stands
|
|
// aside only once RTSS is seen to hold the game (rtss_decide.h rtss_decide): the memory's application list holds this
|
|
// process id with its Vulkan presents (RTSS hooks a process some seconds after it starts, its injection delay), RTSS
|
|
// has read the profile the game wrote, it measured a period of the game's frames lately, no movie plays and pictures
|
|
// go out. Until then, and again whenever one of these ends (RTSS quits, the window is minimised), the game's own limiter
|
|
// paces the frames. While RTSS limits, each new period RTSS measures for the game is checked against the limit: above
|
|
// 1.05 times it for three periods in a row, and the game takes the limiting back for the rest of the run. So it does
|
|
// when RTSS lists the game with the limit for 30 s while pictures go out and never shows its hold: RTSS may then hold
|
|
// the frames beside the game's limiter.
|
|
//
|
|
// The calls run on a thread of their own, every half second, which also looks RTSS up (find_rtss), so a present never
|
|
// waits on the registry or profile files. On every orderly exit (rtss_limit_restore) the profile gets back what it held
|
|
// before the game changed it: the old limit, or no profile of its own when there was none; from then on no want reaches
|
|
// it and the game's own limiter paces the frames left. What it held is also kept in rtss-profile.txt next to the
|
|
// executable (rtss_decide.h RtssSaved), written before the run's first change of the profile and removed once the
|
|
// profile is back: an exit that skips the restore (a kill, a hang, the device-lost watchdog) leaves the line, and the
|
|
// next start puts the profile back from it before it changes anything (left_start, restore_left), unless the player
|
|
// changed the profile since; a start with NX2NAVI_RTSS=0 only logs it. A crash and a fatal error leave it to that start
|
|
// too (rtss_limit_crash), with no call into RTSS's library from the failing process, unless the file could not be
|
|
// written: then they put the profile back as an orderly exit does, but not from the worker's own thread.
|
|
//
|
|
// NX2NAVI_RTSS=0 keeps RTSS out of it. Frame generation keeps it out too (RTSS counts every presented frame, the
|
|
// generated ones included), and so does a driver's low latency (vk_lowlat.c), which holds the frames where they start.
|
|
// While RTSS is kept out the thread does not load the hooks library and only reads RTSS's own settings for the game, to
|
|
// tell when they limit it a second time (watch), except once at the start, to put back a profile that a run which did
|
|
// not close left with its limit (left_start); a later limit (frame generation or low latency switched off) loads it
|
|
// and hands RTSS the limit as usual, and NX2NAVI_RTSS=0 keeps the thread read-only for the run.
|
|
#include <stdatomic.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "gfx.h"
|
|
#include "host.h"
|
|
#include "platform/platform.h"
|
|
#include "rtss_decide.h"
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
|
|
typedef DWORD (*EnumProfilesFn)(LPSTR list, DWORD size);
|
|
typedef void (*ProfileFn)(LPCSTR name);
|
|
typedef BOOL (*PropertyFn)(LPCSTR property, LPBYTE data, DWORD size);
|
|
typedef void (*UpdateFn)(void);
|
|
|
|
#define POLL_MS 500
|
|
|
|
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 int64_t active_for = RTSS_UNSET; // the want RTSS holds the game to now; RTSS_UNSET none
|
|
static _Atomic int64_t movie_mhz; // a movie's rate while one plays (rtss_limit_movie), 0 none
|
|
static _Atomic int failed; // RTSS_FAIL_*: RTSS is out of it for the rest of the run (S.fail)
|
|
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 _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;
|
|
static ProfileFn load_profile, save_profile, delete_profile;
|
|
static PropertyFn get_property, set_property;
|
|
static UpdateFn update_profiles;
|
|
static RtssState S = RTSS_STATE_INIT; // the worker's decisions (rtss_decide.h), under lock
|
|
static int has_denominator; // this RTSS knows FramerateLimitDenominator
|
|
static int had_profile; // the profile existed before this run changed it
|
|
static DWORD old_limit, old_denominator = 1;
|
|
static int owed; // this run changed the profile and has not put it back: the three above stay as found
|
|
static int creations, removals; // the profile's "created" and "removed" lines this run, numbered (under lock)
|
|
static char state_path[MAX_PATH + 32]; // rtss-profile.txt next to the executable; "" when its path is unknown
|
|
static RtssSaved rec; // this run's line in it, while the profile holds a value of this run
|
|
static _Atomic int recorded; // the file holds rec: a crash leaves the profile to the next start
|
|
static int state_off; // a line this run could not act on stays as it is: no writes of the file
|
|
static _Atomic uint32_t rtss_tid; // the worker's thread, which may hold the lock when a crash comes
|
|
|
|
// RTSS's shared memory as rtss_parse reads it: whether RTSS runs, whether it lists this process, and its frame count.
|
|
static void rtss_map(RtssApp *a) {
|
|
memset(a, 0, sizeof *a);
|
|
HANDLE m = OpenFileMappingA(FILE_MAP_READ, FALSE, "RTSSSharedMemoryV2");
|
|
if (!m) return;
|
|
const uint8_t *p = MapViewOfFile(m, FILE_MAP_READ, 0, 0, 0);
|
|
MEMORY_BASIC_INFORMATION mi;
|
|
if (p && VirtualQuery(p, &mi, sizeof mi)) rtss_parse(p, mi.RegionSize, GetCurrentProcessId(), a);
|
|
if (p) UnmapViewOfFile(p);
|
|
CloseHandle(m);
|
|
}
|
|
|
|
static int load_hooks(void) {
|
|
if (dll) return 1;
|
|
dll = LoadLibraryA(dll_path);
|
|
if (!dll) {
|
|
rt_log("frame rate: RivaTuner Statistics Server's %s did not load (%lu)", dll_path, GetLastError());
|
|
return 0;
|
|
}
|
|
enum_profiles = (EnumProfilesFn)(void *)GetProcAddress(dll, "EnumProfiles");
|
|
load_profile = (ProfileFn)(void *)GetProcAddress(dll, "LoadProfile");
|
|
save_profile = (ProfileFn)(void *)GetProcAddress(dll, "SaveProfile");
|
|
delete_profile = (ProfileFn)(void *)GetProcAddress(dll, "DeleteProfile");
|
|
get_property = (PropertyFn)(void *)GetProcAddress(dll, "GetProfileProperty");
|
|
set_property = (PropertyFn)(void *)GetProcAddress(dll, "SetProfileProperty");
|
|
update_profiles = (UpdateFn)(void *)GetProcAddress(dll, "UpdateProfiles");
|
|
if (enum_profiles && load_profile && save_profile && delete_profile && get_property && set_property && update_profiles)
|
|
return 1;
|
|
rt_log("frame rate: RivaTuner Statistics Server's hooks library lacks the profile calls");
|
|
FreeLibrary(dll);
|
|
dll = NULL;
|
|
return 0;
|
|
}
|
|
|
|
// The profile exists on its own: RTSS keeps it as Profiles\<name>.cfg in its folder. EnumProfiles would say the same
|
|
// (a comma-separated list of names) but crashes this process: it walks Profiles\*.cfg with _findfirst and _findnext and
|
|
// keeps the find handle in 32 bits. The handle points into the process heap, which here lies above 4 GB (the guest
|
|
// address space takes the low part, memory.c mem_init), so _findnext gets a cut pointer and faults in ntdll writing to
|
|
// it. NX2NAVI_RTSS_ENUM=1 calls EnumProfiles anyway, to test an RTSS that keeps the whole handle.
|
|
static int profile_exists(void) {
|
|
if (!rt_knob("RTSS_ENUM", "0")) {
|
|
char path[MAX_PATH * 2];
|
|
snprintf(path, sizeof path, "%s\\Profiles\\%s.cfg", rtss_dir, profile);
|
|
return GetFileAttributesA(path) != INVALID_FILE_ATTRIBUTES;
|
|
}
|
|
char list[8192];
|
|
DWORD need = enum_profiles(list, sizeof list);
|
|
list[sizeof list - 1] = 0;
|
|
if (need > sizeof list)
|
|
rt_log("frame rate: RivaTuner Statistics Server lists %lu bytes of profiles; reading the first %zu", need, sizeof list);
|
|
size_t n = strlen(profile);
|
|
for (char *s = list; *s;) {
|
|
char *end = strchr(s, ',');
|
|
size_t len = end ? (size_t)(end - s) : strlen(s);
|
|
if (len == n && !_strnicmp(s, profile, n)) return 1;
|
|
if (!end) break;
|
|
s = end + 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Writes limit/denominator into the profile and reads them back from the saved file: 1 when they stuck, 0 when RTSS
|
|
// refused a value, -1 when the save did not reach the file. RTSS checks each value against the other one already in
|
|
// the profile (the limit over the denominator must be a rate it accepts: 165080 is refused over 1 and taken over 1000),
|
|
// so the limit goes to 0 (none) before the denominator changes.
|
|
static int write_limit(DWORD limit, DWORD denominator) {
|
|
load_profile(profile);
|
|
DWORD none = 0;
|
|
if (!set_property("FramerateLimit", (LPBYTE)&none, sizeof none)) return 0;
|
|
if (has_denominator && !set_property("FramerateLimitDenominator", (LPBYTE)&denominator, sizeof denominator)) return 0;
|
|
if (limit && !set_property("FramerateLimit", (LPBYTE)&limit, sizeof limit)) return 0;
|
|
save_profile(profile);
|
|
load_profile(profile);
|
|
DWORD l = ~0u, d = 1;
|
|
if (!get_property("FramerateLimit", (LPBYTE)&l, sizeof l)) return -1;
|
|
if (has_denominator && !get_property("FramerateLimitDenominator", (LPBYTE)&d, sizeof d)) return -1;
|
|
return l == limit && (!has_denominator || d == denominator) ? 1 : -1;
|
|
}
|
|
|
|
// The state file with this profile's line set to r, or removed for r NULL; the file goes once it holds no line. Under
|
|
// lock: the worker, or rtss_limit_restore's thread at the exit. 1 when the file holds what was asked.
|
|
static int state_put(const RtssSaved *r) {
|
|
if (state_off || !*state_path) return 0;
|
|
uint64_t n = 0;
|
|
char *text = (char *)plat_file_read(state_path, &n);
|
|
// a file that is there but did not read (another program holds it, a scan) keeps the lines of the other executables
|
|
// in the folder: rewritten from nothing, they would go
|
|
if (!text && plat_file_size(state_path) > 0) return 0;
|
|
size_t cap = (size_t)(text ? n : 0) + sizeof RTSS_SAVED_HEADER + 2 * (RTSS_NAME_MAX + 96);
|
|
char *out = malloc(cap);
|
|
int records = 0, ok = 0;
|
|
size_t len = out ? rtss_saved_update(text ? text : "", profile, r, out, cap, &records) : 0;
|
|
if (len && !records) {
|
|
plat_file_delete(state_path);
|
|
ok = !plat_file_exists(state_path);
|
|
} else if (len) {
|
|
ok = plat_file_write_atomic(state_path, out, len);
|
|
}
|
|
free(out);
|
|
free(text);
|
|
return ok;
|
|
}
|
|
|
|
// The state file did not take this run's line: a crash then puts the profile back as an orderly exit does
|
|
// (rtss_limit_crash). Logged once a run.
|
|
static void unrecorded(void) {
|
|
static int said;
|
|
atomic_store(&recorded, 0);
|
|
if (!state_off && !said++)
|
|
rt_log("frame rate: %s could not be written; a crash would leave the game's limit in RivaTuner Statistics "
|
|
"Server's profile %s", *state_path ? state_path : "rtss-profile.txt", profile);
|
|
}
|
|
|
|
// Before a write of limit/denominator into the profile: rec's written value is that one, and the one written before it
|
|
// stays as the previous one (rtss_decide.h RtssSaved), so that a run that ends before the profile changed, or after,
|
|
// leaves a profile the next start tells from a value the player set since.
|
|
static void record_before(DWORD limit, DWORD denominator) {
|
|
if (!atomic_load(&recorded) || (rec.limit == limit && rec.den == denominator)) return;
|
|
rec.prev_limit = rec.limit, rec.prev_den = rec.den;
|
|
rec.limit = limit, rec.den = denominator;
|
|
if (!state_put(&rec)) unrecorded();
|
|
}
|
|
|
|
// After that write: the profile holds the written value alone (landed), or kept the previous one.
|
|
static void record_after(int landed) {
|
|
if (!atomic_load(&recorded) || (rec.limit == rec.prev_limit && rec.den == rec.prev_den)) return;
|
|
if (landed) rec.prev_limit = rec.limit, rec.prev_den = rec.den;
|
|
else rec.limit = rec.prev_limit, rec.den = rec.prev_den;
|
|
if (!state_put(&rec)) unrecorded();
|
|
}
|
|
|
|
// Writes mhz into the profile (rtss_poll's RtssOps.apply): RTSS_WROTE_*. first: the run's first write, which records
|
|
// what to put back on exit, in the state file too before anything changes; clear: on a refusal, clear the limit of this
|
|
// run the profile holds.
|
|
static int apply(void *ctx, int64_t mhz, int first, int clear) {
|
|
(void)ctx;
|
|
if (first && !owed) { // what to put back on exit; after a put back that did not land, the profile still owes it
|
|
had_profile = profile_exists();
|
|
load_profile(profile);
|
|
old_limit = 0, old_denominator = 1;
|
|
get_property("FramerateLimit", (LPBYTE)&old_limit, sizeof old_limit);
|
|
has_denominator = get_property("FramerateLimitDenominator", (LPBYTE)&old_denominator, sizeof old_denominator);
|
|
if (!old_denominator) old_denominator = 1;
|
|
}
|
|
uint32_t l, d;
|
|
rtss_limit_value(mhz, has_denominator, &l, &d);
|
|
DWORD limit = l, denominator = d;
|
|
if (first) { // the line goes in before the profile changes: a kill between the two finds it as it was
|
|
RtssSaved was = rec; // still in the file after a put back that did not land: the profile holds its value
|
|
int kept = atomic_load(&recorded);
|
|
rec = (RtssSaved){had_profile, old_limit, old_denominator, limit, denominator, kept ? was.limit : limit,
|
|
kept ? was.den : denominator, ""};
|
|
snprintf(rec.name, sizeof rec.name, "%s", profile);
|
|
if (state_put(&rec)) atomic_store(&recorded, 1);
|
|
else unrecorded();
|
|
} else {
|
|
record_before(limit, denominator);
|
|
}
|
|
int r = write_limit(limit, denominator);
|
|
if (r < 0) { // the line keeps both values: the profile may hold either
|
|
rt_log("frame rate: RivaTuner Statistics Server's profile %s did not keep the limit: its Profiles folder needs the "
|
|
"game run as administrator", profile);
|
|
if (first && !owed && atomic_exchange(&recorded, 0)) state_put(NULL); // the profile is as it was
|
|
return RTSS_WROTE_READONLY;
|
|
}
|
|
owed = 1;
|
|
if (first) {
|
|
if (!had_profile) // numbered: a want that flips between a limit and kept out creates it again at each flip
|
|
rt_log("frame rate: RivaTuner Statistics Server profile %s created; it goes away when the game closes "
|
|
"(creation %d this run)", profile, ++creations);
|
|
else
|
|
rt_log("frame rate: RivaTuner Statistics Server profile %s held a limit of %lu/%lu; it gets that back when the "
|
|
"game closes", profile, old_limit, old_denominator);
|
|
}
|
|
if (r == 0) { // RTSS must not keep an earlier limit of this run under the game's own one
|
|
rt_log("frame rate: RivaTuner Statistics Server refused a limit of %.3f fps", mhz / 1000.0);
|
|
record_after(0);
|
|
if (!clear) return RTSS_WROTE_REFUSED;
|
|
record_before(0, 1);
|
|
int cleared = write_limit(0, 1) > 0;
|
|
record_after(cleared);
|
|
if (!cleared) return RTSS_WROTE_REFUSED;
|
|
update_profiles();
|
|
return RTSS_WROTE_REFUSED_CLEARED;
|
|
}
|
|
record_after(1);
|
|
update_profiles();
|
|
if (!mhz) rt_log("frame rate: RivaTuner Statistics Server's profile %s has no limit", profile);
|
|
else if ((int64_t)limit * 1000 == mhz * denominator)
|
|
rt_log("frame rate: RivaTuner Statistics Server's profile %s limits to %.3f fps", profile, mhz / 1000.0);
|
|
else // rounded to whole frames by an RTSS without FramerateLimitDenominator: the log says what RTSS holds
|
|
rt_log("frame rate: RivaTuner Statistics Server's profile %s limits to %lu/%lu (%.3f fps; the game asked %.3f)",
|
|
profile, limit, denominator, (double)limit / denominator, mhz / 1000.0);
|
|
return RTSS_WROTE_HELD;
|
|
}
|
|
|
|
// The profile as it was before this run changed it (the caller checked S.changed or owed, and marks S done). Its line
|
|
// in the state file goes once it is back; one that did not go back keeps the line, and owed, for the exit and the next
|
|
// start.
|
|
static void put_back(void) {
|
|
int back = 1;
|
|
if (!had_profile) {
|
|
delete_profile(profile);
|
|
back = !profile_exists();
|
|
if (back)
|
|
rt_log("frame rate: RivaTuner Statistics Server profile %s removed, as before the game started "
|
|
"(removal %d this run)", profile, ++removals);
|
|
else
|
|
rt_log("frame rate: RivaTuner Statistics Server profile %s did not go away", profile);
|
|
} else if (write_limit(old_limit, old_denominator) > 0) {
|
|
rt_log("frame rate: RivaTuner Statistics Server profile %s has its limit of %lu/%lu back", profile, old_limit,
|
|
old_denominator);
|
|
} else {
|
|
rt_log("frame rate: RivaTuner Statistics Server profile %s did not take its old limit back", profile);
|
|
back = 0;
|
|
}
|
|
update_profiles();
|
|
if (back) owed = 0;
|
|
if (back && atomic_exchange(&recorded, 0)) state_put(NULL);
|
|
}
|
|
|
|
// This profile's line in the state file into r: 1 when there is one.
|
|
static int left_line(RtssSaved *r) {
|
|
uint64_t n = 0;
|
|
char *text = *state_path ? (char *)plat_file_read(state_path, &n) : NULL;
|
|
int found = text && rtss_saved_find(text, profile, r);
|
|
free(text);
|
|
return found;
|
|
}
|
|
|
|
// The line r of this profile, left in the state file by a run that did not close (left_start): before this run changes
|
|
// anything, the profile gets back what it held before that run (rtss_restore_plan), unless the player changed it since,
|
|
// and the line goes. A profile that does not go back keeps the line, and this run leaves the file alone.
|
|
static void restore_left(const RtssSaved *r) {
|
|
int exists = profile_exists();
|
|
load_profile(profile);
|
|
DWORD limit = 0, den = 1;
|
|
get_property("FramerateLimit", (LPBYTE)&limit, sizeof limit);
|
|
has_denominator = get_property("FramerateLimitDenominator", (LPBYTE)&den, sizeof den);
|
|
if (!den) den = 1;
|
|
int plan = rtss_restore_plan(r, exists, limit, den);
|
|
if (plan == RTSS_PLAN_DELETE) delete_profile(profile);
|
|
if ((plan == RTSS_PLAN_WRITE && write_limit(r->old_limit, r->old_den) <= 0) ||
|
|
(plan == RTSS_PLAN_DELETE && profile_exists())) {
|
|
rt_log("frame rate: RivaTuner Statistics Server profile %s still has the game's limit of %lu/%lu from a run "
|
|
"that did not close, and did not go back as it was; %s keeps its line", profile, limit, den, state_path);
|
|
state_off = 1;
|
|
return;
|
|
}
|
|
if (plan == RTSS_PLAN_WRITE || plan == RTSS_PLAN_DELETE) update_profiles();
|
|
if (plan == RTSS_PLAN_WRITE)
|
|
rt_log("frame rate: RivaTuner Statistics Server profile %s had the game's limit of %lu/%lu from a run that did "
|
|
"not close (%s); its limit of %u/%u is back", profile, limit, den, state_path, r->old_limit, r->old_den);
|
|
else if (plan == RTSS_PLAN_DELETE)
|
|
rt_log("frame rate: RivaTuner Statistics Server profile %s had the game's limit of %lu/%lu from a run that did "
|
|
"not close (%s); removed, as before that run", profile, limit, den, state_path);
|
|
else if (plan == RTSS_PLAN_LEAVE)
|
|
rt_log("frame rate: RivaTuner Statistics Server profile %s changed since a run that did not close (%s); left as "
|
|
"it is", profile, state_path);
|
|
else
|
|
rt_log("frame rate: RivaTuner Statistics Server profile %s is as it was before a run that did not close (%s)",
|
|
profile, state_path);
|
|
if (!state_put(NULL)) {
|
|
rt_log("frame rate: %s could not be written; its line for %s stays", state_path, profile);
|
|
state_off = 1;
|
|
}
|
|
}
|
|
|
|
// The state file's line for this profile, on the worker's first poll that finds RTSS running (w the want it read,
|
|
// hooks as RtssIn.hooks): 1 once the line is dealt with or there is none, 0 when a restore is still owed, for the first
|
|
// poll with the hooks library. A limit puts the profile back with the library it loads for its own writes. Frame
|
|
// generation and low latency keep RTSS out of the run's limit, but a profile left with an old limit would hold the
|
|
// game beside them (two limiters, and under frame generation RTSS would count the generated frames too): the library is
|
|
// loaded for that one restore. NX2NAVI_RTSS=0 keeps the thread read-only, so the log says what the file holds.
|
|
static int left_start(int64_t w, int hooks) {
|
|
RtssSaved r;
|
|
if (!left_line(&r)) return 1;
|
|
if (hooks != 1 && w == RTSS_OUT_SETTING) {
|
|
rt_log("frame rate: %s holds the game's limit of %u/%u for RivaTuner Statistics Server's profile %s from a run "
|
|
"that did not close; NX2NAVI_RTSS=0 leaves the profile as it is, and a start with RTSS let in puts it "
|
|
"back", state_path, r.limit, r.den, profile);
|
|
return 1;
|
|
}
|
|
if (hooks != 1 && (w >= 0 || !load_hooks())) return 0;
|
|
restore_left(&r);
|
|
return 1;
|
|
}
|
|
|
|
static void put_back_op(void *ctx, const RtssOut *o) { // rtss_poll's RtssOps.put_back
|
|
(void)ctx;
|
|
if (o->why == RTSS_WHY_FAILED && S.fail == RTSS_FAIL_NOT_LIMITING) // the frame count check gave up on RTSS
|
|
rt_log("frame rate: RivaTuner Statistics Server measures %.1f fps with its limit at %.3f fps", o->fps,
|
|
o->mhz / 1000.0);
|
|
else if (o->why == RTSS_WHY_FAILED) // the lines before say which sign of its hold never came
|
|
rt_log("frame rate: RivaTuner Statistics Server listed the game with its limit at %.3f fps for %d s and never "
|
|
"showed it holds it; the game's own limiter for the rest of the run", o->mhz / 1000.0,
|
|
RTSS_UNPROVEN_POLLS * POLL_MS / 1000);
|
|
put_back();
|
|
}
|
|
|
|
// 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 of the limiter or of why the game's own one paces (RTSS not hooked yet, not read the profile, a
|
|
// minimised window), up to LIMITER_LINES lines. 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. The store is made
|
|
// either way (a want that went w, w2, w again must not find an older hold), but a hold is logged only while its want
|
|
// is still the current one: the game's limiter paces a newer want, and the next poll logs what holds it.
|
|
#define LIMITER_LINES 100
|
|
static void set_active(int64_t w, int on, const char *why) {
|
|
static int logged = -1, lines;
|
|
static char said[128];
|
|
atomic_store(&active_for, on ? w : RTSS_UNSET);
|
|
if ((on && atomic_load(&want) != w) || (logged == on && (on || !strcmp(why, said)))) return;
|
|
logged = on;
|
|
snprintf(said, sizeof said, "%s", on ? "" : why);
|
|
if (++lines > LIMITER_LINES) return;
|
|
if (on) rt_log("frame rate: RivaTuner Statistics Server does the limiting; the game's own limiter stands aside");
|
|
else rt_log("frame rate: the game's own limiter (%s)", why);
|
|
if (lines == LIMITER_LINES) rt_log("frame rate: later changes of the limiter in charge are not logged");
|
|
}
|
|
|
|
// RTSS's own settings for the game, read from its profile file as RTSS keeps it: Profiles\<exe>.cfg, else
|
|
// Profiles\Global, and a key the game's profile lacks from Global. The system's ini reader does it, so no call goes into
|
|
// RTSS's hooks library, which a poll of a kept-out want never loads. The files are read again only when the time
|
|
// either was written changes. Worker thread only, while RTSS lists the game (watch).
|
|
static RtssSettings settings;
|
|
static char settings_file[MAX_PATH + 16]; // which file they came from, as Profiles\<name>
|
|
|
|
static uint64_t file_stamp(const char *path) { // 0: no such file
|
|
WIN32_FILE_ATTRIBUTE_DATA a;
|
|
if (!GetFileAttributesExA(path, GetFileExInfoStandard, &a)) return 0;
|
|
return ((uint64_t)a.ftLastWriteTime.dwHighDateTime << 32 | a.ftLastWriteTime.dwLowDateTime) | 1;
|
|
}
|
|
|
|
static int cfg_int(const char *own, const char *global, const char *key) {
|
|
char v[32] = "";
|
|
if (own) GetPrivateProfileStringA("Framerate", key, "", v, sizeof v, own);
|
|
if (!*v) GetPrivateProfileStringA("Framerate", key, "", v, sizeof v, global);
|
|
return (int)strtol(v, NULL, 10); // GetPrivateProfileIntA would read a negative scanline as 0
|
|
}
|
|
|
|
static void read_settings(void) {
|
|
static uint64_t stamps[2] = {~0ull, ~0ull}; // never read
|
|
char own[MAX_PATH * 2], global[MAX_PATH * 2];
|
|
snprintf(own, sizeof own, "%s\\Profiles\\%s.cfg", rtss_dir, profile);
|
|
snprintf(global, sizeof global, "%s\\Profiles\\Global", rtss_dir);
|
|
uint64_t a = file_stamp(own), b = file_stamp(global);
|
|
if (a == stamps[0] && b == stamps[1]) return;
|
|
stamps[0] = a, stamps[1] = b;
|
|
const char *p = a ? own : NULL;
|
|
settings = (RtssSettings){cfg_int(p, global, "Limit"), cfg_int(p, global, "LimitDenominator"),
|
|
cfg_int(p, global, "LimitTime"), cfg_int(p, global, "LimitTimeDenominator"),
|
|
cfg_int(p, global, "SyncScanline0"), cfg_int(p, global, "SyncScanline1"),
|
|
cfg_int(p, global, "SyncDisplay"), cfg_int(p, global, "ReflexSleep"),
|
|
cfg_int(p, global, "PassiveWait")};
|
|
if (a) snprintf(settings_file, sizeof settings_file, "Profiles\\%s.cfg", profile);
|
|
else snprintf(settings_file, sizeof settings_file, "Profiles\\Global");
|
|
}
|
|
|
|
// While RTSS lists the game, the settings it holds for it are logged once per change: what RTSS applies may be more than
|
|
// the FramerateLimit the game writes (a frame time limit, scanline sync, its own Reflex). While RTSS has no part in the
|
|
// game's limit (rtss_out_of_limit: kept out, w < 0, or out for the run because its profile cannot be written or its
|
|
// hooks library did not load) the game's own limiter, the driver's low latency or frame generation holds the frames,
|
|
// and a limit the player set in RTSS itself for the game (its global profile or the game's own, a frame time limit or
|
|
// scanline sync) still holds them a second time. Logged once per change, and again when the other limiter changes;
|
|
// the menu shows it for a kept-out want (rtss_limit_state 4), while a profile that cannot be written keeps its own note
|
|
// there (state 2).
|
|
static void watch(int64_t w, int fail, int listed) {
|
|
static int said, lines, settings_lines;
|
|
static const char *said_by;
|
|
static char said_text[128], said_settings[MAX_PATH + 256];
|
|
int out = rtss_out_of_limit(w, fail), on = 0;
|
|
const char *by = rtss_out_limiter(w); // the limiter beside RTSS's own setting
|
|
char text[128] = "";
|
|
if (listed) {
|
|
read_settings();
|
|
char all[MAX_PATH + 256], modes[192];
|
|
rtss_settings_text(modes, sizeof modes, &settings, 1);
|
|
snprintf(all, sizeof all, "(%s): %s", settings_file, modes);
|
|
if (strcmp(all, said_settings) && settings_lines++ < 40)
|
|
rt_log("frame rate: RivaTuner Statistics Server's settings for the game %s", all);
|
|
snprintf(said_settings, sizeof said_settings, "%s", all);
|
|
}
|
|
if (out && listed) {
|
|
on = rtss_two(w, fail, listed, rtss_settings_limit(&settings));
|
|
rtss_settings_text(text, sizeof text, &settings, 0);
|
|
}
|
|
if (!out) said = 0; // RTSS takes the game's limit: a later want out of it logs afresh
|
|
else if (on != said || (on && (strcmp(text, said_text) || by != said_by))) {
|
|
if (lines++ < 40) {
|
|
if (on)
|
|
rt_log("frame rate: two limiters: RivaTuner Statistics Server's own setting for the game (%s: %s) limits "
|
|
"it beside %s", settings_file, text, by);
|
|
else
|
|
rt_log("frame rate: RivaTuner Statistics Server no longer limits the game beside %s", by);
|
|
}
|
|
said = on, said_by = by;
|
|
snprintf(said_text, sizeof said_text, "%s", text);
|
|
}
|
|
atomic_store(&two, on && w < 0); // the menu's note (rtss_limit_state 4) is for a kept-out want
|
|
}
|
|
|
|
// 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
|
|
char path[MAX_PATH], *cut; // the state file next to the executable, where nx2navi.ini is (cfg.c find_ini)
|
|
if (plat_exe_path(path, sizeof path) && (cut = plat_last_sep(path)))
|
|
snprintf(state_path, sizeof state_path, "%.*s%s", (int)(cut + 1 - path), path, "rtss-profile.txt");
|
|
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), and while it is kept out 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;
|
|
atomic_store(&rtss_tid, plat_thread_id());
|
|
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_listed = 0, left_done = 0, left_tried = 0;
|
|
const RtssOps ops = {NULL, apply, put_back_op};
|
|
for (;; plat_sleep_ms(POLL_MS)) {
|
|
int64_t w = atomic_load(&want);
|
|
if (w == RTSS_UNSET) continue;
|
|
plat_mutex_lock(&lock);
|
|
if (atomic_load(&closed)) { // a want read before rtss_limit_restore must not reach the profile after it
|
|
plat_mutex_unlock(&lock);
|
|
continue;
|
|
}
|
|
RtssIn in = {.want = w, .movie_mhz = atomic_load(&movie_mhz), .now_ns = kern_now_ns(),
|
|
.hosted_ns = gfx_window_hosted_ns()};
|
|
rtss_map(&in.app);
|
|
if (in.app.running != was_running || in.app.listed != was_listed) {
|
|
if (!in.app.running) rt_log("frame rate: RivaTuner Statistics Server is installed but not running");
|
|
else if (!in.app.listed) rt_log("frame rate: RivaTuner Statistics Server runs; it has not hooked the game yet");
|
|
else rt_log("frame rate: RivaTuner Statistics Server has hooked the game");
|
|
was_running = in.app.running, was_listed = in.app.listed;
|
|
}
|
|
in.hooks = dll ? 1 : rtss_wants_hooks(&S, &in) ? load_hooks() : -1;
|
|
if (!left_done && in.app.running && (in.hooks == 1 || !left_tried)) { // before this run writes anything
|
|
left_tried = 1;
|
|
left_done = left_start(w, in.hooks);
|
|
}
|
|
in.current = atomic_load(&want) == w;
|
|
RtssOut o = rtss_poll(&S, &in, &ops);
|
|
atomic_store(&failed, S.fail);
|
|
if (o.why != RTSS_WHY_NEWER) { // a newer want waits for the next poll
|
|
char why[128];
|
|
rtss_why_text(why, sizeof why, &o, S.fail);
|
|
set_active(w, o.holds, why);
|
|
}
|
|
watch(w, S.fail, in.app.listed);
|
|
plat_mutex_unlock(&lock);
|
|
}
|
|
}
|
|
|
|
void rtss_limit_want(int64_t mhz) {
|
|
if (atomic_load_explicit(&closed, memory_order_relaxed)) return; // the exit put the profile back
|
|
// 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");
|
|
}
|
|
|
|
void rtss_limit_movie(int hz) { atomic_store_explicit(&movie_mhz, (int64_t)hz * 1000, memory_order_relaxed); }
|
|
|
|
int rtss_limit_active(void) {
|
|
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) {
|
|
if (atomic_load_explicit(&closed, memory_order_relaxed)) return 0;
|
|
int f = atomic_load_explicit(&failed, memory_order_relaxed);
|
|
int64_t w = atomic_load(&want);
|
|
if (w >= 0 && (f == RTSS_FAIL_READONLY || f == RTSS_FAIL_NOT_LIMITING || f == RTSS_FAIL_UNPROVEN))
|
|
return f == RTSS_FAIL_READONLY ? 2 : 3;
|
|
if (w < 0 && w != RTSS_UNSET && atomic_load_explicit(&two, memory_order_relaxed)) return 4;
|
|
return rtss_limit_active() && w > 0;
|
|
}
|
|
|
|
void rtss_limit_restore(void) {
|
|
// The game presents on after this call (the rest of the exit runs first), so from here the game's own limiter holds
|
|
// the frames left and no want reaches the profile again: the worker would otherwise write the game's limit back into
|
|
// 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 (!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)) {
|
|
if (dll && (S.changed || owed)) put_back(), rtss_put_back_done(&S);
|
|
plat_mutex_unlock(&lock);
|
|
return;
|
|
}
|
|
plat_sleep_ms(10);
|
|
}
|
|
}
|
|
|
|
// A crash's and a fatal error's end (main.c crash_handler, dispatch.c fatal_end), from any thread: no want reaches the
|
|
// profile any more, and no call goes into RTSS's library, which may be what failed and is no safe call on a damaged
|
|
// heap. The state file keeps this run's line, and the next start puts the profile back from it (restore_left). Without
|
|
// that line (the file could not be written) the profile goes back as at an orderly exit, but not from the worker's own
|
|
// thread, which may hold the lock that takes.
|
|
void rtss_limit_crash(void) {
|
|
atomic_store(&closed, 1);
|
|
atomic_store(&active_for, RTSS_UNSET);
|
|
if (atomic_load(&recorded) || plat_thread_id() == atomic_load(&rtss_tid)) return;
|
|
rtss_limit_restore();
|
|
}
|
|
|
|
#else
|
|
void rtss_limit_want(int64_t mhz) { (void)mhz; }
|
|
void rtss_limit_movie(int hz) { (void)hz; }
|
|
int rtss_limit_active(void) { return 0; }
|
|
int rtss_limit_state(void) { return 0; }
|
|
void rtss_limit_restore(void) {}
|
|
void rtss_limit_crash(void) {}
|
|
#endif
|