From b1e0b410068b555a1d7af3948320f127fb8ab585 Mon Sep 17 00:00:00 2001 From: Reonu Date: Thu, 8 Jan 2026 03:41:26 +0000 Subject: [PATCH] Fix intro cutscene timings (#56) * Fix intro cutscene timings * Address review * Move code to individual functions * address review * Fix comments --- patches/graphics_patches.c | 4 ++ patches/load_patches.c | 6 +++ patches/timing_patches.c | 82 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 89 insertions(+), 3 deletions(-) diff --git a/patches/graphics_patches.c b/patches/graphics_patches.c index 757c3c8..22ed134 100644 --- a/patches/graphics_patches.c +++ b/patches/graphics_patches.c @@ -71,6 +71,7 @@ RECOMP_PATCH void graphicsCache_init(void){ gTextureFilterPoint = 0; } +void handle_cutscene_timings(void); // @recomp Patched to check for graphics stack overflow after processing a frame. // Also patched to wait for a message when the displaylist is completed immediately after queueing it to solve vertex modification race conditions. RECOMP_PATCH void game_draw(s32 arg0){ @@ -155,6 +156,9 @@ RECOMP_PATCH void game_draw(s32 arg0){ } } + // @recomp Call the relevant function to fix cutscene timings, if there is one. + handle_cutscene_timings(); + // Allow interpolation for the next frame. set_all_interpolation_skipped(FALSE); } diff --git a/patches/load_patches.c b/patches/load_patches.c index 679e5d0..9bb73bc 100644 --- a/patches/load_patches.c +++ b/patches/load_patches.c @@ -93,11 +93,14 @@ void hotpatch_intro_opa_map_model(BKModelBin* model_bin) { gSPDisplayList(&dl[INTRO_OPA_DL_WALL_PATCH_INDEX], intro_wall_extension_dl); } +void reset_intro_cutscene_timings_state(void); + // @recomp Patched to act as a point to run code when a new map is loaded. // This includes: // * Resetting all extended marker data and skip interpolation for the next frame. // * Hotpatching the map model for the title cutscene to fix ultrawide effects. // * Resetting the spawned static note count. +// * Resetting the variables used to keep track of correcting the intro cutscene timings RECOMP_PATCH void func_803329AC(void){ s32 i; @@ -127,4 +130,7 @@ RECOMP_PATCH void func_803329AC(void){ // @recomp Run note saving map load code. note_saving_on_map_load(); + + // @recomp Reset the intro cutscene timing corrections so the cutscene can be played again + reset_intro_cutscene_timings_state(); } diff --git a/patches/timing_patches.c b/patches/timing_patches.c index a9fa8c2..4d6c56c 100644 --- a/patches/timing_patches.c +++ b/patches/timing_patches.c @@ -104,27 +104,34 @@ RECOMP_PATCH int demo_readInput(OSContPad* arg0, s32* arg1){ return not_eof; } +int extraVis = 0; + // @recomp Patched to override the VI frame divisor when the demo frame divisor has been set. +// Also overrides it if a cutscene needs timing compensation. RECOMP_PATCH s32 viMgr_func_8024BFA0() { if (demo_frame_divisor != -1) { return demo_frame_divisor; } - return D_802808DC; + return D_802808DC + extraVis; } -// @recomp Patched to clear the demo frame divisor after viMgr_func_8024BFD8. +// @recomp Patched to clear lag overrides after viMgr_func_8024BFD8. RECOMP_PATCH void viMgr_func_8024C1B4(void){ viMgr_func_8024BFD8(0); // @recomp Clear the demo frame divisor. demo_frame_divisor = -1; dummy_func_8025AFB8(); + // @recomp Clear the lag override for cutscenes. + extraVis = 0; } -// @recomp Patched to clear the demo frame divisor after viMgr_func_8024BFD8. +// @recomp Patched to clear lag overrides after viMgr_func_8024BFD8. RECOMP_PATCH void viMgr_func_8024C1DC(void){ viMgr_func_8024BFD8(1); // @recomp Clear the demo frame divisor. demo_frame_divisor = -1; + // @recomp Clear the lag override for cutscenes. + extraVis = 0; } // @recomp Patched to use a fixed time delta of 30 FPS when decrementing the hourglass timer during Bottles' Bonus. @@ -142,3 +149,72 @@ RECOMP_PATCH void func_80345EB0(enum item_e item){ func_802FACA4(item); } } + +// The intro cutscene stutters on console, but it does not stutter in recomp. +// The cutscene is timed with the stutters in mind so this causes desyncs with the music and sound effects. +// We have manually analyzed the cutscene and taken note of the exact frames during which it stutters, +// and we lag the game to 15 FPS internally (this cutscene targets 20 FPS) for a few frames when it would +// have stuttered on console in order to keep the cutscene in sync. + +// What frames of the cutscene to lag on, and for how many frames. +int introStuttersStartFrames[] = { 269, 521, 583, 663, 769, 959, 1155, 1182, 1214 }; +int introStutterDurations[] = { 4, 4, 4, 4, 4, 4, 4, 4, 4 }; + +// These are reset on map load, so that the cutscene can be replayed (such as when saving and exiting) +// See func_803329AC in load_patches.c +int introCutsceneCounter = 0; +int introCutsceneNextStutter = 0; +int introCutsceneLagIndex = 0; + +bool should_lag_intro_cutscene(void) { + // No stutters left to compensate for. Exit the function early. + if (introCutsceneNextStutter == -1) { + return FALSE; + } + + // First frame of the cutscene. Set the first stutter frame. + if (introCutsceneNextStutter < introStuttersStartFrames[0]) { + introCutsceneNextStutter = introStuttersStartFrames[0]; + //recomp_printf("Start intro cutscene with timing corrections. First stutter frame: %d\n", introCutsceneNextStutter); + } + + if (introCutsceneCounter >= (introCutsceneNextStutter) && introCutsceneCounter < (introCutsceneNextStutter + introStutterDurations[introCutsceneLagIndex])) { + // A stutter would have occured on console now. Lag the game for a given amount of frames. + //recomp_printf("LAGGING. Stutter number %d. Frame number %d\n", introCutsceneLagIndex, introCutsceneCounter); + return TRUE; + } else if (introCutsceneCounter > (introCutsceneNextStutter)) { + introCutsceneLagIndex++; + if (introCutsceneLagIndex >= (int)sizeof(introStuttersStartFrames) / (int)sizeof(introStuttersStartFrames[0])) { + // That was the last stutter. We're done. + introCutsceneNextStutter = -1; + //recomp_printf("End intro cutscene. %d\n", introCutsceneNextStutter); + } else { + // Set the next stutter frame. + introCutsceneNextStutter = introStuttersStartFrames[introCutsceneLagIndex]; + //recomp_printf("Next stutter: %d\n", introCutsceneNextStutter); + } + } + return FALSE; +} +// Reset the custom cutscene frame counter and the stutter frame index used to +// correct the timings of the intro cutscene. +void reset_intro_cutscene_timings_state(void) { + introCutsceneCounter = 0; + introCutsceneNextStutter = 0; + introCutsceneLagIndex = 0; +} + +// Check the current map to see if it's a cutscene map that requires timing fixes, +// and run the relevant function if so. +void handle_cutscene_timings(void) { + switch (map_get()) { + case MAP_1E_CS_START_NINTENDO: + if (should_lag_intro_cutscene()) { + extraVis = 1; + } + introCutsceneCounter++; + break; + default: + break; + } +} \ No newline at end of file