Keep RivaTuner Statistics Server's profile as it was in rtss-profile.txt, so a start after a kill or crash puts it back, and leave it to that start at a crash.

An exit that skipped rtss_limit_restore (Task Manager, a hang, lost_wait_exit, a kill during the pipeline save) left
the game's limit in Profiles\<exe>.cfg, and the next run's first apply took it for the player's own: that run then
put the stale limit back at its close, and a profile the game had made stayed for good (lane A's probe: kill1, then
kill2 'held a limit of 60/1'). The crash handler and the fatal error's end also called into RTSSHooks64.dll from a
process that had failed (P-13).

rtss_limit.c now keeps one line per profile in rtss-profile.txt next to the executable (rtss_decide.h RtssSaved:
whether the profile existed, its old limit and denominator, the value the run wrote), written atomically before the
run's first change of the profile, updated with each value written, and removed once the profile is back (put_back;
also when the first write did not stick). On the first poll with the hooks library a start that finds a line of its
profile puts the profile back first (restore_left, rtss_restore_plan): the old limit, or no profile, unless the player
changed or removed it since, which the written value tells. main.c crash_handler and dispatch.c fatal_end call the
new rtss_limit_crash, which closes the run and leaves the profile to that start with no call into RTSS's library; only
when the file could not be written does it put the profile back as before, and never from the worker's own thread. A
file that cannot be written leaves the run as it was, with one line in the log. Profiles frozen by earlier versions
cannot be told from the player's own.

Not reproduced before the change. rtss_limit_check (T12: the line's format and parse, the file's rewrite, each plan;
the static check that both crash paths go through rtss_limit_crash), rtss_worker_check (state_record, state_player,
state_unwritable, crash_recorded, and kill_restart with its player and edited variants, each a killed run and the next
start in processes of their own), fatal_exit_check and exit_order_check pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 19:08:10 -05:00
parent 208a10718d
commit 04f4d44747
10 changed files with 602 additions and 38 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 <existed 0|1> <old limit> <old denominator> <written limit> <written denominator> <profile name>
// 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

View File

@@ -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 <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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

View File

@@ -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

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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 <scenario>
#include <windows.h>
@@ -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\<scenario> 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;
}