feat(cpu): use actual fetch regions for boundary bus snapshots

This commit is contained in:
0xrsydn 2026-10-06 15:13:40 +07:00
commit 0a696ea3e7
14 changed files with 465 additions and 28 deletions

View file

@ -38,6 +38,8 @@ mod dma_resume_tests;
#[cfg(test)]
mod exception_tests;
#[cfg(test)]
mod fetch_boundary_tests;
#[cfg(test)]
mod fetch_history_tests;
#[cfg(test)]
mod instruction_tests;

View file

@ -0,0 +1,303 @@
//! Original boundary programs. Expected data follows the newly fetched region,
//! not the region containing the executing instruction. Timing remains nominal.
use super::*;
use crate::{
dma::{DMA_BASE, DMA_STRIDE},
io::WAITCNT,
memory::{ROM_CAPACITY, ROM_START},
timing::{bus_cycles, AccessKind, AccessWidth},
};
const UNUSED: u32 = 0x8000_0000;
const DATA: u32 = 0x9abc_1234;
const SEED: u32 = 0x5678_abcd;
const NOP: u16 = 0x46c0;
const LDR: u16 = 0x6801; // LDR r1,[r0].
fn cpu(pc: u32) -> Cpu {
let mut cpu = Cpu::new(pc);
cpu.instruction_set = InstructionSet::Thumb;
cpu.registers[0] = UNUSED;
cpu
}
fn boundary(boundary: u32, pc: u32, instruction: u16) -> Memory {
let mut memory = Memory::new(DATA.to_le_bytes().to_vec()).unwrap();
if boundary < ROM_START {
memory.write32(boundary, DATA).unwrap();
}
for address in [boundary - 6, boundary - 4, boundary - 2] {
memory
.write16(address, if address == pc { instruction } else { NOP })
.unwrap();
}
memory
}
#[test]
fn mapped_video_and_rom_crossings_use_fetch_region_lanes_for_all_load_widths() {
for boundary_address in [0x0600_0000, 0x0700_0000, 0x0800_0000] {
for distance in [4, 2] {
let pc = boundary_address - distance;
let word: u32 = if boundary_address == 0x0700_0000 {
DATA // OAM supplies a full aligned word at either Thumb alignment.
} else if distance == 4 {
0x1234_1234
} else {
0x9abc_9abc
};
for (instruction, width, signed) in [
(LDR, 4, false),
(0x7801, 1, false),
(0x8801, 2, false),
(0x5681, 1, true),
(0x5e81, 2, true), // Signed loads use r2=0 offset.
] {
for lane in 0..4 {
for warm in [false, true] {
let mut memory = boundary(boundary_address, pc, instruction);
let mut cpu = cpu(if warm { pc - 2 } else { pc });
cpu.registers[0] += lane;
cpu.flags = Flags {
negative: true,
zero: false,
carry: true,
overflow: true,
};
if warm {
cpu.step(&mut memory).unwrap();
}
let mut expected = cpu.clone();
let byte = (word >> (lane * 8)) as u8;
let half = (word >> ((lane & 2) * 8)) as u16;
expected.registers[1] = match (width, signed) {
(4, _) => word.rotate_right(lane * 8),
(1, false) => u32::from(byte),
(1, true) => byte as i8 as i32 as u32,
(2, false) => u32::from(half).rotate_right((lane & 1) * 8),
(2, true) if lane & 1 != 0 => byte as i8 as i32 as u32,
(2, true) => half as i16 as i32 as u32,
_ => unreachable!(),
};
expected.registers[15] += 2;
let timing = cpu.step_timed(&mut memory).unwrap();
assert_cpu_arch_eq!(
cpu,
expected,
"boundary={boundary_address:#x} pc={pc:#x}"
);
assert_eq!(timing.code_cycles, 1);
assert_eq!(timing.data_cycles, 1);
assert_eq!(timing.internal_cycles, 1);
assert_eq!(memory.cycles(), 0);
assert_eq!(memory.read32(UNUSED), Err(MemoryError::Unmapped(UNUSED)));
}
}
}
}
}
}
#[test]
fn entering_iwram_uses_actual_local_history_or_reports_unknown_lanes() {
let boundary_address = 0x0300_0000;
for distance in [4, 2] {
for warm in [false, true] {
for seed in [0, 1, 2] {
// Unknown, CPU data read, DMA source read.
let pc = boundary_address - distance;
let mut memory = boundary(boundary_address, pc, LDR);
let source = 0x0300_1000;
memory.write32(source, SEED).unwrap();
if seed == 1 {
memory.write32(0x0200_0100, 0xe592_3000).unwrap(); // ARM LDR r3,[r2].
let mut seeder = Cpu::new(0x0200_0100);
seeder.registers[2] = source;
seeder.step(&mut memory).unwrap();
} else if seed == 2 {
let dma = DMA_BASE + 3 * DMA_STRIDE;
memory.write32(dma, source).unwrap();
memory.write32(dma + 4, 0x0200_1000).unwrap();
memory.write32(dma + 8, 0x8400_0001).unwrap();
memory.step_dma().unwrap().unwrap();
}
let mut cpu = cpu(if warm { pc - 2 } else { pc });
if warm {
cpu.step(&mut memory).unwrap();
}
let before = cpu.clone();
let result = cpu.step(&mut memory);
if distance == 4 && seed == 0 {
// Only IWRAM's low half has actually been fetched.
assert_eq!(result, Err(CpuError::Memory(MemoryError::Unmapped(UNUSED))));
assert_eq!(cpu, before);
} else {
result.unwrap();
assert_eq!(
cpu.registers[1],
if distance == 4 { 0x5678_1234 } else { DATA }
);
}
}
}
}
}
#[test]
fn failed_boundary_load_discards_new_samples_and_retry_fetches_current_bytes() {
let pc = 0x02ff_fffc;
let mut memory = boundary(0x0300_0000, pc, LDR);
memory.write16(pc + 2, LDR).unwrap();
let mut cpu = cpu(pc);
let before = cpu.clone();
assert!(cpu.step_timed(&mut memory).is_err());
assert_eq!(cpu, before);
memory.write16(0x0300_0000, 0x5678).unwrap();
memory.write32(0x0200_1000, SEED).unwrap();
cpu.registers[0] = 0x0200_1000;
cpu.step(&mut memory).unwrap(); // Ordinary mapped load needs no complete bus snapshot.
assert_eq!(cpu.registers[1], SEED);
cpu.registers[0] = UNUSED;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 0x9abc_5678);
}
#[test]
fn refill_straddling_ewram_and_iwram_keeps_the_captured_target_lane() {
let target = 0x02ff_fffe;
let mut memory = boundary(0x0300_0000, target, LDR);
memory.write32(0x0200_0100, 0xe12f_ff14).unwrap(); // ARM BX r4.
let mut cpu = Cpu::new(0x0200_0100);
cpu.registers[0] = UNUSED;
cpu.registers[4] = target | 1;
cpu.step(&mut memory).unwrap(); // Refill captures IWRAM's low lane (target+2).
memory.write32(0x0300_0000, 0xdef0_5678).unwrap();
cpu.step(&mut memory).unwrap(); // New fetch drives only the high lane.
assert_eq!(cpu.registers[1], 0xdef0_1234);
}
#[test]
fn rom_page_and_wait_window_crossings_use_mapped_halfwords_without_changing_nominal_timing() {
let mut rom = vec![0; ROM_CAPACITY];
for offset in [0, 0x0100_0000] {
rom[offset..offset + 4].copy_from_slice(&DATA.to_le_bytes());
}
for end in [0x0100_0000, ROM_CAPACITY] {
for distance in [4, 2] {
rom[end - distance..end - distance + 2].copy_from_slice(&LDR.to_le_bytes());
}
}
let mut memory = Memory::new(rom).unwrap();
for boundary in [
0x0900_0000,
0x0a00_0000,
0x0b00_0000,
0x0c00_0000,
0x0d00_0000,
] {
for distance in [4, 2] {
for waitcnt in [0, 0x7fff] {
memory.write16(WAITCNT, waitcnt).unwrap();
let pc = boundary - distance;
let mut cpu = cpu(pc);
let timing = cpu.step_timed(&mut memory).unwrap();
assert_eq!(
cpu.registers[1],
if distance == 4 {
0x1234_1234
} else {
0x9abc_9abc
}
);
assert_eq!(
timing.code_cycles,
bus_cycles(
memory.waitcnt(),
pc,
AccessWidth::Halfword,
AccessKind::Sequential
)
);
assert_eq!(timing.data_cycles, 1);
assert_eq!(timing.internal_cycles, 1);
}
}
}
}
#[test]
fn unsupported_io_and_missing_rom_lookahead_remain_diagnostic_but_branches_can_discard_them() {
for pc in [0x03ff_fffc, 0x03ff_fffe, 0x07ff_fffc, 0x07ff_fffe] {
let mut memory = Memory::new(vec![]).unwrap();
memory.write16(pc, LDR).unwrap();
let mut cpu = cpu(pc);
let before = cpu.clone();
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
assert_eq!(cpu, before);
memory.write16(pc, 0x4720).unwrap(); // BX r4 discards the unsupported sequential path.
memory.write16(0x0200_0200, 0x2107).unwrap(); // MOV r1,#7.
cpu.registers[4] = 0x0200_0201;
cpu.step(&mut memory).unwrap();
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 7);
}
}
#[test]
fn a_rom_boundary_requires_only_the_fetched_halfword_not_a_full_word() {
for length in [1, 2] {
let mut memory = Memory::new(DATA.to_le_bytes()[..length].to_vec()).unwrap();
let pc = ROM_START - 4;
memory.write16(pc, LDR).unwrap();
let mut cpu = cpu(pc);
let before = cpu.clone();
let result = cpu.step(&mut memory);
if length == 1 {
assert_eq!(result, Err(CpuError::Memory(MemoryError::Unmapped(UNUSED))));
assert_eq!(cpu, before);
} else {
result.unwrap();
assert_eq!(cpu.registers[1], 0x1234_1234);
}
}
}
#[test]
fn block_and_stack_loads_share_one_boundary_snapshot_and_keep_writeback() {
for instruction in [0xcc06, 0xbc06] {
// LDM r4!,{r1,r2}; POP {r1,r2}.
let pc = 0x06ff_fffc;
let mut memory = boundary(0x0700_0000, pc, instruction);
let mut cpu = cpu(pc);
cpu.registers[4] = UNUSED;
cpu.registers[13] = UNUSED;
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], DATA);
assert_eq!(cpu.registers[2], DATA);
assert_eq!(
cpu.registers[if instruction == 0xcc06 { 4 } else { 13 }],
UNUSED + 8
);
assert_eq!(cpu.pc(), pc + 2);
}
}
#[test]
fn arm_crossings_still_use_the_new_word_and_timed_and_untimed_paths_agree() {
for boundary in [0x0300_0000, 0x0600_0000, 0x0700_0000] {
let pc = boundary - 8;
let mut memory = Memory::new(vec![]).unwrap();
memory.write32(pc, 0xe590_1000).unwrap(); // LDR r1,[r0].
memory.write32(boundary, DATA).unwrap();
let mut cpu = Cpu::new(pc);
cpu.registers[0] = UNUSED;
let mut timed = cpu.clone();
cpu.step(&mut memory).unwrap();
timed.step_timed(&mut memory).unwrap();
assert_eq!(cpu, timed);
assert_eq!(cpu.registers[1], DATA);
}
}

View file

@ -319,7 +319,7 @@ fn failed_refill_instructions_preserve_cpu_and_preexisting_history() {
}
#[test]
fn cold_fill_establishes_lanes_but_region_crossings_and_unmapped_targets_stay_diagnostic() {
fn cold_fill_establishes_lanes_but_io_crossings_and_unmapped_targets_stay_diagnostic() {
let mut bus = Memory::new(vec![]).unwrap();
target(&mut bus, TARGET, PROBE);
let mut cpu = Cpu::new(TARGET);

View file

@ -294,18 +294,23 @@ fn short_rom_needs_only_the_fetched_halfword_and_errors_only_when_used() {
}
#[test]
fn cold_iwram_fills_establish_lanes_but_region_crossing_fetches_remain_diagnostics() {
fn cold_fills_and_mapped_crossings_succeed_only_with_known_fetch_lanes() {
for base in [0x0300_0100, 0x0300_8100, 0x02ff_fffc, 0x05ff_fffc] {
for offset in [0, 2] {
let pc = base + offset;
let mut bus = program(pc, LDR);
let mut cpu = cpu(pc);
if pc >> 24 == 3 {
let expected = match (pc >> 24, offset) {
(3, 0) => Some(0x46c0_80a5),
(3, 2) | (2, 2) => Some(DATA),
(5, 0) => Some(0x80a5_80a5),
(5, 2) => Some(0xf233_f233),
(2, 0) => None, // First IWRAM halfword cannot establish its unknown high lane.
_ => unreachable!(),
};
if let Some(expected) = expected {
cpu.step(&mut bus).unwrap();
assert_eq!(
cpu.registers[1],
if offset == 0 { 0x46c0_80a5 } else { DATA }
);
assert_eq!(cpu.registers[1], expected);
assert_eq!(bus.read32(UNUSED), Err(MemoryError::Unmapped(UNUSED)));
continue;
}

View file

@ -396,11 +396,12 @@ impl Memory {
}
}
self.drive_iwram_fetch(fetch);
let same_region = fetch.address >> 24 == pc >> 24;
// The newly accessed bus supplies the snapshot, even across a region boundary.
// Unknown local lanes and unavailable/unsupported fetch observations stay diagnostic.
let prefetch = match instruction_set {
InstructionSet::Arm => fetch.bus_word,
InstructionSet::Thumb if !same_region => None,
InstructionSet::Thumb if pc >> 24 == 3 => self.iwram_bus.snapshot(),
InstructionSet::Thumb if fetch.instruction.is_err() => None,
InstructionSet::Thumb if fetch.address >> 24 == 3 => self.iwram_bus.snapshot(),
InstructionSet::Thumb => fetch.bus_word,
};
self.cpu_access = Some(CpuAccess { pc, prefetch });