Keep the state file's line ahead of RivaTuner Statistics Server's profile, and keep what a run owes the profile after a put back that did not land.

The review found three ways rtss-profile.txt could fall behind the profile. Every write after a run's first one saved
the profile before the line, so a kill between the two left a line whose value the profile no longer held, and the
next start took the game's limit for the player's (rtss_restore_plan LEAVE). A put back whose write did not land kept
the line, but rtss_put_back_done cleared S.changed, so a later limit took the run's first-write path again and read the
game's own limit from the profile as the value to put back, overwriting the true one in the line. And put_back removed
the line after DeleteProfile without checking that the profile went away.

The line now carries the value the run wrote before the newest one (rtss_decide.h RtssSaved prev_limit and prev_den,
rtss1 with two more fields; the file came with this branch and was never released). rtss_limit.c apply writes the new
value into the line before the profile (record_before) and drops the previous one once the profile holds the new one,
or returns to it when RTSS refused it (record_after), and rtss_restore_plan takes a profile holding either value for
the game's. A new owed flag keeps had_profile, old_limit and old_denominator from the run's first write until a put
back lands: a later first write keeps them and the line's values, a failed first write leaves the line alone, and
rtss_limit_restore tries the put back again at the exit. put_back and restore_left check that a deleted profile is
gone before they remove the line, and restore_left's lines name the value the profile held. tests/rtss_limit_check.c
covers the new fields and the plan for either value; tests/rtss_worker_check.c checks the line before and after a
write (state_record), a run killed inside the save of its second limit and the next start removing the profile
(kill_restart_mid_write), a put back that did not land followed by a limit and the exit (put_back_owed), and a removal
that did not land (put_back_undeleted). Lines over 120 columns from the earlier fixes are wrapped.

Not reproduced before the change. Checks that passed: tests/rtss_limit_check.py (44 of 44), tests/exit_order_check.py,
tests/fatal_exit_check.py, pass_cc_check.py --wall on rtss_limit.c, tools/check_content.py --staged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 19:52:29 -05:00
parent 1744681f1a
commit da822abec3
5 changed files with 194 additions and 74 deletions

View File

@@ -469,8 +469,8 @@ void rtss_limit_want(int64_t mhz); // the pacer's limit i
void rtss_limit_movie(int hz); // the rate of a movie that plays, 0 none: the pacer holds it
int rtss_limit_active(void); // RTSS limits to the wanted rate: the pacer keeps the ceiling
int rtss_limit_state(void); // menu: 1 RTSS limits, 2 its profile is read-only, 3 it let the
// game run past its limit or never showed it holds it, 4 kept
// out while a limit set in RTSS itself holds the game too
// game run past its limit or never showed it holds it, 4
// kept out while a limit set in RTSS 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)

View File

@@ -341,25 +341,29 @@ static inline uint64_t rtss_pace_period(int holds, int movie, uint64_t now_ns, u
// ---- 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.
// rtss1 <existed 0|1> <old limit> <old denominator> <written limit> <written denominator> <previous limit>
// <previous denominator> <profile name>
// on one line. The name comes last and may hold spaces; lines that start with # are comments. The line takes a new
// value before the profile does, and the value written before it stays as the previous one until the profile holds the
// new one: a run that ends between the two leaves either value in the profile, and both are the game's.
#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"
"# limit and denominator, the game's limit and denominator, the game's previous ones, profile. The game puts a\n" \
"# profile back and removes its 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
uint32_t prev_limit, prev_den; // what it wrote before, while the profile may still hold it; else limit and den
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);
int k = snprintf(out, n, "rtss1 %d %u %u %u %u %u %u %s\n", r->existed ? 1 : 0, r->old_limit, r->old_den,
r->limit, r->den, r->prev_limit, r->prev_den, r->name);
return k > 0 && (size_t)k < n ? (size_t)k : 0;
}
@@ -371,11 +375,14 @@ static inline int rtss_saved_parse(const char *line, size_t len, RtssSaved *r) {
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;
unsigned v[6];
if (sscanf(buf, "rtss1 %d %u %u %u %u %u %u %n", &existed, &v[0], &v[1], &v[2], &v[3], &v[4], &v[5], &at) != 7 ||
!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;
if ((existed != 0 && existed != 1) || !v[1] || !v[3] || !v[5] || !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];
r->prev_limit = v[4], r->prev_den = v[5];
memcpy(r->name, buf + at, name + 1);
return 1;
}
@@ -430,7 +437,7 @@ static inline size_t rtss_saved_update(const char *text, const char *name, const
}
// 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.
// and the limit and denominator it holds. Either value of the line is the game's (RtssSaved.prev_limit).
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
@@ -439,7 +446,8 @@ enum {
};
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;
den = den ? den : 1;
if ((limit != r->limit || den != r->den) && (limit != r->prev_limit || den != r->prev_den)) return RTSS_PLAN_LEAVE;
return r->existed ? RTSS_PLAN_WRITE : RTSS_PLAN_DELETE;
}

View File

@@ -80,6 +80,7 @@ static RtssState S = RTSS_STATE_INIT; // the worker's decisions (rtss_decide.h
static int has_denominator; // this RTSS knows FramerateLimitDenominator
static int had_profile; // the profile existed before this run changed it
static DWORD old_limit, old_denominator = 1;
static int owed; // this run changed the profile and has not put it back: the three above stay as found
static int creations, removals; // the profile's "created" and "removed" lines this run, numbered (under lock)
static char state_path[MAX_PATH + 32]; // rtss-profile.txt next to the executable; "" when its path is unknown
static RtssSaved rec; // this run's line in it, while the profile holds a value of this run
@@ -197,20 +198,30 @@ static void unrecorded(void) {
"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) {
// Before a write of limit/denominator into the profile: rec's written value is that one, and the one written before it
// stays as the previous one (rtss_decide.h RtssSaved), so that a run that ends before the profile changed, or after,
// leaves a profile the next start tells from a value the player set since.
static void record_before(DWORD limit, DWORD denominator) {
if (!atomic_load(&recorded) || (rec.limit == limit && rec.den == denominator)) return;
rec.prev_limit = rec.limit, rec.prev_den = rec.den;
rec.limit = limit, rec.den = denominator;
if (!state_put(&rec)) unrecorded();
}
// After that write: the profile holds the written value alone (landed), or kept the previous one.
static void record_after(int landed) {
if (!atomic_load(&recorded) || (rec.limit == rec.prev_limit && rec.den == rec.prev_den)) return;
if (landed) rec.prev_limit = rec.limit, rec.prev_den = rec.den;
else rec.limit = rec.prev_limit, rec.den = rec.prev_den;
if (!state_put(&rec)) unrecorded();
}
// Writes mhz into the profile (rtss_poll's RtssOps.apply): RTSS_WROTE_*. first: the run's first write, which records
// what to put back on exit, in the state file too before anything changes; clear: on a refusal, clear the limit of this
// run the profile holds.
static int apply(void *ctx, int64_t mhz, int first, int clear) {
(void)ctx;
if (first) { // what to put back on exit
if (first && !owed) { // what to put back on exit; after a put back that did not land, the profile still owes it
had_profile = profile_exists();
load_profile(profile);
old_limit = 0, old_denominator = 1;
@@ -222,18 +233,24 @@ static int apply(void *ctx, int64_t mhz, int first, int clear) {
rtss_limit_value(mhz, has_denominator, &l, &d);
DWORD limit = l, denominator = d;
if (first) { // the line goes in before the profile changes: a kill between the two finds it as it was
rec = (RtssSaved){had_profile, old_limit, old_denominator, limit, denominator, ""};
RtssSaved was = rec; // still in the file after a put back that did not land: the profile holds its value
int kept = atomic_load(&recorded);
rec = (RtssSaved){had_profile, old_limit, old_denominator, limit, denominator, kept ? was.limit : limit,
kept ? was.den : denominator, ""};
snprintf(rec.name, sizeof rec.name, "%s", profile);
if (state_put(&rec)) atomic_store(&recorded, 1);
else unrecorded();
} else {
record_before(limit, denominator);
}
int r = write_limit(limit, denominator);
if (r < 0) {
if (r < 0) { // the line keeps both values: the profile may hold either
rt_log("frame rate: RivaTuner Statistics Server's profile %s did not keep the limit: its Profiles folder needs the "
"game run as administrator", profile);
if (first && atomic_exchange(&recorded, 0)) state_put(NULL); // the profile is as it was
if (first && !owed && atomic_exchange(&recorded, 0)) state_put(NULL); // the profile is as it was
return RTSS_WROTE_READONLY;
}
owed = 1;
if (first) {
if (!had_profile) // numbered: a want that flips between a limit and kept out creates it again at each flip
rt_log("frame rate: RivaTuner Statistics Server profile %s created; it goes away when the game closes "
@@ -244,14 +261,16 @@ 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_after(0);
if (!clear) return RTSS_WROTE_REFUSED;
record_before(0, 1);
int cleared = write_limit(0, 1) > 0;
record_after(cleared);
if (!cleared) return RTSS_WROTE_REFUSED;
update_profiles();
return RTSS_WROTE_REFUSED_CLEARED;
}
record(limit, denominator);
record_after(1);
update_profiles();
if (!mhz) rt_log("frame rate: RivaTuner Statistics Server's profile %s has no limit", profile);
else if ((int64_t)limit * 1000 == mhz * denominator)
@@ -262,14 +281,19 @@ 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). 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.
// The profile as it was before this run changed it (the caller checked S.changed or owed, and marks S done). Its line
// in the state file goes once it is back; one that did not go back keeps the line, and owed, for the exit and the next
// start.
static void put_back(void) {
int back = 1;
if (!had_profile) {
delete_profile(profile);
rt_log("frame rate: RivaTuner Statistics Server profile %s removed, as before the game started "
"(removal %d this run)", profile, ++removals);
back = !profile_exists();
if (back)
rt_log("frame rate: RivaTuner Statistics Server profile %s removed, as before the game started "
"(removal %d this run)", profile, ++removals);
else
rt_log("frame rate: RivaTuner Statistics Server profile %s did not go away", profile);
} else if (write_limit(old_limit, old_denominator) > 0) {
rt_log("frame rate: RivaTuner Statistics Server profile %s has its limit of %lu/%lu back", profile, old_limit,
old_denominator);
@@ -278,6 +302,7 @@ static void put_back(void) {
back = 0;
}
update_profiles();
if (back) owed = 0;
if (back && atomic_exchange(&recorded, 0)) state_put(NULL);
}
@@ -292,30 +317,30 @@ static int left_line(RtssSaved *r) {
// The line r of this profile, left in the state file by a run that did not close (left_start): before this run changes
// anything, the profile gets back what it held before that run (rtss_restore_plan), unless the player changed it since,
// and the line goes. A profile that does not take its old limit keeps the line, and this run leaves the file alone.
// and the line goes. A profile that does not go back keeps the line, and this run leaves the file alone.
static void restore_left(const RtssSaved *r) {
int exists = profile_exists();
load_profile(profile);
DWORD limit = 0, den = 1;
get_property("FramerateLimit", (LPBYTE)&limit, sizeof limit);
has_denominator = get_property("FramerateLimitDenominator", (LPBYTE)&den, sizeof den);
if (!den) den = 1;
int plan = rtss_restore_plan(r, exists, limit, den);
if (plan == RTSS_PLAN_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);
if (plan == RTSS_PLAN_DELETE) delete_profile(profile);
if ((plan == RTSS_PLAN_WRITE && write_limit(r->old_limit, r->old_den) <= 0) ||
(plan == RTSS_PLAN_DELETE && profile_exists())) {
rt_log("frame rate: RivaTuner Statistics Server profile %s still has the game's limit of %lu/%lu from a run "
"that did not close, and did not go back as it was; %s keeps its line", profile, limit, den, state_path);
state_off = 1;
return;
}
if (plan == RTSS_PLAN_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);
rt_log("frame rate: RivaTuner Statistics Server profile %s had the game's limit of %lu/%lu from a run that did "
"not close (%s); its limit of %u/%u is back", profile, limit, den, state_path, r->old_limit, r->old_den);
else if (plan == RTSS_PLAN_DELETE)
rt_log("frame rate: RivaTuner Statistics Server profile %s had the game's limit of %u/%u from a run that did not "
"close (%s); removed, as before that run", profile, r->limit, r->den, state_path);
rt_log("frame rate: RivaTuner Statistics Server profile %s had the game's limit of %lu/%lu from a run that did "
"not close (%s); removed, as before that run", profile, limit, den, state_path);
else if (plan == RTSS_PLAN_LEAVE)
rt_log("frame rate: RivaTuner Statistics Server profile %s changed since a run that did not close (%s); left as "
"it is", profile, state_path);
@@ -579,7 +604,7 @@ void rtss_limit_restore(void) {
// the exit may come from a crash on any thread: give up after a second rather than wait on a lock held there
for (int i = 0; i < 100; i++) {
if (plat_mutex_trylock(&lock)) {
if (dll && S.changed) put_back(), rtss_put_back_done(&S);
if (dll && (S.changed || owed)) put_back(), rtss_put_back_done(&S);
plat_mutex_unlock(&lock);
return;
}

View File

@@ -353,7 +353,8 @@ static void unproven(void) {
{0, 1, "no API seen", "RivaTuner Statistics Server lists the game but has not seen its Vulkan presents yet"},
{0x7, 1, "another API's presents",
"RivaTuner Statistics Server lists the game but has not seen its Vulkan presents yet"},
{RTSS_API_VULKAN, 0, "no period measured", "RivaTuner Statistics Server has measured no frames of the game lately"},
{RTSS_API_VULKAN, 0, "no period measured",
"RivaTuner Statistics Server has measured no frames of the game lately"},
};
Run r;
RtssOut o;
@@ -598,26 +599,30 @@ static void pace_period(void) {
// 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;
RtssSaved a = {0, 0, 1, 60, 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(n == strlen(line) && !strcmp(line, "rtss1 0 0 1 60 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"),
r.den == 1 && r.prev_limit == 60 && r.prev_den == 1 && !strcmp(r.name, "ocarina.exe"),
"read back");
const char *spaced = "rtss1 1 59940 1000 144 1 My Game.exe\r";
const char *spaced = "rtss1 1 59940 1000 144 1 60 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"),
r.limit == 144 && r.prev_limit == 60 && !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"};
static const char *bad[] = {"rtss1 2 0 1 60 1 60 1 x.exe", "rtss1 0 0 0 60 1 60 1 x.exe",
"rtss1 0 0 1 60 0 60 1 x.exe", "rtss1 0 0 1 60 1 60 0 x.exe",
"rtss1 0 0 1 60 1 60 1", "rtss1 0 0 1 60 1 60 1 ",
"rtss2 0 0 1 60 1 60 1 x.exe", "rtss1 0 0 1 x 1 60 1 x.exe",
"rtss1 0 0 1 60 1 x.exe", "",
"# rtss1 0 0 1 60 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";
const char *text = RTSS_SAVED_HEADER
"rtss1 9 garbage\nrtss1 1 30 1 144 1 144 1 My Game.exe\nrtss1 0 0 1 60 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");
@@ -628,20 +633,23 @@ static void saved(void) {
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"),
!strcmp(out, RTSS_SAVED_HEADER
"rtss1 1 30 1 144 1 144 1 My Game.exe\nrtss1 0 0 1 144 1 60 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);
CHECK(records == 1 && !strcmp(again, RTSS_SAVED_HEADER "rtss1 1 30 1 144 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(records == 1 && !strcmp(out, RTSS_SAVED_HEADER "rtss1 0 0 1 60 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"};
RtssSaved made = {0, 0, 1, 60, 1, 60, 1, "ocarina.exe"}, had = {1, 30, 1, 60, 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 "
@@ -650,10 +658,22 @@ static void saved(void) {
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"};
RtssSaved frac = {1, 0, 1, 59940, 1000, 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");
// A run that wrote 60/1 and then 144/1 ended between the line and the profile's change, or after it: the profile
// holds 60/1 or 144/1, both the game's; anything else is the player's.
RtssSaved moving = {1, 30, 1, 144, 1, 60, 1, "ocarina.exe"}, moving_made = {0, 0, 1, 144, 1, 60, 1, "ocarina.exe"};
CHECK(rtss_restore_plan(&moving, 1, 60, 1) == RTSS_PLAN_WRITE && rtss_restore_plan(&moving, 1, 144, 1) ==
RTSS_PLAN_WRITE,
"either value of the game's: 30/1 back");
CHECK(rtss_restore_plan(&moving_made, 1, 60, 1) == RTSS_PLAN_DELETE &&
rtss_restore_plan(&moving_made, 1, 144, 1) == RTSS_PLAN_DELETE,
"either value of the game's, in a profile the run made: removed");
CHECK(rtss_restore_plan(&moving, 1, 75, 1) == RTSS_PLAN_LEAVE && rtss_restore_plan(&moving, 1, 60, 1000) ==
RTSS_PLAN_LEAVE,
"another value: the player's");
}
// The profile's values for a limit (T15): whole rates as they are, a fraction over 1000 where RTSS knows the

View File

@@ -905,15 +905,21 @@ static void churn_run(void) {
// 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 char line_at_save[4096]; // the state file as SaveProfile found it (keep_line)
static void keep_line(void) { snprintf(line_at_save, sizeof line_at_save, "%s", state()); }
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(own.exists && own.limit == 60 && state_has("\nrtss1 0 0 1 60 1 60 1 ocarina.exe\n"), "recorded:\n%s",
state());
CHECK(!strncmp(state(), "# RivaTuner Statistics Server's profiles", 40), "the header first");
during_save = keep_line;
settle(144000);
CHECK(own.limit == 144 && state_has("\nrtss1 0 0 1 144 1 ocarina.exe\n"), "the line follows:\n%s", state());
CHECK(strstr(line_at_save, "\nrtss1 0 0 1 144 1 60 1 ocarina.exe\n"),
"before the profile changes, the line holds 144 and the 60 before it:\n%s", line_at_save);
CHECK(own.limit == 144 && state_has("\nrtss1 0 0 1 144 1 144 1 ocarina.exe\n"), "then 144 alone:\n%s", state());
rtss_limit_restore();
show("after the restore");
CHECK(!own.exists && !*state(), "the profile and the state file gone:\n%s", state());
@@ -923,11 +929,11 @@ static void state_player(void) {
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());
CHECK(own.limit == 60 && state_has("\nrtss1 1 30 1 60 1 60 1 ocarina.exe\n"), "recorded:\n%s", state());
settle(RTSS_OUT_LOWLAT); // kept out: put back during the run, and the line goes with it
CHECK(own.limit == 30 && !*state(), "put back, no line:\n%s", state());
settle(60000);
CHECK(own.limit == 60 && state_has("\nrtss1 1 30 1 60 1 ocarina.exe\n"), "recorded again:\n%s", state());
CHECK(own.limit == 60 && state_has("\nrtss1 1 30 1 60 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");
}
@@ -950,6 +956,48 @@ static void state_unwritable(void) {
CHECK(!own.exists && !rtss_limit_active(), "the crash put the profile back");
}
// A put back during the run that does not land (the profile went read-only): the line and the values found at the
// run's first write stay. A later limit is the run's first write again and keeps the player's 30 to put back, not the
// game's 60 the profile still holds, and the exit puts 30 back.
static void put_back_owed(void) {
own = (Prof){1, 30, 1, 0, 0, 0, 0, 0, 0, 0, 1};
app_set(1, 0xA, 1000, 2000, 60);
ask(60000);
poll();
readonly = 1;
settle(RTSS_OUT_LOWLAT);
show("kept out, the put back did not land");
CHECK(own.limit == 60 && logged("did not take its old limit back") &&
state_has("\nrtss1 1 30 1 60 1 60 1 ocarina.exe\n"),
"the line stays:\n%s", state());
readonly = 0;
settle(144000);
show("a limit again");
CHECK(own.limit == 144 && state_has("\nrtss1 1 30 1 144 1 144 1 ocarina.exe\n"), "the player's 30 still owed:\n%s",
state());
rtss_limit_restore();
show("closed");
CHECK(own.exists && own.limit == 30 && !*state(), "the exit puts the player's 30 back:\n%s", state());
}
// The same where the run made the profile: a removal that does not land is no removal; the line stays, and the exit
// removes the profile.
static void put_back_undeleted(void) {
app_set(1, 0xA, 1000, 2000, 60);
ask(60000);
poll();
readonly = 1;
settle(RTSS_OUT_LOWLAT);
show("kept out, the removal did not land");
CHECK(own.exists && logged("profile ocarina.exe did not go away") && !removed &&
state_has("\nrtss1 0 0 1 60 1 60 1 ocarina.exe\n"),
"the line stays:\n%s", state());
readonly = 0;
rtss_limit_restore();
show("closed");
CHECK(!own.exists && removed == 1 && !*state(), "the exit removes it:\n%s", state());
}
// 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) {
@@ -960,7 +1008,8 @@ static void crash_recorded(void) {
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(own.exists && own.limit == 60 && state_has("\nrtss1 0 0 1 60 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();
@@ -972,23 +1021,36 @@ static void crash_recorded(void) {
// variant 2: the player set 75 in RTSS between the two runs; variant 3: the next start has frame generation on, which
// keeps RTSS out, and still puts the profile back (the hooks library loaded for that alone) with no write of its own;
// variant 4: the next start has NX2NAVI_RTSS=0, which keeps the thread read-only: the log says what the file holds,
// and the profile and the line stay for a start with RTSS let in.
// and the profile and the line stay for a start with RTSS let in; variant 5: the run is killed inside the save of its
// second limit, after the line took 144 and before the profile did: the next start finds the 60 before it and removes
// the profile.
static int variant;
static void kill_in_save(void) {
CHECK(state_has("\nrtss1 0 0 1 144 1 60 1 ocarina.exe\n") && own.limit == 60, "the line before the profile:\n%s",
state());
prof_save();
ExitProcess(fails != 0);
}
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"),
CHECK(own.exists && own.limit == 60 && state_has(variant == 1 ? "\nrtss1 1 30 1 60 1 60 1 ocarina.exe\n"
: "\nrtss1 0 0 1 60 1 60 1 ocarina.exe\n"),
"recorded:\n%s", state());
if (variant == 5) { // killed inside the save of 144: the line has 144, the profile still 60
during_save = kill_in_save;
settle(144000);
CHECK(0, "the run went on past the kill");
}
prof_save(); // killed: no restore
}
static void kill_next(void) {
prof_load();
app_set(1, 0xA, 1000, 2000, 144);
if (variant >= 3) {
if (variant == 3 || variant == 4) {
ask(variant == 3 ? RTSS_OUT_FRAMEGEN : RTSS_OUT_SETTING);
poll();
poll();
@@ -1005,7 +1067,8 @@ static void kill_next(void) {
"profile ocarina.exe from a run that did not close; NX2NAVI_RTSS=0 leaves the "
"profile as it is"),
"read only: logged");
CHECK(own.exists && own.limit == 60 && state_has("\nrtss1 0 0 1 60 1 ocarina.exe\n"), "left:\n%s", state());
CHECK(own.exists && own.limit == 60 && state_has("\nrtss1 0 0 1 60 1 60 1 ocarina.exe\n"), "left:\n%s",
state());
}
rtss_limit_restore();
show("closed");
@@ -1015,22 +1078,22 @@ static void kill_next(void) {
ask(144000);
poll();
show("the next start");
if (variant == 0) {
if (variant == 0 || variant == 5) {
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());
CHECK(state_has("\nrtss1 0 0 1 144 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());
CHECK(state_has("\nrtss1 1 30 1 144 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(state_has("\nrtss1 1 75 1 144 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();
@@ -1060,18 +1123,19 @@ static void kill_restart_as(int v) {
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 == 0 || v == 5) 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");
if (v == 3) CHECK(!own.exists, "the profile is gone, as before the killed run");
if (v == 4) CHECK(own.exists && own.limit == 60, "the killed run's 60 stays for a start with RTSS let in");
CHECK(v == 4 ? state_has("\nrtss1 0 0 1 60 1 ocarina.exe\n") : !*state(), "the state file:\n%s", state());
CHECK(v == 4 ? state_has("\nrtss1 0 0 1 60 1 60 1 ocarina.exe\n") : !*state(), "the state file:\n%s", state());
}
static void kill_restart(void) { kill_restart_as(0); }
static void kill_restart_player(void) { kill_restart_as(1); }
static void kill_restart_edited(void) { kill_restart_as(2); }
static void kill_restart_framegen(void) { kill_restart_as(3); }
static void kill_restart_setting(void) { kill_restart_as(4); }
static void kill_restart_mid_write(void) { kill_restart_as(5); }
// 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.
@@ -1151,6 +1215,9 @@ static const struct { const char *name; void (*run)(void); } scenarios[] = {
{"kill_restart_edited", kill_restart_edited},
{"kill_restart_framegen", kill_restart_framegen},
{"kill_restart_setting", kill_restart_setting},
{"kill_restart_mid_write", kill_restart_mid_write},
{"put_back_owed", put_back_owed},
{"put_back_undeleted", put_back_undeleted},
};
int main(int argc, char **argv) {