diff --git a/runtime/host.h b/runtime/host.h index 47045bda..79701b0c 100644 --- a/runtime/host.h +++ b/runtime/host.h @@ -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) diff --git a/runtime/rtss_decide.h b/runtime/rtss_decide.h index a1d20542..b1e10789 100644 --- a/runtime/rtss_decide.h +++ b/runtime/rtss_decide.h @@ -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 -// The name comes last and may hold spaces; lines that start with # are comments. +// rtss1 +// +// 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; } diff --git a/runtime/rtss_limit.c b/runtime/rtss_limit.c index 274b56a8..85d7dff3 100644 --- a/runtime/rtss_limit.c +++ b/runtime/rtss_limit.c @@ -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; } diff --git a/tests/rtss_limit_check.c b/tests/rtss_limit_check.c index 3c7ab9f8..bcafa395 100644 --- a/tests/rtss_limit_check.c +++ b/tests/rtss_limit_check.c @@ -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 diff --git a/tests/rtss_worker_check.c b/tests/rtss_worker_check.c index c0a8f1ec..a080ddff 100644 --- a/tests/rtss_worker_check.c +++ b/tests/rtss_worker_check.c @@ -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) {