Place skipped compatibility data at boot, SoftReset, SWI, and IRQ exit PC+8 locations. Preserve the real IRQ callback return instruction as its readback word. Keep CPU and memory behavior image-derived, including caller-supplied images. Add original boundary, load-lane, caller-state, wait, supplied-image, and builder-fixup regressions. Document the historical failures, research decision, passing public BIOS checkpoint, and remaining fetch-history limits. Validated on Darwin arm64: 868 workspace tests in debug and release; 799 core/demo tests separately in both profiles; 39 preparation tests; native ROM window tests in both profiles; ten graphics/window smoke modes; fmt, clippy, and rustdoc. Hash-verified public ARM, Thumb, memory, and BIOS suites pass unchanged with identical debug/release reports. BIOS: r12=0, 17606 steps, 197456 nominal cycles, one IRQ entry.
6.5 KiB
BIOS protected-read mismatch: root cause and fix
Finding
The public BIOS failure came from the replacement firmware's exit layout, not its protected-load byte lanes. The memory bus correctly returned the replacement image's retained ARM PC+8 word. Before the layout fix, that word differed from the documented value left by the standard GBA firmware. Matching the bus mechanism alone does not make replacement firmware's observable outputs compatible.
Before the firmware-layout fix, at the initial failing checkpoint:
- The unmodified public ROM expects
0xe129f000after boot. - Our replacement's final boot instruction is at
0x60; its PC+8 word at0x68is0xe59f1320. - The returned value matches that image word, rather than the reset vector or ROM caller's prefetch.
- The ROM sets r12 to 1 and reaches its real evaluation checkpoint. This is an assertion failure, not a CPU diagnostic.
The original regression original_boot_protected_read_matches_its_actual_last_bios_snapshot verifies this image-derived behavior.
It computes the expected word from the executed boot trace, not from a Nintendo opcode constant.
Evidence
GBATEK documents PC-dependent BIOS protection and the most recently fetched BIOS opcode. For standard ARM firmware exits, the observed word comes from the exit instruction's PC+8 location. The pinned public test checks these lifecycle values:
| Firmware boundary | Standard source address | Protected-read word |
|---|---|---|
| Boot / SoftReset exit | 0x0dc + 8 |
0xe129f000 |
| SWI return | 0x188 + 8 |
0xe3a02004 |
| Inside external IRQ callback | 0x134 + 8 |
0xe25ef004 |
| IRQ return | 0x13c + 8 |
0xe55ec002 |
The addresses describe standard firmware. Our replacement does not need the same internal addresses to expose the same values.
Smolka's Progress Report #6 directly compares original and replacement BIOS return prefetch. It traces a Pokémon Ruby/Sapphire null-pointer read to this firmware-dependent value. The report notes that Emerald fixed that particular game bug. It does not establish Emerald compatibility for our emulator. The report also shows that different firmware values can be valid under the bus rule while affecting software behavior.
mGBA's software BIOS implementation explicitly sets biosPrefetch to 0xe3a02004 after a returning SWI.
Its memory implementation instead retains fetched BIOS data when executing a mapped BIOS image.
These are different implementation paths for the same observable behavior. Our core uses executed ARM firmware, not intercepted SWIs.
The Cult-of-GBA replacement BIOS was also reviewed. Its original assembly and MIT license illustrate the separate roles of firmware routines and their surrounding image layout. No replacement BIOS code, binary, artwork, or Nintendo BIOS image was copied into this repository.
Implementation decision
Make the original generated firmware expose the documented lifecycle readback values through its real image layout. Do not add a bus override, game identifier, ROM hash check, result-register write, or public-test patch. Do not change caller-supplied BIOS images or their generic protection behavior.
Reserve two words immediately after each relevant non-fallthrough exit:
- Put an intentional diagnostic trap at exit PC+4.
- Put the documented compatibility word at exit PC+8.
The exit skips both words. The normal ARM prefetch snapshot observes the second word. These words are explicit compatibility data in the generated image, not executable copies of firmware routines. The builder's normal fixups relocate branches and literals around them. Cold boot and SoftReset need the reset word. Returning SWIs and IRQs need their respective words.
The IRQ callback path already has the real SUBS pc, lr, #4 instruction at the callback branch's PC+8.
Keep that executed return instruction in place and test the layout invariant separately.
This corrects the earlier overly broad claim that reproducing any documented opcode value would merely force a test pass. Arbitrarily overriding a read to satisfy an assertion would be wrong. Reproducing documented firmware boundary outputs for all callers is a legitimate compatibility feature, provided its scope is explicit.
Implemented result
ArmImage::prefetch_tail adds skipped compatibility data after boot, SoftReset, SWI return, and IRQ return.
The change adds 32 image bytes without adding normally executed instructions.
The callback path retains its actual SUBS pc,lr,#4 at the branch's PC+8.
No CPU or memory-bus implementation changed.
Original regressions in crates/gba-core/tests/bios/readback.rs reproduced the boundary failures before the fix.
They now pass for ARM/Thumb callers, load lanes and widths, User/System state, both reset targets, and interrupt waits.
A supplied-image variant keeps its own changed word, proving that no compatibility constant overrides image data.
Builder regressions verify branch and literal relocation around the skipped data.
The unchanged public BIOS ROM passes in debug and release on Darwin arm64. It reaches its checkpoint with r12 = 0 and one IRQ entry. Public ARM, Thumb, and memory results remain passing. Workspace tests, preparation tests, lint checks, rustdoc, native ROM windows, and graphics smoke tests also pass.
Validation requirements and limits
- Run the pinned BIOS ROM unchanged, with its original budget, checkpoint, and r12 assertion.
- Test boot, SoftReset, returning SWIs, IRQ callbacks, and IRQ return with original programs.
- Verify byte lanes, load widths, caller state, banked stacks, and ARM/Thumb returns.
- Verify that the compatibility words are never executed during normal exit paths.
- Keep synthetic caller-supplied images image-derived, including images with different exit words.
- Rerun public ARM, Thumb, and memory suites and native Darwin regressions.
This change does not implement a persistent instruction pipeline, Thumb BIOS fetch history, or exact firmware timing. It does not prove complete BIOS compatibility or commercial-game support. The public BIOS ROM checks these protected-read values; it is not a comprehensive BIOS service suite.