Let the game's limiter stand aside only once RivaTuner Statistics Server is seen holding the game, and give up on RTSS at 1.05 times the limit over three fresh periods.

The worker stood aside as soon as RTSS listed the process and the profile read back, before RTSS had seen a Vulkan
present (with InjectionDelay=15000 it lists the game some seconds after start) or read the new profile. Its frame
count check took the newest period whatever its age: a period frozen at 144 fps after the limit went to 60 and the
window was minimised failed RTSS for the run, and 1.15 times the limit was never caught (lane A's probe: listed_no_api,
stale_fps, over115). rtss_decide now parses the entry's flags and stands aside only when the profile held the target
at the poll before, RTSS lists the game with the Vulkan API value (flags & 0xFFFF == 0xA), RTSS_UPDATE_ASKED is clear,
a new period was seen within max(2.5 s, 2.5 times its length), no movie plays and a host present went out within
200 ms. Only a new period after a poll that stood aside is checked: three in a row over 1.05 times the limit put the
profile back for the run, and a stale period counts neither way. The worker reads the poll time and the newest host
present (gfx_window_hosted_ns), and logs each change of why the game's limiter paces, up to 100 lines.

Not reproduced before the change. rtss_limit_check (the flags at +264; T2 injection delay through not listed, no API,
the update flag, no period and holding; T3 quit; T6 the frame count at 144, 69, 63 fps, a frozen period and a newer
want; the idle case) and rtss_worker_check (over_run, over_slack_run, new api_run and idle_run, and every scenario that
now holds from the poll after the write) pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 19:13:36 -05:00
parent 04f4d44747
commit f7899e5443
4 changed files with 298 additions and 109 deletions

View File

@@ -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

View File

@@ -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 <stdlib.h>
#include <string.h>
#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\<exe>.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");

View File

@@ -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 <stdint.h>
#include <stdio.h>
@@ -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();

View File

@@ -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},