diff --git a/crates/gba-core/src/cpu/bios_access_tests.rs b/crates/gba-core/src/cpu/bios_access_tests.rs index 0d4de62..976122c 100644 --- a/crates/gba-core/src/cpu/bios_access_tests.rs +++ b/crates/gba-core/src/cpu/bios_access_tests.rs @@ -7,6 +7,9 @@ use crate::{ timing::StepTiming, }; +#[path = "thumb_bios_access_tests.rs"] +mod thumb; + const ENTRY: u32 = 0x100; const DATA: u32 = 0xf233_80a5; const OTHER: u32 = 0x1234_5678; @@ -222,8 +225,8 @@ fn diagnostic_steps_keep_previous_bios_history_but_skipped_instructions_refresh_ } #[test] -fn thumb_bios_execution_and_missing_lookahead_invalidate_unsupported_history() { - for (pc, thumb) in [(0x200, true), (0x3ff8, false), (0x3ffc, false)] { +fn missing_arm_or_thumb_lookahead_invalidates_unsupported_history() { + for (pc, thumb) in [(0x3ffc, true), (0x3ff8, false), (0x3ffc, false)] { let mut bios = image(); word(&mut bios, pc, if thumb { 0x4720 } else { BX_R4 }); // Thumb BX r4 or ARM BX r4 let mut memory = bus(LDR, bios); diff --git a/crates/gba-core/src/cpu/thumb_bios_access_tests.rs b/crates/gba-core/src/cpu/thumb_bios_access_tests.rs new file mode 100644 index 0000000..6c0e931 --- /dev/null +++ b/crates/gba-core/src/cpu/thumb_bios_access_tests.rs @@ -0,0 +1,287 @@ +//! Thumb BIOS fetches drive a complete aligned word, unlike 16-bit ROM open bus. +use super::*; + +const THUMB_ENTRY: u32 = 0x300; +const BX_R5: u16 = 0x4728; + +fn half(bytes: &mut [u8], address: u32, value: u16) { + bytes[address as usize..address as usize + 2].copy_from_slice(&value.to_le_bytes()); +} + +fn thumb_image(pc: u32, instruction: u16) -> Vec { + let mut bios = image(); + half(&mut bios, pc, instruction); + if pc < 0x3ffc { + word(&mut bios, (pc + 4) & !3, OTHER); + } + bios +} + +fn enter_thumb(memory: &mut Memory, pc: u32, target_thumb: bool) -> Cpu { + let mut cpu = Cpu::new(ENTRY); + cpu.registers[4] = pc | 1; + cpu.registers[5] = ROM_START | u32::from(target_thumb); + cpu.step(memory).unwrap(); // Real ARM BX into Thumb BIOS, seed ARM history. + assert_eq!(cpu.pc(), pc); + assert_eq!(cpu.instruction_set(), InstructionSet::Thumb); + cpu +} + +// Inspect via an independent CPU load without changing the BIOS CPU's state. +fn protected_word(memory: &mut Memory) -> u32 { + memory.write32(0x0200_0000, LDR).unwrap(); + let mut probe = Cpu::new(0x0200_0000); + probe.step(memory).unwrap(); + probe.registers[1] +} + +#[test] +fn thumb_exit_retains_both_word_lanes_for_each_code_alignment_and_load_width() { + for offset in [0, 2] { + let pc = THUMB_ENTRY + offset; + for (arm, thumb, expected) in [ + ( + LDR, + 0x6801, + [0xf233_80a5, 0xa5f2_3380, 0x80a5_f233, 0x3380_a5f2], + ), + (0xe5d0_1000, 0x7801, [0xa5, 0x80, 0x33, 0xf2]), + ( + 0xe1d0_10b0, + 0x8801, + [0x80a5, 0xa500_0080, 0xf233, 0x3300_00f2], + ), + ( + 0xe1d0_10d0, + 0x5681, + [0xffff_ffa5, 0xffff_ff80, 0x33, 0xffff_fff2], + ), + ( + 0xe1d0_10f0, + 0x5e81, + [0xffff_80a5, 0xffff_ff80, 0xffff_f233, 0xffff_fff2], + ), + ] { + for target_thumb in [false, true] { + for address in [0, 0x100, 0x3ffc] { + for (lane, value) in expected.into_iter().enumerate() { + let mut bios = thumb_image(pc, BX_R5); + word(&mut bios, ENTRY + 8, OTHER); // Distinct incoming ARM history. + word(&mut bios, THUMB_ENTRY + 4, DATA); + // Poison PC+8, and PC+6's next word for the unaligned case. + word(&mut bios, THUMB_ENTRY + 8, 0xdead_beef); + let mut memory = bus(if target_thumb { thumb } else { arm }, bios); + let mut cpu = enter_thumb(&mut memory, pc, target_thumb); + cpu.step(&mut memory).unwrap(); + assert_eq!(cpu.pc(), ROM_START); + cpu.registers[0] = address + lane as u32; + cpu.flags.carry = true; + let mut after = cpu.clone(); + after.registers[1] = value; + after.registers[15] += if target_thumb { 2 } else { 4 }; + let timing = cpu.step_timed(&mut memory).unwrap(); + assert_eq!(cpu, after, "pc={pc:#x} lane={lane} thumb={target_thumb}"); + assert_eq!(timing.data_cycles, 1); + assert_eq!(timing.internal_cycles, 1); + assert_eq!(memory.cycles(), 0); + } + } + } + } + } +} + +#[test] +fn sequential_thumb_steps_sample_the_aligned_fetch_word_without_extra_cycles() { + let mut bios = thumb_image(THUMB_ENTRY, 0x46c0); // MOV r8,r8 + half(&mut bios, THUMB_ENTRY + 2, 0x46c0); + half(&mut bios, THUMB_ENTRY + 4, BX_R5); + // At PC+4 and PC+6: the exit instruction plus a distinct high halfword. + let first = (OTHER & 0xffff_0000) | u32::from(BX_R5); + word(&mut bios, THUMB_ENTRY + 8, DATA); + let mut memory = bus(LDR, bios); + let mut cpu = enter_thumb(&mut memory, THUMB_ENTRY, false); + assert_eq!(protected_word(&mut memory), DATA); // Incoming ARM prefetch. + for expected in [first, first, DATA] { + let timing = cpu.step_timed(&mut memory).unwrap(); + assert_eq!(timing.data_cycles, 0); + assert_eq!(protected_word(&mut memory), expected); + assert_eq!(memory.cycles(), 0); + } + assert_eq!(cpu.pc(), ROM_START); +} + +#[test] +fn thumb_to_arm_bios_transition_uses_incoming_state_then_refreshes_arm_history() { + for offset in [0, 2] { + let mut bios = thumb_image(THUMB_ENTRY + offset, BX_R5); + word(&mut bios, 0x208, DATA); + let mut memory = bus(LDR, bios); + let mut cpu = enter_thumb(&mut memory, THUMB_ENTRY + offset, false); + cpu.registers[5] = 0x200; // Existing ARM BX r4, now with DATA at PC+8. + cpu.registers[4] = ROM_START; + cpu.step(&mut memory).unwrap(); + assert_eq!(cpu.instruction_set(), InstructionSet::Arm); + assert_eq!(protected_word(&mut memory), OTHER); + cpu.step(&mut memory).unwrap(); + assert_eq!(cpu.pc(), ROM_START); + assert_eq!(protected_word(&mut memory), DATA); + } +} + +#[test] +fn thumb_bios_protection_remains_pc_based_in_every_processor_mode() { + for mode in [ + Mode::User, + Mode::System, + Mode::Fiq, + Mode::Irq, + Mode::Supervisor, + Mode::Abort, + Mode::Undefined, + ] { + for offset in [0, 2] { + let pc = THUMB_ENTRY + offset; + let mut bios = thumb_image(pc, 0x6801); // LDR r1,[r0] inside BIOS. + half(&mut bios, pc + 2, BX_R5); + let mut memory = bus(LDR, bios); + let mut cpu = enter_thumb(&mut memory, pc, false); + cpu.apply_status(0xb000_00e0 | mode as u32, mode); + cpu.registers[0] = ENTRY + 8; + cpu.step(&mut memory).unwrap(); + assert_eq!(cpu.registers[1], DATA); // Raw BIOS data, not retained OTHER. + assert_eq!(cpu.cpsr(), 0xb000_00e0 | mode as u32); + cpu.step(&mut memory).unwrap(); // BX to ARM ROM. + let expected = memory.read32((pc + 6) & !3).unwrap(); + cpu.registers[0] = ENTRY + 8; + cpu.step(&mut memory).unwrap(); + assert_eq!(cpu.registers[1], expected); + assert_eq!(cpu.cpsr(), 0xb000_00c0 | mode as u32); + } + } +} + +#[test] +fn thumb_pop_pc_retains_prefetch_not_stack_data_and_stays_thumb() { + for offset in [0, 2] { + let mut memory = bus(0x6801, thumb_image(THUMB_ENTRY + offset, 0xbd00)); // POP {pc} + let mut cpu = enter_thumb(&mut memory, THUMB_ENTRY + offset, true); + cpu.registers[13] = 0x0300_0100; + memory.write32(0x0300_0100, ROM_START).unwrap(); + let timing = cpu.step_timed(&mut memory).unwrap(); + assert_eq!(timing.data_cycles, 1); + assert_eq!(cpu.registers[13], 0x0300_0104); + assert_eq!(cpu.instruction_set(), InstructionSet::Thumb); + assert_eq!(cpu.pc(), ROM_START); + cpu.step(&mut memory).unwrap(); + assert_eq!(cpu.registers[1], OTHER); + } +} + +#[test] +fn thumb_swi_enters_arm_and_the_next_bios_instruction_replaces_thumb_history() { + for offset in [0, 2] { + let mut bios = thumb_image(THUMB_ENTRY + offset, 0xdf00); // SWI #0 + word(&mut bios, 8, 0xe12f_ff15); // BX r5 + word(&mut bios, 16, DATA); + let mut memory = bus(LDR, bios); + let mut cpu = enter_thumb(&mut memory, THUMB_ENTRY + offset, false); + cpu.step(&mut memory).unwrap(); + assert_eq!(cpu.pc(), 8); + assert_eq!(cpu.mode(), Mode::Supervisor); + assert_eq!(cpu.instruction_set(), InstructionSet::Arm); + assert_eq!(protected_word(&mut memory), OTHER); + cpu.step(&mut memory).unwrap(); + assert_eq!(cpu.pc(), ROM_START); + cpu.step(&mut memory).unwrap(); + assert_eq!(cpu.registers[1], DATA); + } +} + +#[test] +fn last_complete_thumb_fetch_word_is_valid_at_both_alignments() { + for pc in [0x3ff8, 0x3ffa] { + let mut memory = bus(LDR, thumb_image(pc, BX_R5)); + let mut cpu = enter_thumb(&mut memory, pc, false); + cpu.step(&mut memory).unwrap(); + cpu.step(&mut memory).unwrap(); + assert_eq!(cpu.registers[1], OTHER); + } + for pc in [0x3ffc, 0x3ffe] { + let mut memory = bus(LDR, thumb_image(pc, BX_R5)); + let mut cpu = enter_thumb(&mut memory, pc, false); + cpu.step(&mut memory).unwrap(); // Missing lookahead does not fail BX itself. + assert_eq!(cpu.pc(), ROM_START); + let before = cpu.clone(); + assert_eq!( + cpu.step(&mut memory), + Err(CpuError::Memory(MemoryError::Unmapped(0))) + ); + assert_eq!(cpu, before); + } +} + +#[test] +fn failed_thumb_steps_keep_previous_history_and_clear_access_context() { + for instruction in [0xde00, 0x6801] { + // Undefined instruction / failing LDR. + let mut memory = bus(LDR, thumb_image(THUMB_ENTRY, instruction)); + let mut cpu = enter_thumb(&mut memory, THUMB_ENTRY, false); + cpu.registers[0] = 0x0e00_0000; + let before = cpu.clone(); + assert!(cpu.step_timed(&mut memory).is_err()); + assert_eq!(cpu, before); + assert_eq!(memory.cycles(), 0); + assert_eq!(memory.read32(THUMB_ENTRY + 4).unwrap(), OTHER); // Raw host read. + assert_eq!(protected_word(&mut memory), DATA); // Previous ARM history retained. + } +} + +#[test] +fn outside_thumb_execution_keeps_history_but_does_not_enable_general_open_bus() { + let mut memory = bus(0x6801, thumb_image(THUMB_ENTRY, BX_R5)); + let mut cpu = enter_thumb(&mut memory, THUMB_ENTRY, true); + cpu.step(&mut memory).unwrap(); + assert_eq!(protected_word(&mut memory), OTHER); + cpu.registers[0] = 0x4000; + let before = cpu.clone(); + assert_eq!( + cpu.step(&mut memory), + Err(CpuError::Memory(MemoryError::Unmapped(0x4000))) + ); + assert_eq!(cpu, before); + assert_eq!(memory.read32(0x4000), Err(MemoryError::Unmapped(0x4000))); + assert_eq!(protected_word(&mut memory), OTHER); + cpu.registers[0] = 0; + cpu.step(&mut memory).unwrap(); + assert_eq!(cpu.registers[1], OTHER); +} + +#[test] +fn machine_steps_charge_thumb_exit_and_protected_load_without_snapshot_accesses() { + for offset in [0, 2] { + let mut memory = bus(LDR, thumb_image(THUMB_ENTRY + offset, BX_R5)); + let cpu = enter_thumb(&mut memory, THUMB_ENTRY + offset, false); + memory.write32(TIMER_BASE, 0x0080_0000).unwrap(); + let mut machine = Machine::new(cpu, memory); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + let cycles = machine.cycles(); + assert_eq!(machine.last_timing().data_cycles, 0); + assert_eq!(machine.step().unwrap(), StepKind::Instruction); + assert_eq!(machine.cpu().registers()[1], OTHER); + assert_eq!( + machine.last_timing(), + StepTiming { + code_cycles: 8, + data_cycles: 1, + internal_cycles: 1, + idle_cycles: 0, + } + ); + assert_eq!(machine.cycles(), cycles + 10); + assert_eq!( + u64::from(machine.memory().read16(TIMER_BASE).unwrap()), + machine.cycles() + ); + } +} diff --git a/crates/gba-core/src/memory.rs b/crates/gba-core/src/memory.rs index 9cbac47..efd7591 100644 --- a/crates/gba-core/src/memory.rs +++ b/crates/gba-core/src/memory.rs @@ -57,12 +57,13 @@ fn vram_index(address: u32) -> usize { struct CpuAccess { pc: u32, arm_prefetch: Option, + bios_prefetch: Option, } /// Work/video/palette RAM, OAM, cartridge ROM, optional BIOS, and supported I/O. /// ARM data reads from unused memory return a PC+8 snapshot when available. -/// CPU data reads of BIOS from outside BIOS use its retained ARM PC+8 snapshot. -/// Thumb BIOS prefetch, general Thumb open bus, and DMA bus latches are not modeled. +/// Protected BIOS reads retain an ARM PC+8 or aligned Thumb PC+4 word snapshot. +/// General Thumb open bus, a full fetch pipeline, and DMA bus latches are not modeled. pub struct Memory { external_ram: Vec, internal_ram: Vec, @@ -333,17 +334,31 @@ impl Memory { // Sample before execution, without data timing or open-bus recursion. // Missing lookahead bytes must not fail an instruction that never uses them. let arm_prefetch = match instruction_set { - InstructionSet::Arm => self.snapshot_arm_prefetch(pc), + InstructionSet::Arm => self.snapshot_mapped_word(pc.wrapping_add(8)), InstructionSet::Thumb => None, }; - self.cpu_access = Some(CpuAccess { pc, arm_prefetch }); + let bios_prefetch = if pc < BIOS_SIZE as u32 { + match instruction_set { + InstructionSet::Arm => arm_prefetch, + // BIOS drives a full 32-bit word for a Thumb halfword fetch. + // Preserve both lanes of the aligned PC+4 word, not a repeated halfword. + InstructionSet::Thumb => self.snapshot_mapped_word((pc + 4) & !3), + } + } else { + None + }; + self.cpu_access = Some(CpuAccess { + pc, + arm_prefetch, + bios_prefetch, + }); } - fn snapshot_arm_prefetch(&self, pc: u32) -> Option { + fn snapshot_mapped_word(&self, address: u32) -> Option { let mut bytes = [0; 4]; for (offset, byte) in bytes.iter_mut().enumerate() { *byte = self - .read_mapped_byte(pc.wrapping_add(8 + offset as u32)) + .read_mapped_byte(address.wrapping_add(offset as u32)) .ok()?; } Some(u32::from_le_bytes(bytes)) @@ -353,8 +368,8 @@ impl Memory { if let Some(access) = self.cpu_access.take() { if succeeded && access.pc < BIOS_SIZE as u32 { // Commit only successful instruction snapshots under our diagnostic policy. - // Thumb BIOS execution or missing lookahead invalidates the known value. - self.bios_prefetch = access.arm_prefetch; + // Missing lookahead invalidates history rather than exposing stale bytes. + self.bios_prefetch = access.bios_prefetch; } } } diff --git a/docs/architecture.md b/docs/architecture.md index 46856aa..41ea9a6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -28,6 +28,7 @@ The repository is a Cargo workspace with three crates: | `crates/gba-core/src/cpu/exception.rs` | Reset state, exception entry, and interrupt sampling | | `crates/gba-core/src/cpu/load_alias_tests.rs` | Original single-load alias regressions for indexing, banks, widths, alignment, I/O, timing, and diagnostics | | `crates/gba-core/src/cpu/bios_access_tests.rs` | Original CPU BIOS protection regressions for retained history, modes, widths, exception returns, and diagnostics | +| `crates/gba-core/src/cpu/thumb_bios_access_tests.rs` | Thumb BIOS aligned-word snapshots, transitions, load lanes, boundaries, diagnostics, and nominal timing | | `crates/gba-core/src/cpu/compare_psr_tests.rs` | Original Rd=15 test/compare status restoration, flags, bank, and timing regressions | | `crates/gba-core/src/cpu/status_tests.rs`, `crates/gba-core/src/cpu/exception_tests.rs` | Status, banking, exception, and return tests | | `crates/gba-core/src/cpu/thumb_tests.rs` | Thumb instructions, state switching, and decoder checks | diff --git a/docs/hardware/bios.md b/docs/hardware/bios.md index 7a90aa8..5159a77 100644 --- a/docs/hardware/bios.md +++ b/docs/hardware/bios.md @@ -97,13 +97,15 @@ While executing inside BIOS, ARM and Thumb code can read the mapped image direct While executing outside BIOS, both instruction sets read byte lanes from retained BIOS prefetch data instead. Normal word rotation, odd-halfword behavior, sign extension, and load timing still apply. -The current interpreter implements a bounded ARM prefetch model: +The current interpreter implements bounded ARM and Thumb BIOS prefetch snapshots: - Before an ARM BIOS instruction executes, sample its mapped PC+8 word without charging another data access. +- Before a Thumb BIOS instruction executes, sample the full mapped word at `(PC+4) & ~3`. + BIOS drives a 32-bit word even for a halfword fetch; both distinct lanes are retained. - After successful execution, retain that snapshot for later protected reads. Skipped conditional instructions count as successful execution. -- Instructions outside BIOS do not replace the retained value. Re-entering ARM BIOS code refreshes it. +- Instructions outside BIOS do not replace the retained value. Re-entering ARM or Thumb BIOS code refreshes it. - Diagnostic failures preserve the previous value and clear the CPU access context. -- Successful Thumb BIOS execution or unavailable PC+8 bytes invalidate the known value. Their fetch history is not yet modeled. +- Unavailable lookahead bytes invalidate the known value after a successful BIOS instruction. - A protected read without known history returns `MemoryError::Unmapped` at the requested bus address, rather than exposing raw BIOS bytes. The initial history is unknown. Direct cartridge startup with `Cpu::new` does not manufacture boot prefetch data. @@ -129,7 +131,10 @@ Instruction fetches remain strict mapped reads outside the CPU data-access conte BIOS writes remain read-only diagnostics. DMA BIOS sources retain their existing unsupported-source diagnostic. Missing BIOS images remain unmapped. The unused-memory ARM open-bus snapshot is separate from this retained BIOS value. -This models neither a persistent fetch pipeline nor exact bus history across Thumb BIOS code and DMA. +The Thumb rule follows documented BIOS bus lanes and a source comparison with NanoBoyAdvance. +See [Thumb BIOS prefetch research](../research/thumb-bios-prefetch.md) for evidence, emulator differences, and test scope. +This models neither a persistent fetch pipeline nor exact refill, BIOS data-access, or DMA bus history. +The boundary diagnostic policy and precise bus timing remain hardware-unverified. Original regressions cover synthetic images, boot, exception returns, access widths, alignment, modes, and diagnostics. 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. diff --git a/docs/hardware/cpu.md b/docs/hardware/cpu.md index 2ee1cd7..fe6dd88 100644 --- a/docs/hardware/cpu.md +++ b/docs/hardware/cpu.md @@ -98,7 +98,7 @@ Host inspection, ROM-suite memory assertions, instruction fetches, and DMA do no Limits remain explicit: - Thumb open bus depends on instruction region, alignment, and prior bus data; it remains unsupported. -- [BIOS-protected reads](bios.md#cpu-bios-read-protection) use separate retained ARM BIOS snapshots. +- [BIOS-protected reads](bios.md#cpu-bios-read-protection) use separate retained ARM PC+8 or aligned Thumb PC+4 word snapshots. - Unused/write-only I/O reads, DMA latches, and disabled-RAM reads are not modeled here. - Missing BIOS, truncated ROM, unsupported I/O, and save-memory accesses retain their existing diagnostics. - Unused-memory writes remain diagnostics rather than ignored hardware writes. Swaps cannot silently discard their write. diff --git a/docs/public-bios-tests.md b/docs/public-bios-tests.md index adb39e0..503b062 100644 --- a/docs/public-bios-tests.md +++ b/docs/public-bios-tests.md @@ -90,7 +90,8 @@ See the [root-cause research](research/bios-readback.md) and [CPU BIOS read-prot 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. +Separate original tests cover bounded [Thumb BIOS snapshots](research/thumb-bios-prefetch.md); this public ROM does not validate that path. +Full pipeline behavior, physical-hardware Thumb readback, and exact firmware timing remain unverified. ## References diff --git a/docs/references.md b/docs/references.md index 2adf3bd..ac92a41 100644 --- a/docs/references.md +++ b/docs/references.md @@ -95,6 +95,15 @@ BIOS protected-read root-cause research: See [the root-cause analysis and implementation decision](research/bios-readback.md). +Thumb BIOS fetch research: + +- [NanoBoyAdvance bus at revision 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/bus/bus.cc), for full aligned BIOS latch updates on halfword reads. +- [NanoBoyAdvance ARM7TDMI loop at the same revision](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/arm7tdmi.hh), for Thumb PC+4 fetch order. +- [mGBA CPU loop](https://github.com/mgba-emu/mgba/blob/master/src/arm/arm.c), compared with its region-exit retention path; not evidence of exact full-word Thumb BIOS readback. +- GBATEK's BIOS/OAM Thumb lane table above supports aligned-word sampling, but describes general unused-memory reads. + +See [Thumb BIOS prefetch snapshots](research/thumb-bios-prefetch.md) for the inference, implementation limits, and original regressions. + Use [GBATEK](https://problemkaputt.de/gbatek.htm) and ARM7TDMI documentation for further hardware work. Only use game ROMs that you may lawfully use. Do not commit game ROMs, BIOS files, or game assets. The ignore file excludes `roms/`, common game ROM extensions, and save files. diff --git a/docs/research/bios-readback.md b/docs/research/bios-readback.md index 13efc8c..bc33fc8 100644 --- a/docs/research/bios-readback.md +++ b/docs/research/bios-readback.md @@ -93,6 +93,7 @@ Workspace tests, preparation tests, lint checks, rustdoc, native ROM windows, an - 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. +The firmware-layout fix did not implement a persistent instruction pipeline, Thumb BIOS fetch history, or exact firmware timing. +A subsequent [Thumb BIOS snapshot change](thumb-bios-prefetch.md) adds bounded aligned-word history, with separate tests and explicit limits. 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. diff --git a/docs/research/thumb-bios-prefetch.md b/docs/research/thumb-bios-prefetch.md new file mode 100644 index 0000000..c4fd6ac --- /dev/null +++ b/docs/research/thumb-bios-prefetch.md @@ -0,0 +1,75 @@ +# Thumb BIOS prefetch snapshots + +## Decision + +Retain the full mapped word at `(executing PC + 4) & !3` after a successful Thumb BIOS instruction. +Keep ARM's existing PC+8 rule. Keep this BIOS history separate from general unused-memory open bus. + +BIOS has a 32-bit bus. A Thumb instruction fetch selects a halfword, but the BIOS bus drives both word lanes. +This gives the following snapshot for an instruction at address P: + +| Executing address | Low halfword | High halfword | +| --- | --- | --- | +| P aligned to four bytes | BIOS[P+4] | BIOS[P+6] | +| P aligned to two bytes only | BIOS[P+2] | BIOS[P+4] | + +The two halfwords need not match. Do not repeat the fetched halfword, zero-extend it, or read an unaligned word at P+4. +Do not use ARM's PC+8 offset for Thumb execution. + +## Evidence and confidence + +[GBATEK](https://problemkaputt.de/gbatek-gba-unpredictable-things.htm) documents two related behaviors: + +- Protected BIOS reads return the most recently fetched BIOS opcode. +- Thumb open bus while executing in BIOS exposes the two aligned word lanes shown above. + +Its Thumb table describes unused-memory reads, not a dedicated Thumb protected-BIOS test. +The retained-word rule combines that bus-width evidence with the documented BIOS latch behavior. +We have not measured it on physical hardware. + +[NanoBoyAdvance's bus implementation](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/bus/bus.cc) corroborates the full-word latch. +`ReadBIOS` aligns a permitted address down to four bytes and updates its BIOS latch with a full word. +It then selects the requested lanes. A halfword access does not reduce the retained latch to 16 bits. +Its [ARM7TDMI execution loop](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/arm7tdmi.hh) fetches the Thumb PC+4 halfword before dispatching the instruction. +This is a source comparison, not a differential emulator run. No implementation code was copied. + +[mGBA's memory implementation](https://github.com/mgba-emu/mgba/blob/master/src/gba/memory.c) was also reviewed. +Its region-exit path saves `cpu->prefetch[1]`, while its [Thumb execution loop](https://github.com/mgba-emu/mgba/blob/master/src/arm/arm.c) fetches a halfword. +That path does not independently establish both retained word lanes. Its general Thumb BIOS open-bus path also notes an alignment approximation. +Do not treat agreement between all emulators, or exact hardware conformance, as established. + +## Implementation scope + +`Memory::begin_cpu_access` samples before execution, using the incoming instruction state and strict mapped bytes. +`end_cpu_access` commits BIOS history only after success. Failed instructions preserve previous history. +Thumb execution outside BIOS neither initializes nor replaces BIOS history. +Host reads remain raw inspection. CPU data loads outside BIOS select lanes from retained history as before. +Snapshots add no data accesses or nominal cycles. + +The snapshot does not create a persistent instruction pipeline. +It does not model refill-time latches, BIOS data-access ordering within an instruction, DMA bus transitions, or general Thumb open bus. +At BIOS boundaries, unavailable lookahead invalidates history under the existing diagnostic policy. +A branch can still succeed; a later protected read reports unknown history rather than exposing stale bytes. +This boundary policy is conservative, not a hardware claim. + +## Regression coverage + +Original tests in `crates/gba-core/src/cpu/thumb_bios_access_tests.rs` cover: + +- Both Thumb instruction alignments and both output instruction states. +- Full words, byte lanes, halfwords, sign extension, rotation, and all processor modes. +- Sequential history, ARM/Thumb transitions, SWI entry, and POP-to-PC exits with RAM stack data. +- Raw reads inside BIOS and protected reads outside it. +- The last complete fetch word, unavailable lookahead, and failure retention. +- Separate host reads, unchanged general Thumb open-bus diagnostics, nominal costs, and device progression. + +## Validation result + +The Thumb boundary-read regressions failed on the old history-invalidation path and pass with aligned-word snapshots. +Darwin arm64 workspace and core/demo tests pass in debug and release builds. +Preparation tests, formatting, lint checks, rustdoc, native ROM windows, and graphics smoke tests also pass. +The pinned public ARM, Thumb, memory, and BIOS reports match their previous passing reports exactly. +Debug and release reports also match each other for the same fixture paths. + +The pinned public BIOS ROM uses ARM firmware paths. Its unchanged pass is regression evidence, not independent Thumb BIOS coverage. +Full pipeline timing and commercial-game compatibility remain outside this change. diff --git a/docs/status.md b/docs/status.md index 97c0236..f17f75f 100644 --- a/docs/status.md +++ b/docs/status.md @@ -43,7 +43,7 @@ Update this file when a feature lands or a limit is removed. - CPU/device stepping with nominal ARM7 instruction costs and memory wait states. - Game Pak wait-state control (`WAITCNT`) for all three ROM windows. - Sequential/non-sequential data accesses, branch refill costs, and variable multiply timing. -- Optional caller-supplied 16 KiB BIOS mapping, with CPU read protection based on retained ARM BIOS PC+8 snapshots. +- Optional caller-supplied 16 KiB BIOS mapping, with CPU read protection based on retained ARM PC+8 or aligned Thumb PC+4 words. - Optional original BIOS replacement: minimal boot, SoftReset, selective RegisterRamReset, IRQ dispatch, interrupt waits, memory copy/fill, integer/fixed-point arithmetic, affine matrices, bit unpacking, LZ77/run-length/Huffman decompression, and differential filters. - All 16 ARM data-processing operations, with immediate and shifted-register operands. - Logical operations: `AND`, `EOR`, `TST`, `TEQ`, `ORR`, `MOV`, `BIC`, and `MVN`. @@ -94,8 +94,10 @@ The reserved ARM condition code `0xF` and Thumb condition code `0xE` return erro ARMv5+ extensions such as `BLX`, `BKPT`, and Thumb-2 instructions are not supported. The ARMv4T high-register format requires at least one high register; low-to-low forms in that format return errors. `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. +CPU BIOS reads are protected by the executing address using bounded ARM and Thumb prefetch snapshots. +Thumb BIOS execution retains the full word at `(PC+4) & ~3`, not a repeated halfword. +Missing lookahead invalidates that history; protected reads with unknown history remain diagnostics. +Exact refill and BIOS data-access ordering remain unmodeled. The Thumb rule has source-based, not physical-hardware validation. 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. @@ -129,7 +131,7 @@ The runner requires known completion addresses and has no debug-port protocol, s ARM data reads from unused address ranges use a bounded [PC+8 open-bus snapshot](hardware/cpu.md#arm-unused-memory-data-reads). This is not a fetch pipeline. General Thumb, DMA, and unused/write-only I/O open bus remain unmodeled. -BIOS protection uses separate retained ARM BIOS history as described in [BIOS behavior](hardware/bios.md#cpu-bios-read-protection). +BIOS protection uses separate retained BIOS fetch snapshots as described in [BIOS behavior](hardware/bios.md#cpu-bios-read-protection). Host inspection and instruction fetches remain strict. Other unmapped reads return errors. Reads beyond the supplied cartridge bytes also return errors. Direct memory-bus halfword and word accesses require alignment. @@ -138,9 +140,9 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa ## Next steps -1. Extend BIOS prefetch history to Thumb execution and validate broader fetch-history behavior with independent tests. +1. Extend general open-bus support with documented Thumb region/alignment rules and explicit limits for history-dependent cases. + Validate BIOS snapshots with independent hardware tests before claiming exact refill or data-access history. 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. Validate STOP entry/wake edges and add external wake sources and remaining DMA device modes. diff --git a/docs/testing.md b/docs/testing.md index c5b2313..8840a45 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -219,9 +219,12 @@ Status tests cover all modes, flag combinations, interrupt masks, field selectio Exception tests cover vectors, ARM/Thumb return addresses, nested interrupts, mask priority, and return-state restoration. User-bank transfers and failed returns are checked for unintended changes to active and hidden registers. Integration tests execute original vector handlers and check optional BIOS mapping boundaries and write protection. -Original BIOS-access regressions check retained ARM PC+8 data for ARM/Thumb callers, all processor modes, load widths, lanes, and rotation. +Original BIOS-access regressions check retained ARM PC+8 and aligned Thumb PC+4 words for ARM/Thumb callers. +They cover all processor modes, load widths, lanes, rotation, both Thumb code alignments, and distinct halfwords. 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. +Thumb tests cover sequential snapshots, instruction-state transitions, SWI entry, and RAM-backed POP exits. +Unknown history, last valid fetch words, missing lookahead, and diagnostic-step retention have explicit checks. +General Thumb unused-memory reads remain diagnostics; supporting Thumb BIOS snapshots does not enable general open bus. Protected loads keep normal nominal costs and advance devices only through machine stepping. 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.