Let the game's pacer hold a movie while RivaTuner Statistics Server holds the limit, and keep the play limit in RTSS's profile through it.

During a movie frame_step_cap_ns wanted the movie's rate and returned 0 once RTSS held it, so pace() kept none of the
movie's slots while ocarina_step still gave one movie frame per update: each late frame put the picture behind the
sound for good, the fall-behind d3e50328 fixed for the game's pacer. Each movie start and end also rewrote RTSS's
profile (two saves and two UpdateProfiles per movie in lane A's probe). Now frame_step_cap_ns wants the play limit
through the movie and tells RTSS of the movie (rtss_limit_movie); rtss_pace_period keeps the movie's period from its
first frame, and the worker leaves the movie to the pacer (RTSS_WHY_MOVIE) with the profile as it is. Under a limit
below the movie's rate (30, 40, 45) the profile has no limit for the movie's length (rtss_decide.h rtss_target), since
RTSS at the movie's rate would space out the frames the pacer sends at once to catch up; that is still two writes per
movie there, and none at 60 or more.

Not reproduced before the change. rtss_limit_check (the movie case at 144 and 40, rtss_target, rtss_pace_period),
rtss_worker_check (movie_run, new movie_low_run) and frame_step_check (the want and period through a movie under RTSS)
pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 18:58:47 -05:00
parent 27a034e946
commit 208a10718d
7 changed files with 136 additions and 41 deletions

View File

@@ -277,7 +277,8 @@ double frame_step_game_hz(void) {
// RivaTuner Statistics Server (rtss_limit.c) gets the same limit; while it limits this process the pacer holds the
// frames only to LIMIT_FREE_MAX_HZ, the ceiling the game's code needs, so the two never stack, and it takes the limit
// back once no picture went out for 200 ms (rtss_decide.h rtss_pace_period): a minimised window or a frame hold leaves
// RTSS no present to hold. NX2NAVI_RTSS=0 and frame generation (RTSS would count the generated frames) keep it
// RTSS no present to hold. While a movie plays the pacer holds the movie's rate and keeps its slots whatever RTSS does.
// NX2NAVI_RTSS=0 and frame generation (RTSS would count the generated frames) keep it
// out, and so does a driver's low latency while it is in effect (vk_lowlat.c): the driver holds the limit where the
// frame starts, and RTSS would hold the finished picture, the wait that mode is there to remove. RTSS gets the reason
// (RTSS_OUT_*), for the log and for its check of a limit set in RTSS itself.
@@ -311,6 +312,7 @@ uint64_t frame_step_cap_ns(void) {
else rt_log("frame rate: the movie ended: the frame limit again");
}
}
rtss_limit_movie(movie_hz);
if (limit_hz == -2 && !movie_hz && !frame_free_running()) { // unlimited: the pacer does not hold frames
rtss_limit_want(rtss_out); // kept out (RTSS_OUT_*), or 0: RTSS's limit is cleared
paced_hz = 0;
@@ -327,11 +329,15 @@ uint64_t frame_step_cap_ns(void) {
rt_log("frame rate: the game draws up to %.0f frames a second and frame generation shows %d for each (%.0f on "
"screen)", hz, gen, hz * gen);
}
// RTSS keeps the play limit through a movie and the pacer holds the movie (rtss_decide.h rtss_target): the movie's
// rate as the want rewrote RTSS's profile at each start and end, and with the pacer aside under RTSS no movie slot
// was kept, so each late frame put the picture behind the sound for good
int64_t play_mhz = (int64_t)(hz * 1000 + 0.5);
if (movie_hz) hz = movie_hz;
paced_hz = hz;
rtss_limit_want(rtss_out ? rtss_out : (int64_t)(hz * 1000 + 0.5));
return rtss_pace_period(rtss_limit_active(), kern_now_ns(), gfx_window_hosted_ns(), (uint64_t)(1e9 / hz + 0.5),
(uint64_t)(1e9 / LIMIT_FREE_MAX_HZ + 0.5));
rtss_limit_want(rtss_out ? rtss_out : play_mhz);
return rtss_pace_period(rtss_limit_active(), movie_hz, kern_now_ns(), gfx_window_hosted_ns(),
(uint64_t)(1e9 / hz + 0.5), (uint64_t)(1e9 / LIMIT_FREE_MAX_HZ + 0.5));
}
// The limit as it is set, for the performance overlay: the setting, the monitor's rate for "display", 60 at

View File

@@ -466,7 +466,8 @@ void frame_step_flight(float *page, float *game, uint64_t *presents); // fligh
// rtss_limit.c: RivaTuner Statistics Server takes the frame limit where it runs
void rtss_limit_want(int64_t mhz); // the pacer's limit in millihertz: >0, 0 none; or why RTSS is
// kept out (rtss_decide.h RTSS_OUT_*)
int rtss_limit_active(void); // RTSS limits to the wanted rate: the pacer holds no frames
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

View File

@@ -82,6 +82,7 @@ enum {
RTSS_WHY_REFUSED, // RTSS refused this limit
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
};
// What the worker keeps between polls.
@@ -96,20 +97,28 @@ typedef struct {
// One poll's view.
typedef struct {
int64_t want; // the want the poll read
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 want; // the want the poll read
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
RtssApp app;
} RtssIn;
typedef struct {
int act; // RTSS_DO_*
int64_t mhz; // the want decided for
int64_t mhz; // the value decided for: the profile's (rtss_target) for a limit, the want for a kept-out one
int holds; // RTSS holds the game to it: the game's own limiter stands aside
int why; // RTSS_WHY_*
double fps; // RTSS's frame rate of the game, when the frame count check gave up on it
} RtssOut;
// What the profile gets for a want w >= 0 (the play limit: frame_rate.c frame_step_cap_ns wants it through a movie
// too) while a movie plays at movie_mhz: w, so that a movie no longer rewrites the profile at its start and end; but no
// limit (0) while the movie runs faster than w (a limit of 30, 40 or 45). The game's pacer holds every movie at its
// rate and keeps its slots through a late frame (nvn_api_obj.c pace); RTSS at the movie's rate or below would space out
// the frames that catch up, and the picture would fall behind the sound at each late frame for good.
static inline int64_t rtss_target(int64_t w, int64_t movie_mhz) { return w > 0 && movie_mhz > w ? 0 : w; }
// The hooks library is loaded only on a poll that may write the profile: a limit is wanted, RTSS is not out of it for
// the run, and it runs. A poll of a kept-out want never loads it; a later limit does.
static inline int rtss_wants_hooks(const RtssState *s, const RtssIn *in) {
@@ -118,10 +127,11 @@ 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 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. After the
// exit put the profile back (rtss_limit_restore) no poll decides: rtss_limit.c worker skips it under the lock.
// 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.
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};
@@ -133,17 +143,19 @@ static inline RtssOut rtss_decide(RtssState *s, const RtssIn *in) {
if (s->fail) return o.why = RTSS_WHY_FAILED, o;
if (!in->app.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;
if (w == s->refused) return o.why = RTSS_WHY_REFUSED, 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 (w != s->applied) { // the result of the write may still undo the hold (rtss_written)
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)
o.act = RTSS_DO_APPLY;
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 || w == 0 || fps <= w / 1000.0 * RTSS_CHECK_SLACK) s->over = 0;
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;
@@ -208,6 +220,7 @@ static inline void rtss_why_text(char *out, size_t n, const RtssOut *o, int fail
else if (o->why == RTSS_WHY_FAILED) snprintf(out, n, "%s", rtss_fail_text(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 snprintf(out, n, "RivaTuner Statistics Server has not hooked the game yet");
}
@@ -216,15 +229,17 @@ static inline void rtss_why_text(char *out, size_t n, const RtssOut *o, int fail
// 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,
// before the first host present, and once no host present went out for RTSS_HOLD_IDLE_NS: a minimised window or a
// while a movie plays (from its first frame: the pacer keeps the movie's slots, which RTSS does not know, and the
// worker's next poll gives the movie to the pacer for its length, rtss_target), before the first host present, and
// once no host present went out for RTSS_HOLD_IDLE_NS: a minimised window or a
// frame hold (vk_backend.c frame_hold) gives RTSS nothing to hold, and a pacer that held nothing then let the quick menu
// run at the 640-700 fps that read address 0 (frame_rate.c LIMIT_FREE_MAX_HZ). While RTSS holds, the ceiling's period
// and never 0: the frames RTSS lets through come further apart than that, so pace() waits for none of them, and it
// still holds the game's frames to the ceiling the game's code needs.
static inline uint64_t rtss_pace_period(int holds, uint64_t now_ns, uint64_t hosted_ns, uint64_t period,
static inline uint64_t rtss_pace_period(int holds, int movie, uint64_t now_ns, uint64_t hosted_ns, uint64_t period,
uint64_t ceiling) {
uint64_t idle = now_ns > hosted_ns ? now_ns - hosted_ns : 0;
if (!holds || !hosted_ns || idle >= RTSS_HOLD_IDLE_NS) return period;
if (!holds || movie || !hosted_ns || idle >= RTSS_HOLD_IDLE_NS) return period;
return ceiling < period ? ceiling : period;
}

View File

@@ -53,6 +53,7 @@ typedef void (*UpdateFn)(void);
static PlatMutex lock = PLAT_MUTEX_INIT; // the hooks calls and the state below
static _Atomic int64_t want = RTSS_UNSET; // millihertz from the pacer: >0 a limit, 0 none; RTSS_OUT_* kept out
static _Atomic int64_t active_for = RTSS_UNSET; // the want RTSS holds the game to now; RTSS_UNSET none
static _Atomic int64_t movie_mhz; // a movie's rate while one plays (rtss_limit_movie), 0 none
static _Atomic int failed; // RTSS_FAIL_*: RTSS is out of it for the rest of the run (S.fail)
static _Atomic int closed; // rtss_limit_restore ran: nothing reaches the profile any more
static _Atomic int two; // kept out, and RTSS limits the game by a setting of its own (watch)
@@ -188,7 +189,7 @@ static int apply(void *ctx, int64_t mhz, int first, int clear) {
return RTSS_WROTE_REFUSED;
}
update_profiles();
if (!mhz) rt_log("frame rate: RivaTuner Statistics Server's profile %s has no limit (unlimited)", profile);
if (!mhz) rt_log("frame rate: RivaTuner Statistics Server's profile %s has no limit", profile);
else if ((int64_t)limit * 1000 == mhz * denominator)
rt_log("frame rate: RivaTuner Statistics Server's profile %s limits to %.3f fps", profile, mhz / 1000.0);
else // rounded to whole frames by an RTSS without FramerateLimitDenominator: the log says what RTSS holds
@@ -362,7 +363,7 @@ static void worker(void *arg) {
plat_mutex_unlock(&lock);
continue;
}
RtssIn in = {.want = w};
RtssIn in = {.want = w, .movie_mhz = atomic_load(&movie_mhz)};
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");
@@ -396,6 +397,8 @@ void rtss_limit_want(int64_t mhz) {
rt_log("frame rate: the RivaTuner Statistics Server thread did not start");
}
void rtss_limit_movie(int hz) { atomic_store_explicit(&movie_mhz, (int64_t)hz * 1000, memory_order_relaxed); }
int rtss_limit_active(void) {
if (atomic_load_explicit(&closed, memory_order_relaxed)) return 0;
int64_t a = atomic_load_explicit(&active_for, memory_order_relaxed);
@@ -431,6 +434,7 @@ void rtss_limit_restore(void) {
#else
void rtss_limit_want(int64_t mhz) { (void)mhz; }
void rtss_limit_movie(int hz) { (void)hz; }
int rtss_limit_active(void) { return 0; }
int rtss_limit_state(void) { return 0; }
void rtss_limit_restore(void) {}

View File

@@ -17,7 +17,7 @@ uint64_t rt_guest_start_ns;
const char *cfg_get(const char *key) {
if (!strcmp(key, "NX2NAVI_FPS_LIMIT")) return limit;
if (!strcmp(key, "NX2NAVI_STEP_STEADY")) return steady;
if (!strcmp(key, "NX2NAVI_RTSS")) return "0";
if (!strcmp(key, "NX2NAVI_RTSS")) return "1";
return NULL;
}
unsigned cfg_version(void) { return version; }
@@ -38,7 +38,10 @@ static int fg_mult = 1; // frames on screen per game frame
int vk_dlssg_multiplier(void) { return fg_mult; }
int vk_dlssg_active(void) { return fg_mult > 1; }
int vk_lowlat_on(void) { return 0; }
void rtss_limit_want(int64_t millihz) { (void)millihz; }
static int64_t rtss_want; // the newest want (rtss_limit.c)
static int rtss_movie; // the newest movie rate RTSS was told of
void rtss_limit_want(int64_t millihz) { rtss_want = millihz; }
void rtss_limit_movie(int hz) { rtss_movie = hz; }
static int rtss_holds; // RivaTuner Statistics Server holds the game to the want (rtss_limit.c)
static uint64_t kern_ns, hosted; // kern_now_ns, and its time at the newest host present (0: none yet)
int rtss_limit_active(void) { return rtss_holds; }
@@ -303,6 +306,24 @@ int main(int argc, char **argv) {
double hz = 1e9 / (double)frame_step_cap_ns();
CHECK(fabs(hz - cases[i].hz) < 0.01, "%s: paces at %.3f fps, not %.0f", cases[i].what, hz, cases[i].hz);
}
// A movie while RTSS holds 144: RTSS keeps the play limit (no rewrite of its profile at the movie's start and
// end) and the pacer holds the movie's 60 with its slots; at 40 the want stays 40 too (rtss_decide.h rtss_target
// decides what the profile gets).
hosted = kern_ns - 10000000;
static const struct { const char *l; int64_t want; } movies[] = {{"144", 144000}, {"40", 40000}};
for (unsigned i = 0; i < 2; i++) {
set_limit(movies[i].l);
movie = 1;
double hz = 1e9 / (double)frame_step_cap_ns();
CHECK(fabs(hz - 60) < 0.01 && rtss_want == movies[i].want && rtss_movie == 60,
"a movie at a limit of %s under RTSS: paces at %.3f fps, wants %lld, movie %d", movies[i].l, hz,
(long long)rtss_want, rtss_movie);
movie = 0;
hz = 1e9 / (double)frame_step_cap_ns();
CHECK(fabs(hz - 240) < 0.01 && rtss_want == movies[i].want && !rtss_movie,
"after the movie at %s: paces at %.3f fps, wants %lld, movie %d", movies[i].l, hz,
(long long)rtss_want, rtss_movie);
}
rtss_holds = 0, hosted = kern_ns = 0;
}

View File

@@ -151,6 +151,7 @@ typedef struct {
RtssOps ops;
int running, listed;
uint32_t t0, t1, frames;
int64_t movie_mhz; // a movie plays at this rate (rtss_limit_movie)
} Run;
static void run_init(Run *r) {
memset(r, 0, sizeof *r);
@@ -161,7 +162,7 @@ static void run_init(Run *r) {
}
// 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};
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};
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);
@@ -326,15 +327,39 @@ static void churn(void) {
CHECK(r.st.applies == 11 && r.st.put_backs == 10, "%d writes, %d put backs", r.st.applies, r.st.put_backs);
}
// A movie under RTSS: the want follows the movie's rate, and each start and end is a write.
// A movie under RTSS: the want stays the play limit and the game's pacer holds the movie. At a limit of 144 the profile
// keeps 144 through it, with no write; under the movie's rate (40) the profile has no limit for the movie's length, so
// RTSS cannot space out the frames that catch up after a late one, and gets 40 back at its end.
static void movie(void) {
Run r;
run_init(&r);
step(&r, 144000);
RtssOut o = step(&r, 60000);
CHECK(o.act == RTSS_DO_APPLY && o.holds, "the movie's 60 written");
r.movie_mhz = 60000;
RtssOut o = step(&r, 144000);
CHECK(o.act == RTSS_DO_NOTHING && !o.holds && o.why == RTSS_WHY_MOVIE, "a movie at 144: no write, the pacer's");
for (int i = 0; i < 10; i++) o = step(&r, 144000);
CHECK(!o.holds && o.why == RTSS_WHY_MOVIE && r.st.applies == 1, "through the movie");
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);
r.movie_mhz = 0;
o = step(&r, 144000);
CHECK(o.act == RTSS_DO_APPLY && o.holds && r.st.applies == 3, "144 written again at its end");
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);
r.movie_mhz = 60000;
o = step(&r, 40000);
CHECK(o.act == RTSS_DO_APPLY && o.mhz == 0 && !o.holds && o.why == RTSS_WHY_MOVIE && r.st.holds == 0,
"a movie at 40: no limit in the profile");
o = step(&r, 40000);
CHECK(o.act == RTSS_DO_NOTHING && r.st.applies == 2, "once");
r.movie_mhz = 0;
o = step(&r, 40000);
CHECK(o.act == RTSS_DO_APPLY && o.mhz == 40000 && o.holds && r.st.holds == 40000 && r.st.applies == 3,
"40 back at its end");
CHECK(rtss_target(60000, 60000) == 60000 && rtss_target(59940, 60000) == 0 && rtss_target(0, 60000) == 0 &&
rtss_target(144000, 0) == 144000 && rtss_target(RTSS_OUT_LOWLAT, 60000) == RTSS_OUT_LOWLAT,
"rtss_target");
}
// No want yet: nothing at all. The hooks library that does not load fails the run. (A run whose exit put the profile
@@ -384,13 +409,14 @@ static void two(void) {
// otherwise; never 0.
static void pace_period(void) {
const uint64_t p60 = 16666667, ceiling = 4166667, now = 50000000000ull;
CHECK(rtss_pace_period(1, now, now - 10000000, p60, ceiling) == ceiling, "holds, a host present 10 ms ago");
CHECK(rtss_pace_period(1, now, now - RTSS_HOLD_IDLE_NS + 1, p60, ceiling) == ceiling, "just under 200 ms");
CHECK(rtss_pace_period(1, now, now - RTSS_HOLD_IDLE_NS, p60, ceiling) == p60, "200 ms with no host present");
CHECK(rtss_pace_period(1, now, 0, p60, ceiling) == p60, "no host present yet");
CHECK(rtss_pace_period(0, now, now - 10000000, p60, ceiling) == p60, "RTSS does not hold");
CHECK(rtss_pace_period(1, now, now + 1000, p60, ceiling) == ceiling, "a host present stored after now was read");
CHECK(rtss_pace_period(1, now, now - 10000000, ceiling, ceiling) == ceiling, "a limit at the ceiling");
CHECK(rtss_pace_period(1, 0, now, now - 10000000, p60, ceiling) == ceiling, "holds, a host present 10 ms ago");
CHECK(rtss_pace_period(1, 0, now, now - RTSS_HOLD_IDLE_NS + 1, p60, ceiling) == ceiling, "just under 200 ms");
CHECK(rtss_pace_period(1, 0, now, now - RTSS_HOLD_IDLE_NS, p60, ceiling) == p60, "200 ms with no host present");
CHECK(rtss_pace_period(1, 0, now, 0, p60, ceiling) == p60, "no host present yet");
CHECK(rtss_pace_period(0, 0, now, now - 10000000, p60, ceiling) == p60, "RTSS does not hold");
CHECK(rtss_pace_period(1, 0, now, now + 1000, p60, ceiling) == ceiling, "a host present stored after now was read");
CHECK(rtss_pace_period(1, 0, now, now - 10000000, ceiling, ceiling) == ceiling, "a limit at the ceiling");
CHECK(rtss_pace_period(1, 60, now, now - 10000000, p60, ceiling) == p60, "a movie: its period and slots");
}
int main(void) {

View File

@@ -726,19 +726,40 @@ static void churn_run(void) {
CHECK(rtss_limit_active(), "RTSS limits after the last want of a limit");
}
// A movie at 60 under a limit of 144: the want follows the movie, and each start and end is a write.
// A movie at 60 under a limit of 144: the want stays 144 (frame_rate.c frame_step_cap_ns) and RTSS's profile keeps it
// with no write, while the game's pacer holds the movie at its rate; RTSS holds the game again at its end.
static void movie_run(void) {
app_set(1, 0xA, 1000, 2000, 144);
ask(144000);
poll();
long s0 = saves;
ask(60000);
poll();
CHECK(own.limit == 60 && rtss_limit_active(), "RTSS holds the movie's 60");
rtss_limit_movie(60);
ask(144000);
poll();
show("after one movie");
CHECK(own.limit == 144 && saves - s0 == 2, "two writes for one movie");
show("a movie at 60");
CHECK(own.limit == 144 && !rtss_limit_active() && saves == s0, "the profile keeps 144; the game's pacer holds it");
CHECK(logged("the game's own limiter (a movie plays: the game keeps its pictures on time)"), "logged");
rtss_limit_movie(0);
poll();
show("after the movie");
CHECK(own.limit == 144 && rtss_limit_active() && saves == s0, "RTSS holds again, no write for the movie");
}
// The same under a limit of 40: the profile has no limit while the movie plays, so RTSS cannot space out the frames the
// pacer sends at once after a late one, and gets 40 back at its end.
static void movie_low_run(void) {
app_set(1, 0xA, 1000, 2000, 40);
ask(40000);
poll();
long s0 = saves;
rtss_limit_movie(60);
poll();
show("a movie at 60 with the limit at 40");
CHECK(own.exists && own.limit == 0 && !rtss_limit_active(), "no limit in the profile during the movie");
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");
}
static const struct { const char *name; void (*run)(void); } scenarios[] = {
@@ -768,6 +789,7 @@ static const struct { const char *name; void (*run)(void); } scenarios[] = {
{"library_run", library_run},
{"churn_run", churn_run},
{"movie_run", movie_run},
{"movie_low_run", movie_low_run},
};
int main(int argc, char **argv) {