Bejeweled 2: Fix the crash at the end of the loading bar

While loading, the game presents every frame but also pushes its
"next draw" time 35-50ms ahead per frame, so by the end of loading
it's about 7 seconds in the future. If loading finishes while the main
thread is in the middle of a present, that present resets the time and
nothing goes wrong. If it finishes anywhere else, the game stops
presenting for those 7 seconds but keeps drawing every frame into the
same 128KB display list, which overruns into the game's own globals
within a second and crashes.

Which way it goes depends on small timing differences.

A hook on the draw function does what the winning side of the race
does: once loading has completed, it pulls the next draw time back if
it's more than a second ahead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-16 12:42:53 +02:00
1 parent ea8b92ebb7
commit f53c27da1b
2 files changed
+31

No files matched your search

+30
View File
@@ -1758,6 +1758,35 @@ static int Hook_ZZT3_select_hack() {
return 0;
}
// Bejeweled 2 only presents once mNextDrawTick has passed, and while the loading thread runs, it moves
// it 35-50ms on per present to hold the loading screen to 20fps. The PSP port presents every frame while
// loading, but still moves it on, so by the end of loading it's seconds ahead.
// Whether that's undone depends on a race: if mLoadingThreadCompleted is set during a present, the present
// ends by setting mNextDrawTick to now. If not, nothing is presented for those seconds while every frame
// still draws into the same display list, which overruns its 128KB into the game's globals and crashes.
// So, we do what the present would have, once loading has completed.
static int Hook_bejeweled2_draw_dirty_stuff() {
const u32 func = currentMIPS->pc;
const u32 app = currentMIPS->r[MIPS_REG_A0];
// The hash masks immediates, so check the fields are where we expect.
if (Memory::Read_Instruction(func + 0x48).encoding != 0x90840411 || // lbu a0, 0x411(a0) - mLoadingThreadStarted
Memory::Read_Instruction(func + 0x58).encoding != 0x90840412 || // lbu a0, 0x412(a0) - mLoadingThreadCompleted
Memory::Read_Instruction(func + 0xC0).encoding != 0x8C8403C8 || // lw a0, 0x3C8(a0) - mLastDrawTick
Memory::Read_Instruction(func + 0xDC).encoding != 0x8C8403CC) { // lw a0, 0x3CC(a0) - mNextDrawTick
return 0;
}
if (!Memory::IsValidRange(app, 0x414) || Memory::ReadUnchecked_U8(app + 0x412) == 0) {
return 0;
}
// After loading, a present leaves mNextDrawTick at mLastDrawTick.
const u32 lastDrawTick = Memory::ReadUnchecked_U32(app + 0x3C8);
const u32 nextDrawTick = Memory::ReadUnchecked_U32(app + 0x3CC);
if ((s32)(nextDrawTick - lastDrawTick) > 1000) {
Memory::WriteUnchecked_U32(lastDrawTick, app + 0x3CC);
}
return 0;
}
#define JITFUNC(f) (&MIPSComp::MIPSFrontendInterface::f)
// Can either replace with C functions or functions emitted in Asm/ArmAsm.
@@ -1896,6 +1925,7 @@ static const ReplacementTableEntry entries[] = {
{ "resistance_draw_trail", &Hook_resistance_draw_trail, 0, REPFLAG_HOOKENTER, 0x198 },
{ "resistance_update_trail", &Hook_resistance_update_trail, 0, REPFLAG_HOOKENTER, 0x438 },
{ "resistance_muzzle_light", &Hook_resistance_muzzle_light, 0, REPFLAG_HOOKENTER, 0xF0 },
{ "bejeweled2_draw_dirty_stuff", &Hook_bejeweled2_draw_dirty_stuff, 0, REPFLAG_HOOKENTER, 0 },
{}
};
+1
View File
@@ -432,6 +432,7 @@ static const HardHashTableEntry hardcodedHashes[] = {
{ 0xc51519f5dab342d4, 224, "cosf", },
{ 0xc52c14b9af8c3008, 76, "memcmp", },
{ 0xc54eae62622f1e11, 164, "dl_write_bone_matrix_2", },
{ 0xc626403884cabcbf, 532, "bejeweled2_draw_dirty_stuff", }, // Bejeweled 2
{ 0xc6b29de7d3245198, 656, "starocean_write_stencil" }, // Star Ocean 1 (US)
{ 0xc7b1113cfdfedab6, 104, "tonyhawkp8_upload_tutorial_frame", }, // Tony Hawk's Project 8 (US)
{ 0xc96e3a087ebf49a9, 100, "dl_write_light_color", },