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"]
Investigating and Correcting the Stage-Transition Crash
Reproduction in mGBA, isolation of the cause, and creation of a ROM patch
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:
- Statically analyze the ROM to locate stage-ending processing.
- Trace the identified routines in the mGBA debugger.
- Test the value supplied to the second banked call.
- Add a corrective routine in unused ROM space.
- 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.gbGet-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.
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
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 7The 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 retState 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 transitionObserving two addresses is therefore enough to narrow down the fault:
- Reaching
$240Ameans execution has entered the stage-ending routine normally. - Failing to reach
$242Aafterward 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.

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
continuebreak 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 4x/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
continuebreak 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 1listb 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 $FF90Here, $FF90 switches to a ROM bank selected by D and calls the routine at $4000 in that bank.
- The first call sets both
D=04andE=00. - The second sets only
D=05and does not resetE.
$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 retIts behavior is:
- Save the currently selected bank number.
- Use
Dto switch to another ROM bank. - Call
$4000in the selected bank. - 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
statustrace 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,$4406The 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
continueThis 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.

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
continuew/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 40x/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,lA 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 16w/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 12These 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 hlIt 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 bytesOnly 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 bytesA 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 retThe original five bytes become a call to the helper and two NOPs:
241A: CD F9 37 call $37F9
241D: 00 nop
241E: 00 nopNOP 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.ipsnode 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:
- The source ROM is 512 KiB.
- Its SHA-256 matches the analysis target.
- The original instructions at
$241Amatch expectations. $37F9-$37FFcontains seven unusedFFbytes.- Header CRC32 is
8628A287. - The corrected header
$0100-$014Fremains identical to the source. - 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 retArtifact 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=08is 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.