Investigating and Correcting the Stage-Transition Crash

Reproduction in mGBA, isolation of the cause, and creation of a ROM patch

TipConclusion

At the end of a stage, the game calls routines in other ROM banks twice. After the argument to the second call was corrected to DE=1400, mGBA 0.10.3 transitioned normally from Stage 1 to Stage 2.

The ROM header was deliberately preserved so that mGBA could continue recognizing the image as the correct special cartridge type.

About this report

When Pokémon Jade (Special Pikachu Edition) runs in mGBA 0.10.3, the display becomes corrupted and the game stops after the stage goal. This report explains how the cause was isolated and how a corrected IPS patch was created:

  1. Statically analyze the ROM to locate stage-ending processing.
  2. Trace the identified routines in the mGBA debugger.
  3. Test the value supplied to the second banked call.
  4. Add a corrective routine in unused ROM space.
  5. Generate an IPS while preserving the way mGBA identifies the ROM.

The report includes unsuccessful tests with E=00 and selector 06, not only the final correction. Although they are not part of the finished fix, they were necessary to narrow down the cause.

Target and safety policy

Target ROM

Item Value
Size 524,288 bytes (512 KiB)
Source ROM SHA-256 0E90D7659339A1F52C733BDC2B502D033E1C4C4004F761B6C539B236FA7FDD45
mGBA 0.10.3
Header CRC32 used by mGBA 8628A287
Mapper in mGBA GB_UNL_NT_NEW

SHA-256 is an identifier calculated from the entire file. A one-byte change normally produces a different value, helping prevent accidental modification of another ROM with the same filename.

Get-FileHash -Algorithm SHA256 pokemon_jade_pikachu.gb
node tools/verify-analysis.mjs pokemon_jade_pikachu.gb
NoteMeaning of the commands

Get-FileHash is a PowerShell command that calculates the source ROM’s SHA-256. node tools/verify-analysis.mjs runs this report’s verification script, which checks both the hash and the expected instruction sequences at the analyzed locations.

Handling policy

  • The source ROM is never overwritten.
  • ROMs, save files, and save states are not included in Git.
  • Generated corrected ROMs and IPS patches are only for local verification and are not distributed by this repository.
  • The correction script stops if the target ROM hash does not match.
NotePublic version

The public version includes only two game screenshots essential for discussing reproduction conditions and visual corruption, with their source and quotation purpose stated. Verification video, save states, extracted assets, and extensive disassembly are not published. Results are also recorded as factual register values, hashes, and state transitions. See Publication Policy and Rights Notice.

Minimum terminology

Term Meaning in this investigation
ROM A read-only image containing the game’s program and data.
bank A 16 KiB division of a larger ROM. The region visible to the CPU is selected as needed.
mapper Cartridge-side logic that changes which ROM bank the CPU can see.
register A small CPU workspace. This report uses registers such as A, D, and E.
DE A 16-bit value combining the 8-bit D and E registers. In the relevant routine, D selects a bank and E an operation.
PC Program Counter, the address of the next instruction.
SP Stack Pointer, the location of a stack that stores function return addresses and other values.
breakpoint A debugger setting that automatically pauses immediately before a specified address executes.
illegal opcode A state in which the CPU attempts to execute a byte that is not a valid instruction, indicating that execution has left the intended code.
$240A A hexadecimal address. The same value is entered in the debugger as 0x240A.

CPU addresses and ROM file offsets

The addresses seen by the Game Boy CPU do not always equal positions in the ROM file. Fixed bank 0 at $0000-$3FFF maps directly, but the contents visible at $4000-$7FFF depend on the currently selected ROM bank.

CPU $0000-$3FFF  -> Fixed bank 0 at all times
CPU $4000-$7FFF  -> Currently selected ROM bank

Consequently, the bytes visible at CPU address $4000 differ when selector 04 or 14 is active.

Investigation overview

flowchart TD
  A["Locate ending routine by static analysis"] --> B["Stop at state-6 entry $240A"]
  B --> C["Next state $242A is never reached"]
  C --> D["Inspect two banked calls in the interval"]
  D --> E["E changes to C1 after the first return"]
  E --> F["Retest with E=00"]
  F --> G["$4000 under selector 05 contains data"]
  G --> H["Selector 06 also does not match"]
  H --> I["Valid code appears under selector 14"]
  I --> J["Correct second call to DE=1400"]
  J --> K["Generate patch while preserving header"]
  K --> L["Stage 1 to Stage 2 transition succeeds"]

1. Choose stopping points by static analysis

The first step was to read ROM instructions and locate the processing that runs when a stage ends. Identifying the routine in advance avoided searching randomly for debugger stopping points.

Derive routine addresses from the state number

The routine beginning at $2366 reads memory address C21C as a state number and transfers control to the corresponding address. The destination table begins at $2563.

2366: FA 1C C2    ld   a,[$C21C]  ; Read current state number
236D: 21 63 25    ld   hl,$2563   ; Start of destination table
2370: C3 1E 38    jp   $381E      ; Dispatch to the state's routine

256F: 0A 24       dw   $240A      ; State 6
2571: 2A 24       dw   $242A      ; State 7

The table shows that $240A runs for C21C=06, followed by $242A for C21C=07.

Purpose of $240A and $242A

State 6 at $240A checks C213, a value used during the ending animation. It continues updating the animation until the value reaches zero, then advances C21C to 7.

240A: FA 13 C2    ld   a,[$C213]  ; Read ending-animation value
240D: B7          or   a
240E: 28 14       jr   z,$2424    ; Advance to state 7 if zero
      ...
2424: 3E 07       ld   a,$07
2426: EA 1C C2    ld   [$C21C],a
2429: C9          ret

State 7 at $242A increments logical level number C27D and begins the next screen transition.

242A: FA 7D C2    ld   a,[$C27D]
242D: 3C          inc  a
242E: EA 7D C2    ld   [$C27D],a
      ...
2439: 3E 05       ld   a,$05
243B: EA 19 C2    ld   [$C219],a  ; Start next screen transition

Observing two addresses is therefore enough to narrow down the fault:

  • Reaching $240A means execution has entered the stage-ending routine normally.
  • Failing to reach $242A afterward places the problem inside state 6, $240A-$2429.

This is why $240A was the first stopping point. The instruction excerpts required for verification are collected here and in Static Analysis of the Stage Transition.

2. Stop execution at $240A

To avoid replaying the stage for every test, an mGBA save state was stored locally immediately before contact with the goal. Figure 1 shows the relationship between the sign and player, which is difficult to specify in text alone, solely to identify the reproduction condition. The save-state file is not public.

Stage 1 immediately before the player touches the sign on the right side of the screen

Stage 1 immediately before the goal, with the player about to touch the sign on the right side of the screen

Quoted Figure 1: Immediately before the Stage 1 goal

Source: screenshot from Pokémon Jade (Special Pikachu Edition), captured by the author in mGBA 0.10.3. The full screen is included to identify the reproduction condition for ending processing that begins after contact with the sign. The title is an unlicensed work; the identity of its creators, distributors, and owners of individual assets has not been established.

The following commands were entered through Tools → Open debugger console in mGBA:

break 0x240A
continue
NoteMeaning of the commands

break 0x240A creates a breakpoint immediately before the CPU executes $240A. continue resumes the paused game.

Touching the goal stopped execution as follows:

Hit breakpoint 1 at 0x0000240A
PC: 240A  SP: C1FD
ROM: 02
00:240A: FA13C2  ld a, [$C213]

This is not a crash. Hit breakpoint means the configured address was reached normally. Contact with the goal therefore executes successfully through the entry to state 6.

The state values were also inspected while paused:

x/1 0xC219 4
NoteMeaning of the command

x/1 0xC219 4 displays four values of one byte each beginning at $C219.

0x0000C219: 00 02 01 06

The four bytes mean C219=00, C21A=02, C21B=01, and C21C=06. The final value agrees with state 6 identified statically.

3. Confirm the crash occurs before the next state

A breakpoint was then placed at the entry to state 7, $242A.

break 0x242A
continue
NoteMeaning of the commands

break 0x242A creates a breakpoint at the state-7 entry, and continue resumes execution to see whether it is reached.

The breakpoint at $242A was never reached. Instead, the CPU state became corrupted:

PC: 0000  SP: 0002
HL: 0000
00:0000: E9  jp hl

The combination PC=0000, SP=0002, and HL=0000 is implausible for ordinary function execution. Since this was not an intentional breakpoint stop, execution was judged to have left its valid path and crashed.

The investigation could now be restricted to state 6 at $240A-$2429.

listb
delete 1
NoteMeaning of the commands

listb lists configured breakpoints, and delete 1 removes breakpoint 1. mGBA 0.10.3 does not recognize disable 1, so unneeded breakpoints are removed with delete.

4. Inspect the two banked calls in state 6

The range $240A-$2429 calls the shared routine $FF90 twice.

2413: 16 04       ld   d,$04
2415: 1E 00       ld   e,$00
2417: CD 90 FF    call $FF90

241A: 16 05       ld   d,$05
241C: CD 90 FF    call $FF90

Here, $FF90 switches to a ROM bank selected by D and calls the routine at $4000 in that bank.

  • The first call sets both D=04 and E=00.
  • The second sets only D=05 and does not reset E.

$FF90 contains:

FF90: FA 0A C2    ld   a,[$C20A]  ; Read current bank value
FF93: F5          push af         ; Save original value
FF94: 7A          ld   a,d
FF95: EA 0A C2    ld   [$C20A],a
FF98: EA 00 20    ld   [$2000],a  ; Switch to bank selected by D
FF9B: CD 00 40    call $4000      ; Call routine in selected bank
FF9E: F1          pop af
FF9F: EA 0A C2    ld   [$C20A],a
FFA2: EA 00 20    ld   [$2000],a  ; Restore original bank
FFA5: C9          ret

Its behavior is:

  1. Save the currently selected bank number.
  2. Use D to switch to another ROM bank.
  3. Call $4000 in the selected bank.
  4. Restore the original bank when that call returns.

The routine preserves AF but not DE. If the routine beginning at $4000 changes E, that new value remains after returning to the caller.

5. Trace the first banked call

Execution was stopped just before the first call at $2417, then advanced one instruction at a time with trace.

break 0x2417
continue
trace 8
status
NoteMeaning of the commands

trace 8 executes and records the next eight instructions one at a time. status displays current registers, execution position, selected ROM bank, and related state. break and continue are as described above.

Phase Execution position Event
Before call $2417 Call $FF90 with DE=0400.
Bank switch $FF94-$FF98 Write D=04 to the bank-selection register.
Call within bank $FF9B Call $4000 in the selected bank.
Destination $4000 in bank 04 Use E as an operation number; here E=00.
Return $FF9E Restore the original bank after the banked routine finishes.

The trace confirmed that $FF9B transferred control to $4000 in bank 04.

PC: 00:FF9B | call $4000
PC: 04:4000 | ld a,e
ROM: 04

Bank 04 at $4000 is an entry point that selects an operation from E. Here the comparison with E=00 succeeds and branches to $4406.

4000: 7B          ld   a,e
4001: FE 00       cp   $00
4003: CA 06 44    jp   z,$4406

The final instruction shown by mGBA’s status sometimes differed from actual memory. Three sources were therefore compared around bank switches:

  • The instruction actually executed by trace
  • Raw bytes read with x/1
  • Output from disassemble

6. E changes after the first return

A breakpoint was set at $FF9E, immediately after the routine in the selected bank returns.

break 0xFF9E
continue
NoteMeaning of the commands

This breakpoint checks whether the selected-bank routine returns normally to $FF9E.

The first call returned normally, but the registers had become:

D: 9B  E: C1  (DE: 9BC1)
PC: FF9E
ROM: 04

E was 00 before the call and C1 afterward. Since the next instructions change only D to 05, the second call to $FF90 receives DE=05C1.

It never returns to $FF9E a second time and instead stops at this illegal opcode:

Hit illegal opcode at 0x0000CFFB: 0x000000FC
PC: CFFA  SP: EFFB
ROM: 08

The display was corrupted as in Figure 2. This visual evidence distinguishes the failure from a simple breakpoint stop and is presented together with the stopped address and register state.

Corrupted stage and numeric display on a yellow-green background

Corrupted game display after execution reached an illegal opcode

Quoted Figure 2: Display corruption after execution leaves the valid code

Source: screenshot from Pokémon Jade (Special Pikachu Edition), captured by the author in mGBA 0.10.3. It is included to analyze the display corruption together with PC=CFFA and ROM=08 at the illegal-opcode stop. The title is an unlicensed work; the identity of its creators, distributors, and owners of individual assets has not been established.

Failure to reset E for the second call was therefore the first candidate cause.

7. Resetting only E=00 does not fix the crash

Execution was stopped at $241C, and E=00 was written manually before the second call.

w/r e 0
continue
NoteMeaning of the commands

w/r e 0 writes 00 to register E; continue then resumes execution.

The second call now receives DE=0500, but an illegal opcode occurs elsewhere:

Hit illegal opcode at 0x0000CD1B: 0x000000E3
PC: CD1A  SP: 0841

Although E was wrong, correcting only E=00 does not select a valid routine. The other component, bank selector D=05, therefore also had to be examined.

8. $4000 under selectors 05 and 06 is not the required code

With DE=0500, the bytes at $4000 under selector 05 were inspected.

x/1 0x4000 64
disassemble 0x4000 40
NoteMeaning of the commands

x/1 0x4000 64 displays 64 bytes beginning at the currently visible $4000. disassemble 0x4000 40 interprets 40 instructions from the same location for human reading.

The opening bytes were:

10 60 90 65 8E 6D C4 75 5F 7C 5F 7C ...

Interpreting them as instructions produces an implausible function entry:

4000: 10          stop
4001: 60          ld   h,b
4002: 90          sub  b
4003: 65          ld   h,l

A disassembler will display some instruction for any byte sequence. This sequence lacks a conventional function entry or coherent control flow and is probably image or other data, not the required code.

A simple one-bank error was tested by changing the selector to 06.

w/1 0xC20A 6
w/1 0x2000 6
x/1 0x4000 16
NoteMeaning of the commands

w/1 ADDRESS VALUE writes one byte to an address. Here, 06 is written both to the bank-value mirror at $C20A and mapper selection address $2000, after which x/1 checks the newly visible $4000.

Selector 06 exposed the same data at $4000. Simply choosing the next number does not solve the problem.

9. Confirm the required routine under selector 14

Why these values are not ordinary bank numbers

The cartridge type in the ROM header is MBC1, but mGBA 0.10.3 identifies the image by header CRC32 8628A287 and applies the unlicensed-cartridge mapper GB_UNL_NT_NEW.

The investigation therefore cannot assume ordinary MBC1 bank numbering. It must observe what actually appears at $4000 when each selector is written in mGBA. The registration that selects NT-new is visible in mGBA’s official src/gb/overrides.c.

This does not claim to establish the electrical behavior of the physical cartridge; it describes behavior observed in mGBA 0.10.3.

$4000 under selector 14 is a routine entry

Selector 14 and operation 00 were set before inspecting $4000.

w/1 0xC20A 0x14
w/1 0x2000 0x14
x/1 0x4000 16
disassemble 0x4000 12
NoteMeaning of the commands

These are the same writes as in the previous section, now using selector 14, followed by inspection and disassembly of the selected bytes.

This time a coherent routine appeared that uses E to choose a jump target.

4000: 16 00       ld   d,$00
4002: 21 D0 59    ld   hl,$59D0
4005: 19          add  hl,de
4006: 2A          ld   a,[hl+]
4007: 66          ld   h,[hl]
4008: 6F          ld   l,a
4009: E9          jp   hl

It reads the address corresponding to E from a jump table near $59D0 and transfers control there. Passing E=00 selects the table’s first entry.

The routine also returned normally to $FF9E:

Hit breakpoint 2 at 0x0000FF9E
D: 00  E: 00  (DE: 0000)
PC: FF9E
ROM: 14

These results show that, in mGBA 0.10.3, the second call to $FF90 requires:

D=14  : Select selector 14
E=00  : Select the first jump-table operation
DE=1400

10. Call a six-byte operation from a five-byte location

The required instructions occupy six bytes:

ld   de,$1400  ; 3 bytes
call $FF90     ; 3 bytes

Only five bytes are available in the original range $241A-$241E:

241A: 16 05       ld   d,$05       ; 2 bytes
241C: CD 90 FF    call $FF90       ; 3 bytes

A small helper routine was therefore placed in unused space in the fixed bank and called from the original location. Placing additional instructions in unused space of an existing program is often called a code cave.

Static scanning found $2799-$37FF filled with FF. The helper occupies the last seven bytes:

37F9: 11 00 14    ld   de,$1400
37FC: CD 90 FF    call $FF90
37FF: C9          ret

The original five bytes become a call to the helper and two NOPs:

241A: CD F9 37    call $37F9
241D: 00          nop
241E: 00          nop

NOP is a one-byte instruction that does nothing. It preserves all following addresses and leaves the code beginning at $241F unchanged.

File offset Before After Purpose
0x0241A 16 05 CD 90 FF CD F9 37 00 00 Call helper routine.
0x037F9 FF FF FF FF FF FF FF 11 00 14 CD 90 FF C9 Call $FF90 with DE=1400.

11. Why the first corrected ROM would not open

The first corrected build recalculated the ROM’s global checksum after changing the code. mGBA could not run that build normally.

The reason is mGBA’s automatic special-mapper detection. mGBA calculates the CRC32 of ROM header $0100-$014F and applies NT-new when it equals 8628A287. The global checksum is stored at $014E-$014F, inside that CRC32 range.

Update the global checksum
  ↓
Header CRC32 changes
  ↓
No match with mGBA's registered override
  ↓
NT-new mapper is not selected
  ↓
ROM cannot run normally

mGBA’s search for header-CRC32 overrides appears in the 0.10.3 implementation of GBOverrideApplyDefaults.

The final build preserves $0100-$014F, including the global checksum, exactly as in the source ROM. The stored global checksum therefore no longer matches the modified ROM contents, but the header CRC32 remains 8628A287, so mGBA still applies NT-new.

12. Generate a reproducible patch

tools/build-stage-transition-patch.mjs generates the corrected ROM and IPS.

node tools/build-stage-transition-patch.mjs `
  pokemon_jade_pikachu.gb `
  build/pokemon_jade_pikachu_stage_transition_fixed_mgba.gb `
  build/pokemon_jade_pikachu_stage_transition_fix_mgba.ips
NoteMeaning of the command

node tools/build-stage-transition-patch.mjs runs the patch generator. Its three arguments specify the source ROM, corrected ROM output, and IPS output. A backtick at the end of a PowerShell line continues the command on the following line.

Before writing anything, the script checks:

  1. The source ROM is 512 KiB.
  2. Its SHA-256 matches the analysis target.
  3. The original instructions at $241A match expectations.
  4. $37F9-$37FF contains seven unused FF bytes.
  5. Header CRC32 is 8628A287.
  6. The corrected header $0100-$014F remains identical to the source.
  7. Reapplying the generated IPS to the source produces a byte-for-byte match with the corrected ROM.

Success produces a summary like:

PASS source SHA-256 0E90D765...
PASS preserved header CRC32 8628A287
PASS source bytes at $241A and $37F9
PASS IPS round-trip reproduces patched ROM

What is an IPS patch?

IPS is a patch format containing only the differences between the source and corrected ROMs. This IPS does not contain the source ROM; it holds records for the 12 changed bytes plus IPS format metadata.

For this investigation, the IPS was applied locally to a lawfully usable source ROM. Neither the generated ROM nor the IPS file is published or distributed; the correction is documented as offsets, lengths, byte sequences, and a generation script.

13. Final verification

Static verification

Disassembling the generated ROM confirmed these instructions:

241A: CD F9 37    call $37F9
241D: 00          nop
241E: 00          nop

37F9: 11 00 14    ld   de,$1400
37FC: CD 90 FF    call $FF90
37FF: C9          ret

Artifact identifiers are:

Artifact SHA-256
Source ROM 0E90D7659339A1F52C733BDC2B502D033E1C4C4004F761B6C539B236FA7FDD45
mGBA-compatible corrected ROM 7463D30924B2827CFB824F986A5571B079B7F4952E1912023C5705B91E39CFCA

Dynamic verification

mGBA was closed completely and the final header-preserving ROM was reopened. Starting immediately before the Stage 1 goal, the game advanced to Stage 2 without display corruption.

A 13-second screen recording was made during verification but is not included in the public version. The public record instead gives the corrected-ROM SHA-256, reached state, and emulator version.

Verification item Result
Emulator mGBA 0.10.3
Corrected ROM SHA-256 7463D30924B2827CFB824F986A5571B079B7F4952E1912023C5705B91E39CFCA
Starting condition Immediately before the Stage 1 goal
Observation Transitioned to Stage 2 without display corruption

14. Lessons from the investigation

A breakpoint stop is different from a crash

Hit breakpoint means the configured address was reached normally. By contrast, PC=0000 and Hit illegal opcode indicated that execution left its intended path. The message, PC, and current instruction must be considered together.

Both E and the bank selector were wrong

The first call changes E, so the second originally receives E=C1. Correcting only E=00 does not eliminate the crash, however. The selector must also change from 05 to 14, giving DE=1400.

Disassemblable data is not necessarily code

The bytes at $4000 under selector 05 can be displayed as instructions, but their sequence is implausible as a function. A valid interpretation also requires a coherent entry, control flow, and return path.

mGBA also uses the ROM header to select a mapper

Even a correct code change can make mGBA treat the ROM as a different cartridge if the header CRC32 changes. This build leaves the header untouched so that mGBA continues selecting NT-new.

15. Further checks

The core investigation first confirmed the transition from Stage 1 to Stage 2. Additional checks can include:

  • Confirm the same ending routine at each later-stage goal.
  • Check that the final-stage branch C219=08 is unaffected.
  • Compare a start from an ordinary save with loading a save state from the old ROM.
  • Test newer mGBA releases and hhugboy.
  • Confirm that another IPS tool produces the same corrected-ROM SHA-256.

Appendix A: mGBA debugger command reference

Command Description Example
break ADDRESS Add an execution breakpoint. break 0x241C
continue Resume execution. continue
status Display CPU and bank state. status
trace N Execute and record N instructions. trace 8
disassemble ADDRESS N Disassemble N instructions from an address. disassemble 0xFF90 20
r/1 ADDRESS Read one byte from an address. r/1 0xC213
x/1 ADDRESS N Display N one-byte values from an address. x/1 0x4000 16
w/1 ADDRESS VALUE Write one byte to memory. w/1 0x2000 0x14
w/r REGISTER VALUE Change a register value. w/r e 0
listb List breakpoints. listb
delete ID Delete a numbered breakpoint. delete 1

Debugger commands vary by mGBA version. Version 0.10.3 did not accept disable, so delete was used.