diff --git a/docs/architecture.md b/docs/architecture.md index c52680ee..fce49c0d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -288,15 +288,18 @@ keeps per-core data without locks and relies on that, so the host models it (`ru times the limit in three new periods in a row (a period RTSS no longer moves counts neither way); the menu names the first and the last of these. Frame generation, the driver's low latency and `NX2NAVI_RTSS=0` keep RTSS out (`RTSS_OUT_*`): the profile gets back what it held before - the run changed it, and while RTSS is kept out its thread does not load the hooks library and only reads RTSS's own - settings for the game; 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. While RTSS lists the game the log names those + the run changed it, and while RTSS is kept out its thread does not load the hooks library (but for the one restore + below at the start) and only reads RTSS's own settings for the game; 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. While RTSS lists the game the log names those settings (limit, frame time limit, scanline sync, display sync, Reflex sleep, passive wait) once per change. Every orderly exit puts the profile back as it was before the run (`rtss_limit_restore`), and no want reaches it after that. What the profile held is also kept in `rtss-profile.txt` next to the executable, one line per profile, written before the run's first change of it and removed once it is back: a start that finds a line left by a run that did not close (a kill, a hang, the device-lost watchdog, a crash or a fatal error, whose ends make no call into RTSS's - library: `rtss_limit_crash`) puts that profile back first, unless the player changed it since. A file that cannot be + library: `rtss_limit_crash`) puts that profile back first (`left_start`), unless the player changed it since; frame + generation and low latency, which keep RTSS out, have the hooks library loaded for that one restore, and a start with + `NX2NAVI_RTSS=0` leaves the profile and the line as they are and says so in the log. A file that cannot be written leaves the run as before, with a line in the log; profiles that versions before the file left with the game's limit cannot be told from the player's own. - Measuring pacing: `NX2NAVI_TRACE=present` logs a line per picture (the host present's interval, the game's present diff --git a/docs/building.md b/docs/building.md index c4f45d28..ab5cb473 100644 --- a/docs/building.md +++ b/docs/building.md @@ -497,8 +497,8 @@ its rewrite and what a start does with a line a run that did not close left. Sta `rtss_limit.c` against a fake RTSS (its registry key, hooks library, profile files and shared memory) and runs each scenario in a process of its own, among them a want after the exit's restore and one the worker read before it, a worker polling without pause while the want switches, the registry and file calls a present must not make, and a run -killed while RTSS held its limit followed by the next start, each in a process of its own. No GPU, no RTSS, no game -files. +killed while RTSS held its limit followed by the next start, each in a process of its own (a next start with a limit, +with frame generation on and with `NX2NAVI_RTSS=0`). No GPU, no RTSS, no game files. A change to how the mixer applies volumes (`runtime/audio_mixer.c` `accumulate_ramp`, the voice and splitter paths of `audio_mixer_render`) or to what it owes the device after a stall (`ring_pull`, `audio_mixer_owed`) passes diff --git a/nx2navi.ini.example b/nx2navi.ini.example index 367e2711..4ec6c4f6 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -25,14 +25,15 @@ # fps_limit = 60 # Where RivaTuner Statistics Server is installed and running, the limit goes into its profile for the game's # executable and RTSS does the limiting instead of the game, once RTSS is seen holding the game to it; closing the game -# puts back what the profile held, and after a crash or a kill the next start does (rtss-profile.txt next to the game -# keeps what it held). Changing that profile needs the game run as administrator (RTSS keeps its profiles under Program -# Files). Without it the game limits by itself, as it does with frame generation on (RTSS would count the generated -# frames), while a movie plays or the window is minimised, and for the rest of the run when RTSS's own frame counter -# shows the game past the limit (RTSS then hooks the game without holding it back); with low latency on, the driver -# holds the limit. rtss = 0 always leaves it to the game. With rtss = 0, frame generation or low latency, a limit set in -# RTSS itself (its global profile or the game's, a frame time limit or scanline sync) limits the game a second time: -# the log and the settings menu say so, and setting it to 0 in RTSS leaves one limiter. +# puts back what the profile held, and after a crash or a kill the next start does, unless it has rtss = 0 +# (rtss-profile.txt next to the game keeps what it held). Changing that profile needs the game run as administrator +# (RTSS keeps its profiles under Program Files). Without it the game limits by itself, as it does with frame generation +# on (RTSS would count the generated frames), while the window is minimised, and for the rest of the run when RTSS's +# own frame counter shows the game past the limit (RTSS then hooks the game without holding it back); the game paces +# movies itself. With low latency on, the driver holds the limit. rtss = 0 always leaves it to the game. With rtss = 0, +# frame generation or low latency, a limit set in RTSS itself (its global profile or the game's, a frame time limit or +# scanline sync) limits the game a second time: the log and the settings menu say so, and setting it to 0 in RTSS +# leaves one limiter. # rtss = 1 # 1: asks RTSS's EnumProfiles whether the game's profile exists, instead of looking for its file. EnumProfiles crashes # the game (RTSS keeps a pointer of the game's in 32 bits); only for testing an RTSS that fixed it. diff --git a/runtime/rtss_limit.c b/runtime/rtss_limit.c index 4b921bf4..1cd6e02d 100644 --- a/runtime/rtss_limit.c +++ b/runtime/rtss_limit.c @@ -28,14 +28,15 @@ // 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 (restore_left), unless the player changed the -// profile since. 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. +// 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); a later limit (frame generation or low latency switched off) loads it +// 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 #include @@ -278,26 +279,28 @@ static void put_back(void) { if (back && atomic_exchange(&recorded, 0)) state_put(NULL); } -// A line of this profile in the state file, left by a run that did not close: on the first poll with the hooks library, -// 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 take its old limit keeps the line, and this run -// leaves the file alone. -static void restore_left(void) { +// 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; - RtssSaved r; - int found = text && rtss_saved_find(text, profile, &r); + int found = text && rtss_saved_find(text, profile, r); free(text); - if (!found) return; + 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 take its old limit 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); - int plan = rtss_restore_plan(&r, exists, limit, den); - if (plan == RTSS_PLAN_WRITE && write_limit(r.old_limit, r.old_den) <= 0) { + int plan = rtss_restore_plan(r, exists, limit, den); + if (plan == RTSS_PLAN_WRITE && write_limit(r->old_limit, r->old_den) <= 0) { rt_log("frame rate: RivaTuner Statistics Server profile %s still has the game's limit of %u/%u from a run that " - "did not close, and did not take its old limit back; %s keeps its line", profile, r.limit, r.den, + "did not close, and did not take its old limit back; %s keeps its line", profile, r->limit, r->den, state_path); state_off = 1; return; @@ -306,10 +309,11 @@ static void restore_left(void) { 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 %u/%u from a run that did not " - "close (%s); its limit of %u/%u is back", profile, r.limit, r.den, state_path, r.old_limit, r.old_den); + "close (%s); its limit of %u/%u is back", profile, r->limit, r->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 %u/%u from a run that did not " - "close (%s); removed, as before that run", profile, r.limit, r.den, state_path); + "close (%s); removed, as before that run", profile, r->limit, r->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); @@ -322,6 +326,26 @@ static void restore_left(void) { } } +// 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) // the frame count check gave up on RTSS @@ -472,7 +496,7 @@ static void worker(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_checked = 0; + 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); @@ -492,7 +516,10 @@ static void worker(void *arg) { was_running = in.app.running, was_listed = in.app.listed; } in.hooks = dll ? 1 : rtss_wants_hooks(&S, &in) ? load_hooks() : -1; - if (in.hooks == 1 && !left_checked) left_checked = 1, restore_left(); + 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); diff --git a/tests/rtss_worker_check.c b/tests/rtss_worker_check.c index b2685eb8..bc004c66 100644 --- a/tests/rtss_worker_check.c +++ b/tests/rtss_worker_check.c @@ -947,7 +947,10 @@ static void crash_recorded(void) { // A run killed while RTSS held its limit (kill_first), and the next start (kill_next): the profile as before the killed // run, then the next run's own limit, and back again at its close. variant 1: the player had a profile with 30/1; -// variant 2: the player set 75 in RTSS between the two runs. +// variant 2: the player set 75 in RTSS between the two runs; variant 3: the next start has frame generation on, which +// keeps RTSS out, and still puts the profile back (the hooks library loaded for that alone) with no write of its own; +// variant 4: the next start has NX2NAVI_RTSS=0, which keeps the thread read-only: the log says what the file holds, +// and the profile and the line stay for a start with RTSS let in. static int variant; static void kill_first(void) { prof_load(); @@ -963,6 +966,30 @@ static void kill_first(void) { static void kill_next(void) { prof_load(); app_set(1, 0xA, 1000, 2000, 144); + if (variant >= 3) { + ask(variant == 3 ? RTSS_OUT_FRAMEGEN : RTSS_OUT_SETTING); + poll(); + poll(); + show("the next start, kept out"); + CHECK(!saves, "no limit written by this run (%ld saves)", saves); + if (variant == 3) { + CHECK(loads == 1 && deletes == 1 && updates == 1 && + logged("profile ocarina.exe had the game's limit of 60/1 from a run that did not close") && + logged("; removed, as before that run"), + "the killed run's profile removed, with the hooks library loaded once"); + CHECK(!own.exists && !*state(), "the profile and the line gone:\n%s", state()); + } else { + CHECK(!loads && logged("rtss-profile.txt holds the game's limit of 60/1 for RivaTuner Statistics Server's " + "profile ocarina.exe from a run that did not close; NX2NAVI_RTSS=0 leaves the " + "profile as it is"), + "read only: logged"); + CHECK(own.exists && own.limit == 60 && state_has("\nrtss1 0 0 1 60 1 ocarina.exe\n"), "left:\n%s", state()); + } + rtss_limit_restore(); + show("closed"); + prof_save(); + return; + } ask(144000); poll(); show("the next start"); @@ -1014,11 +1041,15 @@ static void kill_restart_as(int v) { if (v == 0) CHECK(!own.exists, "the profile is gone, as before the killed run"); if (v == 1) CHECK(own.exists && own.limit == 30, "the player's 30"); if (v == 2) CHECK(own.exists && own.limit == 75, "the player's 75"); - CHECK(!*state(), "no state file:\n%s", state()); + if (v == 3) CHECK(!own.exists, "the profile is gone, as before the killed run"); + if (v == 4) CHECK(own.exists && own.limit == 60, "the killed run's 60 stays for a start with RTSS let in"); + CHECK(v == 4 ? state_has("\nrtss1 0 0 1 60 1 ocarina.exe\n") : !*state(), "the state file:\n%s", state()); } static void kill_restart(void) { kill_restart_as(0); } static void kill_restart_player(void) { kill_restart_as(1); } static void kill_restart_edited(void) { kill_restart_as(2); } +static void kill_restart_framegen(void) { kill_restart_as(3); } +static void kill_restart_setting(void) { kill_restart_as(4); } // A movie at 60 under a limit of 144: the want stays 144 (frame_rate.c frame_step_cap_ns) and RTSS's profile keeps it // with no write, while the game's pacer holds the movie at its rate; RTSS holds the game again at its end. @@ -1095,6 +1126,8 @@ static const struct { const char *name; void (*run)(void); } scenarios[] = { {"kill_restart", kill_restart}, {"kill_restart_player", kill_restart_player}, {"kill_restart_edited", kill_restart_edited}, + {"kill_restart_framegen", kill_restart_framegen}, + {"kill_restart_setting", kill_restart_setting}, }; int main(int argc, char **argv) {