mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
The review found three ways rtss-profile.txt could fall behind the profile. Every write after a run's first one saved the profile before the line, so a kill between the two left a line whose value the profile no longer held, and the next start took the game's limit for the player's (rtss_restore_plan LEAVE). A put back whose write did not land kept the line, but rtss_put_back_done cleared S.changed, so a later limit took the run's first-write path again and read the game's own limit from the profile as the value to put back, overwriting the true one in the line. And put_back removed the line after DeleteProfile without checking that the profile went away. The line now carries the value the run wrote before the newest one (rtss_decide.h RtssSaved prev_limit and prev_den, rtss1 with two more fields; the file came with this branch and was never released). rtss_limit.c apply writes the new value into the line before the profile (record_before) and drops the previous one once the profile holds the new one, or returns to it when RTSS refused it (record_after), and rtss_restore_plan takes a profile holding either value for the game's. A new owed flag keeps had_profile, old_limit and old_denominator from the run's first write until a put back lands: a later first write keeps them and the line's values, a failed first write leaves the line alone, and rtss_limit_restore tries the put back again at the exit. put_back and restore_left check that a deleted profile is gone before they remove the line, and restore_left's lines name the value the profile held. tests/rtss_limit_check.c covers the new fields and the plan for either value; tests/rtss_worker_check.c checks the line before and after a write (state_record), a run killed inside the save of its second limit and the next start removing the profile (kill_restart_mid_write), a put back that did not land followed by a limit and the exit (put_back_owed), and a removal that did not land (put_back_undeleted). Lines over 120 columns from the earlier fixes are wrapped. Not reproduced before the change. Checks that passed: tests/rtss_limit_check.py (44 of 44), tests/exit_order_check.py, tests/fatal_exit_check.py, pass_cc_check.py --wall on rtss_limit.c, tools/check_content.py --staged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
635 lines
36 KiB
C
635 lines
36 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.
|
|
// 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.
|
|
//
|
|
// 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. Worker
|
|
// thread only, under lock. 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);
|
|
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
|