mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
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>
715 lines
38 KiB
C
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;
|
|
}
|