feat(memory): support ARM unused-memory open-bus reads
Snapshot mapped PC+8 bytes for ARM data reads in the documented unused ranges. Preserve strict host/fetch/Thumb/DMA diagnostics, missing-ROM errors, existing write rules, and nominal timing. Add original regressions and document the bounded model. The unmodified pinned jsmolka ARM ROM now reaches its verified checkpoint with r12=0 in debug and release: 1344 steps and 11257 nominal cycles. No ROM, manifest, or assertion changes. Validated on Darwin arm64: 842 workspace tests in debug and release; 773 core/demo tests separately; Python adapter tests; native ROM windows; all ten demo window smoke tests; fmt, clippy, and rustdoc.
This commit is contained in:
parent
ab625dc55d
commit
a9493d4cc6
11 changed files with 418 additions and 26 deletions
|
|
@ -22,6 +22,8 @@ mod instruction_tests;
|
|||
#[cfg(test)]
|
||||
mod load_alias_tests;
|
||||
#[cfg(test)]
|
||||
mod open_bus_tests;
|
||||
#[cfg(test)]
|
||||
mod status_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
|
@ -200,7 +202,7 @@ impl Cpu {
|
|||
fn execute_fetched(&mut self, instruction: u32, memory: &mut Memory) -> Result<(), CpuError> {
|
||||
// Record the executing instruction, not an operand's pipelined PC value.
|
||||
// Always clear the access context, including on diagnostic errors.
|
||||
memory.begin_cpu_access(self.pc());
|
||||
memory.begin_cpu_access(self.pc(), self.instruction_set);
|
||||
let result = self.execute_instruction(instruction, memory);
|
||||
memory.end_cpu_access();
|
||||
result
|
||||
|
|
|
|||
304
crates/gba-core/src/cpu/open_bus_tests.rs
Normal file
304
crates/gba-core/src/cpu/open_bus_tests.rs
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
//! Original regressions for the ARM unused-memory open-bus subset.
|
||||
use super::*;
|
||||
use crate::{
|
||||
dma::{DmaError, DMA_BASE, DMA_STRIDE},
|
||||
io::TIMER_BASE,
|
||||
machine::{Machine, MachineError, StepKind},
|
||||
memory::{BIOS_SIZE, ROM_START},
|
||||
timing::{bus_cycles, AccessKind, AccessWidth},
|
||||
};
|
||||
|
||||
const DATA: u32 = 0xf233_80a5;
|
||||
const UNUSED: u32 = 0x8000_0000;
|
||||
const RAM: u32 = 0x0200_0100;
|
||||
const LDR: u32 = 0xe590_1000; // LDR r1, [r0]
|
||||
|
||||
fn memory(instruction: u32) -> Memory {
|
||||
let words = [instruction, 0xe1a0_0000, DATA];
|
||||
Memory::new(words.into_iter().flat_map(u32::to_le_bytes).collect()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arm_unused_ranges_return_pc_plus_eight_not_address_mirrors() {
|
||||
for address in [
|
||||
0x0000_4000,
|
||||
0x0000_fffc,
|
||||
0x00ff_fffc,
|
||||
0x0100_0000,
|
||||
0x01ff_fffc,
|
||||
0x1000_0000,
|
||||
0x1800_0000,
|
||||
0x8000_0000,
|
||||
0xffff_fffc,
|
||||
] {
|
||||
let mut bus = memory(LDR);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = address;
|
||||
let mut expected = cpu.clone();
|
||||
expected.registers[1] = DATA;
|
||||
expected.registers[15] += 4;
|
||||
cpu.step(&mut bus).unwrap();
|
||||
assert_eq!(cpu, expected, "{address:#010x}");
|
||||
// Inspection outside CPU execution remains strict, including every lane.
|
||||
for low in 0..4 {
|
||||
assert_eq!(
|
||||
bus.read8(address + low),
|
||||
Err(MemoryError::Unmapped(address + low))
|
||||
);
|
||||
}
|
||||
assert_eq!(bus.read16(address), Err(MemoryError::Unmapped(address)));
|
||||
assert_eq!(bus.read32(address), Err(MemoryError::Unmapped(address)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_bus_loads_select_lanes_rotate_and_sign_extend_normally() {
|
||||
// Independent expectations for bytes [a5,80,33,f2].
|
||||
for (instruction, values) in [
|
||||
(LDR, [0xf233_80a5, 0xa5f2_3380, 0x80a5_f233, 0x3380_a5f2]),
|
||||
(0xe5d0_1000, [0xa5, 0x80, 0x33, 0xf2]), // LDRB
|
||||
(0xe1d0_10b0, [0x80a5, 0xa500_0080, 0xf233, 0x3300_00f2]), // LDRH
|
||||
(0xe1d0_10d0, [0xffff_ffa5, 0xffff_ff80, 0x33, 0xffff_fff2]), // LDRSB
|
||||
(
|
||||
0xe1d0_10f0,
|
||||
[0xffff_80a5, 0xffff_ff80, 0xffff_f233, 0xffff_fff2],
|
||||
), // LDRSH
|
||||
] {
|
||||
for (low, value) in values.into_iter().enumerate() {
|
||||
let mut bus = memory(instruction);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = UNUSED + low as u32;
|
||||
cpu.apply_status(0xb000_00df, Mode::System);
|
||||
let mut expected = cpu.clone();
|
||||
expected.registers[1] = value;
|
||||
expected.registers[15] += 4;
|
||||
let timing = cpu.step_timed(&mut bus).unwrap();
|
||||
assert_eq!(cpu, expected, "{instruction:#010x}, lane {low}");
|
||||
assert_eq!(timing.data_cycles, 1);
|
||||
assert_eq!(timing.internal_cycles, 1);
|
||||
assert_eq!(timing.code_cycles, 8); // ROM boundary; no extra lookahead cost
|
||||
assert_eq!(bus.cycles(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefetch_snapshot_uses_the_executing_region_and_rom_window() {
|
||||
for pc in [
|
||||
0x100,
|
||||
RAM,
|
||||
0x0300_0100,
|
||||
0x0500_0100,
|
||||
0x0600_0100,
|
||||
0x0700_0100,
|
||||
ROM_START,
|
||||
0x0a00_0000,
|
||||
0x0c00_0000,
|
||||
] {
|
||||
let mut bus = if pc < BIOS_SIZE as u32 {
|
||||
let mut bios = vec![0; BIOS_SIZE];
|
||||
bios[pc as usize..pc as usize + 4].copy_from_slice(&LDR.to_le_bytes());
|
||||
bios[pc as usize + 8..pc as usize + 12].copy_from_slice(&DATA.to_le_bytes());
|
||||
Memory::with_bios(vec![], bios).unwrap()
|
||||
} else if pc >= ROM_START {
|
||||
memory(LDR)
|
||||
} else {
|
||||
let mut bus = Memory::new(vec![]).unwrap();
|
||||
bus.write32(pc, LDR).unwrap();
|
||||
bus.write32(pc + 8, DATA).unwrap();
|
||||
bus
|
||||
};
|
||||
let mut cpu = Cpu::new(pc);
|
||||
cpu.registers[0] = UNUSED;
|
||||
let timing = cpu.step_timed(&mut bus).unwrap();
|
||||
assert_eq!(cpu.registers[1], DATA, "{pc:#010x}");
|
||||
assert_eq!(timing.data_cycles, 1);
|
||||
assert_eq!(
|
||||
timing.code_cycles,
|
||||
bus_cycles(0, pc, AccessWidth::Word, AccessKind::Sequential)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rrx_offset_load_preserves_carry_and_writes_back_the_high_address() {
|
||||
let mut bus = memory(0xe7b1_2060); // LDR r2, [r1, r0, RRX]!
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.flags.carry = true;
|
||||
cpu.step(&mut bus).unwrap();
|
||||
assert_eq!(cpu.registers[0], 0);
|
||||
assert_eq!(cpu.registers[1], UNUSED);
|
||||
assert_eq!(cpu.registers[2], DATA);
|
||||
assert_eq!(cpu.cpsr(), 0x2000_001f);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_loads_share_one_prefetch_word_and_single_load_aliases_still_work() {
|
||||
let mut bus = memory(0xe8b0_000e); // LDMIA r0!, {r1-r3}
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = UNUSED;
|
||||
let timing = cpu.step_timed(&mut bus).unwrap();
|
||||
assert_eq!(&cpu.registers[1..4], &[DATA; 3]);
|
||||
assert_eq!(cpu.registers[0], UNUSED + 12);
|
||||
assert_eq!(timing.data_cycles, 3);
|
||||
|
||||
let mut bus = memory(0xe5b0_0004); // LDR r0, [r0, #4]!
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = UNUSED;
|
||||
cpu.step(&mut bus).unwrap();
|
||||
assert_eq!(cpu.registers[0], DATA);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pc_load_uses_old_pc_snapshot_then_resamples_at_the_branch_target() {
|
||||
let mut bus = memory(0xe590_f000); // LDR pc, [r0]
|
||||
bus.write32(RAM, 0xe590_f000).unwrap();
|
||||
bus.write32(RAM + 8, RAM + 0x40).unwrap();
|
||||
bus.write32(RAM + 0x40, LDR).unwrap();
|
||||
bus.write32(RAM + 0x48, DATA).unwrap();
|
||||
let mut cpu = Cpu::new(RAM);
|
||||
cpu.registers[0] = UNUSED;
|
||||
cpu.step(&mut bus).unwrap();
|
||||
assert_eq!(cpu.pc(), RAM + 0x40);
|
||||
cpu.step(&mut bus).unwrap();
|
||||
assert_eq!(cpu.registers[1], DATA);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_prefetch_only_errors_when_an_unused_read_needs_it() {
|
||||
for size in 4..12 {
|
||||
let mut rom = vec![0; size];
|
||||
rom[..4].copy_from_slice(&LDR.to_le_bytes());
|
||||
let mut bus = Memory::new(rom).unwrap();
|
||||
bus.write32(RAM, DATA).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = UNUSED;
|
||||
let before = cpu.clone();
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut bus),
|
||||
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
|
||||
);
|
||||
assert_eq!(cpu, before);
|
||||
assert_eq!(bus.read32(UNUSED), Err(MemoryError::Unmapped(UNUSED)));
|
||||
cpu.registers[0] = RAM;
|
||||
let timing = cpu.step_timed(&mut bus).unwrap();
|
||||
assert_eq!(cpu.registers[1], DATA);
|
||||
assert_eq!(timing.data_cycles, 6); // No trace leaked from failed step.
|
||||
}
|
||||
// PC+8 is itself unused. Do not recursively manufacture a prefetch word.
|
||||
let mut bios = vec![0; BIOS_SIZE];
|
||||
bios[BIOS_SIZE - 8..BIOS_SIZE - 4].copy_from_slice(&LDR.to_le_bytes());
|
||||
let mut bus = Memory::with_bios(vec![], bios).unwrap();
|
||||
let mut cpu = Cpu::new(BIOS_SIZE as u32 - 8);
|
||||
cpu.registers[0] = UNUSED;
|
||||
assert_eq!(
|
||||
cpu.step(&mut bus),
|
||||
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skipped_loads_need_no_prefetch_and_charge_no_data_cycles() {
|
||||
let mut bus = Memory::new((LDR & 0x0fff_ffff).to_le_bytes().to_vec()).unwrap(); // LDREQ
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = UNUSED;
|
||||
let timing = cpu.step_timed(&mut bus).unwrap();
|
||||
assert_eq!(cpu.pc(), ROM_START + 4);
|
||||
assert_eq!(cpu.registers[1], 0);
|
||||
assert_eq!(timing.data_cycles, 0);
|
||||
assert_eq!(timing.internal_cycles, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_bios_rom_save_and_unsupported_io_reads_remain_errors() {
|
||||
for address in [
|
||||
0,
|
||||
0x3ffc,
|
||||
0x0400_00e0,
|
||||
ROM_START + 12,
|
||||
0x0e00_0000,
|
||||
0x0fff_fffc,
|
||||
] {
|
||||
let mut bus = memory(LDR);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = address;
|
||||
let before = cpu.clone();
|
||||
assert_eq!(
|
||||
cpu.step(&mut bus),
|
||||
Err(CpuError::Memory(MemoryError::Unmapped(address)))
|
||||
);
|
||||
assert_eq!(cpu, before);
|
||||
assert_eq!(bus.read32(UNUSED), Err(MemoryError::Unmapped(UNUSED)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thumb_reads_and_unused_instruction_fetches_do_not_reuse_arm_context() {
|
||||
let mut bus = memory(LDR);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = UNUSED;
|
||||
cpu.step(&mut bus).unwrap();
|
||||
// A successful ARM read must not leave a context for later fetches or Thumb.
|
||||
let mut invalid = Cpu::new(UNUSED);
|
||||
assert_eq!(
|
||||
invalid.step(&mut bus),
|
||||
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
|
||||
);
|
||||
bus.write16(RAM, 0x6801).unwrap(); // Thumb LDR r1, [r0]
|
||||
cpu.registers[15] = RAM;
|
||||
cpu.instruction_set = InstructionSet::Thumb;
|
||||
let before = cpu.clone();
|
||||
assert_eq!(
|
||||
cpu.step(&mut bus),
|
||||
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
|
||||
);
|
||||
assert_eq!(cpu, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unused_stores_and_swaps_remain_atomic_diagnostics() {
|
||||
for instruction in [0xe580_1000, 0xe5c0_1000, 0xe1c0_10b0, 0xe100_1092] {
|
||||
let mut bus = memory(instruction); // STR, STRB, STRH, SWP
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = UNUSED;
|
||||
cpu.registers[1] = 0x1234;
|
||||
let before = cpu.clone();
|
||||
assert_eq!(
|
||||
cpu.step(&mut bus),
|
||||
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
|
||||
);
|
||||
assert_eq!(cpu, before);
|
||||
assert_eq!(bus.read32(UNUSED), Err(MemoryError::Unmapped(UNUSED)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn machine_charges_one_data_access_and_dma_does_not_inherit_cpu_context() {
|
||||
let mut bus = memory(LDR);
|
||||
bus.write32(TIMER_BASE, 0x0080_0000).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = UNUSED;
|
||||
let mut machine = Machine::new(cpu, bus);
|
||||
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||
assert_eq!(machine.cpu().registers[1], DATA);
|
||||
assert_eq!(machine.cycles(), 10); // 8 code + 1 data + 1 internal
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 10);
|
||||
let before = machine.cpu().clone();
|
||||
let timing = machine.last_timing();
|
||||
// DMA3 exposes all 28 source address bits. Save memory is still unsupported.
|
||||
let dma = DMA_BASE + 3 * DMA_STRIDE;
|
||||
machine.memory_mut().write32(dma, 0x0e00_0000).unwrap();
|
||||
machine.memory_mut().write32(dma + 4, RAM).unwrap();
|
||||
machine.memory_mut().write32(dma + 8, 0x8400_0001).unwrap();
|
||||
assert_eq!(
|
||||
machine.step(),
|
||||
Err(MachineError::Dma(DmaError::Memory {
|
||||
channel: 3,
|
||||
error: MemoryError::Unmapped(0x0e00_0000),
|
||||
}))
|
||||
);
|
||||
assert_eq!(machine.cpu(), &before);
|
||||
assert_eq!(machine.cycles(), 10);
|
||||
assert_eq!(machine.last_timing(), timing);
|
||||
assert_eq!(machine.memory().read32(RAM).unwrap(), 0);
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
use std::{cell::Cell, error::Error, fmt};
|
||||
|
||||
use crate::{
|
||||
cpu::InstructionSet,
|
||||
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
|
||||
dma::{DmaError, DMA_BASE, DMA_END},
|
||||
input::Buttons,
|
||||
|
|
@ -52,8 +53,15 @@ fn vram_index(address: u32) -> usize {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct CpuAccess {
|
||||
pc: u32,
|
||||
arm_prefetch: Option<u32>,
|
||||
}
|
||||
|
||||
/// Work/video/palette RAM, OAM, cartridge ROM, optional BIOS, and supported I/O.
|
||||
/// BIOS read protection and open-bus behavior are not modeled.
|
||||
/// ARM data reads from unused memory return a PC+8 snapshot when available.
|
||||
/// BIOS protection, Thumb open bus, and DMA bus latches are not modeled.
|
||||
pub struct Memory {
|
||||
external_ram: Vec<u8>,
|
||||
internal_ram: Vec<u8>,
|
||||
|
|
@ -65,7 +73,7 @@ pub struct Memory {
|
|||
io: Io,
|
||||
cycles: u64,
|
||||
data_timing: Cell<Option<DataTiming>>,
|
||||
cpu_access: Option<u32>,
|
||||
cpu_access: Option<CpuAccess>,
|
||||
scanline_capture: Option<video::capture::Capture>,
|
||||
sprite_pipeline: pipeline::SpritePipeline,
|
||||
}
|
||||
|
|
@ -317,9 +325,25 @@ impl Memory {
|
|||
self.io.next_event_cycles()
|
||||
}
|
||||
|
||||
pub(crate) fn begin_cpu_access(&mut self, pc: u32) {
|
||||
pub(crate) fn begin_cpu_access(&mut self, pc: u32, instruction_set: InstructionSet) {
|
||||
debug_assert!(self.cpu_access.is_none());
|
||||
self.cpu_access = Some(pc);
|
||||
// Sample before execution, without data timing or open-bus recursion.
|
||||
// Missing lookahead bytes must not fail an instruction that never uses them.
|
||||
let arm_prefetch = match instruction_set {
|
||||
InstructionSet::Arm => self.snapshot_arm_prefetch(pc),
|
||||
InstructionSet::Thumb => None,
|
||||
};
|
||||
self.cpu_access = Some(CpuAccess { pc, arm_prefetch });
|
||||
}
|
||||
|
||||
fn snapshot_arm_prefetch(&self, pc: u32) -> Option<u32> {
|
||||
let mut bytes = [0; 4];
|
||||
for (offset, byte) in bytes.iter_mut().enumerate() {
|
||||
*byte = self
|
||||
.read_mapped_byte(pc.wrapping_add(8 + offset as u32))
|
||||
.ok()?;
|
||||
}
|
||||
Some(u32::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
pub(crate) fn end_cpu_access(&mut self) {
|
||||
|
|
@ -328,7 +352,8 @@ impl Memory {
|
|||
|
||||
fn can_write_power_control(&self) -> bool {
|
||||
// Bare memory writes are host/debug setup. CPU code must execute in BIOS.
|
||||
self.cpu_access.is_none_or(|pc| pc < BIOS_SIZE as u32)
|
||||
self.cpu_access
|
||||
.is_none_or(|access| access.pc < BIOS_SIZE as u32)
|
||||
}
|
||||
|
||||
/// Current scanline position and frame/VBlank event counters.
|
||||
|
|
@ -390,6 +415,16 @@ impl Memory {
|
|||
|
||||
// Multi-byte reads use this helper to avoid charging each byte as a bus access.
|
||||
fn read_byte(&self, address: u32) -> Result<u8, MemoryError> {
|
||||
if matches!(address, 0x0000_4000..=0x01ff_ffff | 0x1000_0000..=0xffff_ffff) {
|
||||
if let Some(word) = self.cpu_access.and_then(|access| access.arm_prefetch) {
|
||||
return Ok((word >> ((address & 3) * 8)) as u8);
|
||||
}
|
||||
}
|
||||
self.read_mapped_byte(address)
|
||||
}
|
||||
|
||||
// Strict lookup for host access, instruction fetches, and prefetch snapshots.
|
||||
fn read_mapped_byte(&self, address: u32) -> Result<u8, MemoryError> {
|
||||
match address >> 24 {
|
||||
0x00 => self
|
||||
.bios
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue