Let a want reach RivaTuner Statistics Server's profile only once it stayed two polls, and write nothing before the first host present.

A want that flips between a limit and kept-out created and removed RTSS's profile at each flip: with a driver's low
latency vk_lowlat_on() is 0 before the first swapchain and during every swapchain rebuild, so a warm start without the
boot screen wrote the profile and removed it a poll later, and a poll that fell into a rebuild did the same (lane A's
probe: 6 creations and 5 removals for a want flipping each poll, 1 and 1 at start before RTSS hooked the game).
rtss_decide now counts the polls in a row a want stayed: a limit goes into the profile and a kept-out want puts it
back only after RTSS_SETTLE_POLLS (2), while standing aside still ends at once (rtss_limit_active compares the want),
and nothing is written before the first host present (RTSS_WHY_NO_PRESENT). A limit change reaches the profile up to a
second later; the game's limiter holds the new limit meanwhile. The created and removed lines were numbered before.

Not reproduced before the change. rtss_limit_check (T8: kept out for one poll and for two, a second reason, a limit
again; T9: 20 flips after a written limit give no write or put back, 20 from the start none, flips every two polls
act; no write while no host present) and rtss_worker_check (churn_run and every scenario that now settles) pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 19:16:41 -05:00
parent f7899e5443
commit 4d4b97b043
3 changed files with 117 additions and 51 deletions

View File

@@ -190,14 +190,17 @@ static void why_is(const RtssOut *o, const RtssState *s, const char *want, int l
#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, 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.
// second poll (the first sees the want new), with no second write. Once RTSS lists the game the game's limiter stands
// aside only after RTSS has seen its Vulkan presents, read the profile and measured a period of its frames lately.
static void injection_delay(void) {
Run r;
run_init(&r);
r.listed = 0;
RtssOut o = step(&r, 60000);
CHECK(o.act == RTSS_DO_APPLY && !o.holds && o.why == RTSS_WHY_NOT_LISTED, "written at once, not held yet");
CHECK(o.act == RTSS_DO_NOTHING && !o.holds && o.why == RTSS_WHY_SETTLING, "a new want: not written yet");
WHY(o, r, "a new limit: RivaTuner Statistics Server gets it once it stays");
o = step(&r, 60000);
CHECK(o.act == RTSS_DO_APPLY && !o.holds && o.why == RTSS_WHY_NOT_LISTED, "written, not held yet");
CHECK(r.st.firsts == 1 && r.s.applied == 60000 && r.s.changed, "the first write of the run");
for (int i = 0; i < 30; i++) o = step(&r, 60000);
CHECK(r.st.applies == 1 && !o.holds && o.why == RTSS_WHY_NOT_LISTED, "no write while not listed");
@@ -227,6 +230,7 @@ static void quit(void) {
Run r;
run_init(&r);
step(&r, 60000);
step(&r, 60000);
RtssOut o = step(&r, 60000);
CHECK(o.holds, "held");
r.running = 0;
@@ -243,6 +247,7 @@ static void readonly(void) {
Run r;
run_init(&r);
r.st.readonly = 1;
step(&r, 60000);
RtssOut o = step(&r, 60000);
CHECK(o.act == RTSS_DO_APPLY && !o.holds && o.why == RTSS_WHY_FAILED && r.s.fail == RTSS_FAIL_READONLY, "failed");
CHECK(!r.s.changed, "nothing of the run in the profile");
@@ -252,27 +257,32 @@ static void readonly(void) {
}
// A limit out of RTSS's range: refused and kept by the game; the earlier limit of this run is cleared to none; a limit
// in range goes to RTSS at once.
// in range goes to RTSS once it stays.
static void refused(void) {
Run r;
run_init(&r);
r.st.refuse_above = 200000;
step(&r, 60000);
step(&r, 60000);
step(&r, 240000);
RtssOut o = step(&r, 240000);
CHECK(o.act == RTSS_DO_APPLY && !o.holds && o.why == RTSS_WHY_REFUSED, "refused");
CHECK(r.s.refused == 240000 && r.s.applied == 0 && r.st.clears == 1 && r.st.holds == 0, "the 60 cleared to none");
for (int i = 0; i < 5; i++) o = step(&r, 240000);
CHECK(r.st.applies == 2 && o.why == RTSS_WHY_REFUSED && !o.holds, "not written again");
o = step(&r, 144000);
CHECK(o.act == RTSS_DO_APPLY && !o.holds && r.st.holds == 144000, "144 at once");
CHECK(o.act == RTSS_DO_NOTHING && o.why == RTSS_WHY_SETTLING, "144 settles first");
o = step(&r, 144000);
CHECK(o.act == RTSS_DO_APPLY && !o.holds && r.st.holds == 144000, "144 written");
o = step(&r, 144000);
CHECK(o.holds, "held from the next poll");
CHECK(r.st.firsts == 1, "one first write in the run (%d)", r.st.firsts);
o = step(&r, 240000);
CHECK(o.act == RTSS_DO_NOTHING && o.why == RTSS_WHY_REFUSED, "the refused limit stays with the game");
CHECK(o.act == RTSS_DO_NOTHING && o.why == RTSS_WHY_REFUSED, "the refused limit stays with the game at once");
// refused as the run's first write: the profile is the run's from then on
run_init(&r);
r.st.refuse_above = 200000;
step(&r, 240000);
o = step(&r, 240000);
CHECK(o.why == RTSS_WHY_REFUSED && r.s.changed && r.st.clears == 1, "a first refusal clears too, as it stands");
}
@@ -319,7 +329,7 @@ static void frame_count(void) {
// 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);
for (int i = 0; i < 7; i++) step(&r, 60000);
CHECK(r.s.over == 2, "two periods over (%d)", r.s.over);
r.frames = 60;
for (int i = 0; i < 2; i++) step(&r, 60000);
@@ -327,22 +337,25 @@ static void frame_count(void) {
// a want that changed while the poll ran: nothing decided, the count waits
run_init(&r);
r.frames = 144;
for (int i = 0; i < 4; i++) step(&r, 60000);
for (int i = 0; i < 5; i++) step(&r, 60000);
o = step_ex(&r, 60000, 0);
CHECK(o.why == RTSS_WHY_NEWER && !o.holds && o.act == RTSS_DO_NOTHING && r.s.over == 1, "a newer want: no count");
}
// 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.
// game's limiter takes the limit back until pictures go out again. Before the first picture nothing is written.
static void idle(void) {
Run r;
run_init(&r);
r.presenting = 0;
step(&r, 60000);
RtssOut o = step(&r, 60000);
CHECK(!o.holds && o.why == RTSS_WHY_IDLE, "no host present yet");
RtssOut o;
for (int i = 0; i < 4; i++) o = step(&r, 60000);
CHECK(!o.holds && o.why == RTSS_WHY_NO_PRESENT && !r.st.applies, "no host present yet: nothing written");
WHY(o, r, "no picture has gone out yet");
r.presenting = 1;
o = step(&r, 60000);
CHECK(o.act == RTSS_DO_APPLY && !o.holds, "written at the first picture, the want having stayed");
o = step(&r, 60000);
CHECK(o.holds, "held once pictures go out");
r.presenting = 0;
r.hosted_ns = r.now_ns + 500000000ull - 199000000ull; // the next poll comes 199 ms after the last present
@@ -362,16 +375,22 @@ static void kept_out(void) {
Run r;
run_init(&r);
step(&r, 60000);
step(&r, 60000);
RtssOut o = step(&r, RTSS_OUT_FRAMEGEN);
CHECK(o.act == RTSS_DO_NOTHING && o.why == RTSS_WHY_OUT && !o.holds && r.st.holds == 60000,
"the game's limiter at once, the profile once the want stays");
o = step(&r, RTSS_OUT_FRAMEGEN);
CHECK(o.act == RTSS_DO_PUT_BACK && o.why == RTSS_WHY_OUT && !o.holds && r.st.holds == -1, "put back");
CHECK(r.st.last_put_back_why == RTSS_WHY_OUT, "the put back says why");
o = step(&r, RTSS_OUT_FRAMEGEN);
CHECK(o.act == RTSS_DO_NOTHING && r.st.put_backs == 1, "once");
o = step(&r, RTSS_OUT_LOWLAT);
CHECK(o.act == RTSS_DO_NOTHING && o.mhz == RTSS_OUT_LOWLAT, "a second reason: nothing more");
for (int i = 0; i < 3; i++) o = step(&r, RTSS_OUT_LOWLAT);
CHECK(o.act == RTSS_DO_NOTHING && o.mhz == RTSS_OUT_LOWLAT && r.st.put_backs == 1, "a second reason: nothing more");
WHY(o, r, "RivaTuner Statistics Server kept out: the driver's low latency");
o = step(&r, 60000);
CHECK(o.act == RTSS_DO_APPLY && !o.holds && r.st.firsts == 2, "written at once, the run's first write again");
CHECK(o.act == RTSS_DO_NOTHING && o.why == RTSS_WHY_SETTLING, "a limit again: settles first");
o = step(&r, 60000);
CHECK(o.act == RTSS_DO_APPLY && !o.holds && r.st.firsts == 2, "written, the run's first write again");
o = step(&r, 60000);
CHECK(o.holds, "held from the next poll");
// kept out from the start: the hooks library is never wanted, and nothing is written or put back
@@ -380,13 +399,31 @@ static void kept_out(void) {
CHECK(!r.st.loaded && !r.st.applies && !r.st.put_backs && o.why == RTSS_WHY_OUT, "read only");
}
// Low latency switching on and off at every poll: each switch writes or puts back, as it stands.
// Low latency switching on and off at every poll (a swapchain rebuild, the time before the first swapchain): the want
// never stays two polls, so the profile keeps the limit written before, and the game's limiter holds the frames while
// the want is new. From the start, the same flips write nothing.
static void churn(void) {
Run r;
run_init(&r);
step(&r, 60000);
step(&r, 60000);
RtssOut o;
for (int i = 0; i < 20; i++) {
o = step(&r, i & 1 ? 60000 : RTSS_OUT_LOWLAT);
CHECK(!(i & 1) ? !o.holds : 1, "kept out at poll %d: the game's limiter", i);
}
CHECK(r.st.applies == 1 && !r.st.put_backs && r.st.holds == 60000, "%d writes, %d put backs", r.st.applies,
r.st.put_backs);
o = step(&r, 60000);
CHECK(o.holds && r.st.applies == 1, "held once the limit stays");
run_init(&r);
for (int i = 0; i < 20; i++) step(&r, i & 1 ? 60000 : RTSS_OUT_LOWLAT);
CHECK(r.st.applies == 11 && r.st.put_backs == 10, "%d writes, %d put backs", r.st.applies, r.st.put_backs);
CHECK(!r.st.applies && !r.st.put_backs, "from the start: nothing written");
// a flip every two polls is a want that stays: each one acts
run_init(&r);
for (int i = 0; i < 8; i++) step(&r, i & 2 ? RTSS_OUT_LOWLAT : 60000);
CHECK(r.st.applies == 2 && r.st.put_backs == 2, "two polls each: %d writes, %d put backs", r.st.applies,
r.st.put_backs);
}
// A movie under RTSS: the want stays the play limit and the game's pacer holds the movie. At a limit of 144 the profile
@@ -396,6 +433,7 @@ static void movie(void) {
Run r;
run_init(&r);
step(&r, 144000);
step(&r, 144000);
r.movie_mhz = 60000;
RtssOut o = step(&r, 144000);
CHECK(o.act == RTSS_DO_NOTHING && !o.holds && o.why == RTSS_WHY_MOVIE, "a movie at 144: no write, the pacer's");
@@ -407,6 +445,7 @@ static void movie(void) {
CHECK(o.act == RTSS_DO_NOTHING && o.holds && o.why == RTSS_WHY_HOLDS && r.st.applies == 1, "held again after it");
run_init(&r);
step(&r, 40000);
step(&r, 40000);
r.movie_mhz = 60000;
o = step(&r, 40000);
CHECK(o.act == RTSS_DO_APPLY && o.mhz == 0 && !o.holds && o.why == RTSS_WHY_MOVIE && r.st.holds == 0,

View File

@@ -379,6 +379,11 @@ static void ask(int64_t mhz) { // a want at a present; the first one that star
CHECK(WaitForMultipleObjects(2, h, FALSE, 5000) != WAIT_TIMEOUT, "the worker did not finish its first poll");
}
}
static void settle(int64_t mhz) { // a new want, then the polls it takes to reach the profile (RTSS_SETTLE_POLLS)
ask(mhz);
poll();
poll();
}
static void show(const char *what) {
printf(" %s: profile %s %lu/%lu; active %d; state %d; saves %ld deletes %ld updates %ld loads %ld\n", what,
own.exists ? "exists," : "absent,", own.limit, own.denom, rtss_limit_active(), rtss_limit_state(), saves,
@@ -498,8 +503,7 @@ static void kept_lowlat_then_limit(void) {
"the driver's low latency"),
"the two-limiter line names the game's profile and low latency");
CHECK(!loads, "kept out, the hooks library is never loaded");
ask(60000);
poll();
settle(60000);
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");
poll();
@@ -525,6 +529,7 @@ static void active_race(void) {
app_set(1, 0xA, 1000, 2000, 60);
ask(60000);
poll();
poll();
CHECK(rtss_limit_active(), "RTSS limits at 60");
quiet = 1, free_run = 1;
SetEvent(tick);
@@ -555,6 +560,7 @@ static void active_logged(void) {
app_set(1, 0xA, 1000, 2000, 60);
during_save = framegen_on;
ask(60000);
poll();
show("frame generation switched on during the first write");
CHECK(own.exists && own.limit == 60, "the limit went into the profile");
CHECK(!rtss_limit_active(), "the game's limiter paces the kept-out want");
@@ -589,11 +595,9 @@ static void round_logged_whole(void) {
show("display at 59.94 Hz, RTSS without a denominator");
CHECK(own.limit == 60, "rounded to whole frames");
CHECK(logged("profile ocarina.exe limits to 60/1 (60.000 fps; the game asked 59.940)"), "the log names 60/1");
ask(143856);
poll();
settle(143856);
CHECK(logged("profile ocarina.exe limits to 144/1 (144.000 fps; the game asked 143.856)"), "the log names 144/1");
ask(60000);
poll();
settle(60000);
CHECK(logged("profile ocarina.exe limits to 60.000 fps\n"), "a whole limit as it is");
}
@@ -712,9 +716,9 @@ static void refused_run(void) {
app_set(1, 0xA, 1000, 2000, 60);
ask(60000);
poll();
CHECK(own.limit == 60 && rtss_limit_active(), "60 taken");
ask(240000);
poll();
CHECK(own.limit == 60 && rtss_limit_active(), "60 taken");
settle(240000);
show("240 refused");
CHECK(own.exists && own.limit == 0, "the earlier limit of this run is cleared to none");
CHECK(!rtss_limit_active() && rtss_limit_state() == 0, "the game's limiter keeps 240");
@@ -722,15 +726,15 @@ static void refused_run(void) {
poll();
poll();
CHECK(saves == s0, "the refused limit is not written again");
ask(144000);
poll();
settle(144000);
show("144 after the refusal");
CHECK(own.limit == 144, "144 written at once");
CHECK(own.limit == 144, "144 written once it stayed");
poll();
CHECK(rtss_limit_active(), "and held from the next poll");
ask(240000);
CHECK(!rtss_limit_active(), "240 with the game at once");
poll();
CHECK(!rtss_limit_active(), "240 stays with the game");
CHECK(!rtss_limit_active() && own.limit == 144, "240 stays with the game");
}
// 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
@@ -738,15 +742,14 @@ static void refused_run(void) {
static void over_run(void) {
app_set(1, 0xA, 1000, 2000, 144);
ask(60000);
for (int i = 0; i < 3; i++) poll();
for (int i = 0; i < 4; i++) poll();
CHECK(rtss_limit_active() && own.exists, "two periods over: RTSS still limits");
poll();
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");
ask(30000);
poll();
settle(30000);
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)
@@ -796,6 +799,7 @@ static void idle_run(void) {
app_set(1, 0xA, 1000, 2000, 60);
ask(60000);
poll();
poll();
CHECK(rtss_limit_active(), "RTSS limits");
presenting = 0;
poll();
@@ -843,26 +847,36 @@ static void library_run(void) {
CHECK(logged("RivaTuner Statistics Server's hooks library did not load"), "logged");
}
// Low latency switching on and off at every poll (the want flips between a limit and kept out): each switch writes or
// puts back the profile, as it stands, and the log numbers each creation and removal so a play-test can count them.
// Low latency switching on and off at every poll (the want flips between a limit and kept out, as at each swapchain
// rebuild): a want acts on the profile only once it stayed two polls, so the flips write and remove nothing, and the
// game's limiter holds each kept-out want at once. A want that stays two polls each time does act, and the log numbers
// each creation and removal so a play-test can count them.
static void churn_run(void) {
app_set(1, 0xA, 1000, 2000, 60);
ask(60000);
poll();
poll();
for (int i = 0; i < 10; i++) {
ask(i & 1 ? 60000 : RTSS_OUT_LOWLAT);
if (!(i & 1)) CHECK(!rtss_limit_active(), "kept out at flip %d: the game's limiter at once", i);
poll();
}
show("ten polls with the want flipping");
CHECK(created == 1 && removed == 0, "%ld creations, %ld removals", created, removed);
poll();
CHECK(created == 6 && removed == 5, "%ld creations, %ld removals", created, removed);
CHECK(rtss_limit_active(), "RTSS limits once the limit stays");
for (int i = 0; i < 4; i++) {
settle(RTSS_OUT_LOWLAT);
settle(60000);
}
show("four flips of two polls each");
CHECK(created == 5 && removed == 4, "%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"),
logged("(creation 5 this run)\n") && !logged("(creation 6"),
"the created lines are numbered");
CHECK(logged("removed, as before the game started (removal 1 this run)\n") && logged("(removal 5 this run)\n") &&
!logged("(removal 6"),
CHECK(logged("removed, as before the game started (removal 1 this run)\n") && logged("(removal 4 this run)\n") &&
!logged("(removal 5"),
"the removed lines are numbered");
CHECK(rtss_limit_active(), "RTSS limits after the last want of a limit");
}
// The state file: the run's first write records the profile as it was, each later one the value written, and the exit's
@@ -874,8 +888,7 @@ static void state_record(void) {
show("playing at 60");
CHECK(own.exists && own.limit == 60 && state_has("\nrtss1 0 0 1 60 1 ocarina.exe\n"), "recorded:\n%s", state());
CHECK(!strncmp(state(), "# RivaTuner Statistics Server's profiles", 40), "the header first");
ask(144000);
poll();
settle(144000);
CHECK(own.limit == 144 && state_has("\nrtss1 0 0 1 144 1 ocarina.exe\n"), "the line follows:\n%s", state());
rtss_limit_restore();
show("after the restore");
@@ -887,11 +900,9 @@ static void state_player(void) {
ask(60000);
poll();
CHECK(own.limit == 60 && state_has("\nrtss1 1 30 1 60 1 ocarina.exe\n"), "recorded:\n%s", state());
ask(RTSS_OUT_LOWLAT); // kept out: put back during the run, and the line goes with it
poll();
settle(RTSS_OUT_LOWLAT); // kept out: put back during the run, and the line goes with it
CHECK(own.limit == 30 && !*state(), "put back, no line:\n%s", state());
ask(60000);
poll();
settle(60000);
CHECK(own.limit == 60 && state_has("\nrtss1 1 30 1 60 1 ocarina.exe\n"), "recorded again:\n%s", state());
rtss_limit_restore();
CHECK(own.exists && own.limit == 30 && !*state(), "the player's 30 back, no line");
@@ -904,6 +915,7 @@ static void state_unwritable(void) {
app_set(1, 0xA, 1000, 2000, 60);
ask(60000);
poll();
poll();
show("no state file");
CHECK(own.exists && own.limit == 60 && rtss_limit_active(), "RTSS limits as before");
CHECK(logged("rtss-profile.txt could not be written; a crash would leave the game's limit in RivaTuner Statistics "