Files
NX2navi/tests/rtss_limit_check.c
chrg6 da822abec3 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>
2026-11-13 20:12:26 -05:00

715 lines
38 KiB
C

// runtime/rtss_decide.h: the parts of RivaTuner Statistics Server's frame limiting (rtss_limit.c) that make no system
// call: the shared memory's reader against a fake RTSSSharedMemoryV2, the worker's decisions poll by poll against a
// fake profile and a fake RTSS clock, the pacer's period beside RTSS and the state file's lines. Built and run by
// tests/rtss_limit_check.py on any system; prints each failed check and exits 1 on any.
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "rtss_decide.h"
static int fails;
#define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
// RTSS's own settings: which of them hold the game's frames, and the log's text of them.
static void settings(void) {
RtssSettings none = {0, 1, 0, 0, 0, 0, 1, 1, 1};
CHECK(!rtss_settings_limit(&none), "no limit, no frame time limit, no scanline sync: RTSS holds nothing");
RtssSettings s = none;
s.limit = 50;
CHECK(rtss_settings_limit(&s), "a frame rate limit holds the frames");
s = none, s.time = 16667, s.time_den = 1;
CHECK(rtss_settings_limit(&s), "a frame time limit holds the frames");
s = none, s.scan0 = -40;
CHECK(rtss_settings_limit(&s), "scanline sync from the bottom holds the frames");
s = none, s.scan1 = 1;
CHECK(rtss_settings_limit(&s), "the second scanline sync holds the frames");
s = none, s.limit_den = 1000, s.time_den = 7;
CHECK(!rtss_settings_limit(&s), "denominators alone hold nothing");
CHECK(!rtss_settings_limit(&none), "display sync, Reflex sleep and passive wait alone hold nothing");
char text[192];
s = (RtssSettings){59940, 1000, 16667, 1, -40, 0, 1, 2, 0};
rtss_settings_text(text, sizeof text, &s, 0);
CHECK(!strcmp(text, "limit 59940/1000, frame time limit 16667/1, scanline sync -40/0"), "text: %s", text);
rtss_settings_text(text, sizeof text, &s, 1);
CHECK(!strcmp(text, "limit 59940/1000, frame time limit 16667/1, scanline sync -40/0, display sync 1, Reflex sleep 2, "
"passive wait 0"),
"text with the modes: %s", text);
char small[16], mid[70];
rtss_settings_text(small, sizeof small, &s, 1);
CHECK(strlen(small) == sizeof small - 1, "a short buffer is cut, not overrun");
rtss_settings_text(mid, sizeof mid, &s, 1);
CHECK(strlen(mid) == sizeof mid - 1 && !strncmp(mid, text, sizeof mid - 1), "the modes cut at the buffer's end");
}
// The kept-out reasons, for the log.
static void reasons(void) {
CHECK(!strcmp(rtss_out_text(RTSS_OUT_SETTING), "NX2NAVI_RTSS=0"), "the setting");
CHECK(!strcmp(rtss_out_text(RTSS_OUT_FRAMEGEN), "frame generation"), "frame generation");
CHECK(!strcmp(rtss_out_text(RTSS_OUT_LOWLAT), "the driver's low latency"), "low latency");
CHECK(!strcmp(rtss_out_limiter(RTSS_OUT_SETTING), "the game's limiter"), "the setting leaves the game's limiter");
CHECK(!strcmp(rtss_out_limiter(RTSS_OUT_LOWLAT), "the driver's low latency"), "low latency holds the frames");
CHECK(RTSS_UNSET < 0 && RTSS_OUT_SETTING < 0 && RTSS_OUT_FRAMEGEN < 0 && RTSS_OUT_LOWLAT < 0,
"every reason is below 0, where a limit never is");
CHECK(RTSS_UNSET != RTSS_OUT_SETTING && RTSS_UNSET != RTSS_OUT_FRAMEGEN && RTSS_UNSET != RTSS_OUT_LOWLAT &&
RTSS_OUT_SETTING != RTSS_OUT_FRAMEGEN && RTSS_OUT_FRAMEGEN != RTSS_OUT_LOWLAT &&
RTSS_OUT_SETTING != RTSS_OUT_LOWLAT,
"the reasons differ");
}
// ---- a fake RTSSSharedMemoryV2: the header and application entries written as RTSS lays them out
enum { PID = 4242, ESIZE = 1024, OFF = 0x100 };
static void shm_set(uint8_t *v, uint32_t sig, uint32_t ver, uint32_t esize, uint32_t off, uint32_t n) {
uint32_t h[5] = {sig, ver, esize, off, n};
memcpy(v, h, sizeof h);
}
static void app_put(uint8_t *v, uint32_t esize, uint32_t off, uint32_t slot, uint32_t pid, uint32_t t0, uint32_t t1,
uint32_t frames) {
uint8_t *e = v + off + (size_t)slot * esize;
uint32_t t[3] = {t0, t1, frames}, flags = RTSS_API_VULKAN | RTSS_UPDATE_ASKED;
memcpy(e, &pid, 4);
memcpy(e + 4 + 260, &flags, 4); // RTSS_SHARED_MEMORY_APP_ENTRY: dwFlags after the 260-byte path
memcpy(e + RTSS_PERIOD_AT, t, sizeof t);
}
// rtss_parse: where the entry is, and that a bad header, a short entry or an array past the memory finds nothing.
static void parse(void) {
static uint8_t v[65536];
RtssApp a;
shm_set(v, RTSS_SIGNATURE, 0x0002000E, ESIZE, OFF, 3);
app_put(v, ESIZE, OFF, 0, 7, 0, 0, 0);
app_put(v, ESIZE, OFF, 1, 8, 0, 0, 0);
app_put(v, ESIZE, OFF, 2, PID, 1000, 2000, 60);
rtss_parse(v, sizeof v, PID, &a);
CHECK(a.running && a.listed && a.t0 == 1000 && a.t1 == 2000 && a.frames == 60, "the third entry is ours");
CHECK(a.flags == (RTSS_API_VULKAN | RTSS_UPDATE_ASKED) && RTSS_FLAGS_AT == 264 && RTSS_PERIOD_AT == 268,
"the flags at +264, the period at +268: %#x", a.flags);
CHECK(rtss_fps(&a) == 60.0, "60 frames in a second: %f", rtss_fps(&a));
rtss_parse(v, sizeof v, 99, &a);
CHECK(a.running && !a.listed && rtss_fps(&a) == 0, "another process: running, not listed");
shm_set(v, 0xDEAD, 0x0002000E, ESIZE, OFF, 3);
rtss_parse(v, sizeof v, PID, &a);
CHECK(!a.running && !a.listed, "no signature: not running");
shm_set(v, RTSS_SIGNATURE, 0x00010003, ESIZE, OFF, 3);
rtss_parse(v, sizeof v, PID, &a);
CHECK(!a.running, "a version before 2.0: not running");
shm_set(v, RTSS_SIGNATURE, 0x0002000E, RTSS_ENTRY_MIN - 1, OFF, 3);
rtss_parse(v, sizeof v, PID, &a);
CHECK(a.running && !a.listed, "entries shorter than the period: not read");
rtss_parse(v, 19, PID, &a);
CHECK(!a.running, "a memory shorter than the header");
rtss_parse(NULL, 0, PID, &a);
CHECK(!a.running, "no memory");
// entries of the smallest size, ours the last that fits: found; past the memory's end: not read
memset(v, 0, sizeof v);
shm_set(v, RTSS_SIGNATURE, 0x0002000E, RTSS_ENTRY_MIN, OFF, 1000);
uint32_t fit = (uint32_t)((sizeof v - OFF) / RTSS_ENTRY_MIN) - 1;
app_put(v, RTSS_ENTRY_MIN, OFF, fit, PID, 1000, 1500, 30);
rtss_parse(v, sizeof v, PID, &a);
CHECK(a.listed && a.frames == 30, "the last entry inside the memory is read");
rtss_parse(v, OFF + (size_t)fit * RTSS_ENTRY_MIN + RTSS_ENTRY_MIN - 1, PID, &a);
CHECK(a.running && !a.listed, "an entry one byte past the memory is not read");
// more than RTSS_APPS_MAX entries: the walk stops there
size_t big = OFF + (size_t)5000 * RTSS_ENTRY_MIN;
uint8_t *b = calloc(1, big);
shm_set(b, RTSS_SIGNATURE, 0x0002000E, RTSS_ENTRY_MIN, OFF, 5000);
app_put(b, RTSS_ENTRY_MIN, OFF, 4500, PID, 1000, 2000, 60);
rtss_parse(b, big, PID, &a);
CHECK(a.running && !a.listed, "an entry past %d is not looked at", RTSS_APPS_MAX);
app_put(b, RTSS_ENTRY_MIN, OFF, 4095, PID, 1000, 2000, 60);
rtss_parse(b, big, PID, &a);
CHECK(a.listed, "entry %d is", RTSS_APPS_MAX - 1);
free(b);
}
// ---- a fake profile behind RtssOps, and one poll as the worker makes it
typedef struct {
int readonly, no_library, loaded;
int64_t refuse_above; // RTSS refuses a limit above this (millihertz); 0 none
int64_t holds; // the limit the profile holds from this run; -1 none
int applies, firsts, clears, put_backs, last_put_back_why;
} Store;
static int f_apply(void *ctx, int64_t mhz, int first, int clear) {
Store *st = ctx;
st->applies++, st->firsts += first;
if (st->readonly) return RTSS_WROTE_READONLY;
if (st->refuse_above && mhz > st->refuse_above) {
if (!clear) return RTSS_WROTE_REFUSED;
st->clears++, st->holds = 0;
return RTSS_WROTE_REFUSED_CLEARED;
}
st->holds = mhz;
return RTSS_WROTE_HELD;
}
static void f_put_back(void *ctx, const RtssOut *o) {
Store *st = ctx;
st->put_backs++, st->holds = -1, st->last_put_back_why = o->why;
}
// RTSS as the worker sees it: polls 500 ms apart; while it runs, lists the game and counts (counting) it measures a
// period of a second every second poll, with frames frames in it (the frame rate); the game presents (presenting) and
// the newest host present is a millisecond before the poll, else it stays where it was (0: none yet).
typedef struct {
RtssState s;
Store st;
RtssOps ops;
int running, listed, counting, presenting, polls;
uint32_t flags, t0, t1, frames;
int64_t movie_mhz; // a movie plays at this rate (rtss_limit_movie)
uint64_t now_ns, hosted_ns;
} Run;
static void run_init(Run *r) {
memset(r, 0, sizeof *r);
r->s = (RtssState)RTSS_STATE_INIT;
r->st.holds = -1;
r->ops = (RtssOps){&r->st, f_apply, f_put_back};
r->running = r->listed = r->counting = r->presenting = 1;
r->flags = RTSS_API_VULKAN, r->t0 = 1000, r->t1 = 2000, r->frames = 60;
r->now_ns = 10000000000ull;
}
// One poll for want, as rtss_limit.c worker makes it: the hooks library loads on the first poll that wants it and stays.
static RtssOut step_ex(Run *r, int64_t want, int current) {
r->now_ns += 500000000ull;
if (r->running && r->listed && r->counting && ++r->polls % 2 == 0) r->t0 = r->t1, r->t1 += 1000;
if (r->presenting) r->hosted_ns = r->now_ns - 1000000;
RtssIn in = {.want = want, .current = current, .movie_mhz = r->movie_mhz, .now_ns = r->now_ns,
.hosted_ns = r->hosted_ns};
in.app = (RtssApp){r->running, r->running && r->listed, r->flags, r->t0, r->t1, r->frames};
if (r->st.loaded) in.hooks = 1;
else if (rtss_wants_hooks(&r->s, &in)) in.hooks = r->st.no_library ? 0 : (r->st.loaded = 1);
else in.hooks = -1;
return rtss_poll(&r->s, &in, &r->ops);
}
static RtssOut step(Run *r, int64_t want) { return step_ex(r, want, 1); }
static void why_is(const RtssOut *o, const RtssState *s, const char *want, int line) {
char t[128];
rtss_why_text(t, sizeof t, o, s->fail);
if (strcmp(t, want)) fails++, printf("FAIL line %d: the reason is '%s', not '%s'\n", line, t, want);
}
#define WHY(o, r, text) why_is(&(o), &(r).s, text, __LINE__)
// RTSS's injection delay: it runs but has not hooked the game for 30 polls. The limit goes into the profile at the
// second poll (the first sees the want new), with no second write. Once RTSS lists the game the game's limiter stands
// aside only after RTSS has seen its Vulkan presents, read the profile and measured a period of its frames lately.
static void injection_delay(void) {
Run r;
run_init(&r);
r.listed = 0;
RtssOut o = step(&r, 60000);
CHECK(o.act == RTSS_DO_NOTHING && !o.holds && o.why == RTSS_WHY_SETTLING, "a new want: not written yet");
WHY(o, r, "a new limit: RivaTuner Statistics Server gets it once it stays");
o = step(&r, 60000);
CHECK(o.act == RTSS_DO_APPLY && !o.holds && o.why == RTSS_WHY_NOT_LISTED, "written, not held yet");
CHECK(r.st.firsts == 1 && r.s.applied == 60000 && r.s.changed, "the first write of the run");
for (int i = 0; i < 30; i++) o = step(&r, 60000);
CHECK(r.st.applies == 1 && !o.holds && o.why == RTSS_WHY_NOT_LISTED, "no write while not listed");
WHY(o, r, "RivaTuner Statistics Server has not hooked the game yet");
r.listed = 1, r.flags = 0;
o = step(&r, 60000);
CHECK(!o.holds && o.why == RTSS_WHY_NOT_VULKAN, "listed with no API yet");
WHY(o, r, "RivaTuner Statistics Server lists the game but has not seen its Vulkan presents yet");
r.flags = 0x7; // another API's value
o = step(&r, 60000);
CHECK(!o.holds && o.why == RTSS_WHY_NOT_VULKAN, "another API");
r.flags = RTSS_API_VULKAN | RTSS_UPDATE_ASKED;
o = step(&r, 60000);
CHECK(!o.holds && o.why == RTSS_WHY_UPDATE, "the profile not read yet");
WHY(o, r, "RivaTuner Statistics Server has not read the game's limit from its profile yet");
r.flags = RTSS_API_VULKAN, r.counting = 0;
for (int i = 0; i < 6; i++) o = step(&r, 60000);
CHECK(!o.holds && o.why == RTSS_WHY_NOT_COUNTING, "no period for three seconds");
WHY(o, r, "RivaTuner Statistics Server has measured no frames of the game lately");
r.counting = 1;
for (int i = 0; i < 2; i++) o = step(&r, 60000);
CHECK(o.holds && o.why == RTSS_WHY_HOLDS && r.st.applies == 1, "held once counting, no second write");
}
// RTSS quits while it limits, and comes back: the profile stays for its return.
static void quit(void) {
Run r;
run_init(&r);
step(&r, 60000);
step(&r, 60000);
RtssOut o = step(&r, 60000);
CHECK(o.holds, "held");
r.running = 0;
o = step(&r, 60000);
CHECK(!o.holds && o.why == RTSS_WHY_NOT_RUNNING && o.act == RTSS_DO_NOTHING, "not running: the game limits");
CHECK(!r.st.put_backs && r.st.holds == 60000, "the profile stays");
r.running = 1;
for (int i = 0; i < 2; i++) o = step(&r, 60000);
CHECK(o.holds && r.st.applies == 1, "back: held with no new write");
}
// Profiles under Program Files without administrator rights: out for the run.
static void readonly(void) {
Run r;
run_init(&r);
r.st.readonly = 1;
step(&r, 60000);
RtssOut o = step(&r, 60000);
CHECK(o.act == RTSS_DO_APPLY && !o.holds && o.why == RTSS_WHY_FAILED && r.s.fail == RTSS_FAIL_READONLY, "failed");
CHECK(!r.s.changed, "nothing of the run in the profile");
for (int i = 0; i < 20; i++) o = step(&r, i < 10 ? 60000 : 144000);
CHECK(r.st.applies == 1 && o.why == RTSS_WHY_FAILED, "no write after it");
WHY(o, r, "RivaTuner Statistics Server's profile cannot be changed");
}
// A limit out of RTSS's range: refused and kept by the game; the earlier limit of this run is cleared to none; a limit
// in range goes to RTSS once it stays.
static void refused(void) {
Run r;
run_init(&r);
r.st.refuse_above = 200000;
step(&r, 60000);
step(&r, 60000);
step(&r, 240000);
RtssOut o = step(&r, 240000);
CHECK(o.act == RTSS_DO_APPLY && !o.holds && o.why == RTSS_WHY_REFUSED, "refused");
CHECK(r.s.refused == 240000 && r.s.applied == 0 && r.st.clears == 1 && r.st.holds == 0, "the 60 cleared to none");
for (int i = 0; i < 5; i++) o = step(&r, 240000);
CHECK(r.st.applies == 2 && o.why == RTSS_WHY_REFUSED && !o.holds, "not written again");
o = step(&r, 144000);
CHECK(o.act == RTSS_DO_NOTHING && o.why == RTSS_WHY_SETTLING, "144 settles first");
o = step(&r, 144000);
CHECK(o.act == RTSS_DO_APPLY && !o.holds && r.st.holds == 144000, "144 written");
o = step(&r, 144000);
CHECK(o.holds, "held from the next poll");
CHECK(r.st.firsts == 1, "one first write in the run (%d)", r.st.firsts);
o = step(&r, 240000);
CHECK(o.act == RTSS_DO_NOTHING && o.why == RTSS_WHY_REFUSED, "the refused limit stays with the game at once");
// refused as the run's first write: the profile is the run's from then on
run_init(&r);
r.st.refuse_above = 200000;
step(&r, 240000);
o = step(&r, 240000);
CHECK(o.why == RTSS_WHY_REFUSED && r.s.changed && r.st.clears == 1, "a first refusal clears too, as it stands");
}
// RTSS's own frame count: each fresh period after a poll that stood aside is checked, and more than 1.05 times the
// limit for three of them in a row puts the profile back for the run. 69 fps at 60 (1.15 times) is caught, 63 is not;
// a period RTSS no longer moves counts neither way; no limit and not listed never count.
static void frame_count(void) {
Run r;
run_init(&r);
r.frames = 144;
RtssOut o = step(&r, 60000);
int polls = 1;
while (!r.s.fail && polls < 40) o = step(&r, 60000), polls++;
CHECK(o.act == RTSS_DO_PUT_BACK && !o.holds && o.why == RTSS_WHY_FAILED && r.s.fail == RTSS_FAIL_NOT_LIMITING,
"144 fps at 60 gives up");
CHECK(polls == 8, "after three fresh periods it held through (poll %d)", polls);
CHECK(o.fps == 144.0 && r.st.put_backs == 1 && r.st.last_put_back_why == RTSS_WHY_FAILED && !r.s.changed,
"put back, with the measured rate");
o = step(&r, 30000);
CHECK(o.why == RTSS_WHY_FAILED && r.st.applies == 1, "out for the run");
WHY(o, r, "RivaTuner Statistics Server does not hold the game to the limit");
run_init(&r);
r.frames = 69;
for (int i = 0; i < 40 && !r.s.fail; i++) o = step(&r, 60000);
CHECK(r.s.fail == RTSS_FAIL_NOT_LIMITING, "69 fps at 60 is caught");
run_init(&r);
r.frames = 63; // 1.05 times the limit exactly
for (int i = 0; i < 40; i++) o = step(&r, 60000);
CHECK(o.holds && !r.s.fail, "63 fps at 60 is not");
run_init(&r);
r.frames = 700;
for (int i = 0; i < 20; i++) o = step(&r, 0);
CHECK(o.holds && !r.s.fail, "no limit: nothing to check");
run_init(&r);
r.frames = 144, r.listed = 0;
for (int i = 0; i < 20; i++) o = step(&r, 60000);
CHECK(!r.s.fail && !r.s.over, "not listed: no count");
// a period RTSS no longer moves (the player lowered the limit and minimised the window): never counted
run_init(&r);
r.frames = 144, r.counting = 0;
for (int i = 0; i < 20; i++) o = step(&r, 60000);
CHECK(!r.s.fail && !r.s.over && !o.holds && o.why == RTSS_WHY_NOT_COUNTING, "a frozen period: not counting");
// two periods over, then one under: the count starts again
run_init(&r);
r.frames = 144;
for (int i = 0; i < 7; i++) step(&r, 60000);
CHECK(r.s.over == 2, "two periods over (%d)", r.s.over);
r.frames = 60;
for (int i = 0; i < 2; i++) step(&r, 60000);
CHECK(r.s.over == 0 && !r.s.fail, "one under: the count starts again");
// a want that changed while the poll ran: nothing decided, the count waits
run_init(&r);
r.frames = 144;
for (int i = 0; i < 5; i++) step(&r, 60000);
o = step_ex(&r, 60000, 0);
CHECK(o.why == RTSS_WHY_NEWER && !o.holds && o.act == RTSS_DO_NOTHING && r.s.over == 1, "a newer want: no count");
}
// RTSS lists the game with the limit in its profile and pictures go out, but never shows its hold: it may hold the
// frames beside the game's limiter, so after RTSS_UNPROVEN_POLLS such polls in a row the profile goes back once, for
// the run. Each sign that never comes counts; a minimised window, RTSS not hooked and no limit do not count, and a poll
// that shows the hold starts the count again.
static void unproven(void) {
static const struct { uint32_t flags; int period; const char *what, *why; } stuck[] = {
{RTSS_API_VULKAN | RTSS_UPDATE_ASKED, 1, "the profile never read",
"RivaTuner Statistics Server has not read the game's limit from its profile yet"},
{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"},
};
Run r;
RtssOut o;
for (unsigned k = 0; k < sizeof stuck / sizeof *stuck; k++) {
run_init(&r);
r.flags = stuck[k].flags;
if (!stuck[k].period) r.counting = 0, r.t0 = r.t1 = 0;
step(&r, 60000);
o = step(&r, 60000);
CHECK(o.act == RTSS_DO_APPLY, "%s: written", stuck[k].what);
int polls = 0;
for (; polls < RTSS_UNPROVEN_POLLS - 1; polls++) o = step(&r, 60000);
CHECK(!r.s.fail && !o.holds && !r.st.put_backs, "%s: %d polls, not yet", stuck[k].what, polls);
WHY(o, r, stuck[k].why);
o = step(&r, 60000);
CHECK(o.act == RTSS_DO_PUT_BACK && o.why == RTSS_WHY_FAILED && r.s.fail == RTSS_FAIL_UNPROVEN &&
r.st.put_backs == 1 && r.st.last_put_back_why == RTSS_WHY_FAILED && !r.s.changed,
"%s: put back at poll %d", stuck[k].what, RTSS_UNPROVEN_POLLS);
for (int i = 0; i < 10; i++) o = step(&r, i < 5 ? 60000 : 144000);
CHECK(r.st.put_backs == 1 && r.st.applies == 1 && o.why == RTSS_WHY_FAILED && !o.holds,
"%s: once, out for the run", stuck[k].what);
WHY(o, r, "RivaTuner Statistics Server never showed it holds the game");
CHECK(rtss_out_of_limit(60000, r.s.fail), "%s: a setting of RTSS's own may still hold the game", stuck[k].what);
}
// a minimised window, RTSS not hooked, or no limit: nothing to count
run_init(&r);
r.flags = RTSS_API_VULKAN | RTSS_UPDATE_ASKED;
step(&r, 60000);
step(&r, 60000);
r.presenting = 0;
for (int i = 0; i < 3 * RTSS_UNPROVEN_POLLS; i++) o = step(&r, 60000);
CHECK(!r.s.fail && !r.st.put_backs && r.s.unproven == 0, "minimised: not counted");
r.presenting = 1, r.listed = 0;
for (int i = 0; i < 3 * RTSS_UNPROVEN_POLLS; i++) o = step(&r, 60000);
CHECK(!r.s.fail && !r.st.put_backs && o.why == RTSS_WHY_NOT_LISTED, "not hooked: not counted");
r.listed = 1;
for (int i = 0; i < 3 * RTSS_UNPROVEN_POLLS; i++) o = step(&r, 0);
CHECK(!r.s.fail && !r.st.put_backs && r.st.applies == 2, "no limit: not counted");
// one poll that shows the hold starts the count again
run_init(&r);
r.flags = RTSS_API_VULKAN | RTSS_UPDATE_ASKED;
step(&r, 60000);
step(&r, 60000);
for (int i = 0; i < RTSS_UNPROVEN_POLLS - 1; i++) step(&r, 60000);
r.flags = RTSS_API_VULKAN;
o = step(&r, 60000);
CHECK(o.holds && r.s.unproven == 0, "shown once: the count starts again");
r.flags = RTSS_API_VULKAN | RTSS_UPDATE_ASKED;
for (int i = 0; i < RTSS_UNPROVEN_POLLS - 1; i++) o = step(&r, 60000);
CHECK(!r.s.fail && o.why == RTSS_WHY_UPDATE, "%d polls again: not yet", RTSS_UNPROVEN_POLLS - 1);
o = step(&r, 60000);
CHECK(o.act == RTSS_DO_PUT_BACK && r.s.fail == RTSS_FAIL_UNPROVEN, "the full count again: put back");
}
// No picture went out for 200 ms (a minimised window, a frame hold) or none yet: RTSS has nothing to hold, and the
// game's limiter takes the limit back until pictures go out again. Before the first picture nothing is written.
static void idle(void) {
Run r;
run_init(&r);
r.presenting = 0;
RtssOut o;
for (int i = 0; i < 4; i++) o = step(&r, 60000);
CHECK(!o.holds && o.why == RTSS_WHY_NO_PRESENT && !r.st.applies, "no host present yet: nothing written");
WHY(o, r, "no picture has gone out yet");
r.presenting = 1;
o = step(&r, 60000);
CHECK(o.act == RTSS_DO_APPLY && !o.holds, "written at the first picture, the want having stayed");
o = step(&r, 60000);
CHECK(o.holds, "held once pictures go out");
r.presenting = 0;
r.hosted_ns = r.now_ns + 500000000ull - 199000000ull; // the next poll comes 199 ms after the last present
o = step(&r, 60000);
CHECK(o.holds, "199 ms: still held");
o = step(&r, 60000);
CHECK(!o.holds && o.why == RTSS_WHY_IDLE, "minimised");
WHY(o, r, "no picture went out lately: RivaTuner has none to hold");
r.presenting = 1;
o = step(&r, 60000);
CHECK(o.holds && r.st.applies == 1, "held again, no write");
}
// Kept out and let in again: the first kept-out poll puts back what the run wrote, once; a limit afterwards is
// written at once and is the run's first write again.
static void kept_out(void) {
Run r;
run_init(&r);
step(&r, 60000);
step(&r, 60000);
RtssOut o = step(&r, RTSS_OUT_FRAMEGEN);
CHECK(o.act == RTSS_DO_NOTHING && o.why == RTSS_WHY_OUT && !o.holds && r.st.holds == 60000,
"the game's limiter at once, the profile once the want stays");
o = step(&r, RTSS_OUT_FRAMEGEN);
CHECK(o.act == RTSS_DO_PUT_BACK && o.why == RTSS_WHY_OUT && !o.holds && r.st.holds == -1, "put back");
CHECK(r.st.last_put_back_why == RTSS_WHY_OUT, "the put back says why");
o = step(&r, RTSS_OUT_FRAMEGEN);
CHECK(o.act == RTSS_DO_NOTHING && r.st.put_backs == 1, "once");
for (int i = 0; i < 3; i++) o = step(&r, RTSS_OUT_LOWLAT);
CHECK(o.act == RTSS_DO_NOTHING && o.mhz == RTSS_OUT_LOWLAT && r.st.put_backs == 1, "a second reason: nothing more");
WHY(o, r, "RivaTuner Statistics Server kept out: the driver's low latency");
o = step(&r, 60000);
CHECK(o.act == RTSS_DO_NOTHING && o.why == RTSS_WHY_SETTLING, "a limit again: settles first");
o = step(&r, 60000);
CHECK(o.act == RTSS_DO_APPLY && !o.holds && r.st.firsts == 2, "written, the run's first write again");
o = step(&r, 60000);
CHECK(o.holds, "held from the next poll");
// kept out from the start: the hooks library is never wanted, and nothing is written or put back
run_init(&r);
for (int i = 0; i < 10; i++) o = step(&r, RTSS_OUT_SETTING);
CHECK(!r.st.loaded && !r.st.applies && !r.st.put_backs && o.why == RTSS_WHY_OUT, "read only");
}
// Low latency switching on and off at every poll (a swapchain rebuild, the time before the first swapchain): the want
// never stays two polls, so the profile keeps the limit written before, and the game's limiter holds the frames while
// the want is new. From the start, the same flips write nothing.
static void churn(void) {
Run r;
run_init(&r);
step(&r, 60000);
step(&r, 60000);
RtssOut o;
for (int i = 0; i < 20; i++) {
o = step(&r, i & 1 ? 60000 : RTSS_OUT_LOWLAT);
CHECK(!(i & 1) ? !o.holds : 1, "kept out at poll %d: the game's limiter", i);
}
CHECK(r.st.applies == 1 && !r.st.put_backs && r.st.holds == 60000, "%d writes, %d put backs", r.st.applies,
r.st.put_backs);
o = step(&r, 60000);
CHECK(o.holds && r.st.applies == 1, "held once the limit stays");
run_init(&r);
for (int i = 0; i < 20; i++) step(&r, i & 1 ? 60000 : RTSS_OUT_LOWLAT);
CHECK(!r.st.applies && !r.st.put_backs, "from the start: nothing written");
// a flip every two polls is a want that stays: each one acts
run_init(&r);
for (int i = 0; i < 8; i++) step(&r, i & 2 ? RTSS_OUT_LOWLAT : 60000);
CHECK(r.st.applies == 2 && r.st.put_backs == 2, "two polls each: %d writes, %d put backs", r.st.applies,
r.st.put_backs);
}
// A movie under RTSS: the want stays the play limit and the game's pacer holds the movie. At a limit of 144 the profile
// keeps 144 through it, with no write; under the movie's rate (40) the profile has no limit for the movie's length, so
// RTSS does not hold the movie below its rate, and gets 40 back at its end; at the movie's own rate (60) it keeps 60.
static void movie(void) {
Run r;
run_init(&r);
step(&r, 144000);
step(&r, 144000);
r.movie_mhz = 60000;
RtssOut o = step(&r, 144000);
CHECK(o.act == RTSS_DO_NOTHING && !o.holds && o.why == RTSS_WHY_MOVIE, "a movie at 144: no write, the pacer's");
for (int i = 0; i < 10; i++) o = step(&r, 144000);
CHECK(!o.holds && o.why == RTSS_WHY_MOVIE && r.st.applies == 1, "through the movie");
WHY(o, r, "a movie plays: the game keeps its pictures on time");
r.movie_mhz = 0;
o = step(&r, 144000);
CHECK(o.act == RTSS_DO_NOTHING && o.holds && o.why == RTSS_WHY_HOLDS && r.st.applies == 1, "held again after it");
run_init(&r);
step(&r, 40000);
step(&r, 40000);
r.movie_mhz = 60000;
o = step(&r, 40000);
CHECK(o.act == RTSS_DO_APPLY && o.mhz == 0 && !o.holds && o.why == RTSS_WHY_MOVIE && r.st.holds == 0,
"a movie at 40: no limit in the profile");
o = step(&r, 40000);
CHECK(o.act == RTSS_DO_NOTHING && r.st.applies == 2, "once");
r.movie_mhz = 0;
o = step(&r, 40000);
CHECK(o.act == RTSS_DO_APPLY && o.mhz == 40000 && !o.holds && r.st.holds == 40000 && r.st.applies == 3,
"40 back at its end");
o = step(&r, 40000);
CHECK(o.holds, "held from the next poll");
// at the movie's own rate (60, the default limit) the profile keeps 60 through the movie, with no write: RTSS holds
// the movie beside the pacer, as rtss_target says
run_init(&r);
step(&r, 60000);
step(&r, 60000);
r.movie_mhz = 60000;
for (int i = 0; i < 5; i++) o = step(&r, 60000);
CHECK(o.act == RTSS_DO_NOTHING && !o.holds && o.why == RTSS_WHY_MOVIE && r.st.applies == 1 && r.st.holds == 60000,
"a movie at 60: no write, the profile keeps 60");
r.movie_mhz = 0;
o = step(&r, 60000);
CHECK(o.holds && r.st.applies == 1, "held again after it, no write");
CHECK(rtss_target(60000, 60000) == 60000 && rtss_target(59940, 60000) == 0 && rtss_target(0, 60000) == 0 &&
rtss_target(144000, 0) == 144000 && rtss_target(RTSS_OUT_LOWLAT, 60000) == RTSS_OUT_LOWLAT,
"rtss_target");
}
// No want yet: nothing at all. The hooks library that does not load fails the run. (A run whose exit put the profile
// back makes no decision: rtss_limit.c worker skips the poll, which tests/rtss_worker_check.c's restore_* check.)
static void unset_and_library(void) {
Run r;
run_init(&r);
RtssOut o = step(&r, RTSS_UNSET);
CHECK(o.why == RTSS_WHY_UNSET && o.act == RTSS_DO_NOTHING && !r.st.loaded, "no want");
run_init(&r);
r.st.no_library = 1;
o = step(&r, 60000);
CHECK(o.why == RTSS_WHY_FAILED && r.s.fail == RTSS_FAIL_LIBRARY && !r.st.applies, "the library did not load");
WHY(o, r, "RivaTuner Statistics Server's hooks library did not load");
r.st.no_library = 0;
o = step(&r, 60000);
CHECK(o.why == RTSS_WHY_FAILED && !r.st.loaded, "not tried again");
// not running: the library is not wanted until RTSS runs
run_init(&r);
r.running = 0;
o = step(&r, 60000);
CHECK(!r.st.loaded && o.why == RTSS_WHY_NOT_RUNNING, "not loaded while RTSS does not run");
WHY(o, r, "RivaTuner Statistics Server is not running");
}
// Two limiters: RTSS out of the game's limit (kept out, or a profile that cannot be written or a hooks library that did
// not load), listed and a limit of RTSS's own; never for a limit the game hands RTSS.
static void two(void) {
CHECK(rtss_two(RTSS_OUT_SETTING, 0, 1, 1) && rtss_two(RTSS_OUT_FRAMEGEN, 0, 1, 1) &&
rtss_two(RTSS_OUT_LOWLAT, 0, 1, 1),
"every reason");
CHECK(!rtss_two(RTSS_OUT_SETTING, 0, 0, 1), "not listed: RTSS holds nothing");
CHECK(!rtss_two(RTSS_OUT_SETTING, 0, 1, 0), "no limit of its own");
CHECK(!rtss_two(60000, 0, 1, 1) && !rtss_two(0, 0, 1, 1), "the game's limit in RTSS: one limiter");
CHECK(!rtss_two(RTSS_UNSET, 0, 1, 1) && !rtss_two(RTSS_UNSET, RTSS_FAIL_READONLY, 1, 1), "no want yet");
CHECK(rtss_two(60000, RTSS_FAIL_READONLY, 1, 1) && rtss_two(0, RTSS_FAIL_LIBRARY, 1, 1),
"out for the run: the profile cannot be written, or the hooks library did not load");
CHECK(!rtss_two(60000, RTSS_FAIL_READONLY, 0, 1) && !rtss_two(60000, RTSS_FAIL_LIBRARY, 1, 0),
"out for the run, but not listed or no limit of its own");
CHECK(!rtss_two(60000, RTSS_FAIL_NOT_LIMITING, 1, 1), "RTSS seen not to hold the game: not counted");
}
// The period the game's pacer keeps beside RTSS: the ceiling while RTSS holds and host presents go out, the limit's own
// otherwise; never 0.
static void pace_period(void) {
const uint64_t p60 = 16666667, ceiling = 4166667, now = 50000000000ull;
CHECK(rtss_pace_period(1, 0, now, now - 10000000, p60, ceiling) == ceiling, "holds, a host present 10 ms ago");
CHECK(rtss_pace_period(1, 0, now, now - RTSS_HOLD_IDLE_NS + 1, p60, ceiling) == ceiling, "just under 200 ms");
CHECK(rtss_pace_period(1, 0, now, now - RTSS_HOLD_IDLE_NS, p60, ceiling) == p60, "200 ms with no host present");
CHECK(rtss_pace_period(1, 0, now, 0, p60, ceiling) == p60, "no host present yet");
CHECK(rtss_pace_period(0, 0, now, now - 10000000, p60, ceiling) == p60, "RTSS does not hold");
CHECK(rtss_pace_period(1, 0, now, now + 1000, p60, ceiling) == ceiling, "a host present stored after now was read");
CHECK(rtss_pace_period(1, 0, now, now - 10000000, ceiling, ceiling) == ceiling, "a limit at the ceiling");
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, 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 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 && r.prev_limit == 60 && r.prev_den == 1 && !strcmp(r.name, "ocarina.exe"),
"read back");
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 &&
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 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 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");
// 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 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 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 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, 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 "
"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, 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
// denominator, else rounded down, so "display" at 59.94 Hz never holds the game above the refresh.
static void limit_value(void) {
static const struct { int64_t mhz; int has_den; uint32_t limit, den; } cases[] = {
{59940, 0, 59, 1}, {59940, 1, 59940, 1000}, {60000, 0, 60, 1}, {60000, 1, 60, 1},
{143856, 0, 143, 1}, {143856, 1, 143856, 1000}, {0, 0, 0, 1}, {240000, 1, 240, 1}};
for (unsigned i = 0; i < sizeof cases / sizeof *cases; i++) {
uint32_t l = 7, d = 7;
rtss_limit_value(cases[i].mhz, cases[i].has_den, &l, &d);
CHECK(l == cases[i].limit && d == cases[i].den, "%lld mHz, denominator %d: %u/%u, not %u/%u",
(long long)cases[i].mhz, cases[i].has_den, l, d, cases[i].limit, cases[i].den);
}
}
int main(void) {
settings();
reasons();
parse();
injection_delay();
quit();
readonly();
refused();
frame_count();
unproven();
idle();
kept_out();
churn();
movie();
unset_and_library();
two();
pace_period();
saved();
limit_value();
printf("rtss_limit_check: %s\n", fails ? "FAIL" : "ok");
return fails != 0;
}