diff --git a/README.md b/README.md index b8acc20..60a8bad 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Use `--test-suite suite.json` for [repeatable headless ROM assertions](docs/rom- | [docs/public-arm-tests.md](docs/public-arm-tests.md) | Pinned public ARM test, preparation command, results, and limits | | [docs/public-thumb-tests.md](docs/public-thumb-tests.md) | Pinned public Thumb test, result protocol, and compatibility baseline | | [docs/public-memory-tests.md](docs/public-memory-tests.md) | Pinned public memory test, results, and video-byte coverage limits | -| [docs/public-bios-tests.md](docs/public-bios-tests.md) | Pinned BIOS read-protection test and firmware-specific failure | +| [docs/public-bios-tests.md](docs/public-bios-tests.md) | Pinned BIOS read-protection result and firmware limits | | [docs/architecture.md](docs/architecture.md) | Crate boundaries and a file-by-file map | | [docs/hardware/cpu.md](docs/hardware/cpu.md) | ARM/Thumb execution rules, modes, exceptions, and timing | | [docs/hardware/cartridge.md](docs/hardware/cartridge.md) | ROM loading, window/input controls, original ROM generator, and diagnostics | diff --git a/crates/gba-core/src/bios.rs b/crates/gba-core/src/bios.rs index 73ab709..fa7f116 100644 --- a/crates/gba-core/src/bios.rs +++ b/crates/gba-core/src/bios.rs @@ -45,7 +45,8 @@ pub fn boot(rom: Vec) -> Result { } /// Build a deterministic 16 KiB image from original ARM instructions and a -/// mathematically generated sine table. No Nintendo firmware bytes are included. +/// mathematically generated sine table. Documented protected-read compatibility +/// words are explicit exit data; no Nintendo firmware routines are included. /// SWIs use bits 16–23 of the ARM immediate, or the Thumb immediate byte. /// Services support User/System callers. Returning services preserve caller status /// and registers other than each arithmetic service's documented outputs. @@ -90,6 +91,7 @@ pub fn image() -> Vec { a.emit(0xe5c1_0000); // STRB r0,[r1] a.emit(0xe321_f01f); // MSR CPSR_c,#System a.literal(15, ROM_START); // LDR pc,=ROM_START + a.prefetch_tail(0xe129_f000); // Boot / SoftReset protected-read word. a.label("irq"); a.emit(0xe92d_500f); // STMDB sp!,{r0-r3,r12,lr} @@ -103,7 +105,10 @@ pub fn image() -> Vec { a.emit(0xe1a0_e00f); // MOV lr,pc (return after BX) a.emit(0xe12f_ff11); // BX r1 a.emit(0xe8bd_500f); // LDMIA sp!,{r0-r3,r12,lr} + + // This instruction is also the callback branch's PC+8 readback word. a.emit(0xe25e_f004); // SUBS pc,lr,#4 + a.prefetch_tail(0xe55e_c002); // IRQ return protected-read word. a.label("swi"); a.emit(0xe92d_500f); // STMDB sp!,{r0-r3,r12,lr} @@ -268,6 +273,7 @@ pub fn image() -> Vec { a.emit(0xe169_f003); // MSR SPSR_fc,r3 a.emit(0xe8bd_500f); // LDMIA sp!,{r0-r3,r12,lr} a.emit(0xe1b0_f00e); // MOVS pc,lr + a.prefetch_tail(0xe3a0_2004); // SWI return protected-read word. a.label("unsupported"); a.emit(UNSUPPORTED_TRAP); a.label("invalid_argument"); @@ -331,6 +337,14 @@ impl ArmImage { self.code.push(instruction); } + /// Place only after a non-fallthrough exit. Neither word is executed. + /// The normal ARM PC+8 snapshot exposes this documented compatibility data. + /// Keep the bus image-derived, including for caller-supplied BIOS images. + fn prefetch_tail(&mut self, value: u32) { + self.emit(UNSUPPORTED_TRAP); // PC+4: diagnose accidental fallthrough. + self.emit(value); // PC+8: retained BIOS word after the exit. + } + fn label(&mut self, name: &'static str) { assert!( self.labels.insert(name, self.code.len()).is_none(), @@ -418,6 +432,32 @@ mod image_tests { ); } + #[test] + fn prefetch_tail_keeps_branch_and_literal_fixups_outside_compatibility_data() { + let mut a = ArmImage::default(); + a.literal(0, 0x1122_3344); + a.branch(14, "after_tail"); + a.prefetch_tail(0xe129_f000); + a.label("after_tail"); + a.emit(0xe12f_ff1e); // BX lr + let image = a.finish(BIOS_SIZE); + let actual: Vec<_> = image[..24] + .chunks_exact(4) + .map(|bytes| u32::from_le_bytes(bytes.try_into().unwrap())) + .collect(); + assert_eq!( + actual, + [ + 0xe59f_000c, // Literal at 20, relative to PC+8. + 0xea00_0001, // Branch at 4 skips both tail words to 16. + UNSUPPORTED_TRAP, + 0xe129_f000, + 0xe12f_ff1e, + 0x1122_3344, + ] + ); + } + #[test] #[should_panic(expected = "BIOS code overlaps reserved data")] fn code_cannot_overlap_reserved_tables() { diff --git a/crates/gba-core/src/bios/reset.rs b/crates/gba-core/src/bios/reset.rs index fba3428..67935e9 100644 --- a/crates/gba-core/src/bios/reset.rs +++ b/crates/gba-core/src/bios/reset.rs @@ -40,6 +40,7 @@ pub(super) fn emit(a: &mut ArmImage) { // IME, IE, IF, devices, or unrelated CPU banks. IRQ stays masked throughout. a.emit(0xe329_f09f); // MSR CPSR_fc,#System|I a.emit(0xe12f_ff1e); // BX lr (ARM target, no return through erased SWI frame) + a.prefetch_tail(0xe129_f000); // Same protected-read word as cold boot. emit_ram_reset(a); } diff --git a/crates/gba-core/tests/bios.rs b/crates/gba-core/tests/bios.rs index e9ad876..a86a41f 100644 --- a/crates/gba-core/tests/bios.rs +++ b/crates/gba-core/tests/bios.rs @@ -28,6 +28,8 @@ mod huffman; mod lz77; #[path = "bios/ram_reset.rs"] mod ram_reset; +#[path = "bios/readback.rs"] +mod readback; #[path = "bios/reset.rs"] mod reset; #[path = "bios/run_length.rs"] diff --git a/crates/gba-core/tests/bios/readback.rs b/crates/gba-core/tests/bios/readback.rs new file mode 100644 index 0000000..6a66898 --- /dev/null +++ b/crates/gba-core/tests/bios/readback.rs @@ -0,0 +1,253 @@ +//! Firmware boundary outputs, checked through CPU loads rather than host reads. +use super::*; +use gba_core::cpu::Cpu; + +const PROBE: u32 = 0x0203_0000; + +// A separate CPU leaves the caller's registers and device clocks unchanged. +// All probe instructions run outside BIOS, so they cannot replace its retained word. +fn probe(bus: &mut Memory, thumb: bool, address: u32, arm: u32, half: u16) -> u32 { + let code = if thumb { + vec![ + 0xe28f_3001, // ADD r3,pc,#1 + 0xe12f_ff13, // BX r3 + u32::from(half) << 16 | 0x4801, // LDR r0,[pc,#4]; load r1 + 0xe7fe_e7fe, + address, + ] + } else { + vec![0xe59f_0004, arm, 0xeaff_fffe, address] + }; + for (index, word) in code.into_iter().enumerate() { + bus.write32(PROBE + index as u32 * 4, word).unwrap(); + } + let mut cpu = Cpu::new(PROBE); + for _ in 0..if thumb { 4 } else { 2 } { + cpu.step(bus).unwrap(); + } + cpu.registers()[1] +} + +fn assert_readback(machine: &mut Machine, expected: u32) { + for thumb in [false, true] { + for base in [0, 0x3ffc] { + assert_eq!( + probe(machine.memory_mut(), thumb, base, 0xe590_1000, 0x6801), + expected + ); + for lane in 0..4 { + let byte = (expected >> (lane * 8)) & 0xff; + assert_eq!( + probe( + machine.memory_mut(), + thumb, + base + lane, + 0xe5d0_1000, + 0x7801 + ), + byte + ); + assert_eq!( + probe( + machine.memory_mut(), + thumb, + base + lane, + 0xe1d0_10d0, + 0x5681 + ), + byte as u8 as i8 as i32 as u32 + ); + } + for lane in [0, 2] { + let half = (expected >> (lane * 8)) & 0xffff; + assert_eq!( + probe( + machine.memory_mut(), + thumb, + base + lane, + 0xe1d0_10b0, + 0x8801 + ), + half + ); + assert_eq!( + probe( + machine.memory_mut(), + thumb, + base + lane, + 0xe1d0_10f0, + 0x5e81 + ), + half as u16 as i16 as i32 as u32 + ); + } + } + } +} + +// Return the actual last BIOS instruction, not a fixed firmware address. +fn reach_exit(machine: &mut Machine, target: u32, limit: usize) -> u32 { + let mut exit = None; + for _ in 0..limit { + if machine.cpu().pc() == target { + return exit.expect("must leave BIOS before target"); + } + let pc = machine.cpu().pc(); + machine.step().unwrap(); + if pc < BIOS_SIZE as u32 && machine.cpu().pc() >= BIOS_SIZE as u32 { + exit = Some(pc); + } + } + panic!("did not reach {target:#x}"); +} + +fn assert_tail(machine: &Machine, exit: u32, expected: u32) { + assert_eq!(machine.memory().read32(exit + 4).unwrap(), UNSUPPORTED_TRAP); + assert_eq!(machine.memory().read32(exit + 8).unwrap(), expected); +} + +#[test] +fn boot_readback_comes_from_guarded_firmware_tail() { + let mut machine = bios::boot(words(&[0xeaff_fffe])).unwrap(); + let exit = reach_exit(&mut machine, ROM_START, 100); + assert_tail(&machine, exit, 0xe129_f000); + assert_readback(&mut machine, 0xe129_f000); + assert_eq!(machine.cpu().cpsr(), 0x1f); + assert_eq!(machine.cpu().registers()[13], SYSTEM_STACK); +} + +#[test] +fn soft_reset_readback_matches_both_restart_targets_and_caller_states() { + for thumb in [false, true] { + for status in [0x10, 0x1f] { + for flag in [0, 1] { + let (mut machine, _) = call_status(0, thumb, [1, 2, 3], status); + machine.memory_mut().write8(0x0300_7ffa, flag).unwrap(); + machine.step().unwrap(); + let target = if flag == 0 { ROM_START } else { SOURCE }; + let exit = reach_exit(&mut machine, target, 2000); + assert_tail(&machine, exit, 0xe129_f000); + assert_readback(&mut machine, 0xe129_f000); + assert_eq!(machine.cpu().cpsr(), 0x9f); + assert_eq!(&machine.cpu().registers()[..13], &[0; 13]); + assert_eq!(machine.cpu().registers()[13], SYSTEM_STACK); + assert_eq!(machine.cpu().registers()[14], target); + } + } + } +} + +#[test] +fn returning_services_share_readback_without_changing_caller_status() { + for thumb in [false, true] { + for status in [0x10, 0x1f, 0x9f] { + for service in [1, 8, 0x0b, 0x0c] { + let (mut machine, target) = call_status(service, thumb, [0, 0, 0], status); + let before = machine.cpu().clone(); + machine.step().unwrap(); + let exit = reach_exit(&mut machine, target, 1000); + assert_tail(&machine, exit, 0xe3a0_2004); + assert_readback(&mut machine, 0xe3a0_2004); + assert_eq!(machine.cpu().cpsr(), before.cpsr()); + assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]); + } + } + } +} + +#[test] +fn irq_callback_and_return_have_distinct_image_derived_readback() { + for thumb in [false, true] { + for status in [0x10, 0x1f] { + let (mut machine, _) = call_status(8, thumb, [25, 2, 3], status); + let before = machine.cpu().clone(); + callback(machine.memory_mut()); + let prefix = CALLBACK + 0x80; + for (index, word) in [ + 0xe3a0_0000, // MOV r0,#0 + 0xe590_1000, // LDR r1,[r0] (protected BIOS read) + 0xe59f_2008, // LDR r2,[pc,#8] + 0xe582_1000, // STR r1,[r2] + 0xe3a0_0301, // MOV r0,#I/O base + 0xeaff_ffd9, // B CALLBACK (keep lr) + DEST, + ] + .into_iter() + .enumerate() + { + machine + .memory_mut() + .write32(prefix + index as u32 * 4, word) + .unwrap(); + } + machine.memory_mut().write32(IRQ_HANDLER, prefix).unwrap(); + machine.memory_mut().write16(DISPSTAT, 8).unwrap(); + machine.memory_mut().write16(IE, 1).unwrap(); + machine.memory_mut().write16(IME, 1).unwrap(); + machine.memory_mut().advance_cycles(VBLANK_START); + assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); + let dispatch = reach_exit(&mut machine, prefix, 100); + assert_eq!(machine.memory().read32(dispatch + 4).unwrap(), 0xe8bd_500f); + assert_eq!(machine.memory().read32(dispatch + 8).unwrap(), 0xe25e_f004); + assert_readback(&mut machine, 0xe25e_f004); + let exit = reach_exit(&mut machine, before.pc(), 100); + assert_tail(&machine, exit, 0xe55e_c002); + assert_eq!(machine.memory().read32(DEST).unwrap(), 0xe25e_f004); + assert_readback(&mut machine, 0xe55e_c002); + assert_eq!(machine.cpu().cpsr(), before.cpsr()); + assert_eq!(machine.cpu().registers(), before.registers()); + assert_eq!(machine.memory().read16(IF).unwrap(), 0); + assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 1); + } + } +} + +#[test] +fn interrupt_wait_replaces_irq_readback_with_swi_readback_on_return() { + for thumb in [false, true] { + let (mut machine, target) = call(5, thumb, [1, 2, 3]); + let before = machine.cpu().clone(); + callback(machine.memory_mut()); + machine.memory_mut().write16(DISPSTAT, 8).unwrap(); + machine.memory_mut().write16(IE, 1).unwrap(); + machine.step().unwrap(); + let mut slept = false; + let mut irqs = 0; + let mut callback_seen = false; + for _ in 0..1000 { + if machine.cpu().pc() == target { + break; + } + if machine.cpu().pc() == CALLBACK { + callback_seen = true; + assert_readback(&mut machine, 0xe25e_f004); + } + match machine.step().unwrap() { + StepKind::HaltIdle => slept = true, + StepKind::IrqEntry => irqs += 1, + _ => {} + } + } + assert!(slept && callback_seen); + assert_eq!(irqs, 1); + assert_eq!(machine.cpu().pc(), target); + assert_readback(&mut machine, 0xe3a0_2004); + assert_eq!(machine.cpu().cpsr(), before.cpsr()); + assert_eq!(&machine.cpu().registers()[..15], &before.registers()[..15]); + assert_eq!(machine.memory().read16(IRQ_FLAGS).unwrap(), 0); + } +} + +#[test] +fn supplied_image_tail_is_not_overridden_by_compatibility_constants() { + let rom = words(&[0xeaff_fffe]); + let mut original = bios::boot(rom.clone()).unwrap(); + let exit = reach_exit(&mut original, ROM_START, 100); + let mut image = bios::image(); + let value: u32 = 0x89ab_cdef; + image[exit as usize + 8..exit as usize + 12].copy_from_slice(&value.to_le_bytes()); + let mut machine = Machine::new(Cpu::at_reset(), Memory::with_bios(rom, image).unwrap()); + assert_eq!(reach_exit(&mut machine, ROM_START, 100), exit); + assert_readback(&mut machine, value); + assert_ne!(machine.memory().read32(0).unwrap(), value); // Host reads remain raw. +} diff --git a/docs/architecture.md b/docs/architecture.md index f93a426..46856aa 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -125,6 +125,7 @@ The repository is a Cargo workspace with three crates: | `crates/gba-core/tests/bios/stop.rs` | ARM/Thumb Stop calls, caller preservation, keypad wake, and stopped frame running | | `crates/gba-core/tests/halt.rs` | HALT wake masks, idle timing, BIOS-only power writes, DMA progress, and validation | | `crates/gba-core/tests/bios.rs` | ARM/Thumb service calls, copy/fill boundaries, wait races, callback contracts, and boot | +| `crates/gba-core/tests/bios/readback.rs` | Image-derived boot/reset/SWI/IRQ compatibility words, load lanes, caller state, and supplied-image isolation | | `crates/gba-core/tests/bios/ram_reset.rs` | Selective RAM boundaries, flag combinations, I/O reset, display rendering, DMA/timer latches, and diagnostics | | `crates/gba-core/tests/bios/reset.rs` | All restart flags, exact RAM boundaries, CPU banks, restart execution, device continuity, and IRQ masking | | `crates/gba-core/tests/bios/arithmetic.rs` | Arithmetic boundaries, wide-integer references, status restoration, and zero-division diagnostics | diff --git a/docs/hardware/bios.md b/docs/hardware/bios.md index e69593c..7a90aa8 100644 --- a/docs/hardware/bios.md +++ b/docs/hardware/bios.md @@ -1,9 +1,9 @@ # BIOS replacement -The original, Nintendo-free BIOS image built by `crates/gba-core/src/bios/` and the services it implements. +The original BIOS replacement built by `crates/gba-core/src/bios/` and the services it implements. `crates/gba-core/src/bios.rs` builds a deterministic 16 KiB image from original ARM instructions and mathematically generated data. -No Nintendo firmware bytes are included. +No Nintendo firmware routines or dumped image are included. Documented protected-read words are explicit compatibility data. A compile-time integer calculation generates the 512-byte sine table at `0x3e00..0x3fff`. The image builder checks that code and literal pools do not overlap this table. Literal pools follow unconditional branches and stay within each load instruction's address range. @@ -108,8 +108,21 @@ The current interpreter implements a bounded ARM prefetch model: The initial history is unknown. Direct cartridge startup with `Cpu::new` does not manufacture boot prefetch data. Use BIOS execution, such as `bios::boot`, to establish supported history. -All retained words come from the supplied image. No Nintendo opcode constants are substituted. -The original replacement's boot and return paths therefore produce different values from Nintendo firmware. +All retained words come from the supplied image. The bus never substitutes a firmware-specific constant. +Our generated image places documented compatibility data at PC+8 of its non-fallthrough exits: + +| Boundary | Retained word | +| --- | --- | +| Boot and SoftReset | `0xe129f000` | +| SWI return | `0xe3a02004` | +| IRQ callback entry | `0xe25ef004` | +| IRQ return | `0xe55ec002` | + +Boot, SoftReset, SWI return, and IRQ return skip a diagnostic trap at PC+4 and the data word at PC+8. +The callback branch instead has the real `SUBS pc,lr,#4` return instruction at PC+8. +Builder fixups preserve this layout without adding executed instructions or host-side service hooks. +Other supplied BIOS images retain their own values; their data is not rewritten. +See the [root-cause research](../research/bios-readback.md) for evidence and the firmware-versus-bus distinction. Host/debug reads and ROM-suite memory assertions still inspect raw mapped bytes and do not initialize or change the history. Instruction fetches remain strict mapped reads outside the CPU data-access context. @@ -118,7 +131,8 @@ Missing BIOS images remain unmapped. The unused-memory ARM open-bus snapshot is This models neither a persistent fetch pipeline nor exact bus history across Thumb BIOS code and DMA. Original regressions cover synthetic images, boot, exception returns, access widths, alignment, modes, and diagnostics. -The [public BIOS ROM](../public-bios-tests.md) still fails its firmware-specific test 1; this feature is not a public BIOS pass. +The [public BIOS ROM](../public-bios-tests.md) passes its unchanged protected-read assertions in debug and release builds. +This result covers these firmware boundaries, not full BIOS service compatibility. ## Stop diff --git a/docs/public-arm-tests.md b/docs/public-arm-tests.md index 67a0e4c..e087797 100644 --- a/docs/public-arm-tests.md +++ b/docs/public-arm-tests.md @@ -112,7 +112,8 @@ See the bounded [ARM open-bus implementation](hardware/cpu.md#arm-unused-memory- This is a passing result for this pinned ARM ROM, not proof of complete instruction, memory, or timing compatibility. The [pinned public Thumb ROM](public-thumb-tests.md) also passes its verified checkpoint. The [pinned public memory ROM](public-memory-tests.md) passes its limited mirror and video-byte checks. -No public timing, BIOS, graphics, save, or unsafe suite has been claimed as passing. +The [public BIOS read-protection ROM](public-bios-tests.md) has a separate bounded passing result. +No public timing, graphics, save, or unsafe suite has been claimed as passing. These results do not establish Pokémon Emerald compatibility. ## References diff --git a/docs/public-bios-tests.md b/docs/public-bios-tests.md index f75e8c9..adb39e0 100644 --- a/docs/public-bios-tests.md +++ b/docs/public-bios-tests.md @@ -1,7 +1,7 @@ # Pinned public BIOS read-protection test The headless runner executes the unmodified BIOS test ROM from [jsmolka/gba-tests](https://github.com/jsmolka/gba-tests). -It currently fails test 1 on Darwin arm64 in debug and release builds. +It passes its unchanged protected-read assertions on Darwin arm64 in debug and release builds. This ROM tests protected BIOS reads against Nintendo firmware opcode values, not the complete BIOS service set. Our runner uses the original replacement BIOS. It neither supplies nor loads Nintendo firmware. @@ -25,7 +25,7 @@ direnv exec . python3 tools/prepare_gba_tests.py /tmp/gba-public-bios-offline -- ``` ARM remains the default suite. The Thumb and memory selections remain unchanged. -Preparation is not a passing result. The BIOS run currently exits nonzero and writes a failed assertion report. +Preparation is not a passing result. A successful BIOS run exits zero and reports `r12 == 0` at the verified checkpoint. ## Provenance and checkpoint @@ -51,6 +51,8 @@ Timeouts, diagnostics, STOP, and nonzero r12 remain failures under the [normal r ## Observed result on Darwin arm64 +### Historical failures + Before BIOS read protection: - Both builds reach `eval` after 64 successful steps and 543 nominal cycles. @@ -68,12 +70,27 @@ After the bounded ARM BIOS read-protection implementation: - The test still expects `0xe129f000`. No assertion, checkpoint, ROM byte, or returned constant was changed to force a pass. - Tests 2–4 remain unreached. Their SWI and IRQ cases have not passed independently. -The initial failure exposed both a missing bus-protection rule and a firmware-specific expectation. -The supported protection subset now derives data from the actual supplied image. -The original BIOS has a different layout, so its retained prefetch data differs from Nintendo firmware. -See [CPU BIOS read protection and its limits](hardware/bios.md#cpu-bios-read-protection). -No public BIOS pass is claimed. The ARM, Thumb, and memory suites remain separate passing results. -Exact fetch-pipeline behavior and full BIOS compatibility remain unverified. +### Current passing result + +After correcting the replacement firmware's exit layout: + +- Both builds reach `eval` at `0x08000248` after 17,606 successful steps and 197,456 nominal cycles. +- The CPU is in System/ARM state, with CPSR `0x6000001f` and r12 = 0. +- The report says `reason: checkpoint`, with no emulator diagnostic and one IRQ entry. +- Boot, returning Sqrt, external IRQ callback, and IRQ return checks all execute and pass. +- The ROM, checkpoint, r12 assertion, and step budget remain unchanged. +- Debug and release reports match exactly for the same fixture path. + +The initial failure exposed both a missing bus-protection rule and a firmware layout difference. +The protected-read bus still derives its retained word from the actual supplied image. +Our generated firmware now places documented compatibility words at its exit instructions' PC+8 locations. +These words are skipped data, not copied firmware routines. No bus override or ROM-specific condition was added. +The IRQ callback uses its existing real return instruction as the readback word. +See the [root-cause research](research/bios-readback.md) and [CPU BIOS read-protection limits](hardware/bios.md#cpu-bios-read-protection). + +The ARM, Thumb, and memory suites remain separate passing results. +This BIOS result covers boundary readback values, not complete BIOS services, exact fetch history, or commercial-game compatibility. +Thumb BIOS execution, full pipeline behavior, and exact firmware timing remain unverified. ## References diff --git a/docs/public-memory-tests.md b/docs/public-memory-tests.md index 1889aec..43bfb1c 100644 --- a/docs/public-memory-tests.md +++ b/docs/public-memory-tests.md @@ -81,7 +81,8 @@ Adjacent halfwords must remain unchanged. Ignored stores still pay their nominal Halfword and word stores still modify object video RAM and object attribute memory. These are implementation regressions based on documented rules, not new hardware measurements. -No public BIOS, timing, graphics, save, or unsafe suite is claimed as passing. +The [public BIOS read-protection ROM](public-bios-tests.md) has a separate bounded passing result. +No public timing, graphics, save, or unsafe suite is claimed as passing. Pokémon Emerald compatibility remains unverified. ## References diff --git a/docs/public-thumb-tests.md b/docs/public-thumb-tests.md index de4f9a7..efce0bd 100644 --- a/docs/public-thumb-tests.md +++ b/docs/public-thumb-tests.md @@ -82,7 +82,8 @@ Existing empty-list expectations that incorrectly required PC+4 were corrected; See [CPU transfer semantics](hardware/cpu.md#instruction-demo-and-execution-rules). This proves only the pinned suites' checkpoints and assertions, not complete CPU, bus, or timing compatibility. The [pinned public memory ROM](public-memory-tests.md) also passes, with documented video-byte coverage limits. -No public timing, BIOS, graphics, save, or unsafe suite is claimed as passing. +The [public BIOS read-protection ROM](public-bios-tests.md) has a separate bounded passing result. +No public timing, graphics, save, or unsafe suite is claimed as passing. These results do not establish Pokémon Emerald compatibility. ## References diff --git a/docs/research/bios-readback.md b/docs/research/bios-readback.md index 5595b4c..13efc8c 100644 --- a/docs/research/bios-readback.md +++ b/docs/research/bios-readback.md @@ -2,12 +2,12 @@ ## Finding -The remaining public BIOS failure comes from the replacement firmware's exit layout, not its protected-load byte lanes. -The memory bus correctly returns the replacement image's retained ARM PC+8 word. -That word differs from the documented value left by the standard GBA firmware. +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. -At the initial failing checkpoint: +Before the firmware-layout fix, at the initial failing checkpoint: - The unmodified public ROM expects `0xe129f000` after boot. - Our replacement's final boot instruction is at `0x60`; its PC+8 word at `0x68` is `0xe59f1320`. @@ -68,6 +68,22 @@ This corrects the earlier overly broad claim that reproducing any documented opc 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](../public-bios-tests.md) 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. diff --git a/docs/rom-tests.md b/docs/rom-tests.md index 5015328..57f5bef 100644 --- a/docs/rom-tests.md +++ b/docs/rom-tests.md @@ -4,7 +4,7 @@ Use `--test-suite PATH.json` to run repeatable ROM checks without opening a wind Each case has a machine-step budget, a completion address, and explicit assertions. A timeout never counts as a pass. Existing `--rom ... --steps ...` remains a diagnostic mode, not a test verdict. -Original programs cover the runner itself. Pinned public [ARM](public-arm-tests.md), [Thumb](public-thumb-tests.md), and [memory](public-memory-tests.md) ROMs provide independent compatibility results. +Original programs cover the runner itself. Pinned public [ARM](public-arm-tests.md), [Thumb](public-thumb-tests.md), [memory](public-memory-tests.md), and [BIOS](public-bios-tests.md) ROMs provide independent compatibility results. These pinned ROMs pass their checkpoints on Darwin arm64 in debug and release builds. The memory ROM has documented assertion and mode-selection limits; a pass is not exhaustive memory validation. Passing the runner's unit tests does not mean an external suite passes. @@ -137,8 +137,9 @@ The first public integration is [jsmolka/gba-tests ARM](public-arm-tests.md), wi The pinned ARM ROM now passes this checkpoint on Darwin arm64. Historical failures remain documented. The [pinned Thumb ROM](public-thumb-tests.md) also passes its verified r7 checkpoint after the empty-list store correction. The [pinned memory ROM](public-memory-tests.md) also passes, without a core change and with explicit coverage limits. -The [pinned BIOS read-protection ROM](public-bios-tests.md) reaches its checkpoint but fails test 1 with the original replacement BIOS. -Its expected values depend on Nintendo firmware. Record that failure rather than changing the assertion or returning guessed constants. +The [pinned BIOS read-protection ROM](public-bios-tests.md) also passes with the original replacement BIOS. +Its documented boundary values come from compatibility data in our firmware's exit layout, not a bus override. +The ROM, assertions, checkpoint, and budget remain unchanged. This is not a complete BIOS service test. For further public ARM7TDMI/GBA tests, inspect their source, license, entry assumptions, and result protocol. Pin source revisions and build instructions before comparing emulator changes. Adapt suites to verified completion addresses and result locations; do not guess them or treat timeouts as passes. diff --git a/docs/status.md b/docs/status.md index 929552f..97c0236 100644 --- a/docs/status.md +++ b/docs/status.md @@ -67,9 +67,9 @@ Update this file when a feature lands or a limit is removed. - ROM windows with scanline capture, keyboard input, focus-loss release, bounded execution slices, and interactive keypad wake from STOP. - Optional ROM-window frame limits and an original input-test ROM generator. - Headless JSON ROM suites with per-case checkpoints, register/CPSR/memory assertions, bounded budgets, and structured failure reports. -- Hash-pinned preparation of public `jsmolka/gba-tests` ARM, Thumb, and memory ROMs, with verified result-register checkpoints. +- Hash-pinned preparation of public `jsmolka/gba-tests` ARM, Thumb, memory, and BIOS ROMs, with verified result-register checkpoints. - These pinned ROMs pass their checkpoints on Darwin arm64 in debug and release builds; coverage limits remain documented. -- A pinned public BIOS read-protection ROM has a verified checkpoint and an explicit failing baseline with the original BIOS. +- The original BIOS exposes documented boot, SoftReset, SWI, and IRQ protected-read words through its actual exit layout. - ARM test/compare Rd=15 status restoration, without a PC result write or nominal refill. - ARM unused-memory data reads return a PC+8 word snapshot, with normal byte lanes and load rotation. - Little-endian byte, halfword, and word reads/writes. @@ -96,7 +96,7 @@ The ARMv4T high-register format requires at least one high register; low-to-low `LDRT`/`STRT` single-transfer variants and invalid register combinations return errors. CPU BIOS reads are protected by the executing address using a bounded ARM prefetch snapshot. Thumb BIOS execution and missing lookahead invalidate that history; protected reads with unknown history remain diagnostics. -This is not a full fetch pipeline, and the original BIOS does not reproduce Nintendo's opcode-specific retained values. +This is not a full fetch pipeline. The original BIOS reproduces documented boundary readback words, not all firmware side effects. Multiply flags N/Z are implemented. Unspecified multiply C/V outputs are preserved deterministically, not hardware-verified. Instruction and memory errors are development diagnostics, not emulated CPU exceptions. Block transfers validate all accesses before committing register, RAM, or I/O changes. @@ -122,7 +122,8 @@ The [ROM suite runner](rom-tests.md) has original regressions and a [pinned publ The pinned public ARM ROM passes its result checkpoint in debug and release builds. The [pinned public Thumb ROM](public-thumb-tests.md) also passes after correcting its empty-list store PC value. The [pinned public memory ROM](public-memory-tests.md) passes without a core change; its video-byte assertions have documented limits. -The [pinned BIOS read-protection ROM](public-bios-tests.md) fails test 1; it expects Nintendo firmware opcode values. +The [pinned BIOS read-protection ROM](public-bios-tests.md) passes after correcting the replacement firmware's exit layout. +The bus still derives retained words from the actual image; no test-specific override or ROM patch is used. Other public suites remain unverified. These results do not establish full CPU or memory compatibility. The runner requires known completion addresses and has no debug-port protocol, scripted input, or rendered-image assertions. @@ -137,8 +138,8 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa ## Next steps -1. Extend BIOS prefetch history to Thumb execution and validate firmware-specific reads with a lawfully supplied BIOS. - The desktop runner still uses only the original BIOS. Do not force the public test to pass with guessed opcode constants. +1. Extend BIOS prefetch history to Thumb execution and validate broader fetch-history behavior with independent tests. + The desktop runner still uses only the original BIOS. Keep protected reads image-derived for all supplied images. Extend general open-bus support with verified Thumb region/alignment rules and bus history. 2. Refine nominal timing with a fetch pipeline, Game Pak prefetch, per-access device updates, and verified timer/IRQ delays. 3. Validate keypad retrigger behavior with hardware tests and add remaining device registers; extend BIOS reset coverage as sound/serial support becomes available. diff --git a/docs/testing.md b/docs/testing.md index 6abbfad..c5b2313 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -52,7 +52,7 @@ Additional tests cover DMA/IRQ accounting, HALT/STOP, diagnostic state, case iso These original tests validate the runner. The [public ARM adapter](public-arm-tests.md) provides a separate passing checkpoint result for its pinned ROM. The [public Thumb adapter](public-thumb-tests.md) uses a separate lock and r7 assertion after its return to ARM state. The [public memory adapter](public-memory-tests.md) uses its own lock and an ARM-state r12 checkpoint. -The [public BIOS adapter](public-bios-tests.md) also uses an ARM-state r12 checkpoint, but currently reports test 1 as failed. +The [public BIOS adapter](public-bios-tests.md) also passes its ARM-state r12 checkpoint in debug and release builds. Preparation tests use only synthetic original bytes and no network: ```sh @@ -76,7 +76,7 @@ Separate original CPU tests check exact video-byte results across supported mode They require unchanged nonzero sentinels for ignored writes and unchanged adjacent halfwords for duplicated writes. They also check forced blank, full source-value preservation, CPU halfword readback, nominal data costs, and device progress. Halfword/word stores remain writable in object regions. No emulator behavior change was needed for these tests. -These results do not establish public timing, graphics, BIOS, save, or commercial-game compatibility. +These results do not establish full BIOS, timing, graphics, save, or commercial-game compatibility. ROM-runner tests use temporary files containing original instructions, without Nintendo assets or a desktop session. They check file-size boundaries, bounded reads, short reads, I/O errors, paths, symlinks, and read-only loading. @@ -223,7 +223,11 @@ Original BIOS-access regressions check retained ARM PC+8 data for ARM/Thumb call They verify direct reads while executing inside BIOS, re-entry, SWI/IRQ returns, host-inspection isolation, and original-boot provenance. Unknown history, Thumb BIOS execution, missing lookahead, and diagnostic-step retention have explicit checks. Protected loads keep normal nominal costs and advance devices only through machine stepping. -The pinned public BIOS ROM remains a failed assertion, not a conformance pass. +Firmware readback regressions separately verify documented boot, SoftReset, returning SWI, IRQ callback, and IRQ return words. +They execute ARM/Thumb loads across widths and lanes, check caller state, and trace actual exits to their image data. +Interrupt waits replace callback readback with SWI readback on return. Builder tests check branch/literal relocation around exit data. +A modified original image verifies that the bus retains supplied data rather than forcing compatibility constants. +The pinned public BIOS ROM passes unchanged; this is a bounded protected-read result, not complete BIOS conformance. Display tests cover color conversion, framebuffer bounds, cursor movement, pause behavior, and animation wraparound. Input regression tests check focused arrow keys, focused Space presses, and ignored input when unfocused. Command-line tests cover mode selection and invalid arguments.