diff --git a/runtime/cfg.c b/runtime/cfg.c index ea35611c..68933004 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -55,7 +55,7 @@ static const char *const known[] = { "HDR_HIGHLIGHTS", "HDR_HUE", "HDR_HUE_CLIP", "HDR_MATCH_SLOPE", "HDR_MIDGRAY", "HDR_SATURATION", "HDR_SHADOWS", "HDR_TONEMAP", "HDR_TONE_SLOPE", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD", "BIG_BUILD_FREE_MB", "BIG_BUILD_MAX_MB", "BUILD_FREE_MB", "BUILD_RESERVE_MB", "LIB_PREBUILD_BIG", "LIB_PREBUILD_BIG_BG", "LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LIB_WARM", "PIPE_LINK_MS", "FRAME_HOLD_MS", "PACE_SMOOTH", "OQ_BATCH", "GATE_WAIT_MS", "LOCALE", "LOD_GUARD", "LSTICK_DEADZONE", "MEMORY", "MEM_CENSUS", "MEM_NEED_MB", "MENU_LANG", "MENU_SOUND_VOLUME", "MENU_TEST", - "MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_CLICK", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MICROPHONE", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MONITOR", "MOTION", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOUSE_TRACE", "MOVIE_HOST", "MOVIE_PACE", + "MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_CLICK", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MICROPHONE", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MONITOR", "MOTION", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOUSE_TRACE", "MOVIE_HOST", "MOVIE_PACE", "MOVIE_CLOCK", "TEST_HITCH", "MVOBJ_ALL", "MVOBJ_BANKS", "MVOBJ_EQUAL", "MVOBJ_EXACT", "MVOBJ_EXEC", "MVOBJ_NOC4", "MVOBJ_ONLY", "MVOBJ_TIMES", "MVPREV_CHECK", "MVPREV_GROW", "MVPREV_SKIP", "MVPREV_TRUST", "MV_CHECK", "MV_SURVEY", "ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PACE_HZ", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_RES_FIXED", "PATCH_SCENE", "PATCH_WIDE_ASPECT", "PATCH_WIDE_HUD", "PATCH_WIDE_MOVIE", "PATCH_WIDE_OUT", "PATCH_FREECAM", "PATCH_WIDE_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPELINE_CACHE_MAX_MB", "PIPELINE_CACHE_PLAY", "PIPELINE_CACHE_PLAY_MB", "PIPE_BINARIES", "PIPE_BINARIES_MAX_MB", "PIPE_FEEDBACK", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT", "POWER_FULL", "TEX_SAME", "PRESENT_HELD", "HELD_SYNC", diff --git a/runtime/frame_rate.c b/runtime/frame_rate.c index be1ed2c0..b3010554 100644 --- a/runtime/frame_rate.c +++ b/runtime/frame_rate.c @@ -235,6 +235,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 no // frames, so the two never stack. NX2NAVI_RTSS=0 and frame generation (RTSS would count the generated frames) keep it out. static double paced_hz; // the game frames a second the newest frame_step_cap_ns paces at (ocarina_step); 0: none +static int movie_paced; // the movie rate the newest frame_step_cap_ns paces at (ocarina_step, frame_step_movie_hz); 0: none +int frame_step_movie_hz(void) { return movie_paced; } uint64_t frame_step_cap_ns(void) { frame_limit_live(); static int rtss_allowed = -1; @@ -245,6 +247,12 @@ uint64_t frame_step_cap_ns(void) { int rtss = rtss_allowed && !vk_fg_wanted() && !vk_dlssg_active(); int gen = generated_factor(); int movie_hz = movie_cap_hz(); + static int movie_clock = -1; // NX2NAVI_MOVIE_CLOCK=0: the steady step and the usual pacing through a movie too + if (movie_clock < 0) { + const char *e = cfg_get("NX2NAVI_MOVIE_CLOCK"); + movie_clock = !(e && !strcmp(e, "0")); + } + movie_paced = movie_clock ? movie_hz : 0; static int movie_was, movie_logs; if (!!movie_hz != movie_was) { movie_was = !!movie_hz; @@ -392,12 +400,33 @@ static void ocarina_step_reset(void) { os_owed = 0; } +// While a movie plays (frame_step_cap_ns paces the game at the movie's rate) the step is exactly one movie frame, whatever +// the display intervals; NX2NAVI_MOVIE_CLOCK=0 leaves movies to the steady step (comparison runs). The game's movie +// player (main+0x3b5ca60) adds the time manager's seconds times 60 to an accumulator at each update and advances the +// movie by its whole pictures, at most the player's limit per update (+0x20, set at main+0x3b5d094: 1, or 2 in one of +// its modes), keeping the rest. The Triforce movie showed one picture per update (600 pictures in 600 updates at 59.6 +// updates a second), so the time a late frame left owed, which the steady step pays back as steps over one frame, never +// became pictures: the picture fell behind its sound, which plays by the clock, by each late frame for good. The +// movie's event counts updates as 60 Hz frames too (frame_step_cap_ns). So the updates themselves keep the clock +// (nvn_api_obj.c pace keeps the movie's slots through a late frame) and each counts as one movie frame, for the +// picture, the event and the game's clock alike. The filter starts again at a movie's start and end, so neither rate's +// intervals reach the other. static double ocarina_step(double dt) { - static int steady = -1; + static int steady = -1, movie_was; if (steady < 0) { const char *e = cfg_get("NX2NAVI_STEP_STEADY"); steady = !(e && !strcmp(e, "0")); } + if (movie_paced != movie_was) { + movie_was = movie_paced; + ocarina_step_reset(); + } + if (movie_paced > 0) { + double step = 1.0 / movie_paced; + if (os_step_lo <= 0 || step < os_step_lo) os_step_lo = step; + if (step > os_step_hi) os_step_hi = step; + return step; + } if (dt > STEP_MAX_S) dt = STEP_MAX_S; os_ring[os_next++ % OSTEP_WINDOW] = dt; if (os_count < OSTEP_WINDOW) os_count++; diff --git a/runtime/host.h b/runtime/host.h index 8d8c77d2..16cad5b8 100644 --- a/runtime/host.h +++ b/runtime/host.h @@ -384,6 +384,7 @@ void *rt_patch_page(void); // the page the patch void frame_step_init(void); // after rt_patches_init: the real-time step's first value int frame_step_realtime(void); // the real-time step is on: presents ignore the interval uint64_t frame_step_cap_ns(void); // with the real-time step: the shortest frame in ns +int frame_step_movie_hz(void); // the movie rate frame_step_cap_ns paces at; 0: no movie int ov_movie_live(void); // ov_movie2.c: a movie plays (frame_rate.c movie_cap_hz) int frame_limit_adjustable(void); // the frame limit setting changes the rate (always) int frame_free_running(void); // no fps60/realtime patch set: the game steps by the clock diff --git a/runtime/nvn_api_obj.c b/runtime/nvn_api_obj.c index fb102d42..b5aa780c 100644 --- a/runtime/nvn_api_obj.c +++ b/runtime/nvn_api_obj.c @@ -773,12 +773,20 @@ static uint64_t present_period(const win_t *o) { // The present slot is the previous slot plus one period. Early: wait for the slot. Late by // less than a period: present at once and keep the phase. Behind by a period or more: restart from now, no catch-up -// burst. Returns the ns to wait. +// burst, except while a movie plays (frame_step_movie_hz): then the slots are kept through a lateness up to +// MOVIE_CATCHUP_NS and the frames after a late one go at once until the game is back on them. A movie shows one +// picture per game update and its sound plays by the clock (frame_rate.c ocarina_step), so every period dropped here +// was a picture the movie fell behind for good: with a 70 ms frame every two seconds (NX2NAVI_TEST_HITCH=120:70) the +// updates averaged 58.9 a second and the Triforce movie's picture fell 190 ms behind its sound every ten seconds, 2.2 s +// by the end of its 117 s, at every frame limit from 60 to unlimited; with the slots kept it stayed within 22 ms. A +// stall of a second or more (a load) restarts the slots as before. Returns the ns to wait. +#define MOVIE_CATCHUP_NS 1000000000ull static uint64_t pace(uint64_t *target, uint64_t now, uint64_t period) { if (!period || !*target) { *target = now; return 0; } uint64_t next = *target + period; if (now <= next) { *target = next; return next - now; } - *target = now - next < period ? next : now; + uint64_t keep = frame_step_movie_hz() > 0 ? MOVIE_CATCHUP_NS : period; + *target = now - next < keep ? next : now; return 0; } @@ -873,6 +881,15 @@ NVN_API(nvnQueuePresentTexture) { o->presents++; __atomic_store_n(&nvn_submits_in_frame, 0, __ATOMIC_RELAXED); const tex_t *t = (const tex_t *)o->tex[idx]; + static int hitch_every = -1, hitch_ms; // NX2NAVI_TEST_HITCH=:: a late frame every so many presents + if (hitch_every < 0) { + const char *e = cfg_get("NX2NAVI_TEST_HITCH"); + int every = 0, ms = 0; + hitch_every = e && sscanf(e, "%d:%d", &every, &ms) == 2 && every > 0 && ms > 0 ? every : 0; + hitch_ms = ms; + if (hitch_every) rt_log("nvn: a %d ms late frame every %d presents (NX2NAVI_TEST_HITCH)", hitch_ms, hitch_every); + } + if (hitch_every && o->presents % (uint64_t)hitch_every == 0) plat_wait_ns((uint64_t)hitch_ms * 1000000); uint64_t period = present_period(o), now = kern_now_ns(); uint64_t slot = now + pace(&o->pace, now, period); gfx_backend()->present(t, o->crop, slot); diff --git a/runtime/ov_movie2.c b/runtime/ov_movie2.c index 13cebb3e..7766869f 100644 --- a/runtime/ov_movie2.c +++ b/runtime/ov_movie2.c @@ -82,6 +82,11 @@ typedef struct { int64_t ts0; uint64_t shown0_ns; int clock_on; + // The game fetches pictures ahead into a list of its own (main+0x3b61df0) and lets go of each when the next one + // takes its place on screen (ReleaseVideoFrame), so the distance to real time is measured there too: the newest + // released picture's, and how far it moved since the first released one (the drift), in ns + int64_t rel_lag, rel_lag0; + int rel_on; } Video; static Video *video_of(void *obj) { return obj ? *(Video **)obj : NULL; } @@ -588,7 +593,7 @@ static void ov_initialize(Cpu *c) { Video *v = *slot; finalize(v); set_live(v, 1); - v->clock_on = 0; + v->clock_on = v->rel_on = 0; Dav1dSettings s; dav1d_default_settings(&s); // Frame threading on (dav1d picks the delay): heavy stretches of the title movie took longer than a frame to @@ -674,7 +679,7 @@ static void ov_flush(Cpu *c) { if (v) { plat_mutex_lock(&v->lock); v->generation++; // the decoder and convert threads drop what they are making - v->clock_on = 0; // a seek: the report's real-time reference starts again from the next picture + v->clock_on = v->rel_on = 0; // a seek: the report's real-time reference starts again from the next picture clear_queues(v); clear_converts(v); plat_mutex_unlock(&v->lock); @@ -805,9 +810,10 @@ static void ov_acquire(Cpu *c) { rt_log("movie: pacing over 600 frames: acquire gap avg %.2f ms, max %.2f ms; %d pictures waiting; " "per frame shown: decoder thread waiting for samples %.2f ms, send %.2f ms, get %.2f ms; convert thread " "%.2f ms per picture (%llu pictures); " - "picture %.0f ms behind real time since the first picture", + "picture %.0f ms behind real time since the first picture; on screen %.0f ms behind, %+.0f ms since its start", sum_gap / 600 / 1e6, max_gap / 1e6, v->qn, t_wait_ns / 600 / 1e6, t_send_ns / 600 / 1e6, - t_get_ns / 600 / 1e6, n_made ? t_make_ns / n_made / 1e6 : 0.0, (unsigned long long)n_made, lag / 1e6); + t_get_ns / 600 / 1e6, n_made ? t_make_ns / n_made / 1e6 : 0.0, (unsigned long long)n_made, lag / 1e6, v->rel_lag / 1e6, + (v->rel_lag - v->rel_lag0) / 1e6); t_wait_ns = t_send_ns = t_get_ns = t_make_ns = n_made = 0; sum_gap = max_gap = 0; } @@ -816,7 +822,12 @@ static void ov_acquire(Cpu *c) { update_events(v); plat_mutex_unlock(&v->lock); if (p.rgba) gfx_movie_frame_own(p.rgba, p.w, p.h), p.rgba = NULL; // the overlay takes the buffer - if (p.eos) gfx_movie_stop(), set_live(v, 0); + if (p.eos) { // how the movie kept time: the picture on screen against the clock, at its start and its end + if (v->rel_on) + rt_log("movie: end of stream after %llu pictures: on screen %.0f ms behind real time, %+.0f ms since its start", + (unsigned long long)v->shown, v->rel_lag / 1e6, (v->rel_lag - v->rel_lag0) / 1e6); + gfx_movie_stop(), set_live(v, 0); + } picture_free(&p); *out = v->frame[f]; ret_x(c, 0); @@ -828,7 +839,15 @@ static void ov_release(Cpu *c) { if (v) { plat_mutex_lock(&v->lock); for (int i = 0; i < v->nframe; i++) - if (v->frame[i] == fr) v->busy[i] = 0; + if (v->frame[i] == fr) { + v->busy[i] = 0; + int64_t ts; + memcpy(&ts, (uint8_t *)(uintptr_t)fr + 0x78, 8); + if (v->clock_on && !((uint8_t *)(uintptr_t)fr)[0x88]) { + v->rel_lag = (int64_t)(plat_now_ns() - v->shown0_ns) - (ts - v->ts0); + if (!v->rel_on) v->rel_lag0 = v->rel_lag, v->rel_on = 1; + } + } update_events(v); plat_mutex_unlock(&v->lock); } diff --git a/tests/frame_step_check.c b/tests/frame_step_check.c index e4df12f7..07cd1ce0 100644 --- a/tests/frame_step_check.c +++ b/tests/frame_step_check.c @@ -28,7 +28,8 @@ void rt_patch_switch(int set, int on) { (void)set, (void)on; } void *rt_patch_page(void) { return page; } void rt_log(const char *fmt, ...) { (void)fmt; } double gfx_window_refresh_hz(void) { return 144; } -int ov_movie_live(void) { return 0; } +static int movie; // a movie plays (frame_rate.c movie_cap_hz) +int ov_movie_live(void) { return movie; } int vk_fg_generating(void) { return 0; } float dbg_speed_factor(void) { return 1.0f; } // the debug menu's game speed: normal int vk_fg_active(void) { return 0; } @@ -128,6 +129,21 @@ int main(int argc, char **argv) { for (int i = 0; i < 12; i++) f = present(2 * p240); CHECK(f > 0.49f, "240 -> 120 fps: %.4f frames after 12", f); + // A movie started at 240 fps: exactly one frame from its first update, late and short frames included, as the game + // shows one picture per update; at its end the step follows the frame limit again within 12 frames. + set_limit("unlimited"); + for (int i = 0; i < 300; i++) present(p240); + movie = 1; + for (int i = 0; i < 600; i++) { + float g = present(i % 120 == 7 ? 0.070 : i % 120 == 8 ? 0.004 : p60); + CHECK(g == 1.0f, "movie, frame %d: %.6f frames", i, g); + } + CHECK(frame_step_movie_hz() == 60, "the movie's rate: %d", frame_step_movie_hz()); + movie = 0; + float after = 0; + for (int i = 0; i < 12; i++) after = present(p240); + CHECK(fabsf(after - 0.25f) < 0.002f, "after the movie: %.4f frames", after); + // A frame longer than 1/15 s slows the game down; the step never leaves 1/480..1/15 s. set_limit("unlimited"); for (int i = 0; i < 100; i++) {