diff --git a/crates/gba-core/src/cpu.rs b/crates/gba-core/src/cpu.rs index 303430f..00caebf 100644 --- a/crates/gba-core/src/cpu.rs +++ b/crates/gba-core/src/cpu.rs @@ -38,6 +38,8 @@ mod dma_resume_tests; #[cfg(test)] mod exception_tests; #[cfg(test)] +mod fetch_history_tests; +#[cfg(test)] mod instruction_tests; #[cfg(test)] mod iwram_dma_tests; @@ -155,7 +157,7 @@ pub enum InstructionSet { } impl InstructionSet { - fn width(self) -> u32 { + pub(crate) fn width(self) -> u32 { match self { Self::Arm => 4, Self::Thumb => 2, @@ -235,6 +237,7 @@ impl Cpu { let pipeline::Fetched { instruction, continuation, + lookahead, } = fetched; // Record the executing instruction, not an operand's pipelined PC value. // Always clear the access context, including on diagnostic errors. @@ -242,7 +245,7 @@ impl Cpu { let instruction_set = self.instruction_set; let refill = self.instruction_refills(instruction); let sequential_thumb = self.instruction_set == InstructionSet::Thumb && !refill; - memory.begin_cpu_access(pc, self.instruction_set); + memory.begin_cpu_access(pc, self.instruction_set, &lookahead); let result = self.execute_instruction(instruction, memory); let sequential = sequential_thumb && self.instruction_set == InstructionSet::Thumb @@ -250,7 +253,6 @@ impl Cpu { memory.end_cpu_access(result.is_ok(), sequential); if result.is_ok() { if refill { - memory.refill_cpu_bus_history(self.pc(), self.instruction_set); self.refill_pipeline(memory); } else { // A state change without an ordinary refill starts a cold sequence. diff --git a/crates/gba-core/src/cpu/fetch_history_tests.rs b/crates/gba-core/src/cpu/fetch_history_tests.rs new file mode 100644 index 0000000..853a01c --- /dev/null +++ b/crates/gba-core/src/cpu/fetch_history_tests.rs @@ -0,0 +1,114 @@ +//! Original sample-consumption tests. The split fetch/execute is a test-only mutation seam, +//! not a claim that DMA can interrupt an instruction in the current scheduler. +use super::*; + +const UNUSED: u32 = 0x8000_0000; +const DATA: u32 = 0x0300_1000; +const OLD: u32 = 0x1234_5678; +const NEW: u32 = 0x9abc_def0; + +#[test] +fn arm_open_bus_uses_the_sample_already_taken_for_the_pipeline() { + for pc in [0x0200_0100, 0x0300_0100, 0x0700_0100] { + let mut bus = Memory::new(vec![]).unwrap(); + bus.write32(pc, 0xe590_6000).unwrap(); // LDR r6,[r0]. + bus.write32(pc + 8, OLD).unwrap(); + let mut cpu = Cpu::new(pc); + cpu.registers[0] = UNUSED; + let fetched = cpu.fetch(&bus).unwrap(); + bus.write32(pc + 8, NEW).unwrap(); + cpu.execute_fetched(fetched, &mut bus).unwrap(); + assert_eq!(cpu.registers[6], OLD); + assert_eq!(bus.read32(pc + 8).unwrap(), NEW); + assert_eq!(bus.cycles(), 0); + } +} + +#[test] +fn thumb_narrow_open_bus_uses_the_sample_already_taken_for_the_pipeline() { + for pc in [0x0200_0100, 0x0500_0100, 0x0600_0100] { + for alignment in [0, 2] { + let pc = pc + alignment; + let mut bus = Memory::new(vec![]).unwrap(); + bus.write16(pc, 0x6806).unwrap(); // LDR r6,[r0]. + bus.write16(pc + 4, OLD as u16).unwrap(); + let mut cpu = Cpu::new(pc); + cpu.instruction_set = InstructionSet::Thumb; + cpu.registers[0] = UNUSED; + let fetched = cpu.fetch(&bus).unwrap(); + bus.write16(pc + 4, NEW as u16).unwrap(); + cpu.execute_fetched(fetched, &mut bus).unwrap(); + assert_eq!(cpu.registers[6], 0x5678_5678); + } + } +} + +#[test] +fn thumb_wide_open_bus_retains_both_lanes_of_the_fetch_sample() { + for pc in [0x0700_0100, 0x0700_0102] { + let mut bus = Memory::new(vec![]).unwrap(); + bus.write16(pc, 0x6806).unwrap(); + bus.write32((pc + 4) & !3, OLD).unwrap(); + let mut cpu = Cpu::new(pc); + cpu.instruction_set = InstructionSet::Thumb; + cpu.registers[0] = UNUSED; + let fetched = cpu.fetch(&bus).unwrap(); + bus.write32((pc + 4) & !3, NEW).unwrap(); + cpu.execute_fetched(fetched, &mut bus).unwrap(); + assert_eq!(cpu.registers[6], OLD); + } +} + +#[test] +fn iwram_lane_history_uses_new_fetch_data_not_retained_opcodes_or_reread_bytes() { + for pc in [0x0300_0100, 0x0300_0102] { + let mut bus = Memory::new(vec![]).unwrap(); + for (index, half) in [0x680a, 0x6806, 0x46c0, 0x5678].into_iter().enumerate() { + bus.write16(pc + index as u32 * 2, half).unwrap(); + } + bus.write32(DATA, 0x1122_3344).unwrap(); + let mut cpu = Cpu::new(pc); + cpu.instruction_set = InstructionSet::Thumb; + cpu.registers[0] = UNUSED; + cpu.registers[1] = DATA; + cpu.step(&mut bus).unwrap(); // Data read establishes all local bus lanes. + let fetched = cpu.fetch(&bus).unwrap(); + bus.write16(pc + 6, 0xdef0).unwrap(); + cpu.execute_fetched(fetched, &mut bus).unwrap(); + let expected = if pc & 2 == 0 { + 0x5678_3344 + } else { + 0x1122_5678 + }; + assert_eq!(cpu.registers[6], expected); + } +} + +#[test] +fn failed_execution_discards_its_sample_and_retry_takes_a_new_one() { + let pc = 0x0300_0100; + let mut bus = Memory::new(vec![]).unwrap(); + for (index, half) in [0x680a, 0x6823, 0x6806, 0x5678, 0x9abc] + .into_iter() + .enumerate() + { + bus.write16(pc + index as u32 * 2, half).unwrap(); + } + bus.write32(DATA, 0x1122_3344).unwrap(); + let mut cpu = Cpu::new(pc); + cpu.instruction_set = InstructionSet::Thumb; + cpu.registers[0] = UNUSED; + cpu.registers[1] = DATA; + cpu.registers[4] = 0x0e00_0000; + cpu.step(&mut bus).unwrap(); + let before = cpu.clone(); + let fetched = cpu.fetch(&bus).unwrap(); + assert!(cpu.execute_fetched(fetched, &mut bus).is_err()); + assert_eq!(cpu, before); + bus.write16(pc + 6, 0xdef0).unwrap(); + cpu.registers[4] = UNUSED; // Retry reads open bus instead of unsupported save memory. + cpu.step(&mut bus).unwrap(); + assert_eq!(cpu.registers[3], 0xdef0_3344); + cpu.step(&mut bus).unwrap(); + assert_eq!(cpu.registers[6], 0xdef0_9abc); +} diff --git a/crates/gba-core/src/cpu/pipeline.rs b/crates/gba-core/src/cpu/pipeline.rs index ab85afe..e42d00d 100644 --- a/crates/gba-core/src/cpu/pipeline.rs +++ b/crates/gba-core/src/cpu/pipeline.rs @@ -1,6 +1,6 @@ //! ARM/Thumb instruction buffering. Timing and bus ownership remain separate. use super::{Cpu, CpuError, InstructionSet}; -use crate::memory::{Memory, MemoryError}; +use crate::memory::{InstructionFetch, Memory, MemoryError}; #[derive(Debug, Clone, PartialEq, Eq)] pub(super) struct Pipeline { @@ -14,6 +14,7 @@ pub(super) struct Pipeline { pub(super) struct Fetched { pub instruction: u32, pub continuation: Pipeline, + pub lookahead: InstructionFetch, } impl Cpu { @@ -34,11 +35,15 @@ impl Cpu { .filter(|pipe| pipe.pc == pc && pipe.instruction_set == instruction_set); let instruction = match retained { Some(pipe) => pipe.instructions[0].clone(), - None => memory.fetch_instruction(pc, instruction_set), + None => memory.fetch_instruction(pc, instruction_set).instruction, }?; let decode = match retained { Some(pipe) => pipe.instructions[1].clone(), - None => memory.fetch_instruction(pc.wrapping_add(width), instruction_set), + None => { + memory + .fetch_instruction(pc.wrapping_add(width), instruction_set) + .instruction + } }; let fetched = memory.fetch_instruction(pc.wrapping_add(2 * width), instruction_set); Ok(Fetched { @@ -46,23 +51,26 @@ impl Cpu { continuation: Pipeline { pc: pc.wrapping_add(width), instruction_set, - instructions: [decode, fetched], + instructions: [decode, fetched.instruction.clone()], }, + lookahead: fetched, }) } /// Capture the target pair using the resulting state, without early errors. /// Explicit exception entry without Memory leaves a cold buffer instead. - pub(crate) fn refill_pipeline(&mut self, memory: &Memory) { + pub(crate) fn refill_pipeline(&mut self, memory: &mut Memory) { let pc = self.pc(); let instruction_set = self.instruction_set; + let fetches = [ + memory.fetch_instruction(pc, instruction_set), + memory.fetch_instruction(pc.wrapping_add(instruction_set.width()), instruction_set), + ]; + memory.refill_cpu_bus_history(&fetches); self.pipeline = Some(Pipeline { pc, instruction_set, - instructions: [ - memory.fetch_instruction(pc, instruction_set), - memory.fetch_instruction(pc.wrapping_add(instruction_set.width()), instruction_set), - ], + instructions: fetches.map(|fetch| fetch.instruction), }); } } diff --git a/crates/gba-core/src/machine.rs b/crates/gba-core/src/machine.rs index 68826db..0fcd034 100644 --- a/crates/gba-core/src/machine.rs +++ b/crates/gba-core/src/machine.rs @@ -178,8 +178,7 @@ impl Machine { return Ok(StepKind::HaltIdle); } let (kind, timing) = if self.cpu.take_interrupt(self.memory.irq_pending(), false) { - self.memory.invalidate_cpu_bus_history(); - self.cpu.refill_pipeline(&self.memory); + self.cpu.refill_pipeline(&mut self.memory); (StepKind::IrqEntry, self.cpu.exception_timing(&self.memory)) } else { ( diff --git a/crates/gba-core/src/memory.rs b/crates/gba-core/src/memory.rs index 18fa6f5..cac3299 100644 --- a/crates/gba-core/src/memory.rs +++ b/crates/gba-core/src/memory.rs @@ -1,6 +1,10 @@ use std::{cell::Cell, error::Error, fmt}; +mod fetch; +#[cfg(test)] +mod fetch_tests; mod iwram_bus; +pub(crate) use fetch::InstructionFetch; use iwram_bus::IwramBus; use crate::{ @@ -69,7 +73,8 @@ struct CpuAccess { /// Successful refills into Thumb IWRAM sample the target pair to establish history. /// DMA accesses drive existing Thumb IWRAM continuation lanes before the next fetch. /// Channel data remains separate; general and sub-instruction CPU bus handoff is unmodeled. -/// CPU-owned ARM/Thumb instruction buffering does not yet drive complete bus history or timing. +/// Instruction buffering and supported bus history consume shared fetch samples. +/// Complete bus ownership and per-access timing remain unmodeled. pub struct Memory { external_ram: Vec, internal_ram: Vec, @@ -365,80 +370,32 @@ impl Memory { self.io.next_event_cycles() } - pub(crate) fn begin_cpu_access(&mut self, pc: u32, instruction_set: InstructionSet) { + pub(crate) fn begin_cpu_access( + &mut self, + pc: u32, + instruction_set: InstructionSet, + fetch: &InstructionFetch, + ) { debug_assert!(self.cpu_access.is_none()); - // Sample before execution, without data timing or open-bus recursion. - // Missing lookahead bytes must not fail an instruction that never uses them. + debug_assert_eq!(fetch.state, instruction_set); + debug_assert_eq!(fetch.address, pc.wrapping_add(2 * instruction_set.width())); + // Consume the pipeline's new fetch, never a retained opcode or a second read. + // Missing lookahead must not fail an instruction that never uses it. + let same_region = fetch.address >> 24 == pc >> 24; let iwram_pc = (instruction_set == InstructionSet::Thumb && pc >> 24 == 3).then_some(pc); - let fetched = iwram_pc - .filter(|pc| pc.wrapping_add(4) >> 24 == 3) - .and_then(|pc| self.snapshot_mapped_halfword(pc + 4)); + let fetched = same_region + .then(|| fetch.instruction.as_ref().ok().map(|value| *value as u16)) + .flatten(); self.iwram_bus.begin(iwram_pc, fetched); let prefetch = match instruction_set { - InstructionSet::Arm => self.snapshot_mapped_word(pc.wrapping_add(8)), - InstructionSet::Thumb => self.snapshot_thumb_prefetch(pc), + InstructionSet::Arm => fetch.bus_word, + InstructionSet::Thumb if !same_region => None, + InstructionSet::Thumb if pc >> 24 == 3 => self.iwram_bus.snapshot(), + InstructionSet::Thumb => fetch.bus_word, }; self.cpu_access = Some(CpuAccess { pc, prefetch }); } - fn snapshot_thumb_prefetch(&self, pc: u32) -> Option { - let address = pc.wrapping_add(4); - // Region-crossing fetches need pipeline history, not a rule chosen from the old PC. - if address >> 24 != pc >> 24 { - return None; - } - match pc >> 24 { - // BIOS and OAM drive a full word even for a halfword instruction fetch. - 0x00 | 0x07 => self.snapshot_mapped_word(address & !3), - // The IWRAM halfword fetch replaces one lane of its retained bus word. - 0x03 => self.iwram_bus.snapshot(), - // These 16-bit regions repeat the fetched halfword in both word lanes. - 0x02 | 0x05 | 0x06 | 0x08..=0x0d => { - Some(u32::from(self.snapshot_mapped_halfword(address)?) * 0x0001_0001) - } - // Unsupported code regions must not manufacture an open-bus snapshot. - _ => None, - } - } - - fn snapshot_mapped_halfword(&self, address: u32) -> Option { - Some(u16::from_le_bytes([ - self.read_mapped_byte(address).ok()?, - self.read_mapped_byte(address + 1).ok()?, - ])) - } - - /// Strict instruction fetch without data context, timing, or bus-history changes. - /// Thumb reads exactly two bytes, even on a region with a wider data-bus latch. - pub(crate) fn fetch_instruction( - &self, - address: u32, - state: InstructionSet, - ) -> Result { - let width = match state { - InstructionSet::Arm => 4, - InstructionSet::Thumb => 2, - }; - if address & (width as u32 - 1) != 0 { - return Err(MemoryError::Unaligned(address)); - } - let mut bytes = [0; 4]; - for (offset, byte) in bytes[..width].iter_mut().enumerate() { - *byte = self.read_mapped_byte(address.wrapping_add(offset as u32))?; - } - Ok(u32::from_le_bytes(bytes)) - } - - 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(address.wrapping_add(offset as u32)) - .ok()?; - } - Some(u32::from_le_bytes(bytes)) - } - pub(crate) fn end_cpu_access(&mut self, succeeded: bool, sequential: bool) { if succeeded { // Timed and untimed CPU execution both consume a resume. Errors do not. @@ -454,28 +411,27 @@ 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. + /// Consume the same target samples that fill the CPU's instruction buffer. /// 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) { + pub(crate) fn refill_cpu_bus_history(&mut self, fetches: &[InstructionFetch; 2]) { self.iwram_bus.invalidate(); - if instruction_set == InstructionSet::Thumb - && pc >> 24 == 3 - && pc.wrapping_add(2) >> 24 == 3 + let [first, second] = fetches; + debug_assert_eq!(first.state, second.state); + debug_assert_eq!( + second.address, + first.address.wrapping_add(first.state.width()) + ); + if first.state == InstructionSet::Thumb + && first.address >> 24 == 3 + && second.address >> 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]); + if let (Ok(first_value), Ok(second_value)) = (&first.instruction, &second.instruction) { + self.iwram_bus + .refill(first.address, [*first_value as u16, *second_value as u16]); } } } - pub(crate) fn invalidate_cpu_bus_history(&mut self) { - self.iwram_bus.invalidate(); - } - fn can_write_power_control(&self) -> bool { // Bare memory writes are host/debug setup. CPU code must execute in BIOS. self.cpu_access diff --git a/crates/gba-core/src/memory/fetch.rs b/crates/gba-core/src/memory/fetch.rs new file mode 100644 index 0000000..1669f6b --- /dev/null +++ b/crates/gba-core/src/memory/fetch.rs @@ -0,0 +1,65 @@ +//! One mapped sample supplies instruction bits and the supported bus observation. +//! Sampling is side-effect-free; execution commits history under the diagnostic policy. +use super::{InstructionSet, Memory, MemoryError}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct InstructionFetch { + pub(super) address: u32, + pub(super) state: InstructionSet, + pub(crate) instruction: Result, + // None for unsupported regions, failed reads, or lane-based Thumb IWRAM history. + pub(super) bus_word: Option, +} + +impl Memory { + pub(crate) fn fetch_instruction( + &self, + address: u32, + state: InstructionSet, + ) -> InstructionFetch { + let instruction = self.read_instruction(address, state); + let bus_word = instruction.as_ref().ok().copied().and_then(|value| { + if state == InstructionSet::Arm { + return Some(value); + } + match address >> 24 { + // BIOS/OAM drive the aligned word. Read only the other halfword; + // do not sample the instruction bytes a second time. + 0x00 | 0x07 => { + let other = self + .read_instruction(address ^ 2, InstructionSet::Thumb) + .ok()?; + Some(if address & 2 == 0 { + value | (other << 16) + } else { + other | (value << 16) + }) + } + // IWRAM updates its existing addressed lanes at execution entry. + 0x03 => None, + 0x02 | 0x05 | 0x06 | 0x08..=0x0d => Some(value * 0x0001_0001), + _ => None, + } + }); + InstructionFetch { + address, + state, + instruction, + bus_word, + } + } + + /// Strict mapped bytes, without data context, timing, or bus-history changes. + /// An unavailable wider bus observation must not invalidate a valid instruction. + fn read_instruction(&self, address: u32, state: InstructionSet) -> Result { + let width = state.width() as usize; + if address & (width as u32 - 1) != 0 { + return Err(MemoryError::Unaligned(address)); + } + let mut bytes = [0; 4]; + for (offset, byte) in bytes[..width].iter_mut().enumerate() { + *byte = self.read_mapped_byte(address.wrapping_add(offset as u32))?; + } + Ok(u32::from_le_bytes(bytes)) + } +} diff --git a/crates/gba-core/src/memory/fetch_tests.rs b/crates/gba-core/src/memory/fetch_tests.rs new file mode 100644 index 0000000..205bb21 --- /dev/null +++ b/crates/gba-core/src/memory/fetch_tests.rs @@ -0,0 +1,178 @@ +//! Independent fetch-observation checks. Mutation between sample and consumption +//! checks ownership of captured values, not sub-instruction hardware arbitration. +use super::*; + +const WORD: u32 = 0x9abc_1234; +const CHANGED: u32 = 0x5678_def0; +const UNUSED: u32 = 0x8000_0000; + +#[test] +fn mapped_fetches_separate_instruction_width_from_driven_bus_lanes() { + let mut bios = vec![0; BIOS_SIZE]; + bios[..4].copy_from_slice(&WORD.to_le_bytes()); + let mut memory = Memory::with_bios(WORD.to_le_bytes().to_vec(), bios).unwrap(); + // Explicit expected bus words for each Thumb alignment; IWRAM needs prior lanes. + for (address, expected) in [ + (0, [Some(WORD), Some(WORD)]), + (0x0200_0000, [Some(0x1234_1234), Some(0x9abc_9abc)]), + (0x0300_0000, [None, None]), + (0x0500_0000, [Some(0x1234_1234), Some(0x9abc_9abc)]), + (0x0600_0000, [Some(0x1234_1234), Some(0x9abc_9abc)]), + (0x0700_0000, [Some(WORD), Some(WORD)]), + (0x0800_0000, [Some(0x1234_1234), Some(0x9abc_9abc)]), + (0x0a00_0000, [Some(0x1234_1234), Some(0x9abc_9abc)]), + (0x0c00_0000, [Some(0x1234_1234), Some(0x9abc_9abc)]), + ] { + if (0x0200_0000..0x0800_0000).contains(&address) { + memory.write32(address, WORD).unwrap(); + } + let arm = memory.fetch_instruction(address, InstructionSet::Arm); + assert_eq!(arm.instruction, Ok(WORD)); + assert_eq!(arm.bus_word, Some(WORD)); + for (lane, expected) in expected.into_iter().enumerate() { + let thumb = memory.fetch_instruction(address + lane as u32 * 2, InstructionSet::Thumb); + assert_eq!( + thumb.instruction, + Ok(if lane == 0 { 0x1234 } else { 0x9abc }) + ); + assert_eq!(thumb.bus_word, expected); + } + } +} + +#[test] +fn fetch_errors_remain_strict_and_narrow_rom_fetches_need_only_two_bytes() { + let memory = Memory::new(vec![0x34, 0x12]).unwrap(); + let thumb = memory.fetch_instruction(ROM_START, InstructionSet::Thumb); + assert_eq!(thumb.instruction, Ok(0x1234)); + assert_eq!(thumb.bus_word, Some(0x1234_1234)); + for (address, state, error) in [ + ( + ROM_START, + InstructionSet::Arm, + MemoryError::Unmapped(ROM_START + 2), + ), + ( + ROM_START + 2, + InstructionSet::Thumb, + MemoryError::Unmapped(ROM_START + 2), + ), + (0, InstructionSet::Thumb, MemoryError::Unmapped(0)), + (UNUSED, InstructionSet::Arm, MemoryError::Unmapped(UNUSED)), + ( + ROM_START + 1, + InstructionSet::Thumb, + MemoryError::Unaligned(ROM_START + 1), + ), + ( + ROM_START + 2, + InstructionSet::Arm, + MemoryError::Unaligned(ROM_START + 2), + ), + ] { + let fetch = memory.fetch_instruction(address, state); + assert_eq!(fetch.instruction, Err(error)); + assert_eq!(fetch.bus_word, None); + } +} + +#[test] +fn refill_history_consumes_captured_target_halfwords_without_rereading_memory() { + for target in [0x0300_0100, 0x0300_0102] { + let mut memory = Memory::new(vec![]).unwrap(); + memory.write16(target, 0x1234).unwrap(); + memory.write16(target + 2, 0x9abc).unwrap(); + memory.write16(target + 4, 0x5678).unwrap(); + let fetches = [ + memory.fetch_instruction(target, InstructionSet::Thumb), + memory.fetch_instruction(target + 2, InstructionSet::Thumb), + ]; + memory.write16(target, 0xffff).unwrap(); + memory.write16(target + 2, 0xffff).unwrap(); + memory.refill_cpu_bus_history(&fetches); + let lookahead = memory.fetch_instruction(target + 4, InstructionSet::Thumb); + memory.begin_cpu_access(target, InstructionSet::Thumb, &lookahead); + assert_eq!( + memory.read32(UNUSED).unwrap(), + if target & 2 == 0 { + 0x9abc_5678 + } else { + 0x5678_9abc + } + ); + memory.end_cpu_access(true, true); + } +} + +#[test] +fn bios_history_commits_the_captured_bus_word_not_later_image_bytes() { + for (pc, state, fetch_address) in [ + (0x100, InstructionSet::Arm, 0x108), + (0x100, InstructionSet::Thumb, 0x104), + (0x102, InstructionSet::Thumb, 0x106), + ] { + let mut bios = vec![0; BIOS_SIZE]; + let word_address = (fetch_address & !3) as usize; + bios[word_address..word_address + 4].copy_from_slice(&WORD.to_le_bytes()); + let mut memory = Memory::with_bios(vec![], bios).unwrap(); + let fetch = memory.fetch_instruction(fetch_address, state); + // Test-only image mutation: BIOS remains read-only through normal APIs. + memory.bios.as_mut().unwrap()[word_address..word_address + 4] + .copy_from_slice(&CHANGED.to_le_bytes()); + memory.begin_cpu_access(pc, state, &fetch); + memory.end_cpu_access(true, false); + assert_eq!(memory.bios_prefetch, Some(WORD)); + let outside = memory.fetch_instruction(0x0200_0008, InstructionSet::Arm); + memory.begin_cpu_access(0x0200_0000, InstructionSet::Arm, &outside); + assert_eq!(memory.read32(word_address as u32).unwrap(), WORD); + // Instruction reads still bypass the protected data value. + assert_eq!( + memory + .fetch_instruction(word_address as u32, InstructionSet::Arm) + .instruction, + Ok(CHANGED) + ); + memory.end_cpu_access(true, false); + } +} + +#[test] +fn sampling_has_no_timing_or_history_side_effects_and_never_uses_data_fallback() { + let mut bios = vec![0; BIOS_SIZE]; + bios[8..12].copy_from_slice(&WORD.to_le_bytes()); + let mut memory = Memory::with_bios(vec![], bios).unwrap(); + memory.cpu_resume_nonsequential = true; + memory.begin_data_timing(); + let fetch = memory.fetch_instruction(8, InstructionSet::Arm); + memory.fetch_instruction(0x0300_0000, InstructionSet::Thumb); + assert_eq!(memory.end_data_timing(), 0); + assert_eq!(memory.bios_prefetch, None); + assert!(memory.cpu_resume_nonsequential); + assert_eq!(memory.cycles(), 0); + memory.begin_cpu_access(0, InstructionSet::Arm, &fetch); + assert_eq!(memory.read32(UNUSED).unwrap(), WORD); + let missing = memory.fetch_instruction(UNUSED, InstructionSet::Arm); + assert_eq!(missing.instruction, Err(MemoryError::Unmapped(UNUSED))); + assert_eq!(missing.bus_word, None); + memory.end_cpu_access(false, false); + assert_eq!(memory.bios_prefetch, None); + assert!(memory.cpu_resume_nonsequential); +} + +#[test] +fn missing_fetch_observation_invalidates_bios_history_only_on_success() { + let mut memory = Memory::with_bios(vec![], vec![0; BIOS_SIZE]).unwrap(); + memory.bios_prefetch = Some(WORD); + for state in [InstructionSet::Arm, InstructionSet::Thumb] { + let pc = BIOS_SIZE as u32 - state.width(); + let fetch = memory.fetch_instruction(pc + 2 * state.width(), state); + assert!(fetch.instruction.is_err()); + memory.begin_cpu_access(pc, state, &fetch); + memory.end_cpu_access(false, false); + assert_eq!(memory.bios_prefetch, Some(WORD)); + } + let fetch = memory.fetch_instruction(BIOS_SIZE as u32, InstructionSet::Thumb); + memory.begin_cpu_access(BIOS_SIZE as u32 - 4, InstructionSet::Thumb, &fetch); + memory.end_cpu_access(true, false); + assert_eq!(memory.bios_prefetch, None); +} diff --git a/docs/architecture.md b/docs/architecture.md index b9a0dc6..388ebcc 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -42,6 +42,9 @@ The repository is a Cargo workspace with three crates: | `crates/gba-core/src/cpu/tests.rs` | Condition truth tables and immediate arithmetic tests | | `crates/gba-core/src/cpu/instruction_tests.rs` | Register operations, transfers, branches, and edge cases | | `crates/gba-core/src/memory.rs` | Memory mapping, I/O routing, write validation, and device clock | +| `crates/gba-core/src/memory/fetch.rs` | Strict instruction samples with captured region-dependent bus observations | +| `crates/gba-core/src/memory/fetch_tests.rs` | Fetch widths, bus lanes, captured refill/BIOS values, strict errors, and side-effect isolation | +| `crates/gba-core/src/cpu/fetch_history_tests.rs` | Shared sample consumption, no execution-entry rereads, and failed-step retry | | `crates/gba-core/src/io.rs` | I/O registers, timers, HALT/STOP wake-up, next-event bounds, and IRQ latches | | `crates/gba-core/src/display.rs` | Display clock, scanline status, comparison edges, and display IRQ events | | `crates/gba-core/src/dma.rs` | DMA registers, internal pointers, retained channel data, trigger state, priority, and completion IRQs | diff --git a/docs/hardware/bios.md b/docs/hardware/bios.md index f1f57c3..af184bc 100644 --- a/docs/hardware/bios.md +++ b/docs/hardware/bios.md @@ -99,7 +99,7 @@ Normal word rotation, odd-halfword behavior, sign extension, and load timing sti 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 an ARM BIOS instruction executes, use its pipeline's new mapped PC+8 fetch 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. @@ -134,7 +134,8 @@ Missing BIOS images remain unmapped. The unused-memory ARM open-bus snapshot is 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 BIOS readback model remains separate from the CPU's ARM/Thumb instruction buffer. +The BIOS latch remains separate from the CPU's ARM/Thumb instruction buffer. +Both now consume a shared fetch sample; BIOS history no longer rereads lookahead memory at execution entry. It does not model 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. diff --git a/docs/hardware/cpu.md b/docs/hardware/cpu.md index 69f8b0f..c4d9634 100644 --- a/docs/hardware/cpu.md +++ b/docs/hardware/cpu.md @@ -109,8 +109,10 @@ Do not call it for ordinary CPU/DMA stores: that would hide self-modifying-code This is instruction buffering, not a complete timed fetch pipeline. Samples add no nominal data accesses or device cycles. Existing refill costs and one-shot DMA resume costs remain unchanged. -Fetch-driven bus history, exact refill costs, per-access device updates, and DMA arbitration remain incomplete. -See the [ARM evidence and diagnostic policy](../research/arm-instruction-buffer.md) and [Thumb extension](../research/thumb-instruction-buffer.md). +Instruction buffering and supported bus history now consume the same mapped fetch samples. +Complete fetch-driven bus history, exact refill costs, per-access device updates, and DMA arbitration remain incomplete. +See the [ARM evidence and diagnostic policy](../research/arm-instruction-buffer.md), [Thumb extension](../research/thumb-instruction-buffer.md), +and [shared fetch samples](../research/shared-fetch-samples.md). ## ARM unused-memory data reads @@ -118,7 +120,8 @@ During ARM execution, data reads from `0x00004000..0x01ffffff` and `0x10000000.. Open bus means the bus retains previously driven data. GBATEK identifies this ARM value as the instruction word at PC+8. These high addresses do not mirror BIOS, RAM, or cartridge addresses. -The interpreter snapshots mapped bytes at PC+8 before executing each ARM instruction. +The interpreter samples mapped bytes at PC+8 before executing each ARM instruction. +That one fetch supplies both the new instruction-buffer entry and the unused-memory snapshot. All unused-memory reads within that instruction use the same snapshot. Byte and halfword reads select their addressed lanes; the CPU applies normal rotation and sign extension. Base writeback, register aliases, block loads, PC loads, and flags retain their existing instruction semantics. @@ -162,6 +165,8 @@ For BIOS/OAM, aligned instructions expose halfwords at P+4 and P+6. Instructions at two-byte-only alignment expose halfwords at P+2 and P+4. The two lanes can differ. A universal repeated-halfword rule would be incorrect. +The pipeline's new fetch supplies the bus snapshot without rereading its instruction bytes. +BIOS/OAM samples include the other halfword of the aligned bus word, but retain only the selected halfword as an instruction. Only the required mapped bytes are sampled. A 16-bit region needs two lookahead bytes, not four. Normal memory mirrors apply to these bytes, including physical RAM/video-memory wrap within a mapped region. If P+4 crosses a 16 MiB address-region boundary, the snapshot remains unknown. @@ -178,8 +183,8 @@ IWRAM needs prior bus lanes, including IWRAM data-read/write changes and possibl Do not substitute P+2 for that history: it is only the usual case, not a general rule. The original Nintendo DS also differs from GBA-family IWRAM behavior; this core targets GBA. -This remains a bounded snapshot implementation, not a pipeline or a complete bus-history model. -Refill timing, self-modifying code, DMA-to-CPU transitions, cross-state IWRAM history, unused/write-only I/O, and disabled RAM remain incomplete. +Bus history remains bounded despite sharing samples with the persistent instruction buffer. +Refill timing, general DMA-to-CPU transitions, cross-state IWRAM history, unused/write-only I/O, and disabled RAM remain incomplete. Original regressions validate the documented formulas. No physical-hardware or independent public Thumb open-bus pass is claimed. ### Sequential IWRAM history @@ -221,8 +226,8 @@ Saved Thumb state, not the target's low bit alone, controls exception-return sam 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; a separate CPU buffer retains the target instructions for execution. +Target samples use strict mapped reads and add no nominal data or device cycles. +The same captured target pair fills the CPU buffer and updates supported IWRAM history; no second target read occurs. 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 updates this continuation's actual IWRAM access lanes before the target+4 sample. diff --git a/docs/references.md b/docs/references.md index fcbb665..808f751 100644 --- a/docs/references.md +++ b/docs/references.md @@ -152,11 +152,12 @@ ARM/Thumb instruction-buffer research: - [ARM7TDMI instruction pipeline](https://developer.arm.com/documentation/dvi0027/b/arm7tdmi/instruction-pipeline) and [technical reference manual](https://documentation-service.arm.com/static/5e8e1323fd977155116a3129), for fetch/decode/execute stages and the visible ARM/Thumb PC offsets. - The pinned NanoBoyAdvance ARM7TDMI `Run`, `ReloadPipeline32`, and `ReloadPipeline16` helpers above, for retained instructions, pre-execution fetching, and target-pair refill order. - The ARM branch cycle sequence above, for target and target+instruction-width fetches. - - [ARM Thumb branch-with-link timing](https://support.arm.com/documentation/ddi0210/c/Instruction-Cycle-Timings/Thumb-branch-with-link), for sequential prefix execution and suffix target refill. See [persistent ARM instruction buffering](research/arm-instruction-buffer.md) and its [Thumb extension](research/thumb-instruction-buffer.md). -Instruction retention is implemented; unified fetch-driven bus history and per-access timing remain incomplete. +Instruction retention and supported bus snapshots now use [shared fetch samples](research/shared-fetch-samples.md). +That refactor reuses the pinned NanoBoyAdvance loop/refill and ares local-lane evidence above. +Complete bus history and per-access timing remain incomplete; no new hardware conformance result is claimed. 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. diff --git a/docs/research/arm-instruction-buffer.md b/docs/research/arm-instruction-buffer.md index cadbc1e..b88609f 100644 --- a/docs/research/arm-instruction-buffer.md +++ b/docs/research/arm-instruction-buffer.md @@ -73,7 +73,8 @@ The existing destination-based refill summaries and one-shot DMA resume timing r The current instruction address still selects nominal code cost rather than a separately scheduled fetch address. ARM PC+8 open bus and protected BIOS readback remain separate snapshots. -The instruction-buffer sample and those snapshots see mapped memory before instruction effects, but no unified per-access bus model exists yet. +The [shared fetch sample](shared-fetch-samples.md) supplies the instruction-buffer entry and supported bus snapshots without duplicate reads. +No unified per-access bus model exists yet. The target samples do not establish complete ARM IWRAM or BIOS bus history. Instruction-state-changing compare quirks, Game Pak prefetch, and DMA arbitration remain incomplete. DMA still runs between whole instructions, not during their data accesses or internal cycles. diff --git a/docs/research/iwram-bus-history.md b/docs/research/iwram-bus-history.md index e59147c..14725b4 100644 --- a/docs/research/iwram-bus-history.md +++ b/docs/research/iwram-bus-history.md @@ -33,7 +33,8 @@ No physical-hardware measurement or external emulator differential run was perfo ## Bounded implementation `memory/iwram_bus.rs` records lane values and a known-bit mask during consecutive Thumb instructions in IWRAM. -Before execution, the mapped PC+4 halfword updates its addressed lanes. +Before execution, the pipeline's new mapped PC+4 fetch updates its addressed lanes. +The bus-history consumer does not reread memory or reuse the current/decode instruction. During execution, successful IWRAM bus reads and writes update the staged lanes at their actual aligned addresses and widths. The latch sees raw bus data before CPU load rotation and sign extension. Other-region accesses and host inspection do not change it. @@ -65,7 +66,8 @@ 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. +After a successful refill instruction, the pipeline samples the two mapped destination instructions. +The same captured halfwords update bus history 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. @@ -76,7 +78,7 @@ Failed instructions retain the previous committed history. An unsupported or inc 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. +These captured values now supply both bus history and instruction-buffer entries, without extra emulated cycles. ARM and Thumb execution now use a separate instruction buffer. Cold buffer filling does not seed this bus history. 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. diff --git a/docs/research/shared-fetch-samples.md b/docs/research/shared-fetch-samples.md new file mode 100644 index 0000000..bc59712 --- /dev/null +++ b/docs/research/shared-fetch-samples.md @@ -0,0 +1,78 @@ +# Shared instruction-fetch samples + +## Finding and evidence + +Persistent ARM/Thumb instruction buffering exposed duplicate memory reads in the interpreter. +The pipeline sampled the new lookahead instruction, then `Memory::begin_cpu_access` reread those bytes for bus history. +Thumb IWRAM refills also read their target pair once for history and again for instruction buffering. +With instruction-boundary scheduling, those reads normally saw identical bytes. They still represented the same fetch twice. + +This refactor gives instruction buffering and supported bus-history updates a common captured sample. +It does not introduce new hardware formulas or change nominal cycle costs. + +Previously reviewed evidence supports this separation: + +- [NanoBoyAdvance at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/arm7tdmi.hh): `Run` fetches the next instruction before execution; refill helpers fetch the target pair. +- [ares at 6f6786e0](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/memory.cpp): IWRAM updates local addressed lanes; EWRAM halfword reads drive duplicated halves. +- [Thumb BIOS research](thumb-bios-prefetch.md) and [Thumb open-bus rules](../hardware/cpu.md#thumb-unused-memory-data-reads): BIOS/OAM bus observations use an aligned word, not a universally duplicated halfword. + +These sources were reviewed, not copied into the emulator. +No physical-hardware measurements or external-emulator differential runs were performed. + +## Sample and consumers + +`memory/fetch.rs` returns an `InstructionFetch` with: + +- Fetch address and instruction state. +- Strict instruction result or deferred mapped-read error. +- Supported bus-word observation, when it does not require retained IWRAM lanes. + +ARM samples use the fetched word for both instruction bits and bus observation. +Thumb samples retain only their selected halfword as instruction bits. +Supported 16-bit regions duplicate that halfword for bus observation. +BIOS/OAM samples read the adjacent halfword too, without rereading the instruction bytes. +Thumb IWRAM supplies the fetched halfword to the existing lane-retaining transaction instead of creating a full bus word. + +The CPU passes only its newly sampled P+8 or P+4 observation to `begin_cpu_access`. +It does not replay the bus observations of retained current/decode instructions. +The consumer performs no mapped memory read. +After successful control flow, the same target samples fill the instruction buffer and supported Thumb IWRAM refill history. +Machine IRQ entry uses this refill path; the ARM target still invalidates Thumb IWRAM continuation history. + +Instruction buffers and BIOS/IWRAM latches remain separate retained state. +Sampling alone changes no CPU access context, latch, device clock, DMA resume marker, or data-timing trace. +Instruction errors discard staged history and speculative buffer advances. +A retry takes a new lookahead sample while retaining the previously committed instruction pair. +Unavailable lookahead remains diagnostic only when consumed by an instruction or a data read that needs it. +Strict instruction reads never use protected BIOS or unused-memory data fallback. + +## Deliberately unchanged limits + +- Cold pipeline filling does not establish previously unknown IWRAM history. +- Thumb 16 MiB region crossings remain unsupported for bus snapshots. +- ARM-wide local IWRAM history and cross-state latch ownership remain incomplete. +- BIOS history commits after successful BIOS instruction execution; target refills do not establish additional BIOS history. +- All unused-memory data reads within an instruction still use its entry snapshot. +- DMA remains scheduled between whole instructions. Sampling does not add an interleaving point. +- Current-instruction and destination-based nominal timing remain unchanged. +- Per-access device updates, Game Pak prefetch, and exact refill/arbitration timing remain incomplete. + +## Original tests + +CPU tests split the existing internal fetch/execute helpers and mutate lookahead bytes between them. +ARM, narrow Thumb, wide Thumb, and IWRAM tests then require the originally captured values. +Those consumption tests failed with the previous duplicate-read path. +The split is a test-only ownership check, not a hardware DMA experiment. + +Memory tests use explicit expected lane values across mapped regions and both Thumb alignments. +They also check target-pair consumption after mutation, BIOS snapshot retention, strict errors, short ROMs, and data-context isolation. +Tests verify that sampling adds no timing or history effects and that failed execution preserves committed history. +Existing instruction-buffer, bus-history, exception, DMA, and nominal timing tests remain regression coverage. + +## Validation result + +Workspace and core/demo tests pass in debug and release on Darwin arm64. +Formatting, lint checks, rustdoc, preparation tests, native ROM windows, and graphics smoke modes pass. +Public ARM, Thumb, memory, and BIOS reports match the previous Thumb-buffer reports exactly, including nominal cycle counts. +Debug and release reports also match for the same fixture paths. +These checks validate the refactor against existing behavior, not complete hardware bus or timing conformance. diff --git a/docs/research/thumb-bios-prefetch.md b/docs/research/thumb-bios-prefetch.md index d5dc32e..0fb85e0 100644 --- a/docs/research/thumb-bios-prefetch.md +++ b/docs/research/thumb-bios-prefetch.md @@ -40,7 +40,8 @@ Do not treat agreement between all emulators, or exact hardware conformance, as ## Implementation scope -`Memory::begin_cpu_access` samples before execution, using the incoming instruction state and strict mapped bytes. +`Memory::begin_cpu_access` now consumes the pipeline's new fetch sample, using the incoming instruction state. +The [shared sampler](shared-fetch-samples.md) captures strict mapped bytes before execution. `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. diff --git a/docs/research/thumb-instruction-buffer.md b/docs/research/thumb-instruction-buffer.md index cd559c1..ad12df2 100644 --- a/docs/research/thumb-instruction-buffer.md +++ b/docs/research/thumb-instruction-buffer.md @@ -36,7 +36,8 @@ ARM-to-Thumb BX and saved-state exception returns therefore capture T and T+2, n Thumb-to-ARM transitions and SWI/IRQ vectors capture ARM pairs. `Memory::fetch_instruction` uses strict mapped reads with the selected instruction width. -A Thumb fetch reads exactly two bytes, even in a region whose separate bus snapshot uses a full word. +Its instruction result needs only two bytes in Thumb state. +The later [shared fetch sampler](shared-fetch-samples.md) also captures the adjacent halfword for BIOS/OAM bus observations. Memory mirrors apply. Unmapped and misaligned instruction reads retain diagnostics rather than using data open bus. Lookahead errors remain deferred until their slot executes; a branch can discard them. @@ -51,12 +52,12 @@ CPU clones own independent buffers; full CPU equality includes them. Instruction retention and data-bus snapshots remain separate models. Cold instruction-buffer filling does not make unknown IWRAM bus lanes known. Successful DMA can update existing IWRAM bus lanes without changing retained CPU instructions. -Refill bus-history sampling and instruction sampling currently perform separate strict mapped reads. +Refill bus history and instruction buffering now consume the same captured target pair without a second mapped read. Buffer samples add no nominal cycles or data accesses. Timed and untimed execution use the same buffer logic. Destination-based refill summaries and one-shot non-sequential CPU resume costs remain unchanged. -Unified fetch-driven bus history, exact region crossings, per-access timing, Game Pak prefetch, and sub-instruction DMA arbitration remain incomplete. +Complete fetch-driven bus history, exact region crossings, per-access timing, Game Pak prefetch, and sub-instruction DMA arbitration remain incomplete. Instruction-state-changing compare quirks also remain hardware-unverified. ## Original regression coverage diff --git a/docs/status.md b/docs/status.md index 6035d9c..325d6ee 100644 --- a/docs/status.md +++ b/docs/status.md @@ -28,6 +28,7 @@ Update this file when a feature lands or a limit is removed. - Original ARM graphics demo that uses DMA, BIOS VBlank waiting, and a ROM-side IRQ callback. - Sixteen active 32-bit CPU registers, with ARM7 mode-specific register banks. - Persistent ARM/Thumb instruction buffering, sequential fetch retention, branch/vector target pairs, and deferred fetch diagnostics. +- Shared mapped fetch samples for instruction retention, supported open-bus values, BIOS snapshots, and Thumb IWRAM refill history. - User, System, Supervisor, IRQ, FIQ, Abort, and Undefined modes. - Current and saved program status registers (`CPSR`/`SPSR`), with `MRS`/`MSR` transfers. - ARM/Thumb software interrupts (`SWI`), exception vectors, and status-restoring returns. @@ -141,7 +142,8 @@ Cold or invalidated IWRAM history remains diagnostic until all lanes are known. Refills into Thumb IWRAM establish new target-pair history after the instruction succeeds. Accepted machine IRQs, unsupported refills, and other execution states/regions invalidate continuation history. Successful DMA updates existing Thumb IWRAM continuation lanes for actual IWRAM accesses, before the resumed PC+4 sample. -ARM/Thumb instruction buffering retains sequential instructions and refill target pairs, independently of these bus snapshots. +ARM/Thumb instruction buffering retains sequential instructions and refill target pairs. +Instruction buffering and supported bus history now consume the same mapped fetch samples, without sharing retained storage. Complete ARM bus history, exact region-crossing bus behavior, general DMA handoff, and unused/write-only I/O open bus remain incomplete. [DMA channel data latches](hardware/dma.md#retained-channel-data) support blocked-source reuse, but never replace CPU bus history. BIOS protection uses separate retained BIOS fetch snapshots as described in [BIOS behavior](hardware/bios.md#cpu-bios-read-protection). @@ -153,7 +155,7 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa ## Next steps -1. Connect ARM/Thumb instruction fetches to bus history with independent per-access tests. +1. Extend the shared fetch-sample model to verified cross-state bus history and region crossings. General DMA open-bus reads and sub-instruction arbitration remain separate from retained channel data and local IWRAM lanes. 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. diff --git a/docs/testing.md b/docs/testing.md index 3e896ee..1a68f71 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -88,6 +88,10 @@ The display-runner error test allows an already buffered branch to complete befo Thumb instruction-buffer tests verify retained P+2/P+4 halfwords, BL prefix/suffix behavior, and state-tagged target pairs. They also cover stores, host/DMA writes, saved-state returns, deferred errors, mirrors, debugger repair, and nominal timing. Bus-history repair tests explicitly invalidate buffered code or repair a data address; failure checks retain full CPU equality. +Shared fetch-sample tests mutate memory between internal sampling and consumption to reject duplicate reads. +They cover ARM words, Thumb narrow/wide buses, IWRAM lanes, target pairs, BIOS retention, and retry after failure. +This test-only split is not a DMA scheduling feature. Separate tests verify strict fetch errors and no timing/history side effects. +See [shared fetch samples](research/shared-fetch-samples.md) for the unchanged hardware limits. See [ARM instruction buffering](research/arm-instruction-buffer.md) and its [Thumb extension](research/thumb-instruction-buffer.md) for remaining limits. The pinned public ARM ROM now reaches its completion checkpoint with r12 = 0 in debug and release builds. The pinned public Thumb ROM also reaches its ARM-state checkpoint with r7 = 0 in both builds.