Fix intro cutscene timings (#56)

* Fix intro cutscene timings

* Address review

* Move code to individual functions

* address review

* Fix comments
This commit is contained in:
Reonu
2026-01-08 03:41:26 +00:00
committed by GitHub
parent 5fefef83a2
commit b1e0b41006
3 changed files with 89 additions and 3 deletions
+4
View File
@@ -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);
}
+6
View File
@@ -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();
}
+79 -3
View File
@@ -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;
}
}