diff --git a/runtime/rtss_decide.h b/runtime/rtss_decide.h index c413b9a4..682a61fa 100644 --- a/runtime/rtss_decide.h +++ b/runtime/rtss_decide.h @@ -32,13 +32,20 @@ static inline const char *rtss_out_limiter(int64_t want) { // ---- RTSS's shared memory, "RTSSSharedMemoryV2". The header: signature, version, application entry size, offset of // the application array, its length. An entry: the process id, the executable path (260 bytes), flags, then RTSS's -// last frame rate period for the process: its start and end in ms and the frames in it. +// last frame rate period for the process: its start and end in ms and the frames in it. The flags' low 16 bits hold +// the API RTSS saw the process present with, a value rather than bits (Vulkan is 0xA), and RTSS_UPDATE_ASKED stays set +// from UpdateProfiles until the process has read its profile again. #define RTSS_SIGNATURE 0x52545353u // 'RTSS' as RTSS writes it: the memory holds valid data -enum { RTSS_PERIOD_AT = 4 + 260 + 4, RTSS_ENTRY_MIN = RTSS_PERIOD_AT + 12, RTSS_APPS_MAX = 4096 }; +#define RTSS_API_MASK 0xFFFFu +#define RTSS_API_VULKAN 0xAu +#define RTSS_UPDATE_ASKED 0x10000000u +enum { RTSS_FLAGS_AT = 4 + 260, RTSS_PERIOD_AT = RTSS_FLAGS_AT + 4, RTSS_ENTRY_MIN = RTSS_PERIOD_AT + 12, + RTSS_APPS_MAX = 4096 }; typedef struct { int running; // the signature and a version of 2.0 or later int listed; // the application list holds this process: RTSS has hooked it + uint32_t flags; // the entry's flags uint32_t t0, t1, frames; // the period of this process's entry } RtssApp; @@ -56,6 +63,7 @@ static inline void rtss_parse(const uint8_t *p, size_t size, uint32_t pid, RtssA memcpy(&id, p + at, 4); if (id != pid) continue; a->listed = 1; + memcpy(&a->flags, p + at + RTSS_FLAGS_AT, 4); memcpy(t, p + at + RTSS_PERIOD_AT, sizeof t); a->t0 = t[0], a->t1 = t[1], a->frames = t[2]; break; @@ -68,8 +76,10 @@ static inline double rtss_fps(const RtssApp *a) { } // ---- the worker's decisions -#define RTSS_CHECK_POLLS 6 // polls in a row RTSS's own frame rate of the game may stay above the limit -#define RTSS_CHECK_SLACK 1.2 // above limit * this, RTSS is taken not to hold the game to it +#define RTSS_OVER_PERIODS 3 // fresh periods in a row RTSS's frame rate of the game may stay above the limit +#define RTSS_OVER_SLACK 1.05 // above limit * this, RTSS is taken not to hold the game to it +#define RTSS_FRESH_NS 2500000000ull // a period is stale after this, or 2.5 times its own length if longer +#define RTSS_HOLD_IDLE_NS 200000000ull // no host present for this long: RTSS holds nothing (vk_lowlat.c HOLD_IDLE_NS) enum { RTSS_FAIL_NONE, RTSS_FAIL_LIBRARY, RTSS_FAIL_READONLY, RTSS_FAIL_NOT_LIMITING }; // out of it for the run enum { RTSS_DO_NOTHING, RTSS_DO_APPLY, RTSS_DO_PUT_BACK }; @@ -83,17 +93,24 @@ enum { RTSS_WHY_NEWER, // the want changed while the poll ran: nothing decided, the next poll takes the new one RTSS_WHY_UNSET, // no want yet RTSS_WHY_MOVIE, // a movie plays: the game's pacer holds it at its rate and keeps its slots + RTSS_WHY_NOT_VULKAN, // RTSS lists the game but has not seen its Vulkan presents + RTSS_WHY_UPDATE, // RTSS has not read the profile the game wrote yet + RTSS_WHY_NOT_COUNTING, // RTSS has measured no period of the game's frames lately + RTSS_WHY_IDLE, // no picture went out lately (a minimised window, a frame hold): RTSS has none to hold }; // What the worker keeps between polls. typedef struct { - int64_t applied; // the millihertz the profile holds from this run; -1 untouched - int64_t refused; // a limit RTSS refused (out of its range): the game's own limiter keeps that one - int changed; // the profile holds a value from this run: put the old one back on exit - int over; // polls in a row RTSS's frame rate of the game stayed above the limit - int fail; // RTSS_FAIL_* + int64_t applied; // the millihertz the profile holds from this run; -1 untouched + int64_t refused; // a limit RTSS refused (out of its range): the game's own limiter keeps that one + int changed; // the profile holds a value from this run: put the old one back on exit + int over; // fresh periods in a row RTSS's frame rate of the game stayed above the limit + int fail; // RTSS_FAIL_* + int held; // the poll before stood aside: a fresh period now shows how RTSS holds the game + uint32_t t1, span; // the end and length (ms) of the newest period seen + uint64_t fresh_ns; // when a new period was last seen; 0 never } RtssState; -#define RTSS_STATE_INIT {-1, -1, 0, 0, RTSS_FAIL_NONE} +#define RTSS_STATE_INIT {-1, -1, 0, 0, RTSS_FAIL_NONE, 0, 0, 0, 0} // One poll's view. typedef struct { @@ -101,6 +118,8 @@ typedef struct { int current; // the want is still that one when the poll decides int hooks; // RTSS's hooks library: 1 loaded, 0 failed to load, -1 not tried (rtss_wants_hooks) int64_t movie_mhz; // the rate of a movie that plays (rtss_limit_movie), 0 none + uint64_t now_ns; // kern_now_ns at the poll + uint64_t hosted_ns; // kern_now_ns of the newest host present (gfx_present.c gfx_window_hosted_ns), 0 none yet RtssApp app; } RtssIn; @@ -128,39 +147,58 @@ static inline int rtss_wants_hooks(const RtssState *s, const RtssIn *in) { // The decision of one poll, in this order: no want does nothing; a kept-out want puts back what this run wrote (once // the hooks are loaded); a failure lasts the run; RTSS not running leaves the profile for its return; a library that // did not load fails the run; a refused limit stays with the game; a new limit (rtss_target) goes into the profile at -// once, and RTSS holds it as soon as it lists the game. On an unchanged limit RTSS's own frame count is checked: more -// than RTSS_CHECK_SLACK times the limit for RTSS_CHECK_POLLS polls in a row puts the profile back for the run. While a -// movie plays the game's pacer holds it (rtss_target). After the exit put the profile back (rtss_limit_restore) no poll -// decides: rtss_limit.c worker skips it under the lock. +// once. The game's limiter stands aside only once RTSS is seen to hold the game: the profile held the limit at the poll +// before (RTSS reads it after the write), RTSS lists the game with its Vulkan presents, has read the profile +// (RTSS_UPDATE_ASKED clear) and measured a period of the game's frames lately (a fresh one, rtss_fresh), no movie plays +// (rtss_target) and a picture went out within RTSS_HOLD_IDLE_NS. Each fresh period after a poll that stood aside is +// checked against the limit: more than RTSS_OVER_SLACK times it for RTSS_OVER_PERIODS periods in a row puts the profile +// back for the run; a stale period counts neither way. After the exit put the profile back (rtss_limit_restore) no +// poll decides: rtss_limit.c worker skips it under the lock. static inline RtssOut rtss_decide(RtssState *s, const RtssIn *in) { int64_t w = in->want; RtssOut o = {RTSS_DO_NOTHING, w, 0, RTSS_WHY_NOT_LISTED, 0}; if (w == RTSS_UNSET) return o.why = RTSS_WHY_UNSET, o; + const RtssApp *a = &in->app; + int fresh = a->listed && a->t1 > a->t0 && a->frames && a->t1 != s->t1; // a period RTSS measured since + if (fresh) s->t1 = a->t1, s->span = a->t1 - a->t0, s->fresh_ns = in->now_ns; + int held = s->held; + s->held = 0; // until this poll stands aside again if (w < 0) { if (in->hooks == 1 && s->changed) o.act = RTSS_DO_PUT_BACK; + s->over = 0; return o.why = RTSS_WHY_OUT, o; } if (s->fail) return o.why = RTSS_WHY_FAILED, o; - if (!in->app.running) return o.why = RTSS_WHY_NOT_RUNNING, o; + if (!a->running) return o.why = RTSS_WHY_NOT_RUNNING, o; if (in->hooks == 0) return s->fail = RTSS_FAIL_LIBRARY, o.why = RTSS_WHY_FAILED, o; int64_t t = o.mhz = rtss_target(w, in->movie_mhz); if (t == s->refused) return o.why = RTSS_WHY_REFUSED, o; - o.holds = in->app.listed; - o.why = o.holds ? RTSS_WHY_HOLDS : RTSS_WHY_NOT_LISTED; - if (in->movie_mhz) o.holds = 0, o.why = RTSS_WHY_MOVIE; - if (t != s->applied) { // the result of the write may still undo the hold (rtss_written) + if (t != s->applied) { // RTSS reads it after the write: a later poll sees whether it holds o.act = RTSS_DO_APPLY; + o.why = in->movie_mhz ? RTSS_WHY_MOVIE : a->listed ? RTSS_WHY_UPDATE : RTSS_WHY_NOT_LISTED; s->over = 0; return o; } - if (!in->current) return o.holds = 0, o.why = RTSS_WHY_NEWER, o; // these frames are not the new want's - double fps = rtss_fps(&in->app); - if (!in->app.listed || t == 0 || fps <= t / 1000.0 * RTSS_CHECK_SLACK) s->over = 0; - else if (++s->over == RTSS_CHECK_POLLS) { - s->fail = RTSS_FAIL_NOT_LIMITING; - o.act = RTSS_DO_PUT_BACK, o.holds = 0, o.why = RTSS_WHY_FAILED, o.fps = fps; + if (!in->current) return o.why = RTSS_WHY_NEWER, o; // these frames are not the new want's + if (!a->listed) return o.why = RTSS_WHY_NOT_LISTED, o; + if ((a->flags & RTSS_API_MASK) != RTSS_API_VULKAN) return o.why = RTSS_WHY_NOT_VULKAN, o; + if (a->flags & RTSS_UPDATE_ASKED) return o.why = RTSS_WHY_UPDATE, o; + uint64_t stale = (uint64_t)s->span * 2500000ull > RTSS_FRESH_NS ? (uint64_t)s->span * 2500000ull : RTSS_FRESH_NS; + if (!s->fresh_ns || in->now_ns - s->fresh_ns > stale) return o.why = RTSS_WHY_NOT_COUNTING, o; + if (fresh && held && t > 0) { + double fps = rtss_fps(a); + s->over = fps > t / 1000.0 * RTSS_OVER_SLACK ? s->over + 1 : 0; + if (s->over == RTSS_OVER_PERIODS) { + s->fail = RTSS_FAIL_NOT_LIMITING; + o.act = RTSS_DO_PUT_BACK, o.why = RTSS_WHY_FAILED, o.fps = fps; + return o; + } } - return o; + if (in->movie_mhz) return o.why = RTSS_WHY_MOVIE, o; + uint64_t idle = in->now_ns > in->hosted_ns ? in->now_ns - in->hosted_ns : 0; + if (!in->hosted_ns || idle >= RTSS_HOLD_IDLE_NS) return o.why = RTSS_WHY_IDLE, o; + s->held = o.holds = 1; + return o.why = RTSS_WHY_HOLDS, o; } // What a write of the limit did (RtssOps.apply). @@ -221,12 +259,17 @@ static inline void rtss_why_text(char *out, size_t n, const RtssOut *o, int fail else if (o->why == RTSS_WHY_NOT_RUNNING) snprintf(out, n, "RivaTuner Statistics Server is not running"); else if (o->why == RTSS_WHY_REFUSED) snprintf(out, n, "RivaTuner Statistics Server refused this limit"); else if (o->why == RTSS_WHY_MOVIE) snprintf(out, n, "a movie plays: the game keeps its pictures on time"); + else if (o->why == RTSS_WHY_NOT_VULKAN) + snprintf(out, n, "RivaTuner Statistics Server lists the game but has not seen its Vulkan presents yet"); + else if (o->why == RTSS_WHY_UPDATE) + snprintf(out, n, "RivaTuner Statistics Server has not read the game's limit from its profile yet"); + else if (o->why == RTSS_WHY_NOT_COUNTING) + snprintf(out, n, "RivaTuner Statistics Server has measured no frames of the game lately"); + else if (o->why == RTSS_WHY_IDLE) snprintf(out, n, "no picture went out lately: RivaTuner has none to hold"); else snprintf(out, n, "RivaTuner Statistics Server has not hooked the game yet"); } // ---- the game's pacer beside RTSS (frame_rate.c frame_step_cap_ns) -#define RTSS_HOLD_IDLE_NS 200000000ull // no host present for this long: RTSS holds nothing (vk_lowlat.c HOLD_IDLE_NS) - // The period the pacer keeps (now_ns and hosted_ns are kern_now_ns times; hosted_ns of the newest host present, // gfx_present.c gfx_window_hosted_ns, 0 before the first). The limit's own period while RTSS does not hold the game, // while a movie plays (from its first frame: the pacer keeps the movie's slots, which RTSS does not know, and the diff --git a/runtime/rtss_limit.c b/runtime/rtss_limit.c index 83e27f66..2b715562 100644 --- a/runtime/rtss_limit.c +++ b/runtime/rtss_limit.c @@ -13,11 +13,13 @@ // then fails silently. So every change is read back // from the saved profile, and a limit that did not stick leaves the game's own limiter in charge for the session. // -// RTSS runs when its shared memory "RTSSSharedMemoryV2" carries the signature 'RTSS'; it limits this process once the -// memory's application list holds this process id (RTSS hooks a process some seconds after it starts, its injection -// delay). Until then, and again if RTSS quits, the game's own limiter paces the frames. While RTSS limits, the frame -// rate RTSS itself measures for the game is checked against the limit: well above it for three seconds, and the game -// takes the limiting back for the rest of the run. +// RTSS runs when its shared memory "RTSSSharedMemoryV2" carries the signature 'RTSS'. The game's own limiter stands +// aside only once RTSS is seen to hold the game (rtss_decide.h rtss_decide): the memory's application list holds this +// process id with its Vulkan presents (RTSS hooks a process some seconds after it starts, its injection delay), RTSS +// has read the profile the game wrote, it measured a period of the game's frames lately, no movie plays and pictures +// go out. Until then, and again whenever one of these ends (RTSS quits, the window is minimised), the game's own limiter +// paces the frames. While RTSS limits, each new period RTSS measures for the game is checked against the limit: above +// 1.05 times it for three periods in a row, and the game takes the limiting back for the rest of the run. // // The calls run on a thread of their own, every half second, which also looks RTSS up (find_rtss), so a present never // waits on the registry or profile files. On every orderly exit (rtss_limit_restore) the profile gets back what it held @@ -40,6 +42,7 @@ #include #include +#include "gfx.h" #include "host.h" #include "platform/platform.h" #include "rtss_decide.h" @@ -328,17 +331,23 @@ static void put_back_op(void *ctx, const RtssOut *o) { // rtss_poll's RtssOps. } // Whether RTSS holds the game to the want w this poll read, and the log of which limiter is in use: the first decision, -// then each change. The want may have changed since the poll read it: rtss_limit_active compares the two, so a stale -// store never lets the game's limiter stand aside for a newer want. The store is made either way (a want that went -// w, w2, w again must not find an older hold), but a hold is logged only while its want is still the current one: the -// game's limiter paces a newer want, and the next poll logs what holds it. +// then each change of the limiter or of why the game's own one paces (RTSS not hooked yet, not read the profile, a +// minimised window), up to LIMITER_LINES lines. The want may have changed since the poll read it: rtss_limit_active +// compares the two, so a stale store never lets the game's limiter stand aside for a newer want. The store is made +// either way (a want that went w, w2, w again must not find an older hold), but a hold is logged only while its want +// is still the current one: the game's limiter paces a newer want, and the next poll logs what holds it. +#define LIMITER_LINES 100 static void set_active(int64_t w, int on, const char *why) { - static int logged = -1; + static int logged = -1, lines; + static char said[128]; atomic_store(&active_for, on ? w : RTSS_UNSET); - if (logged == on || (on && atomic_load(&want) != w)) return; + if ((on && atomic_load(&want) != w) || (logged == on && (on || !strcmp(why, said)))) return; logged = on; + snprintf(said, sizeof said, "%s", on ? "" : why); + if (++lines > LIMITER_LINES) return; if (on) rt_log("frame rate: RivaTuner Statistics Server does the limiting; the game's own limiter stands aside"); else rt_log("frame rate: the game's own limiter (%s)", why); + if (lines == LIMITER_LINES) rt_log("frame rate: later changes of the limiter in charge are not logged"); } // RTSS's own settings for the game, read from its profile file as RTSS keeps it: Profiles\.cfg, else @@ -473,7 +482,8 @@ static void worker(void *arg) { plat_mutex_unlock(&lock); continue; } - RtssIn in = {.want = w, .movie_mhz = atomic_load(&movie_mhz)}; + RtssIn in = {.want = w, .movie_mhz = atomic_load(&movie_mhz), .now_ns = kern_now_ns(), + .hosted_ns = gfx_window_hosted_ns()}; rtss_map(&in.app); if (in.app.running != was_running || in.app.listed != was_listed) { if (!in.app.running) rt_log("frame rate: RivaTuner Statistics Server is installed but not running"); diff --git a/tests/rtss_limit_check.c b/tests/rtss_limit_check.c index 0c74c279..038f9467 100644 --- a/tests/rtss_limit_check.c +++ b/tests/rtss_limit_check.c @@ -1,7 +1,6 @@ // 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, and the worker's decisions poll by poll against a -// fake profile. The cases pin the decisions as they stand, including those a pending change would alter (writing -// before RTSS lists the game, the frame count rule, a write per movie and per switch of low latency). Built and run by +// 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 #include @@ -68,8 +67,9 @@ static void shm_set(uint8_t *v, uint32_t sig, uint32_t ver, uint32_t esize, uint 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}; + 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); } @@ -83,6 +83,8 @@ static void parse(void) { 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"); @@ -145,34 +147,51 @@ static void f_put_back(void *ctx, const RtssOut *o) { 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; - uint32_t t0, t1, frames; + 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 = 1, r->t0 = 1000, r->t1 = 2000, r->frames = 60; + 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) { - RtssIn in = {.want = want, .current = current, .movie_mhz = r->movie_mhz}; - in.app = (RtssApp){r->running, r->running && r->listed, r->t0, r->t1, r->frames}; + 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 -// first poll, and RTSS holds the game once it lists it, with no second write. +// first poll, 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); @@ -182,9 +201,25 @@ static void injection_delay(void) { 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"); - r.listed = 1; + 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_HOLDS && r.st.applies == 1, "held once listed, no second write"); + 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. @@ -192,12 +227,14 @@ static void quit(void) { Run r; run_init(&r); step(&r, 60000); - r.running = 0; 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; - o = step(&r, 60000); + for (int i = 0; i < 2; i++) o = step(&r, 60000); CHECK(o.holds && r.st.applies == 1, "back: held with no new write"); } @@ -211,9 +248,7 @@ static void readonly(void) { 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"); - char t[128]; - rtss_why_text(t, sizeof t, &o, r.s.fail); - CHECK(!strcmp(t, "RivaTuner Statistics Server's profile cannot be changed"), "%s", t); + 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 @@ -229,7 +264,9 @@ static void refused(void) { 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_APPLY && o.holds && r.st.holds == 144000, "144 at once"); + CHECK(o.act == RTSS_DO_APPLY && !o.holds && r.st.holds == 144000, "144 at once"); + 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"); @@ -240,34 +277,32 @@ static void refused(void) { 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: more than 1.2 times the limit for six polls in a row on an unchanged limit, and the -// profile goes back for the run. The poll that writes the limit does not count; 1.15 times is not caught; a period -// RTSS no longer moves still counts; no limit and not listed never do. +// 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); - for (int i = 0; i < 5; i++) o = step(&r, 60000); - CHECK(o.holds && !r.s.fail && r.s.over == 5, "five polls over: still held"); - 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, - "the sixth gives up"); + "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; i++) o = step(&r, 60000); - CHECK(o.holds && !r.s.fail, "69 fps at 60 is not caught"); - r.frames = 73; - for (int i = 0; i < 6; i++) o = step(&r, 60000); - CHECK(r.s.fail == RTSS_FAIL_NOT_LIMITING, "73 fps at 60 is"); + 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 = 72; // 1.2 times the limit exactly + 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, "72 fps at 60 is not"); + 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); @@ -276,24 +311,51 @@ static void frame_count(void) { 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"); - r.listed = 1; - for (int i = 0; i < 3; i++) step(&r, 60000); - r.listed = 0; - step(&r, 60000); - CHECK(r.s.over == 0, "a poll not listed starts the count again"); + // a period RTSS no longer moves (the player lowered the limit and minimised the window): never counted run_init(&r); - r.frames = 144; // t0 and t1 never move: the same period every poll - for (int i = 0; i < 7; i++) step(&r, 60000); - CHECK(r.s.fail == RTSS_FAIL_NOT_LIMITING, "a period RTSS no longer moves still counts, as it stands"); + 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 < 6; 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; - step(&r, 60000); - step(&r, 60000); + for (int i = 0; i < 4; 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"); } +// 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. +static void idle(void) { + Run r; + run_init(&r); + r.presenting = 0; + step(&r, 60000); + RtssOut o = step(&r, 60000); + CHECK(!o.holds && o.why == RTSS_WHY_IDLE, "no host present yet"); + r.presenting = 1; + 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) { @@ -307,11 +369,11 @@ static void kept_out(void) { CHECK(o.act == RTSS_DO_NOTHING && r.st.put_backs == 1, "once"); o = step(&r, RTSS_OUT_LOWLAT); CHECK(o.act == RTSS_DO_NOTHING && o.mhz == RTSS_OUT_LOWLAT, "a second reason: nothing more"); - char t[128]; - rtss_why_text(t, sizeof t, &o, r.s.fail); - CHECK(!strcmp(t, "RivaTuner Statistics Server kept out: the driver's low latency"), "%s", t); + WHY(o, r, "RivaTuner Statistics Server kept out: the driver's low latency"); o = step(&r, 60000); - CHECK(o.act == RTSS_DO_APPLY && o.holds && r.st.firsts == 2, "written at once, the run's first write again"); + CHECK(o.act == RTSS_DO_APPLY && !o.holds && r.st.firsts == 2, "written at once, 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); @@ -339,9 +401,7 @@ static void movie(void) { 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"); - char t[128]; - rtss_why_text(t, sizeof t, &o, r.s.fail); - CHECK(!strcmp(t, "a movie plays: the game keeps its pictures on time"), "%s", t); + 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"); @@ -355,8 +415,10 @@ static void movie(void) { 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, + 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"); 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"); @@ -373,9 +435,7 @@ static void unset_and_library(void) { 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"); - char t[128]; - rtss_why_text(t, sizeof t, &o, r.s.fail); - CHECK(!strcmp(t, "RivaTuner Statistics Server's hooks library did not load"), "%s", t); + 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"); @@ -384,8 +444,7 @@ static void unset_and_library(void) { r.running = 0; o = step(&r, 60000); CHECK(!r.st.loaded && o.why == RTSS_WHY_NOT_RUNNING, "not loaded while RTSS does not run"); - rtss_why_text(t, sizeof t, &o, r.s.fail); - CHECK(!strcmp(t, "RivaTuner Statistics Server is not running"), "%s", t); + 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 @@ -489,6 +548,7 @@ int main(void) { readonly(); refused(); frame_count(); + idle(); kept_out(); churn(); movie(); diff --git a/tests/rtss_worker_check.c b/tests/rtss_worker_check.c index 477267c4..e6b8b347 100644 --- a/tests/rtss_worker_check.c +++ b/tests/rtss_worker_check.c @@ -127,6 +127,25 @@ static void app_set(int listed, DWORD flags, DWORD t0, DWORD t1, DWORD frames) { memcpy(e + 4 + MAX_PATH, &flags, 4); memcpy(e + 4 + MAX_PATH + 4, t, sizeof t); } +static void app_flags(DWORD flags) { memcpy(shm + ENTRY_AT + 4 + MAX_PATH, &flags, 4); } +static void app_frames(DWORD frames) { memcpy(shm + ENTRY_AT + 4 + MAX_PATH + 4 + 8, &frames, 4); } + +// RTSS's clock and the game's presents as rtss_limit.c reads them: each poll() comes 500 ms after the one before, RTSS +// measures a new period of the game's frames at each (counting), and the game presents a millisecond before it unless +// a scenario stops that (presenting). +static uint64_t fake_now = 10000000000ull, hosted_at; +static int counting = 1, presenting = 1; +uint64_t kern_now_ns(void) { return fake_now; } +uint64_t gfx_window_hosted_ns(void) { return presenting ? fake_now - 1000000 : hosted_at; } +static void time_passes(void) { + if (presenting) hosted_at = fake_now - 1000000; + fake_now += 500000000ull; + if (!counting) return; + DWORD t[2]; + memcpy(t, shm + ENTRY_AT + 4 + MAX_PATH + 4, sizeof t); + t[0] += 500, t[1] += 500; + memcpy(shm + ENTRY_AT + 4 + MAX_PATH + 4, t, sizeof t); +} // The registry and file calls the thread that asks for a limit makes itself: a present must make none. static DWORD present_tid; @@ -348,6 +367,7 @@ static void prof_load(void) { // ---- the scenarios' steps static int wait_done(void) { return WaitForSingleObject(done, 5000) == WAIT_OBJECT_0; } static void poll(void) { // one more poll of the worker, waited for + time_passes(); SetEvent(tick); CHECK(wait_done(), "the worker did not finish a poll"); } @@ -482,7 +502,8 @@ static void kept_lowlat_then_limit(void) { poll(); show("low latency off, limit 60"); CHECK(loads == 1 && own.exists && own.limit == 60, "the limit goes into the profile once RTSS is let in"); - CHECK(rtss_limit_state() == 1 && rtss_limit_active(), "RTSS limits the game"); + poll(); + CHECK(rtss_limit_state() == 1 && rtss_limit_active(), "RTSS limits the game from the next poll"); } // Frame generation keeps RTSS out, with no limit of RTSS's own: one limiter, and the line names frame generation. @@ -545,6 +566,7 @@ static void active_logged(void) { CHECK(!logged("does the limiting"), "still no line says RTSS does the limiting"); ask(60000); poll(); + poll(); show("frame generation off"); CHECK(rtss_limit_active() && logged("does the limiting; the game's own limiter stands aside"), "held and logged"); } @@ -703,22 +725,23 @@ static void refused_run(void) { ask(144000); poll(); show("144 after the refusal"); - CHECK(own.limit == 144 && rtss_limit_active(), "144 taken at once"); + CHECK(own.limit == 144, "144 written at once"); + poll(); + CHECK(rtss_limit_active(), "and held from the next poll"); ask(240000); poll(); CHECK(!rtss_limit_active(), "240 stays with the game"); } -// RTSS lists the game but lets it run well over the limit: after six polls in a row over 1.2 times the limit the -// profile is put back and the game limits for the run. 1.15 times the limit is not caught (the rule as it stands), -// and a period RTSS no longer moves still counts. +// RTSS lists the game but lets it run well over the limit: three fresh periods in a row over 1.05 times the limit, each +// after a poll that stood aside, put the profile back and the game limits for the run. static void over_run(void) { app_set(1, 0xA, 1000, 2000, 144); ask(60000); - for (int i = 0; i < 5; i++) poll(); - CHECK(rtss_limit_active() && own.exists, "five polls over: RTSS still limits"); + for (int i = 0; i < 3; i++) poll(); + CHECK(rtss_limit_active() && own.exists, "two periods over: RTSS still limits"); poll(); - show("six polls at 144 fps with the limit at 60"); + show("three periods at 144 fps with the limit at 60"); CHECK(rtss_limit_state() == 3 && !rtss_limit_active(), "the menu says RTSS does not limit the game"); CHECK(!own.exists, "the profile is put back"); CHECK(logged("RivaTuner Statistics Server measures 144.0 fps with its limit at 60.000 fps"), "logged"); @@ -726,15 +749,63 @@ static void over_run(void) { poll(); CHECK(saves == 1 && rtss_limit_state() == 3, "out for the run"); } +// 1.05 times the limit is not caught; a period RTSS no longer moves (the limit lowered, then the window minimised) +// counts neither way, and the game's limiter holds meanwhile; 1.15 times the limit is caught. static void over_slack_run(void) { - app_set(1, 0xA, 1000, 2000, 69); + app_set(1, 0xA, 1000, 2000, 63); ask(60000); for (int i = 0; i < 20; i++) poll(); - show("20 polls at 69 fps with the limit at 60"); - CHECK(rtss_limit_active() && rtss_limit_state() == 1, "1.15 times the limit is not caught"); - app_set(1, 0xA, 1000, 2000, 73); + show("20 polls at 63 fps with the limit at 60"); + CHECK(rtss_limit_active() && rtss_limit_state() == 1, "1.05 times the limit is not caught"); + counting = 0; + app_frames(144); + for (int i = 0; i < 20; i++) poll(); + show("a period frozen at 144 fps for 20 polls"); + CHECK(!rtss_limit_active() && rtss_limit_state() != 3 && own.exists && own.limit == 60, "never counted"); + CHECK(logged("the game's own limiter (RivaTuner Statistics Server has measured no frames of the game lately)"), + "logged"); + counting = 1; + app_frames(69); for (int i = 0; i < 6; i++) poll(); - CHECK(rtss_limit_state() == 3, "1.22 times the limit is caught after six polls"); + CHECK(rtss_limit_state() == 3, "1.15 times the limit is caught"); +} + +// RTSS lists the game before it has seen a Vulkan present, and asks the game to read its profile again after each +// write: the game's limiter holds until both are done. +static void api_run(void) { + app_set(1, 0, 1000, 2000, 60); + ask(60000); + for (int i = 0; i < 3; i++) poll(); + show("listed, no API yet"); + CHECK(!rtss_limit_active() && own.limit == 60, "the game's limiter"); + CHECK(logged("the game's own limiter (RivaTuner Statistics Server lists the game but has not seen its Vulkan " + "presents yet)"), + "logged"); + app_flags(0xA | 0x10000000u); + poll(); + CHECK(!rtss_limit_active() && logged("has not read the game's limit from its profile yet"), "the update asked"); + app_flags(0xA); + poll(); + show("Vulkan, profile read"); + CHECK(rtss_limit_active() && saves == 1, "RTSS limits, no second write"); +} + +// No picture goes out (a minimised window): RTSS has nothing to hold, and the game's limiter holds until pictures go +// out again; the profile stays as it is. +static void idle_run(void) { + app_set(1, 0xA, 1000, 2000, 60); + ask(60000); + poll(); + CHECK(rtss_limit_active(), "RTSS limits"); + presenting = 0; + poll(); + show("minimised"); + CHECK(!rtss_limit_active() && logged("the game's own limiter (no picture went out lately: RivaTuner has none to hold)"), + "the game's limiter"); + presenting = 1; + poll(); + show("back"); + CHECK(rtss_limit_active() && saves == 1, "RTSS again, no write"); } static void over_unlimited_run(void) { app_set(1, 0xA, 1000, 2000, 700); @@ -783,6 +854,7 @@ static void churn_run(void) { poll(); } show("ten polls with the want flipping"); + poll(); CHECK(created == 6 && removed == 5, "%ld creations, %ld removals", created, removed); CHECK(logged("created; it goes away when the game closes (creation 1 this run)\n") && logged("(creation 6 this run)\n") && !logged("(creation 7"), @@ -967,7 +1039,9 @@ static void movie_low_run(void) { rtss_limit_movie(0); poll(); show("after the movie"); - CHECK(own.limit == 40 && rtss_limit_active() && saves - s0 == 2, "40 back: two writes for the movie"); + CHECK(own.limit == 40 && saves - s0 == 2, "40 back: two writes for the movie"); + poll(); + CHECK(rtss_limit_active(), "RTSS holds 40 again"); } static const struct { const char *name; void (*run)(void); } scenarios[] = { @@ -993,6 +1067,8 @@ static const struct { const char *name; void (*run)(void); } scenarios[] = { {"over_run", over_run}, {"over_slack_run", over_slack_run}, {"over_unlimited_run", over_unlimited_run}, + {"api_run", api_run}, + {"idle_run", idle_run}, {"quit_run", quit_run}, {"library_run", library_run}, {"churn_run", churn_run},