feat(cpu): retain IWRAM bus history across execution states

This commit is contained in:
0xrsydn 2026-10-06 14:30:38 +07:00
commit f2c532f8f6
24 changed files with 522 additions and 359 deletions

View file

@ -238,19 +238,16 @@ impl Cpu {
instruction, instruction,
continuation, continuation,
lookahead, lookahead,
fill,
} = fetched; } = fetched;
// Record the executing instruction, not an operand's pipelined PC value. // Record the executing instruction, not an operand's pipelined PC value.
// Always clear the access context, including on diagnostic errors. // Always clear the access context, including on diagnostic errors.
let pc = self.pc(); let pc = self.pc();
let instruction_set = self.instruction_set; let instruction_set = self.instruction_set;
let refill = self.instruction_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, fill.as_ref(), &lookahead);
memory.begin_cpu_access(pc, self.instruction_set, &lookahead);
let result = self.execute_instruction(instruction, memory); let result = self.execute_instruction(instruction, memory);
let sequential = sequential_thumb memory.end_cpu_access(result.is_ok());
&& self.instruction_set == InstructionSet::Thumb
&& self.pc() == pc.wrapping_add(2);
memory.end_cpu_access(result.is_ok(), sequential);
if result.is_ok() { if result.is_ok() {
if refill { if refill {
self.refill_pipeline(memory); self.refill_pipeline(memory);

View file

@ -205,8 +205,8 @@ fn failed_dma_and_failed_resumed_instruction_preserve_last_completed_dma_history
} }
#[test] #[test]
fn partial_continuation_can_gain_known_lanes_but_cold_direct_entry_stays_unknown() { fn dma_updates_retained_lanes_but_a_cold_pipeline_fill_drives_them_again() {
let (mut cpu, mut bus) = program(CODE, 0x46c0); // NOP fills only the low lane. let (mut cpu, mut bus) = program(CODE, 0x46c0); // Cold NOP fills both lanes.
cpu.step(&mut bus).unwrap(); cpu.step(&mut bus).unwrap();
bus.write32(EWRAM, DATA).unwrap(); bus.write32(EWRAM, DATA).unwrap();
configure(&mut bus, 3, EWRAM, IWRAM, 1, true); configure(&mut bus, 3, EWRAM, IWRAM, 1, true);
@ -214,19 +214,17 @@ fn partial_continuation_can_gain_known_lanes_but_cold_direct_entry_stays_unknown
cpu.step(&mut bus).unwrap(); cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[6], resumed(DATA.to_le_bytes(), CODE)); assert_eq!(cpu.registers[6], resumed(DATA.to_le_bytes(), CODE));
// No executed instruction or refill established an expected continuation PC. // DMA establishes the latch, but the later cold fill drives both instruction lanes.
let (mut cpu, mut bus) = program(CODE, PROBE); let (mut cpu, mut bus) = program(CODE, PROBE);
bus.write32(EWRAM, DATA).unwrap(); bus.write32(EWRAM, DATA).unwrap();
configure(&mut bus, 3, EWRAM, IWRAM, 1, true); configure(&mut bus, 3, EWRAM, IWRAM, 1, true);
bus.step_dma().unwrap().unwrap(); bus.step_dma().unwrap().unwrap();
assert_eq!( cpu.step(&mut bus).unwrap();
cpu.step(&mut bus), assert_eq!(cpu.registers[6], 0x6806_4321);
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
} }
#[test] #[test]
fn irq_and_pc_discontinuity_do_not_reuse_dma_updated_continuations() { fn irq_preserves_dma_history_while_pc_discontinuity_performs_a_new_fill() {
for irq in [false, true] { for irq in [false, true] {
let mut machine = ready(CODE); let mut machine = ready(CODE);
machine.memory_mut().write32(EWRAM, DATA).unwrap(); machine.memory_mut().write32(EWRAM, DATA).unwrap();
@ -245,9 +243,14 @@ fn irq_and_pc_discontinuity_do_not_reuse_dma_updated_continuations() {
cpu.registers[15] = CODE + 0x100; cpu.registers[15] = CODE + 0x100;
machine.memory_mut().write16(cpu.pc(), PROBE).unwrap(); machine.memory_mut().write16(cpu.pc(), PROBE).unwrap();
} }
cpu.step(machine.memory_mut()).unwrap();
assert_eq!( assert_eq!(
cpu.step(machine.memory_mut()), cpu.registers[6],
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED))) if irq {
resumed(DATA.to_le_bytes(), CODE)
} else {
0
}
); );
} }
} }
@ -257,7 +260,7 @@ fn arm_and_other_thumb_regions_keep_their_fetch_snapshot_not_dma_data() {
for thumb in [false, true] { for thumb in [false, true] {
for pc in [0x0200_0100, 0x0300_0100, 0x0700_0100] { for pc in [0x0200_0100, 0x0300_0100, 0x0700_0100] {
if thumb && pc >> 24 == 3 { if thumb && pc >> 24 == 3 {
continue; // The bounded IWRAM continuation is tested separately. continue; // Persistent IWRAM lanes are tested separately.
} }
let mut bus = Memory::new(vec![]).unwrap(); let mut bus = Memory::new(vec![]).unwrap();
let mut cpu = Cpu::new(pc); let mut cpu = Cpu::new(pc);

View file

@ -249,17 +249,18 @@ fn block_transfers_leave_the_last_iwram_word_and_keep_writeback() {
} }
#[test] #[test]
fn cold_history_is_unknown_but_successful_fetches_or_word_accesses_can_establish_it() { fn cold_pipeline_fills_establish_history_before_the_first_thumb_data_read() {
for pc in [CODE, CODE + 2] { for pc in [CODE, CODE + 2] {
let (mut cpu, mut bus) = program(pc, &[PROBE, PROBE, 0x1111, 0x2222]); let (mut cpu, mut bus) = program(pc, &[PROBE, PROBE, 0x1111, 0x2222]);
let before = cpu.clone();
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
assert_eq!(cpu, before);
bus.write16(pc, NOP).unwrap();
cpu.step(&mut bus).unwrap(); cpu.step(&mut bus).unwrap();
assert_eq!(
cpu.registers[6],
if pc & 2 == 0 {
0x6806_1111
} else {
0x1111_6806
}
);
cpu.step(&mut bus).unwrap(); cpu.step(&mut bus).unwrap();
assert_eq!( assert_eq!(
cpu.registers[6], cpu.registers[6],
@ -288,7 +289,12 @@ fn failed_steps_discard_prefetch_history_before_retry() {
cpu.step(&mut bus).unwrap(); cpu.step(&mut bus).unwrap();
assert_eq!( assert_eq!(
cpu.registers[6], cpu.registers[6],
drive(DATA, pc + 6, &0x9bcd_u16.to_le_bytes()) // Debugger invalidation starts a fresh fill: PC+2 is now also fetched.
if pc & 2 == 0 {
0x9bcd_1111
} else {
0x1111_9bcd
}
); );
assert_eq!(bus.cycles(), 0); assert_eq!(bus.cycles(), 0);
} }
@ -319,23 +325,19 @@ fn taken_control_flow_refills_history_even_when_target_is_fallthrough() {
} }
#[test] #[test]
fn pc_discontinuities_and_non_iwram_execution_do_not_reuse_old_history() { fn pc_discontinuities_fill_from_actual_target_bytes_not_stale_instruction_slots() {
let (mut cpu, mut bus) = program(CODE, &[SEED, PROBE, 0x1111, 0x2222]); let (mut cpu, mut bus) = program(CODE, &[SEED, PROBE, 0x1111, 0x2222]);
cpu.step(&mut bus).unwrap(); cpu.step(&mut bus).unwrap();
bus.write16(CODE + 0x100, PROBE).unwrap(); bus.write16(CODE + 0x100, PROBE).unwrap();
cpu.registers[15] = CODE + 0x100; cpu.registers[15] = CODE + 0x100;
assert_eq!( cpu.step(&mut bus).unwrap();
cpu.step(&mut bus), assert_eq!(cpu.registers[6], 0); // New target's next two halfwords are zero.
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
bus.write16(0x0200_0000, NOP).unwrap(); bus.write16(0x0200_0000, NOP).unwrap();
cpu.registers[15] = 0x0200_0000; cpu.registers[15] = 0x0200_0000;
cpu.step(&mut bus).unwrap(); cpu.step(&mut bus).unwrap();
cpu.registers[15] = CODE + 2; cpu.registers[15] = CODE + 2;
assert_eq!( cpu.step(&mut bus).unwrap();
cpu.step(&mut bus), assert_eq!(cpu.registers[6], 0x2222_1111);
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
} }
#[test] #[test]
@ -399,7 +401,7 @@ fn history_updates_add_no_data_accesses_or_device_cycles() {
} }
#[test] #[test]
fn accepted_irq_invalidates_history_before_any_handler_instruction() { fn accepted_irq_preserves_local_history_before_any_handler_iwram_access() {
let (cpu, bus) = program(CODE, &[SEED, PROBE, 0x1111, 0x9bcd]); let (cpu, bus) = program(CODE, &[SEED, PROBE, 0x1111, 0x9bcd]);
let mut machine = Machine::new(cpu, bus); let mut machine = Machine::new(cpu, bus);
machine.step().unwrap(); machine.step().unwrap();
@ -413,8 +415,6 @@ fn accepted_irq_invalidates_history_before_any_handler_instruction() {
machine.memory_mut().advance_cycles(1); machine.memory_mut().advance_cycles(1);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry); assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
// Probe the paused instruction with a separate CPU, without executing BIOS. // Probe the paused instruction with a separate CPU, without executing BIOS.
assert_eq!( interrupted.step(machine.memory_mut()).unwrap();
interrupted.step(machine.memory_mut()), assert_eq!(interrupted.registers[6], 0x9bcd_80a5);
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
} }

View file

@ -319,16 +319,14 @@ fn failed_refill_instructions_preserve_cpu_and_preexisting_history() {
} }
#[test] #[test]
fn cold_entry_region_crossings_and_unmapped_targets_do_not_invent_refill_history() { fn cold_fill_establishes_lanes_but_region_crossings_and_unmapped_targets_stay_diagnostic() {
let mut bus = Memory::new(vec![]).unwrap(); let mut bus = Memory::new(vec![]).unwrap();
target(&mut bus, TARGET, PROBE); target(&mut bus, TARGET, PROBE);
let mut cpu = Cpu::new(TARGET); let mut cpu = Cpu::new(TARGET);
cpu.instruction_set = InstructionSet::Thumb; cpu.instruction_set = InstructionSet::Thumb;
cpu.registers[0] = UNUSED; cpu.registers[0] = UNUSED;
assert_eq!( cpu.step(&mut bus).unwrap();
cpu.step(&mut bus), assert_eq!(cpu.registers[6], expected(TARGET));
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
for destination in [0x03ff_fffc, 0x03ff_fffe, 0x0e00_0000] { for destination in [0x03ff_fffc, 0x03ff_fffe, 0x0e00_0000] {
let mut bus = Memory::new(0xe12f_ff14_u32.to_le_bytes().to_vec()).unwrap(); let mut bus = Memory::new(0xe12f_ff14_u32.to_le_bytes().to_vec()).unwrap();

View file

@ -247,12 +247,8 @@ fn cold_iwram_thumb_reads_and_unused_fetches_do_not_reuse_arm_context() {
bus.write16(0x0300_0100, 0x6801).unwrap(); // IWRAM Thumb LDR r1,[r0] bus.write16(0x0300_0100, 0x6801).unwrap(); // IWRAM Thumb LDR r1,[r0]
cpu.registers[15] = 0x0300_0100; cpu.registers[15] = 0x0300_0100;
cpu.instruction_set = InstructionSet::Thumb; cpu.instruction_set = InstructionSet::Thumb;
let before = cpu.clone(); cpu.step(&mut bus).unwrap();
assert_eq!( assert_eq!(cpu.registers[1], 0); // Actual cold-fill lanes, not the prior ARM snapshot.
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
assert_eq!(cpu, before);
} }
#[test] #[test]

View file

@ -15,6 +15,7 @@ pub(super) struct Fetched {
pub instruction: u32, pub instruction: u32,
pub continuation: Pipeline, pub continuation: Pipeline,
pub lookahead: InstructionFetch, pub lookahead: InstructionFetch,
pub fill: Option<[InstructionFetch; 2]>,
} }
impl Cpu { impl Cpu {
@ -33,17 +34,17 @@ impl Cpu {
.pipeline .pipeline
.as_ref() .as_ref()
.filter(|pipe| pipe.pc == pc && pipe.instruction_set == instruction_set); .filter(|pipe| pipe.pc == pc && pipe.instruction_set == instruction_set);
let instruction = match retained { let (instruction, decode, fill) = if let Some(pipe) = retained {
Some(pipe) => pipe.instructions[0].clone(), (
None => memory.fetch_instruction(pc, instruction_set).instruction, pipe.instructions[0].clone()?,
}?; pipe.instructions[1].clone(),
let decode = match retained { None,
Some(pipe) => pipe.instructions[1].clone(), )
None => { } else {
memory let current = memory.fetch_instruction(pc, instruction_set);
.fetch_instruction(pc.wrapping_add(width), instruction_set) let instruction = current.instruction.clone()?;
.instruction let next = memory.fetch_instruction(pc.wrapping_add(width), instruction_set);
} (instruction, next.instruction.clone(), Some([current, next]))
}; };
let fetched = memory.fetch_instruction(pc.wrapping_add(2 * width), instruction_set); let fetched = memory.fetch_instruction(pc.wrapping_add(2 * width), instruction_set);
Ok(Fetched { Ok(Fetched {
@ -54,6 +55,7 @@ impl Cpu {
instructions: [decode, fetched.instruction.clone()], instructions: [decode, fetched.instruction.clone()],
}, },
lookahead: fetched, lookahead: fetched,
fill,
}) })
} }

View file

@ -242,20 +242,18 @@ fn failed_thumb_steps_keep_previous_history_and_clear_access_context() {
} }
#[test] #[test]
fn outside_thumb_execution_keeps_bios_history_without_seeding_iwram_history() { fn outside_thumb_execution_keeps_bios_history_separate_from_cold_iwram_fills() {
let mut memory = bus(0x6801, thumb_image(THUMB_ENTRY, BX_R5)); let mut memory = bus(0x6801, thumb_image(THUMB_ENTRY, BX_R5));
let mut cpu = enter_thumb(&mut memory, THUMB_ENTRY, true); let mut cpu = enter_thumb(&mut memory, THUMB_ENTRY, true);
cpu.step(&mut memory).unwrap(); cpu.step(&mut memory).unwrap();
assert_eq!(protected_word(&mut memory), OTHER); assert_eq!(protected_word(&mut memory), OTHER);
memory.write16(0x0300_0100, 0x6801).unwrap(); // No preceding sequential IWRAM history. memory.write16(0x0300_0100, 0x6801).unwrap(); // Cold IWRAM entry.
memory.write16(0x0300_0102, 0x6801).unwrap(); // Next protected BIOS read.
memory.write16(0x0300_0104, 0x1357).unwrap();
cpu.registers[15] = 0x0300_0100; cpu.registers[15] = 0x0300_0100;
cpu.registers[0] = 0x4000; cpu.registers[0] = 0x4000;
let before = cpu.clone(); cpu.step(&mut memory).unwrap();
assert_eq!( assert_eq!(cpu.registers[1], 0x6801_1357);
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unmapped(0x4000)))
);
assert_eq!(cpu, before);
assert_eq!(memory.read32(0x4000), Err(MemoryError::Unmapped(0x4000))); assert_eq!(memory.read32(0x4000), Err(MemoryError::Unmapped(0x4000)));
assert_eq!(protected_word(&mut memory), OTHER); assert_eq!(protected_word(&mut memory), OTHER);
cpu.registers[0] = 0; cpu.registers[0] = 0;

View file

@ -294,12 +294,21 @@ fn short_rom_needs_only_the_fetched_halfword_and_errors_only_when_used() {
} }
#[test] #[test]
fn cold_iwram_history_and_region_crossing_fetches_remain_diagnostics() { fn cold_iwram_fills_establish_lanes_but_region_crossing_fetches_remain_diagnostics() {
for base in [0x0300_0100, 0x0300_8100, 0x02ff_fffc, 0x05ff_fffc] { for base in [0x0300_0100, 0x0300_8100, 0x02ff_fffc, 0x05ff_fffc] {
for offset in [0, 2] { for offset in [0, 2] {
let pc = base + offset; let pc = base + offset;
let mut bus = program(pc, LDR); let mut bus = program(pc, LDR);
let mut cpu = cpu(pc); let mut cpu = cpu(pc);
if pc >> 24 == 3 {
cpu.step(&mut bus).unwrap();
assert_eq!(
cpu.registers[1],
if offset == 0 { 0x46c0_80a5 } else { DATA }
);
assert_eq!(bus.read32(UNUSED), Err(MemoryError::Unmapped(UNUSED)));
continue;
}
let before = cpu.clone(); let before = cpu.clone();
assert_eq!( assert_eq!(
cpu.step(&mut bus), cpu.step(&mut bus),

View file

@ -4,6 +4,8 @@ mod fetch;
#[cfg(test)] #[cfg(test)]
mod fetch_tests; mod fetch_tests;
mod iwram_bus; mod iwram_bus;
#[cfg(test)]
mod iwram_history_tests;
pub(crate) use fetch::InstructionFetch; pub(crate) use fetch::InstructionFetch;
use iwram_bus::IwramBus; use iwram_bus::IwramBus;
@ -69,9 +71,9 @@ struct CpuAccess {
/// Work/video/palette RAM, OAM, cartridge ROM, optional BIOS, and supported I/O. /// Work/video/palette RAM, OAM, cartridge ROM, optional BIOS, and supported I/O.
/// Unused-memory reads use ARM PC+8 or supported region-dependent Thumb snapshots. /// Unused-memory reads use ARM PC+8 or supported region-dependent Thumb snapshots.
/// Protected BIOS reads separately retain the snapshot from BIOS execution. /// Protected BIOS reads separately retain the snapshot from BIOS execution.
/// Sequential Thumb IWRAM accesses retain addressed bus lanes transactionally. /// ARM/Thumb IWRAM fetches and data accesses retain local bus lanes transactionally.
/// Successful refills into Thumb IWRAM sample the target pair to establish history. /// Cold fills and target pairs drive the same latch, independently of executing region.
/// DMA accesses drive existing Thumb IWRAM continuation lanes before the next fetch. /// Successful DMA accesses drive local lanes even before the first CPU instruction.
/// Channel data remains separate; general and sub-instruction CPU bus handoff is unmodeled. /// Channel data remains separate; general and sub-instruction CPU bus handoff is unmodeled.
/// Instruction buffering and supported bus history consume shared fetch samples. /// Instruction buffering and supported bus history consume shared fetch samples.
/// Complete bus ownership and per-access timing remain unmodeled. /// Complete bus ownership and per-access timing remain unmodeled.
@ -374,19 +376,27 @@ impl Memory {
&mut self, &mut self,
pc: u32, pc: u32,
instruction_set: InstructionSet, instruction_set: InstructionSet,
fill: Option<&[InstructionFetch; 2]>,
fetch: &InstructionFetch, fetch: &InstructionFetch,
) { ) {
debug_assert!(self.cpu_access.is_none()); debug_assert!(self.cpu_access.is_none());
debug_assert_eq!(fetch.state, instruction_set); debug_assert_eq!(fetch.state, instruction_set);
debug_assert_eq!(fetch.address, pc.wrapping_add(2 * instruction_set.width())); 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. // Cold fills drive current/decode fetches; retained instructions never replay them.
// Missing lookahead must not fail an instruction that never uses it. // Stage the new lookahead last, before data accesses and the entry snapshot.
self.iwram_bus.begin();
if let Some(fill) = fill {
for (index, sample) in fill.iter().enumerate() {
debug_assert_eq!(sample.state, instruction_set);
debug_assert_eq!(
sample.address,
pc.wrapping_add(index as u32 * instruction_set.width())
);
self.drive_iwram_fetch(sample);
}
}
self.drive_iwram_fetch(fetch);
let same_region = fetch.address >> 24 == pc >> 24; let same_region = fetch.address >> 24 == pc >> 24;
let iwram_pc = (instruction_set == InstructionSet::Thumb && pc >> 24 == 3).then_some(pc);
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 { let prefetch = match instruction_set {
InstructionSet::Arm => fetch.bus_word, InstructionSet::Arm => fetch.bus_word,
InstructionSet::Thumb if !same_region => None, InstructionSet::Thumb if !same_region => None,
@ -396,12 +406,12 @@ impl Memory {
self.cpu_access = Some(CpuAccess { pc, prefetch }); self.cpu_access = Some(CpuAccess { pc, prefetch });
} }
pub(crate) fn end_cpu_access(&mut self, succeeded: bool, sequential: bool) { pub(crate) fn end_cpu_access(&mut self, succeeded: bool) {
if succeeded { if succeeded {
// Timed and untimed CPU execution both consume a resume. Errors do not. // Timed and untimed CPU execution both consume a resume. Errors do not.
self.cpu_resume_nonsequential = false; self.cpu_resume_nonsequential = false;
} }
self.iwram_bus.finish(succeeded, sequential); self.iwram_bus.finish(succeeded);
if let Some(access) = self.cpu_access.take() { if let Some(access) = self.cpu_access.take() {
if succeeded && access.pc < BIOS_SIZE as u32 { if succeeded && access.pc < BIOS_SIZE as u32 {
// Commit only successful instruction snapshots under our diagnostic policy. // Commit only successful instruction snapshots under our diagnostic policy.
@ -411,25 +421,30 @@ impl Memory {
} }
} }
/// Consume the same target samples that fill the CPU's instruction buffer. fn drive_iwram_fetch(&self, fetch: &InstructionFetch) {
/// Incomplete or unsupported refills stay unknown without failing the branch early. if let Ok(value) = fetch.instruction {
let width = match fetch.state {
InstructionSet::Arm => AccessWidth::Word,
InstructionSet::Thumb => AccessWidth::Halfword,
};
self.iwram_bus.access(fetch.address, width, value);
}
}
/// Successful IWRAM target fetches drive their lanes in order, in either CPU state.
/// Other-region and unavailable target fetches leave the local latch unchanged.
pub(crate) fn refill_cpu_bus_history(&mut self, fetches: &[InstructionFetch; 2]) { pub(crate) fn refill_cpu_bus_history(&mut self, fetches: &[InstructionFetch; 2]) {
self.iwram_bus.invalidate(); self.iwram_bus.begin();
let [first, second] = fetches; let [first, second] = fetches;
debug_assert_eq!(first.state, second.state); debug_assert_eq!(first.state, second.state);
debug_assert_eq!( debug_assert_eq!(
second.address, second.address,
first.address.wrapping_add(first.state.width()) first.address.wrapping_add(first.state.width())
); );
if first.state == InstructionSet::Thumb for fetch in fetches {
&& first.address >> 24 == 3 self.drive_iwram_fetch(fetch);
&& second.address >> 24 == 3
{
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]);
}
} }
self.iwram_bus.finish(true);
} }
fn can_write_power_control(&self) -> bool { fn can_write_power_control(&self) -> bool {

View file

@ -91,7 +91,7 @@ fn refill_history_consumes_captured_target_halfwords_without_rereading_memory()
memory.write16(target + 2, 0xffff).unwrap(); memory.write16(target + 2, 0xffff).unwrap();
memory.refill_cpu_bus_history(&fetches); memory.refill_cpu_bus_history(&fetches);
let lookahead = memory.fetch_instruction(target + 4, InstructionSet::Thumb); let lookahead = memory.fetch_instruction(target + 4, InstructionSet::Thumb);
memory.begin_cpu_access(target, InstructionSet::Thumb, &lookahead); memory.begin_cpu_access(target, InstructionSet::Thumb, None, &lookahead);
assert_eq!( assert_eq!(
memory.read32(UNUSED).unwrap(), memory.read32(UNUSED).unwrap(),
if target & 2 == 0 { if target & 2 == 0 {
@ -100,7 +100,7 @@ fn refill_history_consumes_captured_target_halfwords_without_rereading_memory()
0x5678_9abc 0x5678_9abc
} }
); );
memory.end_cpu_access(true, true); memory.end_cpu_access(true);
} }
} }
@ -119,11 +119,11 @@ fn bios_history_commits_the_captured_bus_word_not_later_image_bytes() {
// Test-only image mutation: BIOS remains read-only through normal APIs. // Test-only image mutation: BIOS remains read-only through normal APIs.
memory.bios.as_mut().unwrap()[word_address..word_address + 4] memory.bios.as_mut().unwrap()[word_address..word_address + 4]
.copy_from_slice(&CHANGED.to_le_bytes()); .copy_from_slice(&CHANGED.to_le_bytes());
memory.begin_cpu_access(pc, state, &fetch); memory.begin_cpu_access(pc, state, None, &fetch);
memory.end_cpu_access(true, false); memory.end_cpu_access(true);
assert_eq!(memory.bios_prefetch, Some(WORD)); assert_eq!(memory.bios_prefetch, Some(WORD));
let outside = memory.fetch_instruction(0x0200_0008, InstructionSet::Arm); let outside = memory.fetch_instruction(0x0200_0008, InstructionSet::Arm);
memory.begin_cpu_access(0x0200_0000, InstructionSet::Arm, &outside); memory.begin_cpu_access(0x0200_0000, InstructionSet::Arm, None, &outside);
assert_eq!(memory.read32(word_address as u32).unwrap(), WORD); assert_eq!(memory.read32(word_address as u32).unwrap(), WORD);
// Instruction reads still bypass the protected data value. // Instruction reads still bypass the protected data value.
assert_eq!( assert_eq!(
@ -132,7 +132,7 @@ fn bios_history_commits_the_captured_bus_word_not_later_image_bytes() {
.instruction, .instruction,
Ok(CHANGED) Ok(CHANGED)
); );
memory.end_cpu_access(true, false); memory.end_cpu_access(true);
} }
} }
@ -149,12 +149,12 @@ fn sampling_has_no_timing_or_history_side_effects_and_never_uses_data_fallback()
assert_eq!(memory.bios_prefetch, None); assert_eq!(memory.bios_prefetch, None);
assert!(memory.cpu_resume_nonsequential); assert!(memory.cpu_resume_nonsequential);
assert_eq!(memory.cycles(), 0); assert_eq!(memory.cycles(), 0);
memory.begin_cpu_access(0, InstructionSet::Arm, &fetch); memory.begin_cpu_access(0, InstructionSet::Arm, None, &fetch);
assert_eq!(memory.read32(UNUSED).unwrap(), WORD); assert_eq!(memory.read32(UNUSED).unwrap(), WORD);
let missing = memory.fetch_instruction(UNUSED, InstructionSet::Arm); let missing = memory.fetch_instruction(UNUSED, InstructionSet::Arm);
assert_eq!(missing.instruction, Err(MemoryError::Unmapped(UNUSED))); assert_eq!(missing.instruction, Err(MemoryError::Unmapped(UNUSED)));
assert_eq!(missing.bus_word, None); assert_eq!(missing.bus_word, None);
memory.end_cpu_access(false, false); memory.end_cpu_access(false);
assert_eq!(memory.bios_prefetch, None); assert_eq!(memory.bios_prefetch, None);
assert!(memory.cpu_resume_nonsequential); assert!(memory.cpu_resume_nonsequential);
} }
@ -167,12 +167,12 @@ fn missing_fetch_observation_invalidates_bios_history_only_on_success() {
let pc = BIOS_SIZE as u32 - state.width(); let pc = BIOS_SIZE as u32 - state.width();
let fetch = memory.fetch_instruction(pc + 2 * state.width(), state); let fetch = memory.fetch_instruction(pc + 2 * state.width(), state);
assert!(fetch.instruction.is_err()); assert!(fetch.instruction.is_err());
memory.begin_cpu_access(pc, state, &fetch); memory.begin_cpu_access(pc, state, None, &fetch);
memory.end_cpu_access(false, false); memory.end_cpu_access(false);
assert_eq!(memory.bios_prefetch, Some(WORD)); assert_eq!(memory.bios_prefetch, Some(WORD));
} }
let fetch = memory.fetch_instruction(BIOS_SIZE as u32, InstructionSet::Thumb); let fetch = memory.fetch_instruction(BIOS_SIZE as u32, InstructionSet::Thumb);
memory.begin_cpu_access(BIOS_SIZE as u32 - 4, InstructionSet::Thumb, &fetch); memory.begin_cpu_access(BIOS_SIZE as u32 - 4, InstructionSet::Thumb, None, &fetch);
memory.end_cpu_access(true, false); memory.end_cpu_access(true);
assert_eq!(memory.bios_prefetch, None); assert_eq!(memory.bios_prefetch, None);
} }

View file

@ -1,6 +1,5 @@
//! Transactional lane history for Thumb IWRAM execution and target-pair refills. //! Persistent local IWRAM lanes, independent of CPU state and executing region.
//! Completed DMA accesses update existing continuation lanes before the next fetch. //! CPU instructions stage changes; successful DMA units commit accesses in order.
//! Unknown startup history is not replaced with guessed memory bytes.
use std::cell::Cell; use std::cell::Cell;
use crate::timing::AccessWidth; use crate::timing::AccessWidth;
@ -13,6 +12,9 @@ struct Lanes {
impl Lanes { impl Lanes {
fn drive(&mut self, address: u32, width: AccessWidth, value: u32) { fn drive(&mut self, address: u32, width: AccessWidth, value: u32) {
if address >> 24 != 3 {
return;
}
debug_assert_eq!(address & (width.bytes() - 1), 0); debug_assert_eq!(address & (width.bytes() - 1), 0);
let mask = match width { let mask = match width {
AccessWidth::Byte => 0xff, AccessWidth::Byte => 0xff,
@ -30,93 +32,47 @@ impl Lanes {
} }
} }
#[derive(Clone, Copy)]
struct History {
next_pc: u32,
lanes: Lanes,
}
#[derive(Default)] #[derive(Default)]
pub(super) struct IwramBus { pub(super) struct IwramBus {
committed: Option<History>, committed: Lanes,
// Reads use &Memory. Stage their lane changes without changing committed history. // Reads use &Memory. Stage their lane changes without changing committed history.
pending: Cell<Option<History>>, pending: Cell<Option<Lanes>>,
} }
impl IwramBus { impl IwramBus {
pub(super) fn begin(&mut self, pc: Option<u32>, fetched: Option<u16>) { #[cfg(test)]
pub(super) fn committed_word(&self) -> Option<u32> {
self.committed.word()
}
pub(super) fn begin(&mut self) {
debug_assert!(self.pending.get().is_none()); debug_assert!(self.pending.get().is_none());
self.pending.set(pc.map(|pc| { self.pending.set(Some(self.committed));
let mut lanes = self
.committed
.filter(|history| history.next_pc == pc)
.map_or_else(Lanes::default, |history| history.lanes);
if let Some(half) = fetched {
lanes.drive(pc + 4, AccessWidth::Halfword, u32::from(half));
} else {
lanes = Lanes::default(); // Unavailable or region-crossing prefetch.
}
History {
next_pc: pc.wrapping_add(2),
lanes,
}
}));
} }
pub(super) fn snapshot(&self) -> Option<u32> { pub(super) fn snapshot(&self) -> Option<u32> {
self.pending.get().and_then(|history| history.lanes.word()) self.pending.get().and_then(Lanes::word)
} }
pub(super) fn access(&self, address: u32, width: AccessWidth, value: u32) { pub(super) fn access(&self, address: u32, width: AccessWidth, value: u32) {
if address >> 24 != 3 { if let Some(mut lanes) = self.pending.get() {
return; lanes.drive(address, width, value);
} self.pending.set(Some(lanes));
if let Some(mut history) = self.pending.get() {
history.lanes.drive(address, width, value);
self.pending.set(Some(history));
} }
} }
/// DMA runs between instructions. Commit only after the entire unit succeeds. /// DMA runs between instructions. Call only after the entire unit succeeds.
/// Keep the expected CPU continuation PC; do not invent one at cold startup.
pub(super) fn dma_access(&mut self, address: u32, width: AccessWidth, value: u32) { pub(super) fn dma_access(&mut self, address: u32, width: AccessWidth, value: u32) {
debug_assert!(self.pending.get().is_none()); debug_assert!(self.pending.get().is_none());
if address >> 24 == 3 { self.committed.drive(address, width, value);
if let Some(history) = &mut self.committed {
history.lanes.drive(address, width, value);
}
}
} }
pub(super) fn finish(&mut self, succeeded: bool, sequential: bool) { pub(super) fn finish(&mut self, succeeded: bool) {
let pending = self.pending.take(); let pending = self.pending.take();
if succeeded { if succeeded {
self.committed = if sequential { self.committed = pending.expect("IWRAM transaction must have started");
pending.filter(|history| history.next_pc >> 24 == 3)
} else {
None
};
} }
} }
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;
}
} }
#[cfg(test)] #[cfg(test)]
@ -126,31 +82,36 @@ mod tests {
#[test] #[test]
fn incomplete_lanes_stay_unknown_until_all_bytes_have_been_driven() { fn incomplete_lanes_stay_unknown_until_all_bytes_have_been_driven() {
let mut bus = IwramBus::default(); let mut bus = IwramBus::default();
bus.begin(Some(0x0300_0100), Some(0x1234)); bus.begin();
bus.access(0x0300_0104, AccessWidth::Halfword, 0x1234);
assert_eq!(bus.snapshot(), None); assert_eq!(bus.snapshot(), None);
bus.access(0x0300_2002, AccessWidth::Byte, 0x56); bus.access(0x0300_2002, AccessWidth::Byte, 0x56);
assert_eq!(bus.snapshot(), None); assert_eq!(bus.snapshot(), None);
bus.access(0x0200_2003, AccessWidth::Byte, 0xff); // Other memory cannot fill a lane. bus.access(0x0200_2003, AccessWidth::Byte, 0xff);
assert_eq!(bus.snapshot(), None); assert_eq!(bus.snapshot(), None);
bus.access(0x0300_2003, AccessWidth::Byte, 0x78); bus.access(0x0300_2003, AccessWidth::Byte, 0x78);
assert_eq!(bus.snapshot(), Some(0x7856_1234)); assert_eq!(bus.snapshot(), Some(0x7856_1234));
bus.finish(true, true); bus.finish(true);
bus.begin(Some(0x0300_0102), Some(0xabcd)); bus.begin();
bus.access(0x0300_0106, AccessWidth::Halfword, 0xabcd);
assert_eq!(bus.snapshot(), Some(0xabcd_1234)); assert_eq!(bus.snapshot(), Some(0xabcd_1234));
} }
#[test] #[test]
fn failed_transaction_discards_all_staged_accesses_not_only_prefetch() { fn failed_transaction_discards_all_staged_accesses_not_only_prefetch() {
let mut bus = IwramBus::default(); let mut bus = IwramBus::default();
bus.begin(Some(0x0300_0100), Some(0x1234)); bus.begin();
bus.access(0x0300_0104, AccessWidth::Halfword, 0x1234);
bus.access(0x0300_2000, AccessWidth::Word, 0x1122_3344); bus.access(0x0300_2000, AccessWidth::Word, 0x1122_3344);
bus.finish(true, true); bus.finish(true);
bus.begin(Some(0x0300_0102), Some(0xabcd)); bus.begin();
bus.access(0x0300_0106, AccessWidth::Halfword, 0xabcd);
bus.access(0x0300_2000, AccessWidth::Word, 0xdead_beef); bus.access(0x0300_2000, AccessWidth::Word, 0xdead_beef);
bus.access(0x0300_2001, AccessWidth::Byte, 0x55); bus.access(0x0300_2001, AccessWidth::Byte, 0x55);
assert_eq!(bus.snapshot(), Some(0xdead_55ef)); assert_eq!(bus.snapshot(), Some(0xdead_55ef));
bus.finish(false, false); bus.finish(false);
bus.begin(Some(0x0300_0102), Some(0x5678)); bus.begin();
bus.access(0x0300_0106, AccessWidth::Halfword, 0x5678);
assert_eq!(bus.snapshot(), Some(0x5678_3344)); assert_eq!(bus.snapshot(), Some(0x5678_3344));
} }
} }

View file

@ -0,0 +1,192 @@
//! Original local-bus tests. Expected lanes are independent of the CPU instruction buffer.
use super::*;
use crate::{
cpu::Cpu,
dma::DMA_STRIDE,
io::{IE, IME, TIMER_BASE},
machine::{Machine, StepKind},
};
const CODE: u32 = 0x0200_0100;
const IWRAM: u32 = 0x0300_0000;
const WORD: u32 = 0x9abc_1234;
const NOP: u32 = 0xe1a0_0000;
fn arm(code: &[u32]) -> (Cpu, Memory) {
let mut memory = Memory::new(vec![]).unwrap();
for (i, word) in code.iter().enumerate() {
memory.write32(CODE + i as u32 * 4, *word).unwrap();
}
memory.write32(IWRAM, WORD).unwrap();
(Cpu::new(CODE), memory)
}
#[test]
fn arm_data_accesses_from_other_code_regions_drive_only_addressed_iwram_lanes() {
for (instruction, expected) in [
(0xe5d0_1000, None), // LDRB low byte, not a complete word.
(0xe1d0_10b2, None), // LDRH high half, not a complete word.
(0xe590_1000, Some(WORD)), // LDR word.
] {
let (mut cpu, mut memory) = arm(&[0xe3a0_0403, instruction, NOP, NOP]); // MOV r0,#0x03000000.
cpu.step(&mut memory).unwrap();
assert_eq!(memory.iwram_bus.committed_word(), None);
cpu.step(&mut memory).unwrap();
assert_eq!(memory.iwram_bus.committed_word(), expected);
cpu.step(&mut memory).unwrap();
assert_eq!(memory.iwram_bus.committed_word(), expected); // EWRAM code does not erase the local latch.
}
}
#[test]
fn arm_byte_and_halfword_reads_and_stores_preserve_other_lanes_across_nonlocal_code() {
for (instruction, expected) in [
(0xe5d0_1001, 0x9abc_5634), // LDRB r1,[r0,#1].
(0xe1d0_10b2, 0x5678_1234), // LDRH r1,[r0,#2].
(0xe5c0_2001, 0x9abc_0034), // STRB r2,[r0,#1], r2=0.
(0xe1c0_20b2, 0x0000_1234), // STRH r2,[r0,#2].
] {
let (mut cpu, mut memory) = arm(&[0xe3a0_0403, 0xe590_1000, NOP, instruction, NOP, NOP]);
cpu.step(&mut memory).unwrap();
cpu.step(&mut memory).unwrap();
memory.write32(IWRAM, 0x5678_5678).unwrap(); // Host writes do not drive the latch.
cpu.step(&mut memory).unwrap();
assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));
cpu.step(&mut memory).unwrap();
assert_eq!(memory.iwram_bus.committed_word(), Some(expected));
}
}
#[test]
fn arm_and_thumb_target_pairs_drive_the_same_local_bus_in_fetch_order() {
// EWRAM ARM -> IWRAM ARM -> IWRAM Thumb -> unavailable BIOS ARM.
let (mut cpu, mut memory) = arm(&[0xe3a0_0403, 0xe12f_ff10, NOP, NOP]);
memory.write32(IWRAM, 0xe280_1011).unwrap(); // ADD r1,r0,#0x11.
memory.write32(IWRAM + 4, 0xe12f_ff11).unwrap(); // BX r1.
memory.write32(IWRAM + 8, WORD).unwrap();
memory.write32(IWRAM + 12, 0x8765_4321).unwrap();
memory.write16(IWRAM + 16, 0x46c0).unwrap(); // Thumb NOP.
memory.write16(IWRAM + 18, 0x4710).unwrap(); // BX r2 (zero -> BIOS; target error deferred).
memory.write32(IWRAM + 20, 0x5678_1234).unwrap();
cpu.step(&mut memory).unwrap();
cpu.step(&mut memory).unwrap();
assert_eq!(memory.iwram_bus.committed_word(), Some(0xe12f_ff11));
cpu.step(&mut memory).unwrap();
assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
assert_eq!(memory.iwram_bus.committed_word(), Some(0x4710_46c0));
cpu.step(&mut memory).unwrap();
assert_eq!(memory.iwram_bus.committed_word(), Some(0x4710_1234));
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
assert_eq!(memory.iwram_bus.committed_word(), Some(0x5678_1234));
let before = cpu.clone();
assert!(cpu.step(&mut memory).is_err());
assert_eq!(cpu, before);
assert_eq!(memory.iwram_bus.committed_word(), Some(0x5678_1234));
}
#[test]
fn thumb_data_access_from_ewram_drives_local_iwram_history() {
let (mut cpu, mut memory) = arm(&[
0xe3a0_0403, // MOV r0,#0x03000000.
0xe28f_1001, // ADD r1,pc,#1 -> CODE+13.
0xe12f_ff11, // BX r1.
0x46c0_6802, // Thumb LDR r2,[r0]; NOP.
0x46c0_46c0,
]);
for _ in 0..3 {
cpu.step(&mut memory).unwrap();
}
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
assert_eq!(memory.iwram_bus.committed_word(), None);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers()[2], WORD);
assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));
memory.write32(IWRAM, 0).unwrap();
cpu.step(&mut memory).unwrap();
assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));
}
#[test]
fn dma_can_establish_lanes_before_any_cpu_instruction() {
for word in [false, true] {
let (_, mut memory) = arm(&[]);
let dma = DMA_BASE + 3 * DMA_STRIDE;
memory.write32(dma, IWRAM).unwrap();
memory.write32(dma + 4, CODE).unwrap();
memory
.write32(dma + 8, if word { 0x8400_0001 } else { 0x8000_0002 })
.unwrap();
memory.step_dma().unwrap().unwrap();
assert_eq!(
memory.iwram_bus.committed_word(),
if word { Some(WORD) } else { None }
);
if !word {
memory.step_dma().unwrap().unwrap();
assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));
}
}
}
#[test]
fn accepted_irq_and_bios_fetches_preserve_local_lanes_until_actual_iwram_access() {
let mut bios = vec![0; BIOS_SIZE];
bios[0x18..0x1c].copy_from_slice(&NOP.to_le_bytes());
bios[0x1c..0x20].copy_from_slice(&0xe590_1000_u32.to_le_bytes()); // LDR r1,[r0].
let mut memory = Memory::with_bios(vec![], bios).unwrap();
for (i, word) in [0xe3a0_0403, 0xe590_1000, NOP, NOP].into_iter().enumerate() {
memory.write32(CODE + i as u32 * 4, word).unwrap();
}
memory.write32(IWRAM, WORD).unwrap();
let mut machine = Machine::new(Cpu::new(CODE), memory);
machine.step().unwrap();
machine.step().unwrap();
assert_eq!(machine.memory().iwram_bus.committed_word(), Some(WORD));
machine.memory_mut().write32(IWRAM, 0x8765_4321).unwrap();
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);
assert_eq!(machine.memory().iwram_bus.committed_word(), Some(WORD));
machine.step().unwrap();
assert_eq!(machine.memory().iwram_bus.committed_word(), Some(WORD));
machine.step().unwrap();
assert_eq!(
machine.memory().iwram_bus.committed_word(),
Some(0x8765_4321)
);
}
#[test]
fn failed_instruction_discards_its_iwram_fetch_and_failed_dma_preserves_committed_lanes() {
let (mut cpu, mut memory) = arm(&[0xe3a0_0403, 0xe590_1000, 0xe12f_ff10, NOP, NOP]);
cpu.step(&mut memory).unwrap();
cpu.step(&mut memory).unwrap();
memory.write32(IWRAM, 0xe7f0_00f0).unwrap(); // Unsupported ARM instruction.
memory.write32(IWRAM + 4, WORD).unwrap();
memory.write32(IWRAM + 8, 0x8765_4321).unwrap();
cpu.step(&mut memory).unwrap(); // Target pair ends with WORD.
let before = cpu.clone();
assert!(cpu.step_timed(&mut memory).is_err());
assert_eq!(cpu, before);
assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));
// A failing cold entry also discards its current/decode/lookahead lane updates.
let mut cold = Cpu::new(IWRAM);
let before = cold.clone();
assert!(cold.step(&mut memory).is_err());
assert_eq!(cold, before);
assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));
let dma = DMA_BASE + 3 * DMA_STRIDE;
memory.write32(dma, IWRAM + 8).unwrap();
memory.write32(dma + 4, ROM_START).unwrap(); // Read-only destination.
memory.write32(dma + 8, 0x8400_0001).unwrap();
assert!(memory.step_dma().is_err());
assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));
}

View file

@ -30,10 +30,11 @@ The repository is a Cargo workspace with three crates:
| `crates/gba-core/src/cpu/bios_access_tests.rs` | Original CPU BIOS protection regressions for retained history, modes, widths, exception returns, and diagnostics | | `crates/gba-core/src/cpu/bios_access_tests.rs` | Original CPU BIOS protection regressions for retained history, modes, widths, exception returns, and diagnostics |
| `crates/gba-core/src/cpu/thumb_bios_access_tests.rs` | Thumb BIOS aligned-word snapshots, transitions, load lanes, boundaries, diagnostics, and nominal timing | | `crates/gba-core/src/cpu/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/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 invalidation, and nominal timing | | `crates/gba-core/src/cpu/iwram_open_bus_tests.rs` | Persistent IWRAM lanes, cold fills, access widths, isolation, errors, refills, IRQ preservation, and nominal timing |
| `crates/gba-core/src/cpu/iwram_dma_tests.rs` | Bounded DMA continuation lanes, access ordering, channel isolation, diagnostics, and resume snapshots | | `crates/gba-core/src/cpu/iwram_dma_tests.rs` | DMA lane updates, cold/resumed fetch ordering, channel isolation, diagnostics, and IRQ preservation |
| `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/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/memory/iwram_bus.rs` | Transactional persistent IWRAM lanes for ARM/Thumb fetches, CPU data accesses, and DMA |
| `crates/gba-core/src/memory/iwram_history_tests.rs` | Cross-state latch ownership, ARM accesses/refills, nonlocal code, pre-start DMA, IRQ preservation, and rollback |
| `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/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/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 | | `crates/gba-core/src/cpu/thumb_tests.rs` | Thumb instructions, state switching, and decoder checks |

View file

@ -136,7 +136,7 @@ Host inspection, ROM-suite memory assertions, instruction fetches, and DMA do no
Limits remain explicit: Limits remain explicit:
- Thumb open bus uses the supported region rules below, including bounded sequential internal-RAM lane history. - Thumb open bus uses the supported region rules below, including persistent internal-RAM lane history.
- [BIOS-protected reads](bios.md#cpu-bios-read-protection) use separate retained ARM PC+8 or aligned Thumb PC+4 word snapshots. - [BIOS-protected reads](bios.md#cpu-bios-read-protection) use separate retained ARM PC+8 or aligned Thumb PC+4 word snapshots.
- Unused/write-only I/O reads, general DMA-to-CPU bus handoff, and disabled-RAM reads are not modeled here. - Unused/write-only I/O reads, general DMA-to-CPU bus handoff, and disabled-RAM reads are not modeled here.
[DMA channel data](dma.md#retained-channel-data) is separate and never overrides a CPU snapshot. [DMA channel data](dma.md#retained-channel-data) is separate and never overrides a CPU snapshot.
@ -144,7 +144,7 @@ Limits remain explicit:
- Unused-memory writes remain diagnostics rather than ignored hardware writes. Swaps cannot silently discard their write. - Unused-memory writes remain diagnostics rather than ignored hardware writes. Swaps cannot silently discard their write.
- ARM/Thumb instruction buffering is persistent, but these data snapshots remain a separate bounded model. - ARM/Thumb instruction buffering is persistent, but these data snapshots remain a separate bounded model.
Thumb IWRAM bus-history samples do not share storage with retained instructions. Thumb IWRAM bus-history samples do not share storage with retained instructions.
DMA updates existing IWRAM continuation lanes at instruction boundaries, not during CPU accesses. DMA updates IWRAM lanes at instruction boundaries, not during CPU accesses.
This subset lets public ARM test 362 complete. That test does not assert the loaded data value. 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). Original regressions check the values separately. See [the public ARM result](../public-arm-tests.md).
@ -158,7 +158,7 @@ The snapshot depends on the executing code region. P below is the executing inst
| --- | --- | | --- | --- |
| External work RAM, palette RAM, video RAM, all three ROM windows | Halfword at P+4, repeated in both word lanes | | External work RAM, palette RAM, video RAM, all three ROM windows | Halfword at P+4, repeated in both word lanes |
| BIOS and object attribute memory (OAM) | Full word at `(P+4) & ~3` | | BIOS and object attribute memory (OAM) | Full word at `(P+4) & ~3` |
| Internal work RAM (IWRAM) | PC+4 replaces one halfword in known sequential IWRAM history; the other lanes retain their values | | Internal work RAM (IWRAM) | PC+4 replaces one halfword in the persistent IWRAM latch; the other lanes retain their values |
| Other code regions | Unsupported | | Other code regions | Unsupported |
For BIOS/OAM, aligned instructions expose halfwords at P+4 and P+6. For BIOS/OAM, aligned instructions expose halfwords at P+4 and P+6.
@ -184,77 +184,75 @@ Do not substitute P+2 for that history: it is only the usual case, not a general
The original Nintendo DS also differs from GBA-family IWRAM behavior; this core targets GBA. The original Nintendo DS also differs from GBA-family IWRAM behavior; this core targets GBA.
Bus history remains bounded despite sharing samples with the persistent instruction buffer. 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. Refill timing, general DMA-to-CPU transitions, 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. Original regressions validate the documented formulas. No physical-hardware or independent public Thumb open-bus pass is claimed.
### Sequential IWRAM history ### Sequential IWRAM history
For consecutive Thumb instructions in IWRAM, the emulator retains a separate lane value and known-bit mask. The emulator retains a separate IWRAM lane value and known-bit mask, independent of CPU state or executing region.
The PC+4 halfword fetch updates its addressed halfword before execution. Every successful ARM IWRAM fetch drives a word; every Thumb IWRAM fetch drives its addressed halfword.
IWRAM data reads and writes then update only their addressed byte, halfword, or word lanes. IWRAM data reads and writes update their addressed byte, halfword, or word lanes, including accesses from non-IWRAM code.
The recorded value is raw aligned bus data, before CPU rotation or sign extension. The recorded value is raw aligned bus data, before CPU rotation or sign extension.
Accesses to other regions do not replace the IWRAM latch. Accesses to other regions, state changes, and exception entry do not erase this local latch.
See [the evidence and implementation scope](../research/iwram-bus-history.md). See [the evidence and implementation scope](../research/iwram-bus-history.md).
History starts unknown. An unused-memory load requires all word lanes to be known, including for byte/halfword loads. History starts unknown. An unused-memory load requires all word lanes to be known, including for byte/halfword loads.
Ordinary instructions and mapped loads still execute with incomplete history. Cold pipeline filling samples the current instruction, next instruction, and lookahead in order.
Consecutive fetches can establish both halves. An IWRAM word read or write establishes the entire word. A mapped Thumb IWRAM fill therefore establishes both halves before the first data read.
A retained pipeline advances only its new lookahead; old instruction slots do not drive the bus again.
Debugger invalidation does not erase the latch, but the following cold fill updates it from current mapped bytes.
Host inspection and setup writes do not seed or change this emulated history. Host inspection and setup writes do not seed or change this emulated history.
Each instruction stages its fetch and data-access changes. Each instruction stages all new fetches and data-access changes.
Success commits history only for the next sequential Thumb IWRAM PC. Failure preserves the previous committed history. Success commits history regardless of the resulting PC, instruction state, or region. Failure preserves the previous committed history.
Block-transfer data accesses stage updates in order; unused-memory reads still use one entry snapshot per instruction. 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. This preserves the existing atomic diagnostic policy without adding bus or device cycles.
Missing or region-crossing Thumb lookahead still leaves that instruction's unused-memory snapshot unsupported.
Taken branches and PC-writing refills end the old continuation history, even when their target is the fallthrough address. It does not erase independently known IWRAM lanes.
A successful refill into Thumb IWRAM establishes new history from its target pair, as described below.
Untaken conditional branches keep sequential history.
Other execution states/regions and accepted machine IRQs invalidate continuation history.
Successful DMA units update existing continuation lanes as described below.
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 ### Thumb IWRAM refill history
After a successful refill instruction, the emulator uses the resulting PC and instruction state. 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. Thumb target fetches occur at T and T+2; ARM target fetches occur at T and T+4.
Both accesses must remain in IWRAM. Normal physical RAM mirrors apply. Each successful IWRAM fetch drives its own lanes, in order. Other-region or unavailable fetches do not erase local history.
These samples establish both word lanes for execution at T. The first instruction then samples T+4 as usual. Normal physical RAM mirrors apply.
A complete Thumb IWRAM target pair establishes both word lanes; the first target instruction then samples T+4.
An ARM IWRAM target pair leaves the second fetched word in the latch.
This applies to ARM/Thumb BX, taken Thumb branches, BL suffixes, Thumb PC writes, and ARM status-restoring returns. This applies to BX, taken branches, BL suffixes, PC writes, PC loads, and status-restoring returns.
Saved Thumb state, not the target's low bit alone, controls exception-return sampling. Saved instruction 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. Stack and block-load returns finish their data accesses before the target samples.
Failed instructions do not sample a target or replace committed history. Failed instructions do not sample a target or replace committed history.
Target samples use strict mapped reads and add no nominal data or device cycles. 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. The same captured target pair fills the CPU buffer and updates IWRAM history; no second target read occurs.
Host inspection does not change captured lanes. A failed target fetch remains a later instruction diagnostic. 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. IRQ entry samples its ARM BIOS vector pair without erasing IWRAM lanes.
DMA between refill and arrival updates this continuation's actual IWRAM access lanes before the target+4 sample. A BIOS handler can still change those lanes through actual IWRAM data accesses.
The [refill evidence and scope](../research/iwram-bus-history.md#thumb-iwram-refill-extension) distinguish these samples from a full pipeline. The [refill evidence and scope](../research/iwram-bus-history.md#thumb-iwram-refill-extension) distinguish these samples from a fully timed pipeline.
ARM-target refill history, sub-instruction DMA ordering, and exact refill timing remain incomplete. Sub-instruction DMA ordering and exact refill timing remain incomplete.
### DMA effects on Thumb IWRAM continuations ### DMA effects on Thumb IWRAM continuations
Within the instruction-boundary scheduler, DMA finishes before the next CPU PC+4 sample. Within the instruction-boundary scheduler, DMA finishes before the next CPU fetch.
For an existing sequential or refill continuation, each successful DMA unit updates IWRAM source lanes, then IWRAM destination lanes. Each successful DMA unit updates IWRAM source lanes, then IWRAM destination lanes, even before the first CPU instruction.
Word accesses replace all lanes. Halfword accesses replace only the addressed halfword. Word accesses replace all lanes. Halfword accesses replace only the addressed halfword.
Other memory regions do not replace the local IWRAM value, even when DMA's channel data changes. Other memory regions do not replace the local IWRAM value, even when DMA's channel data changes.
The resumed PC+4 fetch then replaces one halfword before the instruction's unused-memory snapshot is taken. A resumed Thumb PC+4 fetch then replaces one halfword before the instruction's unused-memory snapshot is taken.
A cold fill instead drives all its newly fetched instructions.
Channel halfword duplication is not a full-word IWRAM access. Channel halfword duplication is not a full-word IWRAM access.
A blocked source below work RAM does not drive IWRAM; an IWRAM destination still drives the halfword or word actually written. A blocked source below work RAM does not drive IWRAM; an IWRAM destination drives the actual halfword or word written.
Multiple units and channel preemption update lanes in access order, not channel-number order. Multiple units and channel preemption update lanes in access order, not channel-number order.
Host inspection/setup cannot change them. Host inspection/setup cannot change them.
History changes commit only after a unit succeeds. A failed unit retains the previous successful unit's history. History changes commit only after a unit succeeds. A failed unit retains the previous successful unit's history.
A failed resumed instruction discards its staged changes, not the completed DMA history. A failed resumed instruction discards its staged changes, not the completed DMA history.
DMA does not establish an expected PC at cold direct entry. Existing partially known continuations can gain known lanes. No expected PC or Thumb continuation is needed to record DMA activity.
PC discontinuities, unsupported state changes, IRQ entry, and region-crossing lookahead keep their existing conservative rules.
This implements local lane effects, not a general last-DMA-value override or a persistent pipeline. This implements local lane effects, not a general last-DMA-value override.
Sub-instruction arbitration, DMA during CPU internal cycles, cold/cross-state IWRAM history, and exact resume timing remain incomplete. Sub-instruction arbitration, DMA during CPU internal cycles, and exact resume timing remain incomplete.
See [the source evidence and limits](../research/iwram-bus-history.md#dma-continuation-extension). See [the source evidence and limits](../research/iwram-bus-history.md#dma-continuation-extension).
## Processor status and exceptions ## Processor status and exceptions
@ -354,7 +352,7 @@ Important timing limits:
- Code S/N counts follow instruction summaries, not a simulated fetch pipeline. - 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. - 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. - PC writes use destination-region costs and the restored instruction width for nominal refill accesses.
- Refill cost calculation does not read target bytes. ARM instruction-buffer and Thumb IWRAM bus-history samples add no extra cycles. - Refill cost calculation does not read target bytes. ARM/Thumb instruction-buffer and local IWRAM bus-history samples add no extra cycles.
- Target-pair sampling cannot fail the branch early. An invalid branch target still fails on the following instruction fetch. - 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. - 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. - PHI and SRAM wait fields are stored; PHI output and SRAM mapping are not implemented.

View file

@ -58,10 +58,12 @@ DMA writes to DMA registers return `DmaError::RegisterDestination`; self-modifyi
Other accesses use the existing memory map, alignment checks, mirrors, and I/O write rules. Other accesses use the existing memory map, alignment checks, mirrors, and I/O write rules.
Other unmapped reads and cartridge writes remain diagnostics, not general DMA open-bus behavior. Other unmapped reads and cartridge writes remain diagnostics, not general DMA open-bus behavior.
A failed unit changes no memory, device state, or clock. Earlier units remain committed; the failed unit remains pending. A failed unit changes no memory, device state, or clock. Earlier units remain committed; the failed unit remains pending.
Successful units update existing Thumb IWRAM continuation lanes for their actual IWRAM reads and writes, in that order. Successful units update local IWRAM lanes for their actual IWRAM reads and writes, in that order.
No CPU continuation is required; DMA can establish lanes before the first CPU instruction.
Accesses elsewhere leave those local lanes unchanged. Failed units preserve history. Accesses elsewhere leave those local lanes unchanged. Failed units preserve history.
The resumed CPU's PC+4 fetch then updates its addressed halfword. No universal DMA-value override is used. A resumed Thumb PC+4 fetch updates its addressed halfword; a cold pipeline fill drives all its new samples.
See [bounded IWRAM DMA history](cpu.md#dma-effects-on-thumb-iwram-continuations) for the scope. No universal DMA-value override is used.
See [local IWRAM DMA history](cpu.md#dma-effects-on-thumb-iwram-continuations) for the scope.
## Retained channel data ## Retained channel data
@ -88,5 +90,5 @@ Earlier successful units remain committed. Normal destination validation, nomina
This error policy is not a model of hardware data aborts. This error policy is not a model of hardware data aborts.
See [the latch evidence and scope](../research/dma-data-latches.md). See [the latch evidence and scope](../research/dma-data-latches.md).
General DMA open-bus reads, cross-state IWRAM history, and sub-instruction bus ownership remain unimplemented. General DMA open-bus reads and sub-instruction bus ownership remain unimplemented.
In particular, the known channel value does not supply unused/write-only I/O reads or CPU open-bus snapshots. In particular, the known channel value does not supply unused/write-only I/O reads or CPU open-bus snapshots.

View file

@ -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. - [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. - [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. - [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 and Thumb target-pair refill samples; 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 persistent local lanes across ARM/Thumb fetches, data accesses, and DMA; region-crossing snapshots 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. - [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. - [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). - [GBATEK GBA memory map and bus widths](https://problemkaputt.de/gbatek-gba-memory-map.htm).
@ -110,7 +110,10 @@ IWRAM lane-history research:
- [ares IWRAM accesses](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/memory.cpp) and [bus dispatch](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/bus.cpp), for separate byte-lane retention and debugger isolation. - [ares IWRAM accesses](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/memory.cpp) and [bus dispatch](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/bus.cpp), for separate byte-lane retention and debugger isolation.
- [jgenesis issue 676](https://github.com/jsgroth/jgenesis/issues/676) and [its correction](https://github.com/jsgroth/jgenesis/commit/fab6e2ccc60e492dd68b7f1e927b0829a6d80195), for distinguishing the IWRAM latch from reads to other regions. - [jgenesis issue 676](https://github.com/jsgroth/jgenesis/issues/676) and [its correction](https://github.com/jsgroth/jgenesis/commit/fab6e2ccc60e492dd68b7f1e927b0829a6d80195), for distinguishing the IWRAM latch from reads to other regions.
See [IWRAM Thumb bus history](research/iwram-bus-history.md). These were source reviews, not local hardware or external-emulator test runs. See [persistent IWRAM bus history](research/iwram-bus-history.md).
The pinned ares helpers also support state-independent local ownership: they do not gate updates on the executing PC or CPU state.
The pinned NanoBoyAdvance loop/refill helpers below support driving actual new samples, without replaying retained instruction slots.
These were source reviews, not local hardware or external-emulator test runs.
IWRAM refill research: IWRAM refill research:
@ -136,7 +139,7 @@ DMA IWRAM continuation research:
Its [DMA controller](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/dma.rs) and [memory storage](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/memory.rs) distinguish channel data from local bus lanes. Its [DMA controller](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/dma.rs) and [memory storage](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/memory.rs) distinguish channel data from local bus lanes.
- The pinned ares DMA, bus dispatch, and IWRAM helpers above independently use the same local lane ownership. - The pinned ares DMA, bus dispatch, and IWRAM helpers above independently use the same local lane ownership.
See [the bounded DMA continuation extension](research/iwram-bus-history.md#dma-continuation-extension). See [the DMA local-lane extension](research/iwram-bus-history.md#dma-continuation-extension).
These source reviews do not establish sub-instruction arbitration, exact CPU resume timing, or hardware conformance. These source reviews do not establish sub-instruction arbitration, exact CPU resume timing, or hardware conformance.
DMA resume-timing research: DMA resume-timing research:

View file

@ -75,7 +75,8 @@ The current instruction address still selects nominal code cost rather than a se
ARM PC+8 open bus and protected BIOS readback remain separate snapshots. ARM PC+8 open bus and protected BIOS readback remain separate snapshots.
The [shared fetch sample](shared-fetch-samples.md) supplies the instruction-buffer entry and supported bus snapshots without duplicate reads. 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. No unified per-access bus model exists yet.
The target samples do not establish complete ARM IWRAM or BIOS bus history. ARM IWRAM target samples now drive the [persistent local latch](iwram-bus-history.md).
Complete BIOS and general-bus history remain unimplemented.
Instruction-state-changing compare quirks, Game Pak prefetch, and DMA arbitration remain incomplete. 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. DMA still runs between whole instructions, not during their data accesses or internal cycles.

View file

@ -56,7 +56,7 @@ Stores and 128 KiB ROM boundaries already use N and receive no duplicate cost.
Game Pak prefetch remains unimplemented; WAITCNT bit 14 does not alter this nominal timing path. Game Pak prefetch remains unimplemented; WAITCNT bit 14 does not alter this nominal timing path.
This change does not select a DMA value for CPU open-bus reads. This change does not select a DMA value for CPU open-bus reads.
DMA channel data, bounded IWRAM lane history, and protected BIOS history remain separate. DMA channel data, persistent local IWRAM lanes, and protected BIOS history remain separate.
General bus-data ownership and exact resume timing still need independent per-access validation. General bus-data ownership and exact resume timing still need independent per-access validation.
## Original regression coverage ## Original regression coverage

View file

@ -1,11 +1,12 @@
# IWRAM Thumb bus history # Persistent IWRAM bus history
## Finding ## Finding
Internal work RAM (IWRAM) retains its own bus lanes. A previous CPU load result is not a universal replacement. Internal work RAM (IWRAM) retains its own bus lanes, independently of the CPU's instruction state and executing region.
An IWRAM byte, halfword, or word access updates only the addressed lanes of that latch. An IWRAM byte, halfword, or word access updates only the addressed lanes of that latch.
A Thumb fetch updates one halfword. Reads and writes elsewhere do not replace the IWRAM latch. ARM instruction fetches drive words. Thumb instruction fetches drive halfwords.
This distinction matters when Thumb code reads another memory region before an unused-memory read. Reads and writes elsewhere do not replace the IWRAM latch.
Neither a prior CPU destination register nor a DMA channel value is a universal replacement.
For sequential Thumb execution at P, the fetch at P+4 updates: For sequential Thumb execution at P, the fetch at P+4 updates:
@ -21,127 +22,107 @@ Using current bytes at P+2 would lose observed history and ignore data-access ch
- [GBATEK](https://problemkaputt.de/gbatek-gba-unpredictable-things.htm) documents the two Thumb IWRAM lanes and warns about overwritten history. - [GBATEK](https://problemkaputt.de/gbatek-gba-unpredictable-things.htm) documents the two Thumb IWRAM lanes and warns about overwritten history.
- [mGBA issue 1575](https://github.com/mgba-emu/mgba/issues/1575) reports hardware-based alignment expectations and read/write effects on IWRAM open bus. - [mGBA issue 1575](https://github.com/mgba-emu/mgba/issues/1575) reports hardware-based alignment expectations and read/write effects on IWRAM open bus.
- [ares IWRAM access implementation](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/memory.cpp) retains separate IWRAM lanes for byte, halfword, and word reads and writes. - [ares IWRAM access implementation](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/memory.cpp) retains separate IWRAM lanes for byte, halfword, and word reads and writes.
Its [bus implementation](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/bus.cpp) distinguishes this latch from the general data bus and debugger access. Its helpers update the same latch without testing CPU state, executing PC, or sequential continuation.
Its [bus implementation](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/bus.cpp) selects those helpers for CPU and DMA accesses and isolates debugger reads.
- [jgenesis issue 676](https://github.com/jsgroth/jgenesis/issues/676) identifies failures after byte/halfword reads from other regions. - [jgenesis issue 676](https://github.com/jsgroth/jgenesis/issues/676) identifies failures after byte/halfword reads from other regions.
The [correction at fab6e2cc](https://github.com/jsgroth/jgenesis/commit/fab6e2ccc60e492dd68b7f1e927b0829a6d80195) adds a separate IWRAM latch. The [correction at fab6e2cc](https://github.com/jsgroth/jgenesis/commit/fab6e2ccc60e492dd68b7f1e927b0829a6d80195) adds a separate IWRAM latch.
The commit reports that its openbuster tests pass. That report is upstream evidence, not a test run by this project. The commit reports that its openbuster tests pass. That report is upstream evidence, not a test run by this project.
- [NanoBoyAdvance's CPU loop and refill helpers](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/arm7tdmi.hh) fetch the new instruction before execution and sample target pairs in instruction-width order.
These sources support a separate lane-retaining latch, rather than a copy of the last CPU destination register. These sources support local bus ownership rather than a latch restricted to consecutive Thumb instructions.
Our implementation and synthetic programs are original. No external source code or test ROM was imported. Our implementation and synthetic programs are original. No external source code or test ROM was imported.
No physical-hardware measurement or external emulator differential run was performed here. No physical-hardware measurement or external emulator differential run was performed here.
## Bounded implementation ## Implementation and cold fills
`memory/iwram_bus.rs` records lane values and a known-bit mask during consecutive Thumb instructions in IWRAM. `memory/iwram_bus.rs` stores persistent lane values and a known-bit mask.
Before execution, the pipeline's new mapped PC+4 fetch updates its addressed lanes. There is no expected PC or CPU-state tag on this latch. The instruction buffer retains its own PC/state tags.
The bus-history consumer does not reread memory or reuse the current/decode instruction. Every instruction stages changes from the previously committed lanes.
During execution, successful IWRAM bus reads and writes update the staged lanes at their actual aligned addresses and widths. The [shared fetch samples](shared-fetch-samples.md) drive successful IWRAM fetches at their actual address and instruction width.
The latch sees raw bus data before CPU load rotation and sign extension. Successful IWRAM data accesses then drive raw bus values before load rotation or sign extension.
Other-region accesses and host inspection do not change it. CPU code can run in BIOS, ROM, EWRAM, or IWRAM without changing that rule.
A completed sequential instruction commits the staged history for the next exact virtual PC. A retained pipeline drives only its newly fetched lookahead. It never replays current/decode bus samples.
A failed instruction discards all staged changes and preserves the last committed history. A cold or discontinuous pipeline supplies current, next, and lookahead samples in order.
The existing whole-instruction diagnostic policy still applies; this is not hardware data-abort behavior. A valid Thumb IWRAM cold fill therefore establishes both lanes before the first data read.
All unused-memory reads within one instruction use its entry snapshot. This follows actual samples already performed by the interpreter; it is not an invented previous P+2 memory read.
Debugger invalidation clears only instruction retention, but the following cold fill drives its new samples.
The model starts with unknown lanes. An unused-memory load requires a complete known word, even for a narrower load. A successful instruction commits its staged lanes regardless of its resulting state, PC, or region.
Ordinary instructions and mapped data accesses can establish history without requiring a prior complete word. A failed instruction discards every staged change, including cold-fill samples.
Consecutive halfword fetches or an IWRAM word access can make every lane known. Successful PC-writing instructions then apply target fetches after their data accesses.
Missing lookahead and 16 MiB region-crossing fetches retain diagnostics. Other-region execution, BIOS IRQ entry, and unavailable targets do not erase known local lanes.
Host inspection and setup never drive the latch.
The initial sequential-only implementation invalidated history after all refill instructions. Initial lanes remain unknown until observed accesses establish them.
The extension below now establishes target-pair history for successful refills into Thumb IWRAM. Byte/halfword accesses may establish only part of the word; a word access establishes all lanes.
The existing ARM/Thumb timing classifiers identify these refills for both timed and untimed CPU stepping. All unused-memory reads within an instruction still use one entry snapshot.
Execution outside Thumb IWRAM still ends a sequential history sequence. A Thumb fetch crossing a 16 MiB region boundary leaves that instruction's unused-memory snapshot unsupported.
An accepted machine IRQ invalidates history. Failed DMA units preserve it. Successful IWRAM accesses still update the local latch independently of this conservative snapshot policy.
The DMA extension below updates existing continuation lanes from successful IWRAM accesses.
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. Exact per-access device timing remains unverified.
## Thumb IWRAM refill extension ## 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. 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. Thumb destinations use T and T+2; ARM destinations use T and T+4.
[NanoBoyAdvance's refill helpers](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/arm7tdmi.hh) use that order. The same captured samples fill the instruction buffer and drive local IWRAM lanes, with no duplicate memory reads.
Its execution loop then fetches T+4 before executing the first Thumb instruction. Each successful IWRAM sample drives its lanes independently. Other-region or unavailable samples leave them unchanged.
After a successful refill instruction, the pipeline samples the two mapped destination instructions. A complete Thumb IWRAM target pair replaces both halfwords.
The same captured halfwords update bus history if both remain in Thumb IWRAM. The first target instruction fetches T+4, replacing one halfword while retaining the captured T+2 lane.
The samples replace the old lane history and establish the next expected PC as T. An ARM IWRAM target pair leaves T+4's word in the latch.
The next instruction's normal PC+4 sample updates one lane while the other retains the captured T+2 halfword. Data loads and status restoration finish first, so saved instruction state determines the refill width.
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. This covers BX, taken branches, BL suffixes, PC writes/loads, and status-restoring returns.
The Thumb BL prefix alone does not refill. 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. A branch with an unavailable target can complete; the later instruction fetch retains its strict diagnostic.
Unmapped instruction targets still fail on the following fetch. A failed branch or return does not commit data or target lane changes.
Cold direct startup and arbitrary PC changes do not manufacture a refill. IRQ vector samples in BIOS leave IWRAM unchanged, but an IRQ handler's IWRAM data access updates it normally.
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.
The refill-only implementation invalidated history on successful DMA, including between refill and arrival.
The DMA extension below replaces that invalidation within existing continuations.
ARM-target refill history, exact pipeline timing, and sub-instruction DMA ordering remain incomplete.
No external emulator differential run or physical-hardware test was performed.
## DMA continuation extension ## DMA continuation extension
Two independent implementations route DMA through the same local IWRAM lane updates as CPU accesses: Two independent implementations route DMA through the same local IWRAM lane updates as CPU accesses:
- [ares bus dispatch](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/bus.cpp) selects its IWRAM helpers for both CPU and DMA. - [ares DMA](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/dma.cpp) performs a source read before its destination write through the shared bus dispatch above.
[Its DMA controller](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/dma.cpp) performs a source read before its destination write.
- [jgenesis bus at fab6e2cc](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/bus.rs) routes `AccessCtx::DMA` through `read_iwram` and `write_iwram`. - [jgenesis bus at fab6e2cc](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/bus.rs) routes `AccessCtx::DMA` through `read_iwram` and `write_iwram`.
Both call `iwram_update_open_bus`, which changes only addressed lanes and drives the general bus from the local word. Both call `iwram_update_open_bus`. Its DMA controller performs source reads and destination writes in order.
Its `try_progress_dma` performs source reads and destination writes in order.
Both run pending DMA before a CPU bus access. The access that resumes determines the subsequent bus value. Our instruction-boundary scheduler commits source lanes, then destination lanes, only after a complete unit succeeds.
These source reviews support local lane effects, not a universal first-instruction DMA override. A blocked source below work RAM drives no IWRAM read lanes.
They do not determine every hardware arbitration edge or validate our instruction-boundary scheduling. A halfword destination drives its actual selected half, not both halves of a duplicated channel word.
DMA can establish local lanes before any CPU instruction, without an expected continuation PC.
Failed DMA units preserve the previous committed latch.
Our bounded model applies those effects only when an existing Thumb IWRAM continuation has an expected next PC. A retained Thumb pipeline resumes with one PC+4 sample; a cold pipeline resumes with its full fill sequence.
After a successful unit, the source and destination independently update their addressed IWRAM lanes. These actual fetches determine how much DMA history remains visible.
DMA elsewhere leaves those lanes unchanged. A blocked source drives no IWRAM read lanes. DMA channel values never overwrite CPU instruction-buffer slots or protected BIOS history automatically.
The destination sees the actual selected halfword, not an assumed duplicated full word. Sub-instruction arbitration and DMA during CPU internal cycles remain unmodeled.
The next PC+4 sample updates one halfword before the resumed instruction takes its snapshot.
This also applies between a completed refill and its first target instruction.
Only complete successful units commit history, preserving the project's atomic diagnostic policy. ## Limits and original coverage
Cold direct entry still has no expected continuation PC. DMA does not manufacture one, even after a word access.
An existing partially known continuation can gain lanes. IRQ entry and CPU discontinuities retain their invalidation rules.
General DMA open-bus reads, cross-state history, sub-instruction arbitration, and DMA during CPU internal cycles remain outside this subset.
This extension models bus lanes, not self-modifying instruction execution.
The later [ARM](arm-instruction-buffer.md) and [Thumb](thumb-instruction-buffer.md) instruction buffers separately retain instructions across DMA.
No physical-hardware measurements or external emulator differential runs were performed. This is persistent local IWRAM history, not a complete general-bus implementation or cycle-accurate pipeline.
All regression programs are original, with a separate byte-array lane reference in the DMA matrix tests. ARM unused-memory reads still use their supported PC+8 snapshot.
BIOS protection remains separately retained and image-derived.
General DMA open bus, unused/write-only I/O, disabled RAM, exact region-crossing snapshots, and per-access timing remain incomplete.
Nominal CPU/DMA costs do not change.
## Original regression coverage Original tests cover:
- Both code alignments, IWRAM mirrors, and physical RAM wrap. - Cold fills, both Thumb alignments, mirrors, physical RAM wrap, and entry snapshots.
- Captured halfwords that remain unchanged by host inspection or later host writes. - ARM word fetches and ARM/Thumb target pairs, without replaying retained instruction slots.
- Byte/halfword/word data reads and writes, sign extension, load rotation, and all processor modes. - ARM byte/halfword/word reads and stores from EWRAM, plus Thumb data access from EWRAM.
- Other-region accesses that leave IWRAM lanes unchanged. - Preserved lanes through state changes, non-IWRAM execution, IRQ entry, and BIOS handler fetches.
- Multiple-register transfers, final-word retention, and writeback. - DMA before CPU startup, source/destination ordering, channel preemption, and blocked-source lane selection.
- Unknown lanes, establishment of known history, staged-access rollback, and retry. - Failed sequential instructions, failed cold fills, failed refills, and failed DMA units.
- Taken and untaken branches, PC writes to fallthrough, discontinuities, IRQ entry, and DMA success/failure. - Separate BIOS values, host isolation, debugger refill effects, timed/untimed equality, and unchanged nominal costs.
- 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. The persistent-history regressions failed under the previous Thumb-continuation-only implementation.
- DMA source/destination regions and alignments, word/halfword widths, mirrors, blocked-source lane selection, and other-region isolation. Existing cold-entry tests now assert captured lane values instead of the removed unsupported-history diagnostic.
- Channel preemption, completed-unit order, DMA/instruction failure retention, partial/cold continuations, and unchanged other CPU snapshot rules. Region-crossing and strict unmapped-fetch diagnostics remain covered.
- Equal timed/untimed CPU results and unchanged nominal data/device costs.
## Validation result ## Validation result
The sequential-history regressions reproduced the old unsupported-read failures before implementation. Workspace and core/demo tests pass in debug and release on Darwin arm64.
The refill regressions also failed on the prior invalidation-only path, then passed with target-pair sampling. Formatting, lint checks, rustdoc, preparation tests, native ROM windows, and graphics smoke modes pass.
The DMA continuation regressions reproduced unsupported-read failures under unconditional DMA invalidation, then passed with local lane updates. Public ARM, Thumb, memory, and BIOS reports match the prior shared-fetch reports exactly, including nominal cycle counts.
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.
Debug and release reports also match for the same fixture paths. Debug and release reports also match for the same fixture paths.
These public suites remain regression checks, not independent IWRAM-history or hardware-timing conformance tests.
Those public suites remain regression checks, not independent IWRAM-history conformance tests.

View file

@ -33,11 +33,12 @@ Supported 16-bit regions duplicate that halfword for bus observation.
BIOS/OAM samples read the adjacent halfword too, without rereading the instruction bytes. 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. 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`. For retained pipelines, the CPU passes only its new P+8 or P+4 sample to `begin_cpu_access`.
The later persistent IWRAM extension also passes newly sampled current/decode entries for cold fills.
It does not replay the bus observations of retained current/decode instructions. It does not replay the bus observations of retained current/decode instructions.
The consumer performs no mapped memory read. 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. After successful control flow, the same target samples fill the instruction buffer and update local IWRAM history in either state.
Machine IRQ entry uses this refill path; the ARM target still invalidates Thumb IWRAM continuation history. Machine IRQ entry uses this refill path; BIOS vector fetches leave IWRAM lanes unchanged.
Instruction buffers and BIOS/IWRAM latches remain separate retained state. 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. Sampling alone changes no CPU access context, latch, device clock, DMA resume marker, or data-timing trace.
@ -46,11 +47,10 @@ A retry takes a new lookahead sample while retaining the previously committed in
Unavailable lookahead remains diagnostic only when consumed by an instruction or a data read that needs it. 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. Strict instruction reads never use protected BIOS or unused-memory data fallback.
## Deliberately unchanged limits ## Current scope and limits
- Cold pipeline filling does not establish previously unknown IWRAM history. - Cold fills and cross-state local IWRAM history are now implemented in the [persistent-latch extension](iwram-bus-history.md).
- Thumb 16 MiB region crossings remain unsupported for bus snapshots. - 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. - 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. - 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. - DMA remains scheduled between whole instructions. Sampling does not add an interleaving point.

View file

@ -63,7 +63,7 @@ Original tests in `crates/gba-core/src/cpu/thumb_bios_access_tests.rs` cover:
- Sequential history, ARM/Thumb transitions, SWI entry, and POP-to-PC exits with RAM stack data. - Sequential history, ARM/Thumb transitions, SWI entry, and POP-to-PC exits with RAM stack data.
- Raw reads inside BIOS and protected reads outside it. - Raw reads inside BIOS and protected reads outside it.
- The last complete fetch word, unavailable lookahead, and failure retention. - The last complete fetch word, unavailable lookahead, and failure retention.
- Separate host reads, retained cold-IWRAM Thumb open-bus diagnostics, nominal costs, and device progression. - Separate host reads, independent cold-IWRAM fetch history, nominal costs, and device progression.
## Validation result ## Validation result

View file

@ -50,8 +50,8 @@ CPU clones own independent buffers; full CPU equality includes them.
## Separation from bus history and timing ## Separation from bus history and timing
Instruction retention and data-bus snapshots remain separate models. Instruction retention and data-bus snapshots remain separate models.
Cold instruction-buffer filling does not make unknown IWRAM bus lanes known. Cold instruction-buffer filling now drives its actual IWRAM samples under the [persistent-latch model](iwram-bus-history.md).
Successful DMA can update existing IWRAM bus lanes without changing retained CPU instructions. Successful DMA can establish or update IWRAM bus lanes without changing retained CPU instructions.
Refill bus history and instruction buffering now consume the same captured target pair without a second mapped read. 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. Buffer samples add no nominal cycles or data accesses.

View file

@ -28,7 +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. - 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. - 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. - 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. - Shared mapped fetch samples for instruction retention, supported open-bus values, BIOS snapshots, and IWRAM refill history.
- User, System, Supervisor, IRQ, FIQ, Abort, and Undefined modes. - User, System, Supervisor, IRQ, FIQ, Abort, and Undefined modes.
- Current and saved program status registers (`CPSR`/`SPSR`), with `MRS`/`MSR` transfers. - Current and saved program status registers (`CPSR`/`SPSR`), with `MRS`/`MSR` transfers.
- ARM/Thumb software interrupts (`SWI`), exception vectors, and status-restoring returns. - ARM/Thumb software interrupts (`SWI`), exception vectors, and status-restoring returns.
@ -77,8 +77,8 @@ Update this file when a feature lands or a limit is removed.
- ARM test/compare Rd=15 status restoration, without a PC result write or nominal refill. - ARM test/compare Rd=15 status restoration, without a PC result write or nominal refill.
- ARM unused-memory data reads return a PC+8 word snapshot, with normal byte lanes and load rotation. - 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. - 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. - Persistent IWRAM lanes track ARM/Thumb fetches, CPU data accesses, and successful DMA, 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. - Cold fills and ARM/Thumb target pairs update local history; state changes and BIOS IRQ entry do not erase it.
- Little-endian byte, halfword, and word reads/writes. - Little-endian byte, halfword, and word reads/writes.
- ARM7TDMI word-load rotation, aligned stores, and odd-address halfword-load behavior. - 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. - Explicit errors for unsupported instructions when their condition passes, and for unsupported memory accesses.
@ -137,11 +137,11 @@ Other public suites remain unverified. These results do not establish full CPU o
The runner requires known completion addresses and has no debug-port protocol, scripted input, or rendered-image assertions. The runner requires known completion addresses and has no debug-port protocol, scripted input, or rendered-image assertions.
ARM data reads from unused address ranges use a bounded [PC+8 open-bus snapshot](hardware/cpu.md#arm-unused-memory-data-reads). 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. Thumb reads use [region-dependent snapshots](hardware/cpu.md#thumb-unused-memory-data-reads) and persistent local IWRAM lanes.
Cold or invalidated IWRAM history remains diagnostic until all lanes are known. Only actual IWRAM fetches and data accesses drive those lanes, independently of CPU state and executing region.
Refills into Thumb IWRAM establish new target-pair history after the instruction succeeds. Cold fills and target pairs establish both lanes when all required samples are mapped.
Accepted machine IRQs, unsupported refills, and other execution states/regions invalidate continuation history. State changes, other-region execution, and BIOS IRQ entry preserve local history; failed instructions discard staged updates.
Successful DMA updates existing Thumb IWRAM continuation lanes for actual IWRAM accesses, before the resumed PC+4 sample. Successful DMA can establish lanes before CPU startup. Resumed fetches then update their addressed lanes.
ARM/Thumb instruction buffering retains sequential instructions and refill target pairs. 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. 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. Complete ARM bus history, exact region-crossing bus behavior, general DMA handoff, and unused/write-only I/O open bus remain incomplete.
@ -155,7 +155,7 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa
## Next steps ## Next steps
1. Extend the shared fetch-sample model to verified cross-state bus history and region crossings. 1. Extend shared fetch samples to verified general-bus history and region-crossing snapshots.
General DMA open-bus reads and sub-instruction arbitration remain separate from retained channel data and local IWRAM lanes. 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. 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. Validate BIOS snapshots with independent hardware tests before claiming exact refill or data-access history.

View file

@ -65,10 +65,11 @@ Compare/status CPU regressions verify saved flags/masks, bank switching, sequent
Load-alias regressions cover widths, signed/unaligned values, immediate/register offsets, pre/post-indexing, every non-PC register bank, and address wrapping. Load-alias regressions cover widths, signed/unaligned values, immediate/register offsets, pre/post-indexing, every non-PC register bank, and address wrapping.
They verify loaded-value precedence, preserved flags, unchanged stores, nominal timing, I/O reads before device progress, deferred IRQ delivery, and atomic errors. They verify loaded-value precedence, preserved flags, unchanged stores, nominal timing, I/O reads before device progress, deferred IRQ delivery, and atomic errors.
ARM open-bus regressions cover unused-range boundaries, PC+8 source regions, byte lanes, signed/unaligned loads, writeback, and block loads. ARM open-bus regressions cover unused-range boundaries, PC+8 source regions, byte lanes, signed/unaligned loads, writeback, and block loads.
They check branch-target resampling, missing lookahead, skipped conditions, nominal timing, device progress, and retained host/fetch/cold-IWRAM/DMA diagnostics. They check branch-target resampling, missing lookahead, skipped conditions, nominal timing, device progress, and retained host/fetch/DMA diagnostics.
Thumb open-bus tests cover supported code regions, both instruction alignments, ROM windows, mirrors, and physical memory wrap. Thumb open-bus tests cover supported code regions, both instruction alignments, ROM windows, mirrors, and physical memory wrap.
They check widths, sign extension, rotation, block/stack/PC loads, aliases, modes, state preservation, and sequential/branch resampling. They check widths, sign extension, rotation, block/stack/PC loads, aliases, modes, state preservation, and sequential/branch resampling.
Short-ROM tests require only the fetched halfword in 16-bit regions. Cold IWRAM and region-crossing tests retain explicit diagnostics. Short-ROM tests require only the fetched halfword in 16-bit regions. Region-crossing tests retain explicit diagnostics.
Cold IWRAM tests now assert actual current/decode/lookahead lane updates before the first data read.
Sequential IWRAM regressions check retained lanes across fetches, reads, writes, mirrors, and physical RAM wrap. 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. 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. Known-bit tests reject incomplete words. Transaction tests verify discarded partial updates and preserved history after errors.
@ -77,8 +78,9 @@ Refill tests cover ARM/Thumb BX, Thumb branches and BL, high-register PC writes,
They check both target alignments, mirrors/wrap, captured target+2 versus later target+4, widths, sign extension, and rotation. 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 lane effects, and BIOS isolation retain checks. Original SWI/IRQ handlers verify return-time history. Failed returns, cold entry, region boundaries, DMA lane effects, and BIOS isolation retain checks.
DMA tests compare source/destination lane updates against a byte-array reference, then apply the resumed PC+4 fetch. DMA tests compare source/destination lane updates against a byte-array reference, then apply the resumed PC+4 fetch.
They cover channel preemption, blocked sources, failures, mirrors, partial/cold continuations, and other-region snapshot isolation. They cover channel preemption, blocked sources, failures, mirrors, pre-start lane establishment, cold fills, and other-region snapshot isolation.
IRQ and PC-discontinuity tests retain conservative invalidation; timed/untimed tests verify CPU results and nominal clock costs. IRQ tests verify local-latch preservation; PC discontinuities verify new pipeline-fill samples.
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. Host/fetch isolation, separate BIOS history, unsupported stores/data regions, nominal device progression, and DMA diagnostics also have checks.
ARM instruction-buffer tests verify retained P+4/P+8 words across CPU stores, DMA transfers, and host writes. ARM instruction-buffer tests verify retained P+4/P+8 words across CPU stores, DMA transfers, and host writes.
They cover refill target pairs, PC writes to fallthrough, conditional branches, ARM/Thumb transitions, exception entry/return, and deferred fetch errors. They cover refill target pairs, PC writes to fallthrough, conditional branches, ARM/Thumb transitions, exception entry/return, and deferred fetch errors.
@ -88,6 +90,9 @@ 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. 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. 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. Bus-history repair tests explicitly invalidate buffered code or repair a data address; failure checks retain full CPU equality.
Debugger invalidation itself preserves local lanes, but the next cold fill drives current mapped instruction bytes.
Persistent-latch tests cover ARM data accesses, ARM/Thumb target pairs, non-IWRAM code, IRQ handlers, and pre-start DMA.
They also verify partial lanes and rollback of both cold-fill and resumed-instruction samples.
Shared fetch-sample tests mutate memory between internal sampling and consumption to reject duplicate reads. 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. 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. This test-only split is not a DMA scheduling feature. Separate tests verify strict fetch errors and no timing/history side effects.
@ -257,7 +262,8 @@ 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. 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. 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 history, last valid fetch words, missing lookahead, and diagnostic-step retention have explicit checks.
Unknown Thumb IWRAM history remains diagnostic. Separate tests cover sequential/refill history, general Thumb snapshots, and retained BIOS history. Unknown IWRAM lanes remain diagnostic until observed accesses establish them; mapped cold pipeline fills now establish both halves.
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. 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. 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. They execute ARM/Thumb loads across widths and lanes, check caller state, and trace actual exits to their image data.