Give up on RivaTuner Statistics Server for the run when it hooks the game with the limit for 30 s and never shows it holds it.

The review found that nothing ended an RTSS hold that was never proven: when RTSS listed the game with the limit in
its profile but one sign never came (presents of another API value than Vulkan's, the update flag never cleared, a
period that never moved), the profile kept the limit and the game's pacer held the frames too for the whole run, two
limiters where the pacing rules allow one, with only a reason line in the log and nothing in the menu. Before the
hold had to be proven, RTSS alone held the game once it listed it. rtss_decide.h rtss_decide now counts the polls in a
row where RTSS lists the game, the profile holds a limit of this run (not "none"), a picture went out within 200 ms
and the hold is not shown; at RTSS_UNPROVEN_POLLS (60, 30 s) it puts the profile back and RTSS is out for the run
(RTSS_FAIL_UNPROVEN, "RivaTuner Statistics Server never showed it holds the game"). A minimised window, RTSS not
hooked, a want of no limit, a kept-out want or a poll that shows the hold start the count again. rtss_limit.c logs the
give-up, rtss_limit_state returns 3 for it, so the menu says RivaTuner does not limit the game and the game does, and
rtss_out_of_limit counts it, so a limit set in RTSS itself is reported as a second limiter. The comment above
rtss_decide, rewritten for the rule, is wrapped at 120 columns and no longer cites rtss_fresh, which does not exist.
tests/rtss_limit_check.c checks each sign that never comes (given up at the 60th poll, once, for the run), the polls
that do not count and a shown hold restarting the count; tests/rtss_worker_check.c unproven_run checks it against the
real worker. docs/architecture.md, docs/building.md and nx2navi.ini.example describe the rule and the remaining window
of two limiters before it.

This changes when the game takes the limit back from RTSS, beyond the approved rules: an RTSS whose hold never shows
in its shared memory (a version or setting that differs) is now left out after 30 s and the game's limiter holds
alone, as with rtss = 0, where it held the frames beside the game's limiter before. Not reproduced before the change.
Checks that passed: tests/rtss_limit_check.py (41 of 41), pass_cc_check.py --wall on rtss_limit.c,
tools/check_content.py --staged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 19:46:48 -05:00
parent a7b4f0c7cb
commit 1744681f1a
8 changed files with 164 additions and 35 deletions

View File

@@ -260,16 +260,20 @@ keeps per-core data without locks and relies on that, so the host models it (`ru
The pacer is the fallback wherever the other limiter's hold is not proven: before the first host present, while a
movie plays (the pacer keeps the movie's slots, which no other limiter knows), once no picture went out for 200 ms (a
minimised window, `frame_hold`), for the rest of the run once the frames show the other limiter letting them through
faster than the limit, and after any failure. While RTSS holds the limit the pacer keeps only the 240-frame ceiling
faster than the limit or once it was not seen holding them for 30 s, and after any failure. While RTSS holds the
limit the pacer keeps only the 240-frame ceiling
the game's code needs (`rtss_decide.h` `rtss_pace_period`). Frame generation keeps RTSS out (it would count the
generated frames), and the game's pacer holds the drawn frames beside it. The exceptions as they stand: `pace_smooth`
still waits on the present thread while RTSS holds the limit; DLSS Frame Generation's Reflex sleeps where the frame
starts while the pacer holds the limit; at a play limit equal to a movie's rate (60, the default, for the game's 60
fps movies) RTSS's profile keeps that limit and RTSS holds the movie beside the pacer; and a limit set in RTSS itself
fps movies) RTSS's profile keeps that limit and RTSS holds the movie beside the pacer; once RTSS hooks the game with
the limit in its profile it holds the frames beside the pacer until it is seen holding them, for 30 s at most, after
which the profile goes back for the run; and a limit set in RTSS itself
still holds the game where the game keeps RTSS out, which the log ("two limiters") and the
frame rate row of the menu point out, and the same where RTSS is out of it for the run because its profile cannot be
written or its hooks library does not load, which the log points out the same way (the menu shows the administrator
note for the first and nothing for the second).
written, its hooks library does not load or it never showed its hold, which the log points out the same way (the
menu shows the administrator note for the first, nothing for the second and that RivaTuner does not limit the game
for the third).
- RivaTuner Statistics Server (`rtss_limit.c`, `NX2NAVI_RTSS`, restart; its decisions in `rtss_decide.h`): the game's
first present starts a thread that looks RTSS up (`InstallDir` under `HKLM\SOFTWARE\WOW6432Node\Unwinder\RTSS`,
`RTSSHooks64.dll`) and reads its shared memory (`RTSSSharedMemoryV2`) every 500 ms. While RTSS runs, the limit goes
@@ -285,8 +289,10 @@ keeps per-core data without locks and relies on that, so the host models it (`ru
frames lately, no movie plays and a picture went out within 200 ms. The pacer keeps the limit while RTSS does not run
or has not hooked the game, for a limit RTSS refuses, and for the rest of the run when the profile cannot be written
(no administrator rights), the hooks library does not load, or RTSS's own frame count of the game is above 1.05
times the limit in three new periods in a row (a period RTSS no longer moves counts neither way); the menu names the
first and the last of these. Frame generation, the
times the limit in three new periods in a row (a period RTSS no longer moves counts neither way), or RTSS lists the
game with the limit while pictures go out for 60 polls in a row (30 s) and never shows its hold (no Vulkan presents
seen, the profile not read, no period measured); the menu names the first and the last two of these. Frame
generation, the
driver's low latency and `NX2NAVI_RTSS=0` keep RTSS out (`RTSS_OUT_*`): the profile gets back what it held before
the run changed it, and while RTSS is kept out its thread does not load the hooks library (but for the one restore
below at the start) and only reads RTSS's own settings for the game; a later limit (frame generation or low latency

View File

@@ -488,8 +488,9 @@ memory against a fake `RTSSSharedMemoryV2` (a bad header, short entries, an entr
find nothing; the flags and the period where RTSS keeps them) and the worker's decisions poll by poll against a fake
profile and a fake RTSS clock: RTSS's injection delay through listed with no API, the profile not read yet and no
period measured, RTSS quitting and coming back, a read-only profile, a refused limit, the frame count rule (1.05 times
the limit over three new periods, a frozen period never counted), no picture going out, kept-out wants and their
reasons, a want that must stay two polls and low latency switching at every poll, a movie at 144 and at 40, a closed
the limit over three new periods, a frozen period never counted), an RTSS that lists the game and never shows its
hold (given up after 30 s, a minimised window not counted), no picture going out, kept-out wants and their
reasons, a want that must stay two polls and low latency switching at every poll, a movie at 144, 60 and 40, a closed
run, a hooks library that does not load, and two limiters. It also checks the pacer's period beside RTSS
(`rtss_pace_period`), the limit's rounding without a denominator, and the state file `rtss-profile.txt`: its lines,
its rewrite and what a start does with a line a run that did not close left. Statically, a crash and a fatal error

View File

@@ -29,8 +29,9 @@
# (rtss-profile.txt next to the game keeps what it held). Changing that profile needs the game run as administrator
# (RTSS keeps its profiles under Program Files). Without it the game limits by itself, as it does with frame generation
# on (RTSS would count the generated frames), while the window is minimised, and for the rest of the run when RTSS's
# own frame counter shows the game past the limit (RTSS then hooks the game without holding it back); the game paces
# movies itself. With low latency on, the driver holds the limit. rtss = 0 always leaves it to the game. With rtss = 0,
# own frame counter shows the game past the limit (RTSS then hooks the game without holding it back) or RTSS is not
# seen holding the game within 30 s; the game paces movies itself. With low latency on, the driver holds the limit.
# rtss = 0 always leaves it to the game. With rtss = 0,
# frame generation or low latency, a limit set in RTSS itself (its global profile or the game's, a frame time limit or
# scanline sync) limits the game a second time: the log and the settings menu say so, and setting it to 0 in RTSS
# leaves one limiter.

View File

@@ -469,8 +469,8 @@ void rtss_limit_want(int64_t mhz); // the pacer's limit i
void rtss_limit_movie(int hz); // the rate of a movie that plays, 0 none: the pacer holds it
int rtss_limit_active(void); // RTSS limits to the wanted rate: the pacer keeps the ceiling
int rtss_limit_state(void); // menu: 1 RTSS limits, 2 its profile is read-only, 3 it let the
// game run past its limit, 4 kept out while a limit set in
// RTSS itself holds the game too
// game run past its limit or never showed it holds it, 4 kept
// out while a limit set in RTSS itself holds the game too
void rtss_limit_restore(void); // before the process ends: the profile's old limit back
void rtss_limit_crash(void); // a crash or fatal error: no call into RTSS, the next start
// puts the profile back (rtss-profile.txt)

View File

@@ -81,8 +81,15 @@ static inline double rtss_fps(const RtssApp *a) {
#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)
#define RTSS_SETTLE_POLLS 2 // polls in a row a want must stay before it acts on the profile
#define RTSS_UNPROVEN_POLLS 60 // polls (30 s) RTSS may hook the game with the limit and not show it holds it
enum { RTSS_FAIL_NONE, RTSS_FAIL_LIBRARY, RTSS_FAIL_READONLY, RTSS_FAIL_NOT_LIMITING }; // out of it for the run
enum { // out of it for the run
RTSS_FAIL_NONE,
RTSS_FAIL_LIBRARY, // its hooks library did not load
RTSS_FAIL_READONLY, // its profile cannot be written
RTSS_FAIL_NOT_LIMITING, // its frame count showed the game past the limit
RTSS_FAIL_UNPROVEN, // it hooked the game with the limit for RTSS_UNPROVEN_POLLS and never showed it holds it
};
enum { RTSS_DO_NOTHING, RTSS_DO_APPLY, RTSS_DO_PUT_BACK };
enum {
RTSS_WHY_HOLDS, // RTSS holds the game to the want
@@ -114,8 +121,9 @@ typedef struct {
uint64_t fresh_ns; // when a new period was last seen; 0 never
int64_t seen; // the want of the poll before
int polls; // polls in a row with that want
int unproven; // polls in a row RTSS hooked the game with a limit in the profile and did not show its hold
} RtssState;
#define RTSS_STATE_INIT {-1, -1, 0, 0, RTSS_FAIL_NONE, 0, 0, 0, 0, RTSS_UNSET, 0}
#define RTSS_STATE_INIT {-1, -1, 0, 0, RTSS_FAIL_NONE, 0, 0, 0, 0, RTSS_UNSET, 0, 0}
// One poll's view.
typedef struct {
@@ -170,13 +178,17 @@ static inline int rtss_wants_hooks(const RtssState *s, const RtssIn *in) {
// (rtss_limit_active). A settled 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;
// nothing is written before the first picture went out; a refused limit stays with the game; a settled new limit
// (rtss_target) goes into the profile. 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.
// (rtss_target) goes into the profile. 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: its end, dwTime1, moved within RTSS_FRESH_NS, or within 2.5 times the period's length if that is longer), no
// movie plays (rtss_target) and a picture went out within RTSS_HOLD_IDLE_NS. An RTSS that lists the game with a limit
// in its profile while pictures go out, but does not show its hold (another API's presents, the profile never read, no
// period measured), may hold the frames beside the game's limiter: after RTSS_UNPROVEN_POLLS such polls in a row the
// profile goes back for the run.
// 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};
@@ -184,8 +196,8 @@ static inline RtssOut rtss_decide(RtssState *s, const RtssIn *in) {
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
int held = s->held, unproven = s->unproven;
s->held = s->unproven = 0; // until this poll stands aside again, or counts as unproven again
s->polls = w == s->seen ? s->polls + 1 : 1;
s->seen = w;
int settled = s->polls >= RTSS_SETTLE_POLLS;
@@ -208,11 +220,21 @@ static inline RtssOut rtss_decide(RtssState *s, const RtssIn *in) {
return o;
}
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 idle = in->now_ns > in->hosted_ns ? in->now_ns - in->hosted_ns : 0;
int shown = idle < RTSS_HOLD_IDLE_NS; // a picture went out lately (one went out: RTSS_WHY_NO_PRESENT above)
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;
int unseen = !a->listed ? RTSS_WHY_NOT_LISTED
: (a->flags & RTSS_API_MASK) != RTSS_API_VULKAN ? RTSS_WHY_NOT_VULKAN
: a->flags & RTSS_UPDATE_ASKED ? RTSS_WHY_UPDATE
: !s->fresh_ns || in->now_ns - s->fresh_ns > stale ? RTSS_WHY_NOT_COUNTING
: RTSS_WHY_HOLDS;
if (unseen != RTSS_WHY_HOLDS) { // hooked with a limit while pictures go out, RTSS holds them beside the pacer
s->unproven = a->listed && t > 0 && shown ? unproven + 1 : 0;
if (s->unproven < RTSS_UNPROVEN_POLLS) return o.why = unseen, o;
s->fail = RTSS_FAIL_UNPROVEN;
o.act = RTSS_DO_PUT_BACK, o.why = RTSS_WHY_FAILED;
return o;
}
if (fresh && held && t > 0) {
double fps = rtss_fps(a);
s->over = fps > t / 1000.0 * RTSS_OVER_SLACK ? s->over + 1 : 0;
@@ -223,8 +245,7 @@ static inline RtssOut rtss_decide(RtssState *s, const RtssIn *in) {
}
}
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;
if (!shown) return o.why = RTSS_WHY_IDLE, o;
s->held = o.holds = 1;
return o.why = RTSS_WHY_HOLDS, o;
}
@@ -277,6 +298,7 @@ static inline RtssOut rtss_poll(RtssState *s, const RtssIn *in, const RtssOps *o
static inline const char *rtss_fail_text(int fail) {
return fail == RTSS_FAIL_READONLY ? "RivaTuner Statistics Server's profile cannot be changed"
: fail == RTSS_FAIL_NOT_LIMITING ? "RivaTuner Statistics Server does not hold the game to the limit"
: fail == RTSS_FAIL_UNPROVEN ? "RivaTuner Statistics Server never showed it holds the game"
: "RivaTuner Statistics Server's hooks library did not load";
}
@@ -435,10 +457,12 @@ typedef struct {
static inline int rtss_settings_limit(const RtssSettings *s) { return s->limit || s->time || s->scan0 || s->scan1; }
// RTSS has no part in the game's limit although it may list the game: the want keeps it out, or it is out for the run
// because its profile cannot be written or its hooks library did not load. A run whose frame count check gave up on
// RTSS does not count: RTSS was seen not to hold the game.
// because its profile cannot be written, its hooks library did not load or it never showed it holds the game (its
// profile put back, a setting of its own may still hold the game). A run whose frame count check gave up on RTSS does
// not count: RTSS was seen not to hold the game.
static inline int rtss_out_of_limit(int64_t want, int fail) {
return want != RTSS_UNSET && (want < 0 || fail == RTSS_FAIL_READONLY || fail == RTSS_FAIL_LIBRARY);
return want != RTSS_UNSET &&
(want < 0 || fail == RTSS_FAIL_READONLY || fail == RTSS_FAIL_LIBRARY || fail == RTSS_FAIL_UNPROVEN);
}
// Two limiters: RTSS is out of the game's limit (rtss_out_of_limit), and while it lists the game a setting of its own

View File

@@ -20,7 +20,9 @@
// 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.
// 1.05 times it for three periods in a row, and the game takes the limiting back for the rest of the run. So it does
// when RTSS lists the game with the limit for 30 s while pictures go out and never shows its hold: RTSS may then hold
// the frames beside the game's limiter.
//
// 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
@@ -348,9 +350,13 @@ static int left_start(int64_t w, int hooks) {
static void put_back_op(void *ctx, const RtssOut *o) { // rtss_poll's RtssOps.put_back
(void)ctx;
if (o->why == RTSS_WHY_FAILED) // the frame count check gave up on RTSS
if (o->why == RTSS_WHY_FAILED && S.fail == RTSS_FAIL_NOT_LIMITING) // the frame count check gave up on RTSS
rt_log("frame rate: RivaTuner Statistics Server measures %.1f fps with its limit at %.3f fps", o->fps,
o->mhz / 1000.0);
else if (o->why == RTSS_WHY_FAILED) // the lines before say which sign of its hold never came
rt_log("frame rate: RivaTuner Statistics Server listed the game with its limit at %.3f fps for %d s and never "
"showed it holds it; the game's own limiter for the rest of the run", o->mhz / 1000.0,
RTSS_UNPROVEN_POLLS * POLL_MS / 1000);
put_back();
}
@@ -557,7 +563,8 @@ int rtss_limit_state(void) {
if (atomic_load_explicit(&closed, memory_order_relaxed)) return 0;
int f = atomic_load_explicit(&failed, memory_order_relaxed);
int64_t w = atomic_load(&want);
if (w >= 0 && (f == RTSS_FAIL_READONLY || f == RTSS_FAIL_NOT_LIMITING)) return f == RTSS_FAIL_READONLY ? 2 : 3;
if (w >= 0 && (f == RTSS_FAIL_READONLY || f == RTSS_FAIL_NOT_LIMITING || f == RTSS_FAIL_UNPROVEN))
return f == RTSS_FAIL_READONLY ? 2 : 3;
if (w < 0 && w != RTSS_UNSET && atomic_load_explicit(&two, memory_order_relaxed)) return 4;
return rtss_limit_active() && w > 0;
}

View File

@@ -342,6 +342,72 @@ static void frame_count(void) {
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) {
@@ -613,6 +679,7 @@ int main(void) {
readonly();
refused();
frame_count();
unproven();
idle();
kept_out();
churn();

View File

@@ -795,6 +795,28 @@ static void api_run(void) {
CHECK(rtss_limit_active() && saves == 1, "RTSS limits, no second write");
}
// RTSS lists the game with the limit and pictures go out, but it never reads the profile again (its update flag stays
// set): after 30 s the profile goes back, the menu says RTSS does not limit the game, and the game's limiter holds for
// the run.
static void unproven_run(void) {
app_set(1, 0xA | 0x10000000u, 1000, 2000, 60);
ask(60000);
poll();
CHECK(own.exists && own.limit == 60, "written");
for (int i = 0; i < RTSS_UNPROVEN_POLLS - 1; i++) poll();
show("29.5 s with the update flag set");
CHECK(own.exists && rtss_limit_state() == 0 && !rtss_limit_active(), "not yet");
poll();
show("30 s");
CHECK(!own.exists && rtss_limit_state() == 3 && !rtss_limit_active(), "put back; the menu's note");
CHECK(logged("RivaTuner Statistics Server listed the game with its limit at 60.000 fps for 30 s and never showed "
"it holds it; the game's own limiter for the rest of the run"),
"logged");
CHECK(logged("the game's own limiter (RivaTuner Statistics Server never showed it holds the game)"), "the reason");
settle(144000);
CHECK(saves == 1 && !own.exists && rtss_limit_state() == 3, "out for the run");
}
// 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) {
@@ -1113,6 +1135,7 @@ static const struct { const char *name; void (*run)(void); } scenarios[] = {
{"over_slack_run", over_slack_run},
{"over_unlimited_run", over_unlimited_run},
{"api_run", api_run},
{"unproven_run", unproven_run},
{"idle_run", idle_run},
{"quit_run", quit_run},
{"library_run", library_run},