From 8b0f1a5ead48985f5f15ebe8b698dcb212ce02df Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Tue, 6 Oct 2026 02:04:07 +0700 Subject: [PATCH] feat(memory): sample Thumb IWRAM refill targets After successful refill instructions, sample the Thumb IWRAM target and target+2 halfwords to establish bus history. Share ARM/Thumb refill classification with nominal timing. Respect restored instruction state, completed data accesses, region limits, and diagnostic rollback; keep instruction execution and nominal costs unchanged. Add original regressions for ARM/Thumb BX, Thumb branches and BL, PC writes, stack/block PC loads, status-restoring returns, real SWI/IRQ returns, both target alignments, mirrors/wrap, captured sample ordering, load lanes, failure retention, BIOS isolation, and DMA invalidation. Document official ARM7TDMI refill evidence and the distinction from a full pipeline. Darwin arm64 validation: 917 workspace tests and 848 core/demo tests per debug/release profile; 39 preparation tests; native ROM windows in both profiles; ten graphics/window smoke modes; fmt, clippy, and rustdoc. Public ARM, Thumb, memory, and BIOS reports remain identical to prior passing results and between profiles. Cold startup, ARM-target refill history, and DMA-to-CPU ordering remain incomplete; no hardware-conformance or commercial-game claim. --- crates/gba-core/src/cpu.rs | 9 +- .../gba-core/src/cpu/iwram_open_bus_tests.rs | 10 +- crates/gba-core/src/cpu/iwram_refill_tests.rs | 425 ++++++++++++++++++ crates/gba-core/src/cpu/timing.rs | 9 +- crates/gba-core/src/memory.rs | 21 +- crates/gba-core/src/memory/iwram_bus.rs | 18 +- docs/architecture.md | 3 +- docs/hardware/cpu.md | 35 +- docs/references.md | 11 +- docs/research/iwram-bus-history.md | 43 +- docs/status.md | 8 +- docs/testing.md | 7 +- 12 files changed, 565 insertions(+), 34 deletions(-) create mode 100644 crates/gba-core/src/cpu/iwram_refill_tests.rs diff --git a/crates/gba-core/src/cpu.rs b/crates/gba-core/src/cpu.rs index 6e4cbad..ca5cb4b 100644 --- a/crates/gba-core/src/cpu.rs +++ b/crates/gba-core/src/cpu.rs @@ -24,6 +24,8 @@ mod instruction_tests; #[cfg(test)] mod iwram_open_bus_tests; #[cfg(test)] +mod iwram_refill_tests; +#[cfg(test)] mod load_alias_tests; #[cfg(test)] mod open_bus_tests; @@ -211,14 +213,17 @@ impl Cpu { // Record the executing instruction, not an operand's pipelined PC value. // Always clear the access context, including on diagnostic errors. let pc = self.pc(); - let sequential_thumb = - self.instruction_set == InstructionSet::Thumb && !self.thumb_refills(instruction); + let refill = self.instruction_refills(instruction); + let sequential_thumb = self.instruction_set == InstructionSet::Thumb && !refill; memory.begin_cpu_access(pc, self.instruction_set); let result = self.execute_instruction(instruction, memory); let sequential = sequential_thumb && self.instruction_set == InstructionSet::Thumb && self.pc() == pc.wrapping_add(2); memory.end_cpu_access(result.is_ok(), sequential); + if result.is_ok() && refill { + memory.refill_cpu_bus_history(self.pc(), self.instruction_set); + } result } diff --git a/crates/gba-core/src/cpu/iwram_open_bus_tests.rs b/crates/gba-core/src/cpu/iwram_open_bus_tests.rs index 066a807..eb87b84 100644 --- a/crates/gba-core/src/cpu/iwram_open_bus_tests.rs +++ b/crates/gba-core/src/cpu/iwram_open_bus_tests.rs @@ -295,7 +295,7 @@ fn failed_steps_discard_prefetch_history_before_retry() { } #[test] -fn taken_control_flow_invalidates_history_even_when_target_is_fallthrough() { +fn taken_control_flow_refills_history_even_when_target_is_fallthrough() { for branch in [0x4720, 0xe7ff, 0x46a7, 0xd0ff, 0xbd00] { // BX, B, MOV pc, BEQ, POP pc. let (mut cpu, mut bus) = program(CODE, &[SEED, branch, PROBE, 0x5566, 0x99aa]); @@ -306,12 +306,8 @@ fn taken_control_flow_invalidates_history_even_when_target_is_fallthrough() { cpu.step(&mut bus).unwrap(); cpu.step(&mut bus).unwrap(); assert_eq!(cpu.pc(), CODE + 4); - let before = cpu.clone(); - assert_eq!( - cpu.step(&mut bus), - Err(CpuError::Memory(MemoryError::Unmapped(UNUSED))) - ); - assert_eq!(cpu, before); + cpu.step(&mut bus).unwrap(); + assert_eq!(cpu.registers[6], 0x5566_99aa); } let (mut cpu, mut bus) = program(CODE, &[SEED, 0xd0ff, PROBE, 0x5566, 0x99aa]); cpu.flags.zero = false; // Untaken BEQ has no refill. diff --git a/crates/gba-core/src/cpu/iwram_refill_tests.rs b/crates/gba-core/src/cpu/iwram_refill_tests.rs new file mode 100644 index 0000000..7ed5b81 --- /dev/null +++ b/crates/gba-core/src/cpu/iwram_refill_tests.rs @@ -0,0 +1,425 @@ +//! Original control-flow programs for Thumb IWRAM refill-lane snapshots. +use super::*; +use crate::{ + dma::{DMA_BASE, DMA_STRIDE}, + io::{IE, IME, TIMER_BASE}, + machine::{Machine, MachineError, StepKind}, + memory::{BIOS_SIZE, ROM_START}, +}; + +const SOURCE: u32 = 0x0300_0100; +const TARGET: u32 = 0x0300_0200; +const STACK: u32 = 0x0300_1000; +const UNUSED: u32 = 0x8000_0000; +const PROBE: u16 = 0x6806; // LDR r6,[r0] +const MARKER: u16 = 0x91ab; +const LOOKAHEAD: u16 = 0xd234; + +fn target(bus: &mut Memory, pc: u32, instruction: u16) { + for (index, half) in [instruction, MARKER, LOOKAHEAD, 0x5678] + .into_iter() + .enumerate() + { + bus.write16(pc + index as u32 * 2, half).unwrap(); + } +} + +fn expected(pc: u32) -> u32 { + if pc & 2 == 0 { + 0x91ab_d234 + } else { + 0xd234_91ab + } +} + +fn branch(source: u32, thumb: bool, destination: u32) -> (Cpu, Memory) { + let instruction: u32 = if thumb { 0x46c0_4720 } else { 0xe12f_ff14 }; // BX r4 + let mut bus = if source < BIOS_SIZE as u32 { + let mut bios = vec![0; BIOS_SIZE]; + bios[source as usize..source as usize + 4].copy_from_slice(&instruction.to_le_bytes()); + Memory::with_bios(vec![], bios).unwrap() + } else if source == ROM_START { + Memory::new(instruction.to_le_bytes().to_vec()).unwrap() + } else { + let mut bus = Memory::new(vec![]).unwrap(); + bus.write32(source, instruction).unwrap(); + bus + }; + target(&mut bus, destination, PROBE); + let mut cpu = Cpu::new(source); + cpu.instruction_set = if thumb { + InstructionSet::Thumb + } else { + InstructionSet::Arm + }; + cpu.registers[0] = UNUSED; + cpu.registers[4] = destination | 1; + (cpu, bus) +} + +#[test] +fn arm_and_thumb_bx_establish_history_at_both_target_alignments_and_mirrors() { + for source in [0x100, 0x0200_0100, SOURCE, ROM_START] { + for thumb in [false, true] { + for destination in [ + TARGET, + TARGET + 2, + TARGET + 0x8000, + 0x0300_7ffc, + 0x0300_7ffe, + ] { + let (mut cpu, mut bus) = branch(source, thumb, destination); + let status = cpu.cpsr() | 0xb000_00c0; + cpu.apply_status(status, Mode::System); + cpu.step(&mut bus).unwrap(); + assert_eq!(cpu.pc(), destination); + assert_eq!(cpu.instruction_set(), InstructionSet::Thumb); + cpu.step(&mut bus).unwrap(); + assert_eq!(cpu.registers[6], expected(destination)); + assert_eq!(cpu.cpsr(), status | 0x20); + assert_eq!(cpu.registers[4], destination | 1); + } + } + } +} + +#[test] +fn refill_samples_target_plus_two_at_branch_time_but_plus_four_on_arrival() { + for destination in [TARGET, TARGET + 2] { + let (mut cpu, mut bus) = branch(ROM_START, false, destination); + cpu.step(&mut bus).unwrap(); + bus.write16(destination + 2, 0xeeee).unwrap(); + bus.write16(destination + 4, 0xabcd).unwrap(); + assert_eq!(bus.read16(destination + 2).unwrap(), 0xeeee); + cpu.step(&mut bus).unwrap(); + assert_eq!( + cpu.registers[6], + if destination & 2 == 0 { + 0x91ab_abcd + } else { + 0xabcd_91ab + } + ); + } +} + +#[test] +fn refill_history_supports_normal_load_lanes_rotation_and_sign_extension() { + for destination in [TARGET, TARGET + 2] { + let word = expected(destination); + for (instruction, width, signed) in [ + (PROBE, 4, false), + (0x7806, 1, false), + (0x8806, 2, false), + (0x5746, 1, true), + (0x5f46, 2, true), // r5=0 offset, r6 result. + ] { + for lane in 0..4 { + let (mut cpu, mut bus) = branch(ROM_START, false, destination); + target(&mut bus, destination, instruction); + cpu.registers[0] += lane; + cpu.step(&mut bus).unwrap(); + let byte = (word >> (lane * 8)) as u8; + let half = (word >> ((lane & 2) * 8)) as u16; + let value = match (width, signed) { + (4, _) => word.rotate_right(lane * 8), + (1, false) => u32::from(byte), + (1, true) => byte as i8 as i32 as u32, + (2, false) => u32::from(half).rotate_right((lane & 1) * 8), + (2, true) if lane & 1 != 0 => byte as i8 as i32 as u32, + (2, true) => half as i16 as i32 as u32, + _ => unreachable!(), + }; + let mut after = cpu.clone(); + after.registers[6] = value; + after.registers[15] += 2; + cpu.step(&mut bus).unwrap(); + assert_eq!(cpu, after); + } + } + } +} + +#[test] +fn thumb_pc_writes_refill_after_data_loads_and_preserve_transfer_semantics() { + for destination in [TARGET, TARGET + 2] { + for instruction in [0x4720, 0x46a7, 0x44a7, 0xbd00, 0xbc00, 0xcc00] { + // BX r4, MOV pc,r4, ADD pc,r4, POP {pc}, empty POP, empty LDM r4!. + let (mut cpu, mut bus) = branch(SOURCE, true, destination); + bus.write16(SOURCE, instruction).unwrap(); + bus.write32(STACK, destination).unwrap(); + cpu.registers[13] = STACK; + cpu.registers[4] = match instruction { + 0xcc00 => STACK, + 0x44a7 => destination.wrapping_sub(SOURCE + 4), + _ => destination | 1, + }; + cpu.step(&mut bus).unwrap(); + assert_eq!(cpu.pc(), destination); + assert_eq!(cpu.instruction_set(), InstructionSet::Thumb); + if instruction == 0xbd00 { + assert_eq!(cpu.registers[13], STACK + 4); + } + if instruction == 0xbc00 { + assert_eq!(cpu.registers[13], STACK + 64); + } + if instruction == 0xcc00 { + assert_eq!(cpu.registers[4], STACK + 64); + } + cpu.step(&mut bus).unwrap(); + assert_eq!( + cpu.registers[6], + expected(destination), + "instruction={instruction:#x}" + ); + } + } +} + +#[test] +fn thumb_taken_branches_and_bl_suffix_establish_target_history() { + for destination in [TARGET, TARGET + 2] { + for kind in 0..3 { + let (mut cpu, mut bus) = branch(SOURCE, true, destination); + if kind == 2 { + bus.write16(SOURCE, 0xf000).unwrap(); // BL prefix: high offset zero. + bus.write16(SOURCE + 2, 0xf800 | ((destination - SOURCE - 4) / 2) as u16) + .unwrap(); + cpu.step(&mut bus).unwrap(); + assert_eq!(cpu.pc(), SOURCE + 2); + } else { + let offset = ((destination - SOURCE - 4) / 2) as u16; + bus.write16( + SOURCE, + if kind == 0 { + 0xe000 | offset + } else { + 0xd000 | offset + }, + ) + .unwrap(); + cpu.flags.zero = true; + } + cpu.step(&mut bus).unwrap(); + assert_eq!(cpu.pc(), destination); + if kind == 2 { + assert_eq!(cpu.registers[14], (SOURCE + 4) | 1); + } + cpu.step(&mut bus).unwrap(); + assert_eq!(cpu.registers[6], expected(destination)); + } + } +} + +#[test] +fn arm_exception_returns_use_saved_thumb_state_and_restored_banks() { + for destination in [TARGET, TARGET + 2] { + for mode in [Mode::User, Mode::System] { + for instruction in [0xe1b0_f00e_u32, 0xe25e_f004, 0xe8fd_8004] { + // MOVS pc,lr; SUBS pc,lr,#4; LDMIA sp!,{r2,pc}^. + let mut bus = Memory::new(instruction.to_le_bytes().to_vec()).unwrap(); + target(&mut bus, destination, PROBE); + bus.write32(STACK, 0xfeed_beef).unwrap(); + bus.write32(STACK + 4, destination).unwrap(); + let mut cpu = Cpu::new(ROM_START); + cpu.registers[0] = UNUSED; + cpu.registers[13] = 0x0300_7000; + cpu.registers[14] = 0x1234; + cpu.enter_exception(Exception::SoftwareInterrupt); + cpu.registers[15] = ROM_START; + cpu.registers[13] = STACK; + cpu.registers[14] = destination + if instruction == 0xe25e_f004 { 4 } else { 0 }; + let status = 0xa000_00e0 | mode as u32; + cpu.set_spsr(status); + cpu.step(&mut bus).unwrap(); + assert_eq!(cpu.pc(), destination); + assert_eq!(cpu.cpsr(), status); + assert_eq!(cpu.registers[13], 0x0300_7000); + assert_eq!(cpu.registers[14], 0x1234); + if instruction == 0xe8fd_8004 { + assert_eq!(cpu.registers[2], 0xfeed_beef); + } + cpu.step(&mut bus).unwrap(); + assert_eq!(cpu.registers[6], expected(destination)); + cpu.switch_mode(Mode::Supervisor); + assert_eq!( + cpu.registers[13], + STACK + if instruction == 0xe8fd_8004 { 8 } else { 0 } + ); + } + } + } +} + +#[test] +fn real_swi_and_irq_returns_can_resume_thumb_iwram_with_known_lanes() { + for irq in [false, true] { + let destination = if irq { TARGET } else { TARGET + 2 }; + let mut bios = vec![0; BIOS_SIZE]; + bios[8..12].copy_from_slice(&0xe1b0_f00e_u32.to_le_bytes()); + bios[0x18..0x1c].copy_from_slice(&0xe25e_f004_u32.to_le_bytes()); + let mut bus = Memory::with_bios(vec![], bios).unwrap(); + target(&mut bus, destination, PROBE); + if !irq { + bus.write16(TARGET, 0xdf00).unwrap(); + } + let mut cpu = Cpu::new(TARGET); + cpu.instruction_set = InstructionSet::Thumb; + cpu.registers[0] = UNUSED; + let status = cpu.cpsr(); + let mut machine = Machine::new(cpu, bus); + if irq { + machine.memory_mut().write16(IE, 8).unwrap(); + machine.memory_mut().write16(IME, 1).unwrap(); + machine + .memory_mut() + .write32(TIMER_BASE, 0x00c0_ffff) + .unwrap(); + machine.memory_mut().advance_cycles(1); + assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); + machine.memory_mut().write16(IME, 0).unwrap(); // Keep this test to one IRQ entry. + } else { + machine.step().unwrap(); + } + machine.step().unwrap(); // Execute the original ARM return handler. + assert_eq!(machine.cpu().pc(), destination); + assert_eq!(machine.cpu().cpsr(), status); + machine.step().unwrap(); + assert_eq!(machine.cpu().registers()[6], expected(destination)); + } +} + +#[test] +fn failed_refill_instructions_preserve_cpu_and_preexisting_history() { + for instruction in [0xe1b0_f00e_u32, 0xe8fd_8004] { + let mut bus = Memory::new(instruction.to_le_bytes().to_vec()).unwrap(); + for (index, half) in [0x680a_u16, PROBE, 0x1111, LOOKAHEAD] + .into_iter() + .enumerate() + { + bus.write16(SOURCE + index as u32 * 2, half).unwrap(); + } + bus.write32(STACK, 0x1234_abcd).unwrap(); + let mut paused = Cpu::new(SOURCE); + paused.instruction_set = InstructionSet::Thumb; + paused.registers[0] = UNUSED; + paused.registers[1] = STACK; + paused.step(&mut bus).unwrap(); // Seed sequential history with a word load. + let mut failing = Cpu::at_reset(); + failing.registers[15] = ROM_START; + failing.registers[13] = STACK; + failing.registers[14] = TARGET; + failing.set_spsr(0); // Invalid return mode; no refill must commit. + let before = failing.clone(); + assert!(failing.step_timed(&mut bus).is_err()); + assert_eq!(failing, before); + paused.step(&mut bus).unwrap(); + assert_eq!(paused.registers[6], 0xd234_abcd); + } +} + +#[test] +fn cold_entry_region_crossings_and_unmapped_targets_do_not_invent_refill_history() { + let mut bus = Memory::new(vec![]).unwrap(); + target(&mut bus, TARGET, PROBE); + let mut cpu = Cpu::new(TARGET); + cpu.instruction_set = InstructionSet::Thumb; + cpu.registers[0] = UNUSED; + assert_eq!( + cpu.step(&mut bus), + Err(CpuError::Memory(MemoryError::Unmapped(UNUSED))) + ); + + for destination in [0x03ff_fffc, 0x03ff_fffe, 0x0e00_0000] { + let mut bus = Memory::new(0xe12f_ff14_u32.to_le_bytes().to_vec()).unwrap(); + if destination >> 24 == 3 { + bus.write16(destination, PROBE).unwrap(); + } + let mut cpu = Cpu::new(ROM_START); + cpu.registers[0] = UNUSED; + cpu.registers[4] = destination | 1; + cpu.step(&mut bus).unwrap(); // An unavailable target/refill never fails BX early. + assert_eq!(cpu.pc(), destination); + let before = cpu.clone(); + assert_eq!( + cpu.step(&mut bus), + Err(CpuError::Memory(MemoryError::Unmapped( + if destination >> 24 == 3 { + UNUSED + } else { + destination + } + ))) + ); + assert_eq!(cpu, before); + } +} + +#[test] +fn target_pair_refill_does_not_replace_the_separate_bios_retained_word() { + for destination in [TARGET, TARGET + 2] { + let mut bios = vec![0; BIOS_SIZE]; + bios[0x100..0x104].copy_from_slice(&0xe12f_ff14_u32.to_le_bytes()); + bios[0x108..0x10c].copy_from_slice(&0x89ab_cdef_u32.to_le_bytes()); + let mut bus = Memory::with_bios(vec![], bios).unwrap(); + target(&mut bus, destination, PROBE); + let mut cpu = Cpu::new(0x100); + cpu.registers[4] = destination | 1; + cpu.step(&mut bus).unwrap(); + cpu.step(&mut bus).unwrap(); // r0=0: protected BIOS read, not unused memory. + assert_eq!(cpu.registers[6], 0x89ab_cdef); + assert_eq!(bus.read32(0x108).unwrap(), 0x89ab_cdef); + } +} + +#[test] +fn successful_dma_between_refill_and_target_execution_invalidates_refill_history() { + let (cpu, mut bus) = branch(ROM_START, false, TARGET); + bus.write32(STACK, 0x1234_abcd).unwrap(); + let mut machine = Machine::new(cpu, bus); + machine.step().unwrap(); + let dma = DMA_BASE + 3 * DMA_STRIDE; + machine.memory_mut().write32(dma, STACK).unwrap(); + machine.memory_mut().write32(dma + 4, 0x0200_0000).unwrap(); + machine.memory_mut().write32(dma + 8, 0x8400_0001).unwrap(); + assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 }); + let before = machine.cpu().clone(); + let timing = machine.last_timing(); + let cycles = machine.cycles(); + assert_eq!( + machine.step(), + Err(MachineError::Cpu(CpuError::Memory(MemoryError::Unmapped( + UNUSED + )))) + ); + assert_eq!(machine.cpu(), &before); + assert_eq!(machine.last_timing(), timing); + assert_eq!(machine.cycles(), cycles); + assert_eq!(machine.cpu().pc(), TARGET); +} + +#[test] +fn refill_snapshots_keep_existing_nominal_costs_and_timed_untimed_results() { + for thumb in [false, true] { + for destination in [TARGET, TARGET + 2] { + let (cpu, mut bus) = branch(ROM_START, thumb, destination); + bus.write32(TIMER_BASE, 0x0080_0000).unwrap(); + let mut machine = Machine::new(cpu, bus); + let (mut untimed, mut bus) = branch(ROM_START, thumb, destination); + for index in 0..2 { + untimed.step(&mut bus).unwrap(); + machine.step().unwrap(); + assert_eq!(machine.cpu(), &untimed); + assert_eq!( + machine.last_timing().code_cycles, + if index == 0 { 3 } else { 1 } + ); + assert_eq!(machine.last_timing().data_cycles, index); + assert_eq!(machine.last_timing().internal_cycles, index); + } + assert_eq!(machine.cycles(), 6); + assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 6); + assert_eq!(bus.cycles(), 0); + } + } +} diff --git a/crates/gba-core/src/cpu/timing.rs b/crates/gba-core/src/cpu/timing.rs index 3ad306d..a215ae1 100644 --- a/crates/gba-core/src/cpu/timing.rs +++ b/crates/gba-core/src/cpu/timing.rs @@ -1,5 +1,5 @@ //! Nominal ARM7 instruction-cycle summaries, separate from instruction semantics. -//! Data costs come from actual bus accesses. No speculative pipeline reads occur. +//! Data costs come from actual bus accesses. Bus-history samples are separate from timing calculations. //! PC writes use target-region code costs, following GBATEK's cycle summary. use super::{Cpu, CpuError, InstructionSet}; @@ -151,8 +151,11 @@ impl Cpu { // Share the existing control-flow classification with bounded bus history. // A PC write can refill even when its target equals the sequential address. - pub(super) fn thumb_refills(&self, instruction: u32) -> bool { - self.thumb_summary(instruction).refill + pub(super) fn instruction_refills(&self, instruction: u32) -> bool { + match self.instruction_set { + InstructionSet::Arm => self.arm_summary(instruction).refill, + InstructionSet::Thumb => self.thumb_summary(instruction).refill, + } } fn thumb_summary(&self, instruction: u32) -> Summary { diff --git a/crates/gba-core/src/memory.rs b/crates/gba-core/src/memory.rs index ec55b35..74a196e 100644 --- a/crates/gba-core/src/memory.rs +++ b/crates/gba-core/src/memory.rs @@ -66,7 +66,8 @@ struct CpuAccess { /// Unused-memory reads use ARM PC+8 or supported region-dependent Thumb snapshots. /// Protected BIOS reads separately retain the snapshot from BIOS execution. /// Sequential Thumb IWRAM accesses retain addressed bus lanes transactionally. -/// Refill history, a full fetch pipeline, and DMA bus latches are not modeled. +/// Successful refills into Thumb IWRAM sample the target pair to establish history. +/// A full fetch pipeline, ARM-target refill history, and DMA bus latches are not modeled. pub struct Memory { external_ram: Vec, internal_ram: Vec, @@ -400,6 +401,24 @@ impl Memory { } } + /// Sample only the two target fetches of a completed refill into Thumb IWRAM. + /// This updates bus history, not the CPU's instruction buffer or nominal costs. + /// Incomplete or unsupported refills stay unknown without failing the branch early. + pub(crate) fn refill_cpu_bus_history(&mut self, pc: u32, instruction_set: InstructionSet) { + self.iwram_bus.invalidate(); + if instruction_set == InstructionSet::Thumb + && pc >> 24 == 3 + && pc.wrapping_add(2) >> 24 == 3 + { + if let (Some(first), Some(second)) = ( + self.snapshot_mapped_halfword(pc), + self.snapshot_mapped_halfword(pc + 2), + ) { + self.iwram_bus.refill(pc, [first, second]); + } + } + } + pub(crate) fn invalidate_cpu_bus_history(&mut self) { self.iwram_bus.invalidate(); } diff --git a/crates/gba-core/src/memory/iwram_bus.rs b/crates/gba-core/src/memory/iwram_bus.rs index 6372d7a..d116f70 100644 --- a/crates/gba-core/src/memory/iwram_bus.rs +++ b/crates/gba-core/src/memory/iwram_bus.rs @@ -1,5 +1,5 @@ -//! Transactional lane history for consecutive Thumb instructions in IWRAM. -//! Unknown startup/refill/DMA history is not replaced with current memory bytes. +//! Transactional lane history for Thumb IWRAM execution and target-pair refills. +//! Unknown startup/DMA history is not replaced with guessed memory bytes. use std::cell::Cell; use crate::timing::AccessWidth; @@ -87,6 +87,20 @@ impl IwramBus { } } + pub(super) fn refill(&mut self, pc: u32, fetched: [u16; 2]) { + debug_assert!(self.pending.get().is_none()); + debug_assert_eq!(pc & 1, 0); + let mut lanes = Lanes::default(); + for (index, half) in fetched.into_iter().enumerate() { + lanes.drive( + pc + index as u32 * 2, + AccessWidth::Halfword, + u32::from(half), + ); + } + self.committed = Some(History { next_pc: pc, lanes }); + } + pub(super) fn invalidate(&mut self) { debug_assert!(self.pending.get().is_none()); self.committed = None; diff --git a/docs/architecture.md b/docs/architecture.md index 6d39ef8..0123597 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -31,7 +31,8 @@ The repository is a Cargo workspace with three crates: | `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/thumb_open_bus_tests.rs` | Region-dependent Thumb open bus, mirrors, widths, transfers, source boundaries, isolation, and nominal timing | | `crates/gba-core/src/cpu/iwram_open_bus_tests.rs` | Sequential IWRAM lane history, access widths, isolation, errors, refills, IRQ/DMA invalidation, and nominal timing | -| `crates/gba-core/src/memory/iwram_bus.rs` | Transactional known-lane history for sequential Thumb IWRAM execution | +| `crates/gba-core/src/cpu/iwram_refill_tests.rs` | Thumb IWRAM target-pair refills, state changes, exception returns, boundaries, isolation, and nominal costs | +| `crates/gba-core/src/memory/iwram_bus.rs` | Transactional known-lane history for Thumb IWRAM execution and target-pair refills | | `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/cpu.md b/docs/hardware/cpu.md index a51ea68..09ed1d1 100644 --- a/docs/hardware/cpu.md +++ b/docs/hardware/cpu.md @@ -102,7 +102,7 @@ Limits remain explicit: - 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. -- There is no persistent instruction pipeline. Sequential Thumb IWRAM history does not model refills or DMA-to-CPU bus transitions. +- There is no persistent instruction pipeline. Thumb IWRAM refills sample their target pair, but DMA-to-CPU transitions remain unmodeled. This subset lets public ARM test 362 complete. That test does not assert the loaded data value. Original regressions check the values separately. See [the public ARM result](../public-arm-tests.md). @@ -162,11 +162,33 @@ Success commits history only for the next sequential Thumb IWRAM PC. Failure pre Block-transfer data accesses stage updates in order; unused-memory reads still use one entry snapshot per instruction. This preserves the existing atomic diagnostic policy without adding bus or device cycles. -Taken branches and PC-writing refills invalidate continuation history, even when their target is the fallthrough address. +Taken branches and PC-writing refills end the old continuation history, even when their target is the fallthrough address. +A successful refill into Thumb IWRAM establishes new history from its target pair, as described below. Untaken conditional branches keep sequential history. -Other instruction states/regions, accepted machine IRQs, and successful DMA units end the supported history sequence. -Failed DMA units leave history unchanged. Discontinuous entry starts unknown instead of borrowing stale lanes. -Refill fetches, ARM-to-Thumb history, and DMA latch contents remain unmodeled. +Other execution states/regions, accepted machine IRQs, and successful DMA units invalidate continuation history. +A later exception return into Thumb IWRAM can establish new history through its refill. +Failed DMA units leave history unchanged. Discontinuous entry without an executed refill starts unknown. + +### Thumb IWRAM refill history + +After a successful refill instruction, the emulator uses the resulting PC and instruction state. +If the destination is Thumb IWRAM, it samples two halfwords in order: target T, then T+2. +Both accesses must remain in IWRAM. Normal physical RAM mirrors apply. +These samples establish both word lanes for execution at T. The first instruction then samples T+4 as usual. + +This applies to ARM/Thumb BX, taken Thumb branches, BL suffixes, Thumb PC writes, and ARM status-restoring returns. +Saved Thumb state, not the target's low bit alone, controls exception-return sampling. +Stack and block-load returns finish their data accesses before the target samples. +Failed instructions do not sample a target or replace committed history. + +Snapshots use strict mapped reads and add no nominal data or device cycles. +They update bus history only; the interpreter does not cache these instructions for execution. +Host inspection does not change captured lanes. A failed target fetch remains a later instruction diagnostic. +Cold direct startup, unsupported target states/regions, and region-crossing lookahead still have no inferred refill history. +DMA between refill and arrival still invalidates this history rather than supplying a guessed DMA value. + +The [refill evidence and scope](../research/iwram-bus-history.md#thumb-iwram-refill-extension) distinguish these samples from a full pipeline. +ARM-target refill history, DMA ordering, self-modifying instruction execution, and exact refill timing remain incomplete. ## Processor status and exceptions @@ -246,7 +268,8 @@ Important timing limits: - Code S/N counts follow instruction summaries, not a simulated fetch pipeline. - Ordinary code costs use the current instruction address. The ARM open-bus PC+8 snapshot has no fetch timing or startup pipeline fill. - PC writes use destination-region costs and the restored instruction width for nominal refill accesses. -- Refill cost calculation does not read target bytes. An invalid branch target fails on the following instruction fetch. +- Refill cost calculation does not read target bytes. Separate Thumb IWRAM target-pair samples update bus history without extra cycles. +- Target-pair sampling cannot fail the branch early. An invalid branch target still fails on the following instruction fetch. - Game Pak prefetch is not implemented. WAITCNT bit 14 is stored but does not accelerate execution. - PHI and SRAM wait fields are stored; PHI output and SRAM mapping are not implemented. - External work RAM timing is fixed. The undocumented memory-control register is not implemented. diff --git a/docs/references.md b/docs/references.md index 6ff42a0..5dfa86f 100644 --- a/docs/references.md +++ b/docs/references.md @@ -50,7 +50,7 @@ Hardware references used for the core: - [NanoBoyAdvance keypad implementation](https://github.com/nba-emu/NanoBoyAdvance/blob/master/src/nba/src/hw/keypad/keypad.cc), compared for control access widths; its AND/retrigger behavior differs from the chosen polling model. - [mGBA keypad timing issue](https://github.com/mgba-emu/mgba/issues/2490), for frontend frame-based input timing limits. - [Tonc hardware interrupts](https://gbadev.net/tonc/interrupts.html), for keypad source enable and IF/IE bit 12. -- [GBATEK memory mirrors, video byte writes, and unused-memory reads](https://problemkaputt.de/gbatek-gba-unpredictable-things.htm), for unused address ranges, ARM PC+8 open bus, byte lanes, the distinct Thumb region/history rules, and PC-dependent BIOS read protection. The Thumb implementation follows its 16-bit-region and BIOS/OAM formulas. IWRAM has bounded sequential lane history; refills and region transitions remain unsupported. +- [GBATEK memory mirrors, video byte writes, and unused-memory reads](https://problemkaputt.de/gbatek-gba-unpredictable-things.htm), for unused address ranges, ARM PC+8 open bus, byte lanes, the distinct Thumb region/history rules, and PC-dependent BIOS read protection. The Thumb implementation follows its 16-bit-region and BIOS/OAM formulas. IWRAM has bounded sequential lane history and Thumb target-pair refill samples; region transitions remain unsupported. - [nocash GBA open-bus discussion](https://www.ngemu.com/threads/gba-open-bus.170809/), for ARM prefetch, repeated Thumb halfwords in 16-bit regions, aligned BIOS/OAM words, and history-dependent IWRAM lanes. Its data-load overwrite note prevents treating P+2 as a universal IWRAM rule. - [mGBA memory implementation](https://github.com/mgba-emu/mgba/blob/master/src/gba/memory.c), reviewed for `GBALoadBad`, ARM prefetch sourcing, byte/halfword lane selection, and separate DMA/history behavior. Also reviewed its BIOS region-exit prefetch retention and protected-load lane selection. - [GBATEK GBA memory map and bus widths](https://problemkaputt.de/gbatek-gba-memory-map.htm). @@ -112,6 +112,15 @@ IWRAM lane-history research: See [IWRAM Thumb bus history](research/iwram-bus-history.md). These were source reviews, not local hardware or external-emulator test runs. +IWRAM refill research: + +- [ARM7TDMI branch cycle sequence](https://support.arm.com/documentation/ddi0029/g/instruction-cycle-timings/branch-and-branch-with-link), for destination and destination+instruction-width fetches. +- [ARM7TDMI Thumb BL sequence](https://support.arm.com/documentation/ddi0210/c/Instruction-Cycle-Timings/Thumb-branch-with-link), for the prefix/suffix distinction. +- [ARM7TDMI load-multiple sequence](https://support.arm.com/documentation/ddi0029/g/instruction-cycle-timings/load-multiple-registers), for data-transfer ordering before PC-load refills. +- [NanoBoyAdvance refill helpers](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/arm7tdmi.hh), reviewed for Thumb target/target+2 fetch order and the following execution fetch. + +These sources support bounded refill sampling, not a claim of cycle-accurate pipeline or DMA behavior. + 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/iwram-bus-history.md b/docs/research/iwram-bus-history.md index 7933b51..4d603d0 100644 --- a/docs/research/iwram-bus-history.md +++ b/docs/research/iwram-bus-history.md @@ -48,15 +48,43 @@ Ordinary instructions and mapped data accesses can establish history without req Consecutive halfword fetches or an IWRAM word access can make every lane known. Missing lookahead and 16 MiB region-crossing fetches retain diagnostics. -Successful branches and other refill instructions invalidate continuation history, even when the target equals the next sequential address. -The existing Thumb timing classifier identifies these refills for both timed and untimed CPU stepping. -Execution outside Thumb IWRAM also ends this bounded history sequence. -An accepted machine IRQ and each successful DMA unit invalidate the history. Failed DMA units preserve it. -The implementation does not guess refill or DMA latch contents. +The initial sequential-only implementation invalidated history after all refill instructions. +The extension below now establishes target-pair history for successful refills into Thumb IWRAM. +The existing ARM/Thumb timing classifiers identify these refills for both timed and untimed CPU stepping. +Execution outside Thumb IWRAM still ends a sequential history sequence. +An accepted machine IRQ and each successful DMA unit invalidate history. Failed DMA units preserve it. +The implementation does not guess DMA latch contents. -This is not a full IWRAM bus model across all CPU states. ARM access history, refill fetches, and DMA-to-CPU ordering remain incomplete. +This is not a full IWRAM bus model across all CPU states. ARM-target history and DMA-to-CPU ordering remain incomplete. It is not a persistent instruction pipeline. Self-modifying instruction execution and exact per-access device timing remain unverified. +## Thumb IWRAM refill extension + +The [ARM7TDMI branch sequence](https://support.arm.com/documentation/ddi0029/g/instruction-cycle-timings/branch-and-branch-with-link) fetches the destination, then destination plus instruction width. +For Thumb destinations, those addresses are T and T+2. +[NanoBoyAdvance's refill helpers](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/arm7tdmi.hh) use that order. +Its execution loop then fetches T+4 before executing the first Thumb instruction. + +After a successful refill instruction, we sample the two mapped destination halfwords if both remain in Thumb IWRAM. +The samples replace the old lane history and establish the next expected PC as T. +The next instruction's normal PC+4 sample updates one lane while the other retains the captured T+2 halfword. +The samples occur after data accesses and status restoration, so loaded PC values and saved Thumb state select the destination. + +This covers actual ARM/Thumb BX, taken Thumb branches, BL suffixes, PC-writing Thumb operations, and ARM status-restoring returns. +The Thumb BL prefix alone does not refill. +Failed instructions retain the previous committed history. An unsupported or incomplete target sample does not fail the branch early. +Unmapped instruction targets still fail on the following fetch. +Cold direct startup and arbitrary PC changes do not manufacture a refill. + +These are bus-history samples, not instruction-buffer contents or extra emulated cycles. +The interpreter still reads instructions when it executes them. +Changing T+2 after the branch does not replace its captured bus value; changing T+4 before arrival affects the later sample. +Those original tests verify sample ordering, not hardware-accurate self-modifying instruction execution. + +Successful DMA still invalidates history, including between refill and arrival. +ARM-target refill history, exact pipeline timing, and DMA-to-CPU ordering remain outside this extension. +No external emulator differential run or physical-hardware test was performed. + ## Original regression coverage - Both code alignments, IWRAM mirrors, and physical RAM wrap. @@ -66,11 +94,14 @@ It is not a persistent instruction pipeline. Self-modifying instruction executio - Multiple-register transfers, final-word retention, and writeback. - Unknown lanes, establishment of known history, staged-access rollback, and retry. - Taken and untaken branches, PC writes to fallthrough, discontinuities, IRQ entry, and DMA success/failure. +- ARM/Thumb source regions, both target alignments, refill sample ordering, stack/block returns, and saved User/System banks. +- Actual SWI/IRQ return handlers, failed refills, unmapped targets, separate BIOS history, and DMA between refill and arrival. - Equal timed/untimed CPU results and unchanged nominal data/device costs. ## Validation result The sequential-history regressions reproduced the old unsupported-read failures before implementation. +The refill regressions also failed on the prior invalidation-only path, then passed with target-pair sampling. Workspace and core/demo tests pass on Darwin arm64 in debug and release builds. Formatting, lint checks, rustdoc, preparation tests, native ROM windows, and graphics smoke modes also pass. Public ARM, Thumb, memory, and BIOS reports match their previous passing results exactly. diff --git a/docs/status.md b/docs/status.md index 5e7364f..ee9b25d 100644 --- a/docs/status.md +++ b/docs/status.md @@ -74,6 +74,7 @@ Update this file when a feature lands or a limit is removed. - ARM unused-memory data reads return a PC+8 word snapshot, with normal byte lanes and load rotation. - Thumb unused-memory reads use repeated PC+4 halfwords in 16-bit code regions and aligned PC+4 words in BIOS/OAM. - Sequential Thumb IWRAM history retains fetched and data-access lanes, with known-bit tracking and diagnostic rollback. +- Successful refills into Thumb IWRAM establish bus history from the target and target+2 halfwords, including exception returns. - Little-endian byte, halfword, and word reads/writes. - ARM7TDMI word-load rotation, aligned stores, and odd-address halfword-load behavior. - Explicit errors for unsupported instructions when their condition passes, and for unsupported memory accesses. @@ -134,8 +135,9 @@ 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). Thumb reads use [region-dependent snapshots](hardware/cpu.md#thumb-unused-memory-data-reads) and bounded sequential IWRAM lane history. Cold or invalidated IWRAM history remains diagnostic until all lanes are known. -Refill instructions, accepted machine IRQs, successful DMA, and execution outside Thumb IWRAM end the supported history sequence. -This is not a fetch pipeline. Refill history, region-crossing fetches, DMA latches, and unused/write-only I/O open bus remain unsupported. +Refills into Thumb IWRAM establish new target-pair history after the instruction succeeds. +Accepted machine IRQs, successful DMA, unsupported refills, and other execution states/regions invalidate continuation history. +This is not a fetch pipeline. ARM-target refill history, region-crossing fetches, DMA latches, and unused/write-only I/O open bus remain unsupported. 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. @@ -145,7 +147,7 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa ## Next steps -1. Extend IWRAM history across validated refill and DMA-to-CPU transitions. +1. Research and validate DMA-to-CPU bus history, including latch ownership and IWRAM effects. Keep unknown history diagnostic until fetch ordering and latch ownership have independent coverage. 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. diff --git a/docs/testing.md b/docs/testing.md index fad7067..fc27ad3 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -72,7 +72,10 @@ Short-ROM tests require only the fetched halfword in 16-bit regions. Cold IWRAM Sequential IWRAM regressions check retained lanes across fetches, reads, writes, mirrors, and physical RAM wrap. They cover raw bus values before load rotation/sign extension, all modes, block transfers, and isolation from other memory regions. Known-bit tests reject incomplete words. Transaction tests verify discarded partial updates and preserved history after errors. -Control-flow tests distinguish taken/untaken branches and invalidate PC writes even when the target equals fallthrough. +Control-flow tests distinguish taken/untaken branches and refill Thumb IWRAM targets even when the target equals fallthrough. +Refill tests cover ARM/Thumb BX, Thumb branches and BL, high-register PC writes, stack/block PC loads, and saved-state returns. +They check both target alignments, mirrors/wrap, captured target+2 versus later target+4, widths, sign extension, and rotation. +Original SWI/IRQ handlers verify return-time history. Failed returns, cold entry, region boundaries, DMA invalidation, and BIOS isolation retain checks. DMA and IRQ tests check conservative invalidation; timed/untimed tests verify CPU results and nominal clock costs. Host/fetch isolation, separate BIOS history, unsupported stores/data regions, nominal device progression, and DMA diagnostics also have checks. The pinned public ARM ROM now reaches its completion checkpoint with r12 = 0 in debug and release builds. @@ -233,7 +236,7 @@ They cover all processor modes, load widths, lanes, rotation, both Thumb code al They verify direct reads while executing inside BIOS, re-entry, SWI/IRQ returns, host-inspection isolation, and original-boot provenance. 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. -Unknown Thumb IWRAM history remains diagnostic. Separate tests cover known sequential history, general Thumb snapshots, and retained BIOS history. +Unknown Thumb IWRAM history remains diagnostic. Separate tests cover sequential/refill history, general Thumb snapshots, and retained BIOS history. 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.