diff --git a/runtime/dispatch.c b/runtime/dispatch.c index 13001ac6..843f330a 100644 --- a/runtime/dispatch.c +++ b/runtime/dispatch.c @@ -122,12 +122,10 @@ static void fatal_end(int code, const char *fmt, va_list ap) { kern_free_cores_note(); fs_save_crash_wait(); // a save the game already reported as done finishes first (bounded) prof_sampler_write(); // NX2NAVI_PROFILE: what was sampled until now, else a crash loses the whole profile - // RivaTuner Statistics Server's profile as it was: left with the game's limit in it, the next run would take that - // limit for the player's own (rtss_limit.c; it gives up after a second if the failing thread holds its lock). Not - // from RTSS's own thread, which may hold that lock: the restore would spend its second for nothing (as in main.c - // crash_handler). - const char *thread = plat_thread_name(); - if (!thread || strcmp(thread, "rtss")) rtss_limit_restore(); + // RivaTuner Statistics Server's profile goes back at the next start, from the line the run keeps for it in + // rtss-profile.txt (rtss_limit.c rtss_limit_crash, as in main.c crash_handler): no call into RTSS's library here, + // from any thread + rtss_limit_crash(); rt_rumble_stop_bounded(); // a controller on HD rumble: nothing renews or stops its vibration after the end // "frametime: at a fatal error" and the stutter map, the session's last period: after what a kill of the process // must not lose, as in the orderly end (gfx_present.c rt_exit_steps); ft_stop waits up to 50 ms diff --git a/runtime/host.h b/runtime/host.h index 096fa4ed..b503b0fd 100644 --- a/runtime/host.h +++ b/runtime/host.h @@ -472,6 +472,8 @@ int rtss_limit_state(void); // menu: 1 RTSS limits // game run past its limit, 4 kept out while a limit set in // RTSS itself holds the game too void rtss_limit_restore(void); // before the process ends: the profile's old limit back +void rtss_limit_crash(void); // a crash or fatal error: no call into RTSS, the next start + // puts the profile back (rtss-profile.txt) int64_t frame_step_world_us(void); // game time since the loaded save's first frame, -1 before // (NX2NAVI_FPS_LIMIT, live) uint64_t frame_step_ticks(void); // game time since the first present in 60 Hz ticks (input scripts) diff --git a/runtime/main.c b/runtime/main.c index ed9035e6..130288b7 100644 --- a/runtime/main.c +++ b/runtime/main.c @@ -436,9 +436,9 @@ static void crash_handler(const PlatCrash *x) { rt_fatal_notes(); // the card, its driver and the memory settings in effect (vk_device.c fatal_note) kern_free_cores_note(); fs_save_crash_wait(); // a save the game already reported as done finishes first (bounded; fs_host.c) - // after the report: RivaTuner Statistics Server's profile as it was; not from its own thread, which holds the lock - // the restore would try for a second - if (!on_rtss) rtss_limit_restore(); + // after the report: RivaTuner Statistics Server's profile goes back at the next start, from the line the run keeps + // for it (rtss_limit.c rtss_limit_crash): no call into RTSS's library from a process that failed + rtss_limit_crash(); rt_rumble_stop_bounded(); // a controller on HD rumble stops (dispatch.c: 300 ms at most, SDL may be the crash) fflush(stderr); prof_sampler_write(); // NX2NAVI_PROFILE: what was sampled until the crash (it gives up after 2 s if the sampler hangs) diff --git a/runtime/rtss_decide.h b/runtime/rtss_decide.h index 0347a297..c413b9a4 100644 --- a/runtime/rtss_decide.h +++ b/runtime/rtss_decide.h @@ -231,11 +231,11 @@ static inline void rtss_why_text(char *out, size_t n, const RtssOut *o, int fail // gfx_present.c gfx_window_hosted_ns, 0 before the first). The limit's own period while RTSS does not hold the game, // while a movie plays (from its first frame: the pacer keeps the movie's slots, which RTSS does not know, and the // worker's next poll gives the movie to the pacer for its length, rtss_target), before the first host present, and -// once no host present went out for RTSS_HOLD_IDLE_NS: a minimised window or a -// frame hold (vk_backend.c frame_hold) gives RTSS nothing to hold, and a pacer that held nothing then let the quick menu -// run at the 640-700 fps that read address 0 (frame_rate.c LIMIT_FREE_MAX_HZ). While RTSS holds, the ceiling's period -// and never 0: the frames RTSS lets through come further apart than that, so pace() waits for none of them, and it -// still holds the game's frames to the ceiling the game's code needs. +// once no host present went out for RTSS_HOLD_IDLE_NS: a minimised window or a frame hold (vk_backend.c frame_hold) +// gives RTSS nothing to hold, and a pacer that held nothing then let the quick menu run at the 640-700 fps that read +// address 0 (frame_rate.c LIMIT_FREE_MAX_HZ). While RTSS holds, the ceiling's period and never 0: the frames RTSS lets +// through come further apart than that, so pace() waits for none of them, and it still holds the game's frames to the +// ceiling the game's code needs. static inline uint64_t rtss_pace_period(int holds, int movie, uint64_t now_ns, uint64_t hosted_ns, uint64_t period, uint64_t ceiling) { uint64_t idle = now_ns > hosted_ns ? now_ns - hosted_ns : 0; @@ -243,6 +243,111 @@ static inline uint64_t rtss_pace_period(int holds, int movie, uint64_t now_ns, u return ceiling < period ? ceiling : period; } +// ---- the state file (rtss_limit.c, rtss-profile.txt next to the executable): each RTSS profile a run changed, as it +// was before, so that a start after a run that did not close (a kill, a hang, a crash) puts it back. One line per +// profile, so executables that share the folder keep their own: +// rtss1 +// The name comes last and may hold spaces; lines that start with # are comments. +#define RTSS_SAVED_HEADER \ + "# RivaTuner Statistics Server's profiles as they were before the game changed them (rtss_limit.c): existed, old\n" \ + "# limit and denominator, the game's limit and denominator, profile. The game puts a profile back and removes its\n" \ + "# line when it closes; a start that finds a line puts that profile back first.\n" +enum { RTSS_NAME_MAX = 260 }; + +typedef struct { + int existed; // the profile existed before the run changed it + uint32_t old_limit, old_den; // what it held then + uint32_t limit, den; // what the run wrote last + char name[RTSS_NAME_MAX]; // the profile's name, the executable's (ocarina.exe) +} RtssSaved; + +// The line of r, with its newline: its length, 0 when it does not fit n. +static inline size_t rtss_saved_format(char *out, size_t n, const RtssSaved *r) { + int k = snprintf(out, n, "rtss1 %d %u %u %u %u %s\n", r->existed ? 1 : 0, r->old_limit, r->old_den, r->limit, + r->den, r->name); + return k > 0 && (size_t)k < n ? (size_t)k : 0; +} + +// One line (len bytes, no newline) into r: 1 when it is a well-formed line of this format, 0 otherwise. +static inline int rtss_saved_parse(const char *line, size_t len, RtssSaved *r) { + char buf[RTSS_NAME_MAX + 96]; + while (len && (line[len - 1] == '\r' || line[len - 1] == ' ')) len--; + if (len >= sizeof buf) return 0; + memcpy(buf, line, len); + buf[len] = 0; + int existed = -1, at = 0; + unsigned v[4]; + if (sscanf(buf, "rtss1 %d %u %u %u %u %n", &existed, &v[0], &v[1], &v[2], &v[3], &at) != 5 || !at) return 0; + size_t name = len - (size_t)at; + if ((existed != 0 && existed != 1) || !v[1] || !v[3] || !name || name >= RTSS_NAME_MAX) return 0; + r->existed = existed, r->old_limit = v[0], r->old_den = v[1], r->limit = v[2], r->den = v[3]; + memcpy(r->name, buf + at, name + 1); + return 1; +} + +// Profile names compare as Windows compares file names: ASCII letters in either case. +static inline int rtss_name_eq(const char *a, const char *b) { + for (;; a++, b++) { + int x = *a >= 'A' && *a <= 'Z' ? *a + 32 : *a, y = *b >= 'A' && *b <= 'Z' ? *b + 32 : *b; + if (x != y) return 0; + if (!x) return 1; + } +} + +// The line of profile name in the file's text (NUL-terminated): 1 and r, or 0. A line that is not well formed is +// passed over, and the lines after it are still read. +static inline int rtss_saved_find(const char *text, const char *name, RtssSaved *r) { + for (const char *p = text; p && *p;) { + const char *end = strchr(p, '\n'); + size_t len = end ? (size_t)(end - p) : strlen(p); + if (*p != '#' && rtss_saved_parse(p, len, r) && rtss_name_eq(r->name, name)) return 1; + p = end ? end + 1 : NULL; + } + return 0; +} + +// The file's text with profile name's line replaced by r's (r NULL: removed): the header, every other profile's +// well-formed line as it was, then r's. Comments, blank and malformed lines are dropped. Writes the text into out and +// returns its length (0 when it does not fit cap); *records is the number of profile lines in it, 0 when the file is +// to go. +static inline size_t rtss_saved_update(const char *text, const char *name, const RtssSaved *r, char *out, size_t cap, + int *records) { + size_t at = (size_t)snprintf(out, cap, "%s", RTSS_SAVED_HEADER); + *records = 0; + if (at >= cap) return 0; + for (const char *p = text; p && *p;) { + const char *end = strchr(p, '\n'); + size_t len = end ? (size_t)(end - p) : strlen(p); + RtssSaved other; + if (*p != '#' && rtss_saved_parse(p, len, &other) && !rtss_name_eq(other.name, name)) { + size_t k = rtss_saved_format(out + at, cap - at, &other); + if (!k) return 0; + at += k, ++*records; + } + p = end ? end + 1 : NULL; + } + if (r) { + size_t k = rtss_saved_format(out + at, cap - at, r); + if (!k) return 0; + at += k, ++*records; + } + return at; +} + +// What a start does with a line a run that did not close left (r), given the profile as it is now: whether it exists +// and the limit and denominator it holds. +enum { + RTSS_PLAN_NONE, // no profile, and there was none before that run: as it was + RTSS_PLAN_LEAVE, // the player changed or removed the profile since: theirs as it is + RTSS_PLAN_WRITE, // it still holds what the run wrote: the old limit and denominator back + RTSS_PLAN_DELETE, // the same, for a profile that run made: removed +}; +static inline int rtss_restore_plan(const RtssSaved *r, int exists, uint32_t limit, uint32_t den) { + if (!exists) return r->existed ? RTSS_PLAN_LEAVE : RTSS_PLAN_NONE; + if (limit != r->limit || (den ? den : 1) != r->den) return RTSS_PLAN_LEAVE; + return r->existed ? RTSS_PLAN_WRITE : RTSS_PLAN_DELETE; +} + // ---- RTSS's own settings // RTSS's own [Framerate] settings for the game, as its profile file holds them (rtss_limit.c read_settings): a frame // rate limit (Limit over LimitDenominator), a frame time limit (LimitTime over LimitTimeDenominator) and scanline sync diff --git a/runtime/rtss_limit.c b/runtime/rtss_limit.c index b62f9310..83e27f66 100644 --- a/runtime/rtss_limit.c +++ b/runtime/rtss_limit.c @@ -22,9 +22,12 @@ // 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. An exit that skips it (a kill, the device-lost watchdog, a crash -// inside the crash handler) leaves the game's limit in the profile, and the next run takes that limit for the player's -// own. +// 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. // // 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. @@ -34,6 +37,7 @@ #include #include #include +#include #include #include "host.h" @@ -70,6 +74,11 @@ 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 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) { @@ -150,8 +159,48 @@ static int write_limit(DWORD limit, DWORD denominator) { 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); +} + +// rec's written value is limit/denominator now: the line follows it, so that the next start tells the game's value from +// a value the player set since. +static void record(DWORD limit, DWORD denominator) { + if (!atomic_load(&recorded) || (rec.limit == limit && rec.den == denominator)) return; + rec.limit = limit, rec.den = denominator; + 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; clear: on a refusal, clear the limit of this run the profile holds. +// 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) { // what to put back on exit @@ -166,10 +215,17 @@ static int apply(void *ctx, int64_t mhz, int first, int clear) { if (mhz % 1000 == 0) limit = (DWORD)(mhz / 1000); else if (has_denominator) limit = (DWORD)mhz, denominator = 1000; else limit = (DWORD)((mhz + 500) / 1000); + if (first) { // the line goes in before the profile changes: a kill between the two finds it as it was + rec = (RtssSaved){had_profile, old_limit, old_denominator, limit, denominator, ""}; + snprintf(rec.name, sizeof rec.name, "%s", profile); + if (state_put(&rec)) atomic_store(&recorded, 1); + else unrecorded(); + } int r = write_limit(limit, denominator); if (r < 0) { 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 && atomic_exchange(&recorded, 0)) state_put(NULL); // the profile is as it was return RTSS_WROTE_READONLY; } if (first) { @@ -183,11 +239,13 @@ static int apply(void *ctx, int64_t mhz, int first, int clear) { 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); if (clear && write_limit(0, 1) > 0) { + record(0, 1); update_profiles(); return RTSS_WROTE_REFUSED_CLEARED; } return RTSS_WROTE_REFUSED; } + record(limit, denominator); 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) @@ -198,8 +256,10 @@ static int apply(void *ctx, int64_t mhz, int first, int clear) { return RTSS_WROTE_HELD; } -// The profile as it was before this run changed it (the caller checked S.changed, and marks it done). +// The profile as it was before this run changed it (the caller checked S.changed, and marks it done). Its line in the +// state file goes once it is back; one that did not take its old limit keeps it for the next start. static void put_back(void) { + int back = 1; if (!had_profile) { delete_profile(profile); rt_log("frame rate: RivaTuner Statistics Server profile %s removed, as before the game started " @@ -209,8 +269,54 @@ static void put_back(void) { 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 && 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) { + 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); + free(text); + if (!found) return; + 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) { + 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, + state_path); + state_off = 1; + return; + } + if (plan == RTSS_PLAN_DELETE) delete_profile(profile); + 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); + 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); + 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; + } } static void put_back_op(void *ctx, const RtssOut *o) { // rtss_poll's RtssOps.put_back @@ -339,6 +445,9 @@ static int find_rtss(void) { 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); @@ -351,9 +460,10 @@ static int find_rtss(void) { 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; + int was_running = -1, was_listed = 0, left_checked = 0; const RtssOps ops = {NULL, apply, put_back_op}; for (;; plat_sleep_ms(POLL_MS)) { int64_t w = atomic_load(&want); @@ -372,6 +482,7 @@ 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(); in.current = atomic_load(&want) == w; RtssOut o = rtss_poll(&S, &in, &ops); atomic_store(&failed, S.fail); @@ -432,10 +543,23 @@ void rtss_limit_restore(void) { } } +// 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 diff --git a/tests/fatal_exit_check.c b/tests/fatal_exit_check.c index 7b8639da..c223efd4 100644 --- a/tests/fatal_exit_check.c +++ b/tests/fatal_exit_check.c @@ -8,7 +8,7 @@ // fatal_exit_check fatal_crash a fault on another thread while rt_fatal reports: its FATAL line alone, exit code 70 // fatal_exit_check crash_fatal rt_fatal on another thread while a crash is reported: the whole report, exit code 71 // fatal_exit_check fatal_rumble rt_fatal while the HD rumble stop hangs (SDL's lock held): exit code 70 all the same -// fatal_exit_check fatal_rtss rt_fatal on RTSS's thread ("rtss"): exit code 70, no RTSS restore from there +// fatal_exit_check fatal_rtss rt_fatal on RTSS's thread ("rtss"): exit code 70, through rtss_limit_crash too // // The crash handler here is a stand-in for main.c crash_handler with its first line (the static check in // tests/fatal_exit_check.py holds main.c to it): it writes three report lines 100 ms apart and ends with plat_exit(71). @@ -59,7 +59,7 @@ void rt_log(const char *fmt, ...) { } void kern_free_cores_note(void) {} void prof_sampler_write(void) {} -void rtss_limit_restore(void) { say("rtss: restored\n"); } +void rtss_limit_crash(void) { say("rtss: left to the next start\n"); } // rtss_limit.c: no call into RTSS void kern_checkpoint(void) {} // perf.c's summary at a fatal error, which dispatch.c calls through a weak symbol diff --git a/tests/fatal_exit_check.py b/tests/fatal_exit_check.py index 3b97f8bc..725d77be 100644 --- a/tests/fatal_exit_check.py +++ b/tests/fatal_exit_check.py @@ -5,8 +5,9 @@ checks statically that no runtime file outside the platform layer ends the proce error's end and the lost device's (runtime/vk_device.c lost_wait_exit) end with plat_terminate. No game files, no GPU. A fatal error stops HD rumble on a thread of its own and waits for it 300 ms at most (runtime/dispatch.c -rt_rumble_stop_bounded), so a stop that hangs on SDL's lock does not hold the end. A fatal error on RTSS's own thread -("rtss") skips RivaTuner's restore, whose lock that thread may hold. +rt_rumble_stop_bounded), so a stop that hangs on SDL's lock does not hold the end. RivaTuner's profile is left to the +next start (runtime/rtss_limit.c rtss_limit_crash), which on RTSS's own thread ("rtss"), whose lock that thread may +hold, makes no call into RTSS's library and takes no lock. Two failures at once end once: two threads that fault, a fault while a fatal error is reported and a fatal error while a crash is: the first report is written whole, the process ends with its code, and the second adds no line @@ -86,13 +87,13 @@ def static_checks(): print(f'static: perf.c perf_summary_flush_try tries the stutter map\'s lock and leaves the back end out: ' f'{"ok" if ok else "FAIL"}', flush=True) bad += not ok - # both failed ends stop HD rumble through the bounded stop, after RivaTuner's restore + # both failed ends stop HD rumble through the bounded stop, after RivaTuner's profile is left to the next start fatal = re.sub(r'//[^\n]*', '', function((RT / 'dispatch.c').read_text(encoding='utf-8'), 'static void fatal_end(int code, const char *fmt, va_list ap) {')) for name, code in (('dispatch.c fatal_end', fatal), ('main.c crash_handler', body)): - r, s = code.find('rtss_limit_restore('), code.find('rt_rumble_stop_bounded(') + r, s = code.find('rtss_limit_crash('), code.find('rt_rumble_stop_bounded(') ok = 0 <= r < s and 'input_rumble_hd_stop(' not in code - print(f'static: {name} stops HD rumble through rt_rumble_stop_bounded after the RTSS restore: ' + print(f'static: {name} stops HD rumble through rt_rumble_stop_bounded after rtss_limit_crash: ' f'{"ok" if ok else "FAIL"}', flush=True) bad += not ok # a first report that hangs: the thread that waits for it ends the process after 30 s with that report's code, and @@ -104,10 +105,10 @@ def static_checks(): print(f'static: dispatch.c rt_ending_enter ends a hung first report with that report\'s code: ' f'{"ok" if ok else "FAIL"}', flush=True) bad += not ok - # the fatal error's frame-time summary comes after the RTSS restore and the rumble stop, as in the orderly end + # the fatal error's frame-time summary comes after RTSS's end and the rumble stop, as in the orderly end s, p = fatal.find('rt_rumble_stop_bounded('), fatal.find('perf_summary_flush_try(') ok = 0 <= s < p - print(f'static: dispatch.c fatal_end writes the frame-time summary after the RTSS restore and the rumble stop: ' + print(f'static: dispatch.c fatal_end writes the frame-time summary after RTSS\'s end and the rumble stop: ' f'{"ok" if ok else "FAIL"}', flush=True) bad += not ok return bad @@ -115,10 +116,11 @@ def static_checks(): CASES = [ # mode, exit code, lines that must be there, lines that must not - ('fatal', 70, ['FATAL: the test\'s fatal error 1', 'frametime: at a fatal error', 'rumble: stopped', 'rtss: restored'], + ('fatal', 70, ['FATAL: the test\'s fatal error 1', 'frametime: at a fatal error', 'rumble: stopped', + 'rtss: left to the next start'], ['detach:']), + # rt_fatal on RTSS's own thread: rtss_limit_crash runs from there too, and takes no lock there + ('fatal_rtss', 70, ['FATAL: the test\'s fatal error 3', 'rumble: stopped', 'rtss: left to the next start'], ['detach:']), - # rt_fatal on RTSS's own thread, which may hold the lock the restore tries: no restore from there - ('fatal_rtss', 70, ['FATAL: the test\'s fatal error 3', 'rumble: stopped'], ['rtss: restored', 'detach:']), # the rumble stop hangs (SDL's lock held by a thread that crashed): the end goes on after 300 ms ('fatal_rumble', 70, ['FATAL: the test\'s fatal error 1'], ['rumble: stopped', 'detach:']), # two faults at once: the first report whole, the second thread waits (dispatch.c rt_ending_enter) diff --git a/tests/rtss_limit_check.c b/tests/rtss_limit_check.c index 11203d58..0c74c279 100644 --- a/tests/rtss_limit_check.c +++ b/tests/rtss_limit_check.c @@ -419,6 +419,67 @@ static void pace_period(void) { CHECK(rtss_pace_period(1, 60, now, now - 10000000, p60, ceiling) == p60, "a movie: its period and slots"); } +// The state file (rtss-profile.txt): its lines, the file's text as the worker rewrites it, and what a start does with a +// line that a run which did not close left behind. +static void saved(void) { + RtssSaved a = {0, 0, 1, 60, 1, "ocarina.exe"}, r; + char line[400], out[2048]; + size_t n = rtss_saved_format(line, sizeof line, &a); + CHECK(n == strlen(line) && !strcmp(line, "rtss1 0 0 1 60 1 ocarina.exe\n"), "the line: %s", line); + CHECK(rtss_saved_parse(line, n - 1, &r) && !r.existed && !r.old_limit && r.old_den == 1 && r.limit == 60 && + r.den == 1 && !strcmp(r.name, "ocarina.exe"), + "read back"); + const char *spaced = "rtss1 1 59940 1000 144 1 My Game.exe\r"; + CHECK(rtss_saved_parse(spaced, strlen(spaced), &r) && r.existed && r.old_limit == 59940 && r.old_den == 1000 && + !strcmp(r.name, "My Game.exe"), + "a name with a space, a line ending in CR"); + static const char *bad[] = {"rtss1 2 0 1 60 1 x.exe", "rtss1 0 0 0 60 1 x.exe", "rtss1 0 0 1 60 0 x.exe", + "rtss1 0 0 1 60 1", "rtss1 0 0 1 60 1 ", "rtss2 0 0 1 60 1 x.exe", "rtss1 0 0 1 x 1 x.exe", + "", "# rtss1 0 0 1 60 1 x.exe"}; + for (unsigned i = 0; i < sizeof bad / sizeof *bad; i++) + CHECK(!rtss_saved_parse(bad[i], strlen(bad[i]), &r), "not a line: '%s'", bad[i]); + CHECK(!rtss_saved_format(line, 20, &a), "a line that does not fit"); + + // found past a comment, a malformed line and another profile's line, whatever the case of its letters + const char *text = RTSS_SAVED_HEADER "rtss1 9 garbage\nrtss1 1 30 1 144 1 My Game.exe\nrtss1 0 0 1 60 1 OCARINA.EXE"; + CHECK(rtss_saved_find(text, "ocarina.exe", &r) && r.limit == 60 && !r.existed, "ours, the last line, no newline"); + CHECK(rtss_saved_find(text, "my game.exe", &r) && r.limit == 144 && r.old_limit == 30, "the other one"); + CHECK(!rtss_saved_find(text, "ocarina_dev.exe", &r) && !rtss_saved_find("", "ocarina.exe", &r), "none"); + + // rewritten: ours replaced, the other kept, the malformed line and comments dropped, the header first + int records = -1; + RtssSaved a2 = a; + a2.limit = 144; + n = rtss_saved_update(text, "ocarina.exe", &a2, out, sizeof out, &records); + CHECK(n == strlen(out) && records == 2 && + !strcmp(out, RTSS_SAVED_HEADER "rtss1 1 30 1 144 1 My Game.exe\nrtss1 0 0 1 144 1 ocarina.exe\n"), + "replaced:\n%s", out); + char again[2048]; + n = rtss_saved_update(out, "OCARINA.exe", NULL, again, sizeof again, &records); + CHECK(records == 1 && !strcmp(again, RTSS_SAVED_HEADER "rtss1 1 30 1 144 1 My Game.exe\n"), "removed:\n%s", again); + n = rtss_saved_update(again, "My Game.exe", NULL, out, sizeof out, &records); + CHECK(n && !records, "the last line removed: no record left, the file goes"); + n = rtss_saved_update("", "ocarina.exe", &a, out, sizeof out, &records); + CHECK(records == 1 && !strcmp(out, RTSS_SAVED_HEADER "rtss1 0 0 1 60 1 ocarina.exe\n"), "a new file:\n%s", out); + CHECK(!rtss_saved_update(text, "ocarina.exe", &a, out, sizeof RTSS_SAVED_HEADER + 10, &records), + "a text that does not fit"); + + // What a start does. Run A made the profile and wrote 60/1, and was killed: B finds 60/1 and removes the profile. + RtssSaved made = {0, 0, 1, 60, 1, "ocarina.exe"}, had = {1, 30, 1, 60, 1, "ocarina.exe"}; + CHECK(rtss_restore_plan(&made, 1, 60, 1) == RTSS_PLAN_DELETE, "the run's own profile: removed"); + CHECK(rtss_restore_plan(&made, 1, 75, 1) == RTSS_PLAN_LEAVE, "the player set 75 since: theirs"); + CHECK(rtss_restore_plan(&made, 0, 0, 1) == RTSS_PLAN_NONE, "the player removed it, or the run was killed before " + "its first write: nothing to do"); + CHECK(rtss_restore_plan(&had, 1, 60, 1) == RTSS_PLAN_WRITE, "the player's profile with 30/1 before: 30/1 back"); + CHECK(rtss_restore_plan(&had, 1, 30, 1) == RTSS_PLAN_LEAVE, "killed before the write landed: the player's 30/1"); + CHECK(rtss_restore_plan(&had, 0, 0, 1) == RTSS_PLAN_LEAVE, "the player removed their profile since: left so"); + CHECK(rtss_restore_plan(&had, 1, 60, 0) == RTSS_PLAN_WRITE, "an RTSS without the denominator reads none: 1"); + RtssSaved frac = {1, 0, 1, 59940, 1000, "ocarina.exe"}; + CHECK(rtss_restore_plan(&frac, 1, 59940, 1) == RTSS_PLAN_LEAVE && rtss_restore_plan(&frac, 1, 59940, 1000) == + RTSS_PLAN_WRITE, + "the denominator is compared too"); +} + int main(void) { settings(); reasons(); @@ -434,6 +495,7 @@ int main(void) { unset_and_library(); two(); pace_period(); + saved(); printf("rtss_limit_check: %s\n", fails ? "FAIL" : "ok"); return fails != 0; } diff --git a/tests/rtss_limit_check.py b/tests/rtss_limit_check.py index c55c62a0..643864ac 100644 --- a/tests/rtss_limit_check.py +++ b/tests/rtss_limit_check.py @@ -2,13 +2,16 @@ tests/rtss_limit_check.c builds runtime/rtss_decide.h alone, the parts that make no system call. tests/rtss_worker_check.c (Windows only, skipped elsewhere) compiles the real rtss_limit.c against a fake RTSS and runs each of its scenarios in a -process of its own. +process of its own. Checks statically that a crash and a fatal error (runtime/main.c crash_handler, runtime/dispatch.c +fatal_end) leave RTSS's profile to the next start through rtss_limit_crash, which calls into RTSS's library only +through the orderly restore and only when the state file holds no line of the run. No GPU, no game files, no RTSS, any Python. Usage: rtss_limit_check.py """ import os +import re import shutil import subprocess import sys @@ -32,13 +35,44 @@ def build(src, exe, env): str(ROOT / 'tests' / src), '-o', str(exe)], env=env) +def function(source, signature): + start = source.index(signature) + brace = source.index('{', start) + depth, end = 1, brace + 1 + while depth: + depth += (source[end] == '{') - (source[end] == '}') + end += 1 + return re.sub(r'//[^\n]*', '', source[start:end]) + + +def static_checks(): + rt = ROOT / 'runtime' + bad = 0 + for file, sig in (('main.c', 'static void crash_handler(const PlatCrash *x)'), + ('dispatch.c', 'static void fatal_end(int code, const char *fmt, va_list ap) {')): + body = function((rt / file).read_text(encoding='utf-8'), sig) + ok = 'rtss_limit_crash(' in body and 'rtss_limit_restore(' not in body + print(f'static: {file} {sig.split("(")[0].split()[-1]} leaves RTSS\'s profile to the next start ' + f'(rtss_limit_crash, no rtss_limit_restore): {"ok" if ok else "FAIL"}') + bad += not ok + crash = function((rt / 'rtss_limit.c').read_text(encoding='utf-8'), 'void rtss_limit_crash(void) {') + calls = ('load_profile', 'save_profile', 'delete_profile', 'set_property', 'get_property', 'update_profiles', + 'put_back', 'write_limit', 'plat_mutex_lock', 'plat_mutex_trylock') + gate, restore = crash.find('recorded'), crash.find('rtss_limit_restore(') + ok = (crash.find('&closed, 1)') >= 0 and not any(c + '(' in crash for c in calls) + and 0 <= gate < restore and 'rtss_tid' in crash[:restore]) + print(f'static: rtss_limit.c rtss_limit_crash closes the run and calls into RTSS only through the restore, when the ' + f'state file holds no line and not on the worker\'s thread: {"ok" if ok else "FAIL"}') + return bad + (not ok) + + def main(): + failed = ['static checks'] if static_checks() else [] work = ROOT / 'build' / 'test' / 'rtss_limit' work.mkdir(parents=True, exist_ok=True) env = dict(os.environ) if os.name == 'nt' and MSYS_BIN.exists(): env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '') - failed = [] pure = work / ('rtss_limit_check.exe' if os.name == 'nt' else 'rtss_limit_check') build('rtss_limit_check.c', pure, env) if subprocess.run([str(pure)], env=env).returncode != 0: @@ -54,12 +88,13 @@ def main(): p = subprocess.run([str(exe), name], env=env, capture_output=True, text=True, timeout=120) ok, out = p.returncode == 0, p.stdout + p.stderr except subprocess.TimeoutExpired as e: - ok, out = False, (e.stdout or b'').decode(errors='replace') + '\nTIMEOUT' + out = e.stdout if isinstance(e.stdout, str) else (e.stdout or b'').decode(errors='replace') + ok, out = False, out + '\nTIMEOUT' print(f'rtss_worker_check {name}: {"ok" if ok else "FAIL"}') if not ok: print(out) failed.append(name) - print(f'{len(names) + 1 - len(failed)} of {len(names) + 1} checks ok' + (f'; failed: {", ".join(failed)}' if failed else '')) + print(f'{len(names) + 2 - len(failed)} of {len(names) + 2} checks ok' + (f'; failed: {", ".join(failed)}' if failed else '')) return 1 if failed else 0 diff --git a/tests/rtss_worker_check.c b/tests/rtss_worker_check.c index 691990b9..477267c4 100644 --- a/tests/rtss_worker_check.c +++ b/tests/rtss_worker_check.c @@ -1,8 +1,10 @@ // The real runtime/rtss_limit.c against a fake RivaTuner Statistics Server: the Win32 calls it makes go to the fakes // below (its folder in the registry, its hooks library with the profile calls, the profile files, the shared memory // RTSSSharedMemoryV2), and its worker thread polls when a scenario says so (plat_sleep_ms on that thread waits for the -// next poll). Each scenario runs in a process of its own, since rtss_limit.c's state lasts the process. Windows only; -// built and run by tests/rtss_limit_check.py. Prints the log and each failed check, and exits 1 on any. +// next poll). Each scenario runs in a process of its own, since rtss_limit.c's state lasts the process, with the state +// file (rtss-profile.txt) in a folder of its own beside this program; the kill scenarios run two more processes of it, +// a run that is killed and the next start, which share the fake profile through a file there. Windows only; built and +// run by tests/rtss_limit_check.py. Prints the log and each failed check, and exits 1 on any. // // rtss_worker_check #include @@ -42,6 +44,7 @@ DWORD stub_GetCurrentProcessId(void); #define GetCurrentProcessId stub_GetCurrentProcessId #include "rtss_limit.c" +#undef GetModuleFileNameA // this program's own path (main), not the game's static int fails; #define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0) @@ -237,6 +240,52 @@ void rt_log(const char *fmt, ...) { static int logged(const char *text) { return strstr(logged_lines, text) != NULL; } int rt_knob(const char *name, const char *def) { (void)name, (void)def; return 0; } +// The state file next to the executable: here the scenario's folder. +static char exe_dir[MAX_PATH]; +static int state_readonly; // the state file cannot be written +static int no_exe_path; // the executable's path is unknown: no state file at all +int plat_exe_path(char *out, size_t cap) { + int k = snprintf(out, cap, "%s\\ocarina.exe", exe_dir); + return !no_exe_path && k > 0 && (size_t)k < cap; +} +uint8_t *plat_file_read(const char *path, uint64_t *size) { + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + uint8_t *b = n > 0 ? malloc((size_t)n + 1) : NULL; + if (b && fread(b, 1, (size_t)n, f) != (size_t)n) free(b), b = NULL; + fclose(f); + if (b) b[n] = 0, *size = (uint64_t)n; + return b; +} +int plat_file_write_atomic(const char *path, const void *data, size_t n) { + if (state_readonly) return 0; + FILE *f = fopen(path, "wb"); + if (!f) return 0; + int ok = fwrite(data, 1, n, f) == n; + return !fclose(f) && ok; +} +void plat_file_delete(const char *path) { remove(path); } +int plat_file_exists(const char *path) { + FILE *f = fopen(path, "rb"); + if (f) fclose(f); + return f != NULL; +} +uint32_t plat_thread_id(void) { return GetCurrentThreadId(); } +static char state_text[4096]; +static const char *state(void) { // the state file as it is now; "" when there is none + char path[MAX_PATH + 32]; + snprintf(path, sizeof path, "%s\\rtss-profile.txt", exe_dir); + uint64_t n = 0; + uint8_t *b = plat_file_read(path, &n); + snprintf(state_text, sizeof state_text, "%s", b ? (char *)b : ""); + free(b); + return state_text; +} +static int state_has(const char *line) { return strstr(state(), line) != NULL; } + static DWORD worker_tid; static HANDLE tick, done, at_lock, go; static volatile LONG pause_at_lock; // the worker's next plat_mutex_lock stops before it takes the lock @@ -279,6 +328,23 @@ void plat_sleep_ms(uint32_t ms) { WaitForSingleObject(tick, INFINITE); // the next one starts when the scenario says } +// The fake profile kept across the processes of a kill scenario. +static void prof_save(void) { + char path[MAX_PATH + 16]; + snprintf(path, sizeof path, "%s\\own.bin", exe_dir); + FILE *f = fopen(path, "wb"); + if (f) fwrite(&own, sizeof own, 1, f), fclose(f); +} +static void prof_load(void) { + char path[MAX_PATH + 16]; + snprintf(path, sizeof path, "%s\\own.bin", exe_dir); + FILE *f = fopen(path, "rb"); + if (f) { + if (fread(&own, sizeof own, 1, f) != 1) memset(&own, 0, sizeof own); + fclose(f); + } +} + // ---- the scenarios' steps static int wait_done(void) { return WaitForSingleObject(done, 5000) == WAIT_OBJECT_0; } static void poll(void) { // one more poll of the worker, waited for @@ -434,6 +500,7 @@ static void kept_framegen(void) { // game's limiter paces, never RTSS's hold of the limit before (the worker's store for a want that changed after it // read it must not count for the new one). static void active_race(void) { + no_exe_path = 1; // 400000 writes and removals of the state file would take minutes app_set(1, 0xA, 1000, 2000, 60); ask(60000); poll(); @@ -726,6 +793,147 @@ static void churn_run(void) { CHECK(rtss_limit_active(), "RTSS limits after the last want of a limit"); } +// The state file: the run's first write records the profile as it was, each later one the value written, and the exit's +// restore removes the line and the file with it. +static void state_record(void) { + app_set(1, 0xA, 1000, 2000, 60); + ask(60000); + poll(); + show("playing at 60"); + CHECK(own.exists && own.limit == 60 && state_has("\nrtss1 0 0 1 60 1 ocarina.exe\n"), "recorded:\n%s", state()); + CHECK(!strncmp(state(), "# RivaTuner Statistics Server's profiles", 40), "the header first"); + ask(144000); + poll(); + CHECK(own.limit == 144 && state_has("\nrtss1 0 0 1 144 1 ocarina.exe\n"), "the line follows:\n%s", state()); + rtss_limit_restore(); + show("after the restore"); + CHECK(!own.exists && !*state(), "the profile and the state file gone:\n%s", state()); +} +static void state_player(void) { + own = (Prof){1, 30, 1, 0, 0, 0, 0, 0, 0, 0, 1}; + app_set(1, 0xA, 1000, 2000, 60); + ask(60000); + poll(); + CHECK(own.limit == 60 && state_has("\nrtss1 1 30 1 60 1 ocarina.exe\n"), "recorded:\n%s", state()); + ask(RTSS_OUT_LOWLAT); // kept out: put back during the run, and the line goes with it + poll(); + CHECK(own.limit == 30 && !*state(), "put back, no line:\n%s", state()); + ask(60000); + poll(); + CHECK(own.limit == 60 && state_has("\nrtss1 1 30 1 60 1 ocarina.exe\n"), "recorded again:\n%s", state()); + rtss_limit_restore(); + CHECK(own.exists && own.limit == 30 && !*state(), "the player's 30 back, no line"); +} + +// The state file cannot be written: the run goes on as before it, with a line in the log, and a crash puts the profile +// back as an orderly exit does. +static void state_unwritable(void) { + state_readonly = 1; + app_set(1, 0xA, 1000, 2000, 60); + ask(60000); + poll(); + show("no state file"); + CHECK(own.exists && own.limit == 60 && rtss_limit_active(), "RTSS limits as before"); + CHECK(logged("rtss-profile.txt could not be written; a crash would leave the game's limit in RivaTuner Statistics " + "Server's profile ocarina.exe"), + "logged"); + rtss_limit_crash(); + show("a crash"); + CHECK(!own.exists && !rtss_limit_active(), "the crash put the profile back"); +} + +// A crash with the line in the state file: no call into RTSS's library, the profile and the line stay for the next +// start, and nothing after it reaches the profile. +static void crash_recorded(void) { + app_set(1, 0xA, 1000, 2000, 60); + ask(60000); + poll(); + long s0 = saves, d0 = deletes, u0 = updates; + rtss_limit_crash(); + show("a crash"); + CHECK(saves == s0 && deletes == d0 && updates == u0, "no profile call"); + CHECK(own.exists && own.limit == 60 && state_has("\nrtss1 0 0 1 60 1 ocarina.exe\n"), "left for the next start"); + CHECK(!rtss_limit_active() && !rtss_limit_state(), "the game's limiter"); + ask(144000); + poll(); + CHECK(saves == s0 && own.limit == 60, "a want after it changes nothing"); +} + +// 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. +static int variant; +static void kill_first(void) { + prof_load(); + app_set(1, 0xA, 1000, 2000, 60); + ask(60000); + poll(); + show("the run before the kill"); + CHECK(own.exists && own.limit == 60 && state_has(variant == 1 ? "\nrtss1 1 30 1 60 1 ocarina.exe\n" + : "\nrtss1 0 0 1 60 1 ocarina.exe\n"), + "recorded:\n%s", state()); + prof_save(); // killed: no restore +} +static void kill_next(void) { + prof_load(); + app_set(1, 0xA, 1000, 2000, 144); + ask(144000); + poll(); + show("the next start"); + if (variant == 0) { + CHECK(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"); + CHECK(logged("created; it goes away when the game closes (creation 1 this run)"), "made again by this run"); + CHECK(state_has("\nrtss1 0 0 1 144 1 ocarina.exe\n"), "this run's line:\n%s", state()); + } else if (variant == 1) { + CHECK(logged("profile ocarina.exe had the game's limit of 60/1 from a run that did not close") && + logged("; its limit of 30/1 is back"), + "the player's 30 back"); + CHECK(logged("held a limit of 30/1; it gets that back"), "this run takes 30 for the player's"); + CHECK(state_has("\nrtss1 1 30 1 144 1 ocarina.exe\n"), "this run's line:\n%s", state()); + } else { + CHECK(logged("profile ocarina.exe changed since a run that did not close") && logged("; left as it is"), + "the player's 75 left"); + CHECK(state_has("\nrtss1 1 75 1 144 1 ocarina.exe\n"), "this run's line:\n%s", state()); + } + CHECK(own.exists && own.limit == 144, "this run's limit"); + rtss_limit_restore(); + show("closed"); + CHECK(!*state(), "no line left:\n%s", state()); + prof_save(); +} +static int run_phase(const char *phase) { + char exe[MAX_PATH], cmd[3 * MAX_PATH]; + GetModuleFileNameA(NULL, exe, sizeof exe); + snprintf(cmd, sizeof cmd, "\"%s\" --phase %s %d \"%s\"", exe, phase, variant, exe_dir); + STARTUPINFOA si = {sizeof si}; + PROCESS_INFORMATION pi; + if (!CreateProcessA(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) return -1; + WaitForSingleObject(pi.hProcess, 60000); + DWORD code = 1; + GetExitCodeProcess(pi.hProcess, &code); + return (int)code; +} +static void kill_restart_as(int v) { + variant = v; + if (v == 1) own = (Prof){1, 30, 1, 0, 0, 0, 0, 0, 0, 0, 1}; + prof_save(); + CHECK(run_phase("kill_first") == 0, "the killed run"); + prof_load(); + if (v == 2) own.limit = 75, touch(&own), prof_save(); + CHECK(run_phase("kill_next") == 0, "the next start"); + prof_load(); + show("after the next start closed"); + 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()); +} +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); } + // 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. static void movie_run(void) { @@ -790,6 +998,13 @@ static const struct { const char *name; void (*run)(void); } scenarios[] = { {"churn_run", churn_run}, {"movie_run", movie_run}, {"movie_low_run", movie_low_run}, + {"state_record", state_record}, + {"state_player", state_player}, + {"state_unwritable", state_unwritable}, + {"crash_recorded", crash_recorded}, + {"kill_restart", kill_restart}, + {"kill_restart_player", kill_restart_player}, + {"kill_restart_edited", kill_restart_edited}, }; int main(int argc, char **argv) { @@ -800,9 +1015,30 @@ int main(int argc, char **argv) { for (size_t i = 0; i < sizeof scenarios / sizeof *scenarios; i++) printf("%s\n", scenarios[i].name); return 0; } + if (argc == 5 && !strcmp(argv[1], "--phase")) { // a process of a kill scenario, in that scenario's folder + variant = atoi(argv[3]); + snprintf(exe_dir, sizeof exe_dir, "%s", argv[4]); + printf(" phase %s\n", argv[2]); + if (!strcmp(argv[2], "kill_first")) kill_first(); + else if (!strcmp(argv[2], "kill_next")) kill_next(); + else return 2; + return fails != 0; + } for (size_t i = 0; argc == 2 && i < sizeof scenarios / sizeof *scenarios; i++) { if (strcmp(argv[1], scenarios[i].name)) continue; printf("scenario %s\n", argv[1]); + char exe[MAX_PATH], *cut; // the scenario's folder, emptied: rtss_state\ beside this program + GetModuleFileNameA(NULL, exe, sizeof exe); + if ((cut = strrchr(exe, '\\'))) *cut = 0; + snprintf(exe_dir, sizeof exe_dir, "%s\\rtss_state", exe); + CreateDirectoryA(exe_dir, NULL); + snprintf(exe_dir, sizeof exe_dir, "%s\\rtss_state\\%s", exe, argv[1]); + CreateDirectoryA(exe_dir, NULL); + char path[MAX_PATH + 32]; + snprintf(path, sizeof path, "%s\\rtss-profile.txt", exe_dir); + remove(path); + snprintf(path, sizeof path, "%s\\own.bin", exe_dir); + remove(path); scenarios[i].run(); return fails != 0; }