diff --git a/runtime/frame_rate.c b/runtime/frame_rate.c index 031f782a..13c8c328 100644 --- a/runtime/frame_rate.c +++ b/runtime/frame_rate.c @@ -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 diff --git a/runtime/host.h b/runtime/host.h index 703442d9..096fa4ed 100644 --- a/runtime/host.h +++ b/runtime/host.h @@ -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 diff --git a/runtime/rtss_decide.h b/runtime/rtss_decide.h index 96124a4d..0347a297 100644 --- a/runtime/rtss_decide.h +++ b/runtime/rtss_decide.h @@ -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; } diff --git a/runtime/rtss_limit.c b/runtime/rtss_limit.c index 30ff4147..b62f9310 100644 --- a/runtime/rtss_limit.c +++ b/runtime/rtss_limit.c @@ -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) {} diff --git a/tests/frame_step_check.c b/tests/frame_step_check.c index 01bee9dd..01ffc8ba 100644 --- a/tests/frame_step_check.c +++ b/tests/frame_step_check.c @@ -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; } diff --git a/tests/rtss_limit_check.c b/tests/rtss_limit_check.c index b058845c..11203d58 100644 --- a/tests/rtss_limit_check.c +++ b/tests/rtss_limit_check.c @@ -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) { diff --git a/tests/rtss_worker_check.c b/tests/rtss_worker_check.c index 42e63824..691990b9 100644 --- a/tests/rtss_worker_check.c +++ b/tests/rtss_worker_check.c @@ -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) {