feat(memory): add region-dependent Thumb open-bus snapshots

Repeat the mapped PC+4 halfword for Thumb code in EWRAM, palette RAM, VRAM, and ROM. Use the aligned PC+4 word for BIOS/OAM. Keep retained BIOS history separate from current unused-memory reads. Reuse one instruction snapshot without extra data cycles or host/fetch/DMA fallbacks.

Keep IWRAM and 16 MiB region-crossing fetches diagnostic until prior bus history is modeled. Add original regressions for regions, alignments, mirrors, physical wrap, signed and unaligned loads, block/stack/PC loads, aliases, modes, short ROMs, strict errors, BIOS isolation, and nominal device progression. Record documented rules and remaining limits.

Darwin arm64 validation: 890 workspace tests and 821 core/demo tests in each of debug and release; 39 preparation tests; native ROM windows in both profiles; ten window/graphics smoke modes; fmt, clippy, and rustdoc. Public ARM, Thumb, memory, and BIOS reports remain identical to previous passing results and between profiles. No physical-hardware or independent public Thumb open-bus conformance claim.
This commit is contained in:
0xrsydn 2026-10-06 01:25:41 +07:00
commit 8360ba4652
11 changed files with 528 additions and 36 deletions

View file

@ -32,6 +32,8 @@ mod tests;
#[cfg(test)] #[cfg(test)]
mod thumb_empty_tests; mod thumb_empty_tests;
#[cfg(test)] #[cfg(test)]
mod thumb_open_bus_tests;
#[cfg(test)]
mod thumb_tests; mod thumb_tests;
#[cfg(test)] #[cfg(test)]
mod timing_tests; mod timing_tests;

View file

@ -233,7 +233,7 @@ fn missing_bios_rom_save_and_unsupported_io_reads_remain_errors() {
} }
#[test] #[test]
fn thumb_reads_and_unused_instruction_fetches_do_not_reuse_arm_context() { fn unsupported_iwram_thumb_reads_and_unused_fetches_do_not_reuse_arm_context() {
let mut bus = memory(LDR); let mut bus = memory(LDR);
let mut cpu = Cpu::new(ROM_START); let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = UNUSED; cpu.registers[0] = UNUSED;
@ -244,8 +244,8 @@ fn thumb_reads_and_unused_instruction_fetches_do_not_reuse_arm_context() {
invalid.step(&mut bus), invalid.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED))) Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
); );
bus.write16(RAM, 0x6801).unwrap(); // Thumb LDR r1, [r0] bus.write16(0x0300_0100, 0x6801).unwrap(); // IWRAM Thumb LDR r1,[r0]
cpu.registers[15] = RAM; cpu.registers[15] = 0x0300_0100;
cpu.instruction_set = InstructionSet::Thumb; cpu.instruction_set = InstructionSet::Thumb;
let before = cpu.clone(); let before = cpu.clone();
assert_eq!( assert_eq!(

View file

@ -238,11 +238,13 @@ fn failed_thumb_steps_keep_previous_history_and_clear_access_context() {
} }
#[test] #[test]
fn outside_thumb_execution_keeps_history_but_does_not_enable_general_open_bus() { fn outside_thumb_execution_keeps_bios_history_without_enabling_iwram_open_bus() {
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(); // IWRAM needs unmodeled history.
cpu.registers[15] = 0x0300_0100;
cpu.registers[0] = 0x4000; cpu.registers[0] = 0x4000;
let before = cpu.clone(); let before = cpu.clone();
assert_eq!( assert_eq!(

View file

@ -0,0 +1,435 @@
//! Original tests for region-dependent Thumb unused-memory snapshots.
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: u16 = 0x6801; // LDR r1,[r0]
const REGIONS: [u32; 11] = [
0x100,
RAM,
0x0500_0100,
0x0600_0100,
0x0700_0100,
ROM_START,
0x0900_0000,
0x0a00_0000,
0x0b00_0000,
0x0c00_0000,
0x0d00_0000,
];
fn cpu(pc: u32) -> Cpu {
let mut cpu = Cpu::new(pc);
cpu.instruction_set = InstructionSet::Thumb;
cpu.registers[0] = UNUSED;
cpu
}
fn program(pc: u32, instruction: u16) -> Memory {
let base = pc & !3;
let mut code: Vec<u8> = [0x46c0_46c0_u32, DATA, 0xdead_beef]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect();
let offset = (pc & 2) as usize;
code[offset..offset + 2].copy_from_slice(&instruction.to_le_bytes());
if pc < BIOS_SIZE as u32 {
let mut bios = vec![0; BIOS_SIZE];
bios[base as usize..base as usize + code.len()].copy_from_slice(&code);
Memory::with_bios(vec![], bios).unwrap()
} else if pc >= ROM_START {
let offset = (base & 0x01ff_ffff) as usize;
let mut rom = vec![0; offset + code.len()];
rom[offset..].copy_from_slice(&code);
Memory::new(rom).unwrap()
} else {
let mut bus = Memory::new(vec![]).unwrap();
for (index, bytes) in code.chunks_exact(2).enumerate() {
bus.write16(
base + index as u32 * 2,
u16::from_le_bytes(bytes.try_into().unwrap()),
)
.unwrap();
}
bus
}
}
// Independent expected words for the bytes written above.
fn expected(pc: u32) -> u32 {
match pc >> 24 {
0 | 7 => DATA,
_ if pc & 2 == 0 => 0x80a5_80a5,
_ => 0xf233_f233,
}
}
#[test]
fn every_supported_region_and_alignment_supplies_its_own_bus_width() {
for base in REGIONS {
for offset in [0, 2] {
let pc = base + offset;
let mut bus = program(pc, LDR);
let mut cpu = cpu(pc);
let timing = cpu.step_timed(&mut bus).unwrap();
assert_eq!(cpu.registers[1], expected(pc), "pc={pc:#x}");
assert_eq!(cpu.pc(), pc + 2);
assert_eq!(
timing.code_cycles,
bus_cycles(0, pc, AccessWidth::Halfword, AccessKind::Sequential)
);
assert_eq!(timing.data_cycles, 1);
assert_eq!(timing.internal_cycles, 1);
assert_eq!(bus.cycles(), 0);
}
}
}
#[test]
fn loads_select_lanes_rotate_and_sign_extend_without_changing_flags() {
for base in [0x100, RAM, 0x0700_0100, ROM_START] {
for offset in [0, 2] {
let pc = base + offset;
let word = expected(pc);
for (instruction, width, signed) in [
(LDR, 4, false),
(0x5881, 4, false), // Immediate/register word loads.
(0x7801, 1, false),
(0x8801, 2, false),
(0x5681, 1, true),
(0x5e81, 2, true),
] {
for lane in 0..4 {
let byte = (word >> (lane * 8)) as u8;
let half = (word >> ((lane & 2) * 8)) as u16;
let value = match (width, signed) {
(4, _) => word.rotate_right(lane * 8),
(1, false) => u32::from(byte),
(1, true) => byte as i8 as i32 as u32,
(2, false) => u32::from(half).rotate_right((lane & 1) * 8),
(2, true) if lane & 1 != 0 => byte as i8 as i32 as u32,
(2, true) => half as i16 as i32 as u32,
_ => unreachable!(),
};
let mut bus = program(pc, instruction);
let mut cpu = cpu(pc);
cpu.apply_status(0xb000_00ff, Mode::System);
cpu.registers[0] = UNUSED + lane;
let mut after = cpu.clone();
after.registers[1] = value;
after.registers[15] += 2;
cpu.step(&mut bus).unwrap();
assert_eq!(
cpu, after,
"pc={pc:#x} instruction={instruction:#x} lane={lane}"
);
}
}
}
}
}
#[test]
fn unused_range_edges_are_not_mirrors_and_host_reads_remain_strict() {
for address in [
0x4000,
0x00ff_fffc,
0x0100_0000,
0x01ff_fffc,
0x1000_0000,
0x1800_0000,
UNUSED,
0xffff_fffc,
] {
let mut bus = program(ROM_START, LDR);
let mut cpu = cpu(ROM_START);
cpu.registers[0] = address;
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[1], 0x80a5_80a5);
for lane in 0..4 {
assert_eq!(
bus.read8(address + lane),
Err(MemoryError::Unmapped(address + lane))
);
}
assert_eq!(bus.read16(address), Err(MemoryError::Unmapped(address)));
assert_eq!(bus.read32(address), Err(MemoryError::Unmapped(address)));
let mut invalid = super::Cpu::new(address);
invalid.instruction_set = InstructionSet::Thumb;
assert_eq!(
invalid.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(address)))
);
}
}
#[test]
fn mirrored_code_and_physical_memory_wrap_use_mapped_fetch_bytes() {
for base in [
0x0204_0100,
0x0203_fffc,
0x0500_0500,
0x0500_03fc,
0x0601_8100,
0x0601_fffc,
0x0700_0500,
0x0700_03fc,
] {
for offset in [0, 2] {
let pc = base + offset;
let mut bus = program(pc, LDR);
let mut cpu = cpu(pc);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[1], expected(pc), "pc={pc:#x}");
}
}
}
#[test]
fn block_and_stack_loads_share_snapshot_and_keep_writeback_semantics() {
for pc in [RAM, RAM + 2, 0x0700_0100, 0x0700_0102] {
for instruction in [0xc80e, 0xc803, 0xbc06, 0xbd00, 0xc800] {
let mut bus = program(pc, instruction);
let mut cpu = cpu(pc);
cpu.registers[0] = UNUSED + 3;
cpu.registers[13] = UNUSED + 3;
let timing = cpu.step_timed(&mut bus).unwrap();
let value = expected(pc);
match instruction {
0xc80e => {
assert_eq!(&cpu.registers[1..4], &[value; 3]);
assert_eq!(cpu.registers[0], UNUSED + 15);
assert_eq!(timing.data_cycles, 3);
}
0xc803 => {
assert_eq!(&cpu.registers[..2], &[value; 2]); // Base in list: no writeback.
assert_eq!(timing.data_cycles, 2);
}
0xbc06 => {
assert_eq!(&cpu.registers[1..3], &[value; 2]);
assert_eq!(cpu.registers[13], UNUSED + 11);
assert_eq!(timing.data_cycles, 2);
}
0xbd00 => {
assert_eq!(cpu.pc(), value & !1);
assert_eq!(cpu.registers[13], UNUSED + 7);
assert_eq!(timing.data_cycles, 1);
}
0xc800 => {
assert_eq!(cpu.pc(), value & !1);
assert_eq!(cpu.registers[0], UNUSED + 67);
assert_eq!(timing.data_cycles, 1);
}
_ => unreachable!(),
}
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
}
let mut bus = program(pc, 0x6800); // LDR r0,[r0], base/destination alias.
let mut cpu = cpu(pc);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], expected(pc));
}
}
#[test]
fn sequential_execution_and_branches_resample_instead_of_reusing_old_context() {
let mut bus = program(RAM, LDR);
bus.write16(RAM + 2, 0x6802).unwrap(); // LDR r2,[r0]
bus.write16(RAM + 4, 0x4718).unwrap(); // BX r3
bus.write16(0x0700_0100, LDR).unwrap();
bus.write32(0x0700_0104, DATA).unwrap();
let mut cpu = cpu(RAM);
cpu.registers[3] = 0x0700_0101;
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[1], 0x4718_4718);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[2], 0xf233_f233);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.pc(), 0x0700_0100);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[1], DATA);
}
#[test]
fn short_rom_needs_only_the_fetched_halfword_and_errors_only_when_used() {
for offset in [0, 2] {
for size in offset + 2..=offset + 6 {
let mut rom = vec![0; size];
rom[offset..offset + 2].copy_from_slice(&LDR.to_le_bytes());
if size == offset + 6 {
rom[offset + 4..].copy_from_slice(&0xabcd_u16.to_le_bytes());
}
let mut bus = Memory::new(rom).unwrap();
bus.write32(RAM, DATA).unwrap();
let mut cpu = cpu(ROM_START + offset as u32);
if size == offset + 6 {
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[1], 0xabcd_abcd);
} else {
let before = cpu.clone();
assert_eq!(
cpu.step_timed(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
assert_eq!(cpu, before);
cpu.registers[0] = RAM;
let timing = cpu.step_timed(&mut bus).unwrap();
assert_eq!(cpu.registers[1], DATA);
assert_eq!(timing.data_cycles, 6);
}
}
}
let mut bus = Memory::new(0x46c0_u16.to_le_bytes().to_vec()).unwrap();
let mut cpu = cpu(ROM_START);
assert_eq!(cpu.step_timed(&mut bus).unwrap().data_cycles, 0);
}
#[test]
fn iwram_history_and_region_crossing_fetches_remain_diagnostics() {
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);
let before = cpu.clone();
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
assert_eq!(cpu, before);
bus.write32(RAM, DATA).unwrap();
cpu.registers[0] = RAM;
cpu.step(&mut bus).unwrap(); // Missing snapshot must not break ordinary loads.
assert_eq!(cpu.registers[1], DATA);
}
}
for pc in [0x3ffc, 0x3ffe] {
let mut bios = vec![0; BIOS_SIZE];
bios[pc..pc + 2].copy_from_slice(&LDR.to_le_bytes());
let mut bus = Memory::with_bios(vec![], bios).unwrap();
let mut cpu = cpu(pc as u32);
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
}
}
#[test]
fn unsupported_data_regions_and_unused_stores_do_not_gain_fallbacks() {
for address in [
0,
0x3ffc,
0x0400_00e0,
ROM_START + 12,
0x0e00_0000,
0x0fff_fffc,
] {
let mut bus = program(ROM_START, LDR);
let mut cpu = cpu(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);
}
for instruction in [0x6001, 0x7001, 0x8001, 0xc006] {
// STR, STRB, STRH, STMIA
let mut bus = program(ROM_START, instruction);
let mut cpu = cpu(ROM_START);
cpu.registers[1] = DATA;
let before = cpu.clone();
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
assert_eq!(cpu, before);
}
}
#[test]
fn mode_flags_and_sp_relative_addressing_do_not_select_the_open_bus_rule() {
for mode in [
Mode::User,
Mode::System,
Mode::Fiq,
Mode::Irq,
Mode::Supervisor,
Mode::Abort,
Mode::Undefined,
] {
for pc in [ROM_START, ROM_START + 2, 0x0700_0100] {
let mut bus = program(pc, 0x9900); // LDR r1,[sp]
let mut cpu = cpu(pc);
cpu.apply_status(0xb000_00e0 | mode as u32, mode);
cpu.registers[13] = UNUSED;
let status = cpu.cpsr();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[1], expected(pc));
assert_eq!(cpu.cpsr(), status);
assert_eq!(cpu.registers[13], UNUSED);
}
}
}
#[test]
fn retained_bios_word_is_separate_from_current_thumb_unused_memory_snapshot() {
let mut bios = vec![0; BIOS_SIZE];
bios[0x100..0x104].copy_from_slice(&0xe12f_ff14_u32.to_le_bytes()); // BX r4
bios[0x108..0x10c].copy_from_slice(&DATA.to_le_bytes());
let rom: Vec<_> = [0x6802_6801_u32, 0x1234_5678]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect();
let mut bus = Memory::with_bios(rom, bios).unwrap();
let mut cpu = Cpu::new(0x100);
cpu.registers[4] = ROM_START | 1;
cpu.step(&mut bus).unwrap();
cpu.step(&mut bus).unwrap(); // LDR r1,[r0=0]: retained ARM BIOS word.
assert_eq!(cpu.registers[1], DATA);
cpu.registers[0] = UNUSED;
cpu.step(&mut bus).unwrap(); // LDR r2,[r0]: current Thumb ROM halfword.
assert_eq!(cpu.registers[2], 0x1234_1234);
assert_eq!(bus.read32(0x108).unwrap(), DATA);
}
#[test]
fn machine_timing_and_dma_remain_separate_from_thumb_snapshot_reads() {
let mut bus = program(ROM_START, LDR);
bus.write32(TIMER_BASE, 0x0080_0000).unwrap();
let mut machine = Machine::new(cpu(ROM_START), bus);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[1], 0x80a5_80a5);
assert_eq!(machine.last_timing().code_cycles, 5); // ROM boundary: 1 + 4 wait cycles.
assert_eq!(machine.last_timing().data_cycles, 1);
assert_eq!(machine.last_timing().internal_cycles, 1);
assert_eq!(machine.cycles(), 7);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 7);
let before = machine.cpu().clone();
let timing = machine.last_timing();
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(), 7);
assert_eq!(machine.last_timing(), timing);
assert_eq!(machine.memory().read32(RAM).unwrap(), 0);
}

View file

@ -56,14 +56,13 @@ fn vram_index(address: u32) -> usize {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
struct CpuAccess { struct CpuAccess {
pc: u32, pc: u32,
arm_prefetch: Option<u32>, prefetch: Option<u32>,
bios_prefetch: Option<u32>,
} }
/// 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.
/// ARM data reads from unused memory return a PC+8 snapshot when available. /// Unused-memory reads use ARM PC+8 or supported region-dependent Thumb snapshots.
/// Protected BIOS reads retain an ARM PC+8 or aligned Thumb PC+4 word snapshot. /// Protected BIOS reads separately retain the snapshot from BIOS execution.
/// General Thumb open bus, a full fetch pipeline, and DMA bus latches are not modeled. /// Thumb IWRAM history, a full fetch pipeline, and DMA bus latches are not modeled.
pub struct Memory { pub struct Memory {
external_ram: Vec<u8>, external_ram: Vec<u8>,
internal_ram: Vec<u8>, internal_ram: Vec<u8>,
@ -333,25 +332,34 @@ impl Memory {
debug_assert!(self.cpu_access.is_none()); debug_assert!(self.cpu_access.is_none());
// Sample before execution, without data timing or open-bus recursion. // Sample before execution, without data timing or open-bus recursion.
// Missing lookahead bytes must not fail an instruction that never uses them. // Missing lookahead bytes must not fail an instruction that never uses them.
let arm_prefetch = match instruction_set { let prefetch = match instruction_set {
InstructionSet::Arm => self.snapshot_mapped_word(pc.wrapping_add(8)), InstructionSet::Arm => self.snapshot_mapped_word(pc.wrapping_add(8)),
InstructionSet::Thumb => None, InstructionSet::Thumb => self.snapshot_thumb_prefetch(pc),
}; };
let bios_prefetch = if pc < BIOS_SIZE as u32 { self.cpu_access = Some(CpuAccess { pc, prefetch });
match instruction_set { }
InstructionSet::Arm => arm_prefetch,
// BIOS drives a full 32-bit word for a Thumb halfword fetch. fn snapshot_thumb_prefetch(&self, pc: u32) -> Option<u32> {
// Preserve both lanes of the aligned PC+4 word, not a repeated halfword. let address = pc.wrapping_add(4);
InstructionSet::Thumb => self.snapshot_mapped_word((pc + 4) & !3), // Region-crossing fetches need pipeline history, not a rule chosen from the old PC.
if address >> 24 != pc >> 24 {
return None;
}
match pc >> 24 {
// BIOS and OAM drive a full word even for a halfword instruction fetch.
0x00 | 0x07 => self.snapshot_mapped_word(address & !3),
// These 16-bit regions repeat the fetched halfword in both word lanes.
0x02 | 0x05 | 0x06 | 0x08..=0x0d => {
let half = u16::from_le_bytes([
self.read_mapped_byte(address).ok()?,
self.read_mapped_byte(address + 1).ok()?,
]);
Some(u32::from(half) * 0x0001_0001)
} }
} else { // IWRAM needs the previous bus value, including intervening data loads/DMA.
None // Unsupported code regions must not manufacture an open-bus snapshot.
}; _ => None,
self.cpu_access = Some(CpuAccess { }
pc,
arm_prefetch,
bios_prefetch,
});
} }
fn snapshot_mapped_word(&self, address: u32) -> Option<u32> { fn snapshot_mapped_word(&self, address: u32) -> Option<u32> {
@ -369,7 +377,7 @@ impl Memory {
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.
// Missing lookahead invalidates history rather than exposing stale bytes. // Missing lookahead invalidates history rather than exposing stale bytes.
self.bios_prefetch = access.bios_prefetch; self.bios_prefetch = access.prefetch;
} }
} }
} }
@ -452,7 +460,7 @@ impl Memory {
.ok_or(MemoryError::Unmapped(address)); .ok_or(MemoryError::Unmapped(address));
} }
if matches!(address, 0x0000_4000..=0x01ff_ffff | 0x1000_0000..=0xffff_ffff) { if matches!(address, 0x0000_4000..=0x01ff_ffff | 0x1000_0000..=0xffff_ffff) {
if let Some(word) = self.cpu_access.and_then(|access| access.arm_prefetch) { if let Some(word) = self.cpu_access.and_then(|access| access.prefetch) {
return Ok((word >> ((address & 3) * 8)) as u8); return Ok((word >> ((address & 3) * 8)) as u8);
} }
} }

View file

@ -29,6 +29,7 @@ The repository is a Cargo workspace with three crates:
| `crates/gba-core/src/cpu/load_alias_tests.rs` | Original single-load alias regressions for indexing, banks, widths, alignment, I/O, timing, and diagnostics | | `crates/gba-core/src/cpu/load_alias_tests.rs` | Original single-load alias regressions for indexing, banks, widths, alignment, I/O, timing, 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/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/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

@ -97,7 +97,7 @@ Host inspection, ROM-suite memory assertions, instruction fetches, and DMA do no
Limits remain explicit: Limits remain explicit:
- Thumb open bus depends on instruction region, alignment, and prior bus data; it remains unsupported. - Thumb open bus uses the supported region rules below. Internal work RAM still needs unmodeled bus 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, DMA latches, and disabled-RAM reads are not modeled here. - Unused/write-only I/O reads, DMA latches, and disabled-RAM reads are not modeled here.
- Missing BIOS, truncated ROM, unsupported I/O, and save-memory accesses retain their existing diagnostics. - Missing BIOS, truncated ROM, unsupported I/O, and save-memory accesses retain their existing diagnostics.
@ -107,6 +107,42 @@ Limits remain explicit:
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).
## Thumb unused-memory data reads
Thumb data reads use the same unused address ranges and lane rules as ARM reads above.
The snapshot depends on the executing code region. P below is the executing instruction address, not the visible r15 operand.
| Code region | Snapshot |
| --- | --- |
| 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` |
| Internal work RAM (IWRAM) | Unsupported: one lane depends on previous bus activity |
| Other code regions | Unsupported |
For BIOS/OAM, aligned instructions expose halfwords at P+4 and P+6.
Instructions at two-byte-only alignment expose halfwords at P+2 and P+4.
The two lanes can differ. A universal repeated-halfword rule would be incorrect.
Only the required mapped bytes are sampled. A 16-bit region needs two lookahead bytes, not four.
Normal memory mirrors apply to these bytes, including physical RAM/video-memory wrap within a mapped region.
If P+4 crosses a 16 MiB address-region boundary, the snapshot remains unknown.
This conservative diagnostic policy avoids guessing pipeline behavior during a region transition.
Missing lookahead or unknown history fails only an unused-memory load; ordinary instructions and mapped loads can still execute.
All loads in one instruction share the snapshot, including block loads and stack loads.
Normal sign extension, unaligned-load rotation, writeback, register aliases, and PC-load semantics still apply.
Snapshots add no nominal cycles. Host reads, instruction fetches, DMA, and unsupported stores do not gain fallback behavior.
Protected BIOS reads use separate retained history, not the current ROM/RAM snapshot.
GBATEK and nocash's [open-bus findings](https://www.ngemu.com/threads/gba-open-bus.170809/) document these region differences.
IWRAM needs prior bus lanes, including data-load overwrites and possible DMA effects.
Do not substitute P+2 for that history: it is only the usual case, not a general rule.
The original Nintendo DS also differs from GBA-family IWRAM behavior; this core targets GBA.
This remains a bounded snapshot implementation, not a pipeline or a complete bus-history model.
Refill timing, self-modifying code, DMA-to-CPU transitions, IWRAM, 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.
## Processor status and exceptions ## Processor status and exceptions
User and System share registers. Each exception mode has its own stack pointer, link register, and SPSR. User and System share registers. Each exception mode has its own stack pointer, link register, and SPSR.

View file

@ -50,8 +50,8 @@ 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. - [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 and region-transition history remain unsupported.
- [nocash GBA open-bus discussion](https://www.ngemu.com/threads/gba-open-bus.170809/), for the distinction between ARM prefetch and region-dependent Thumb values. - [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).
- [GBATEK GBA DMA transfers](https://problemkaputt.de/gbatek-gba-dma-transfers.htm). - [GBATEK GBA DMA transfers](https://problemkaputt.de/gbatek-gba-dma-transfers.htm).

View file

@ -47,7 +47,8 @@ Host reads remain raw inspection. CPU data loads outside BIOS select lanes from
Snapshots add no data accesses or nominal cycles. Snapshots add no data accesses or nominal cycles.
The snapshot does not create a persistent instruction pipeline. The snapshot does not create a persistent instruction pipeline.
It does not model refill-time latches, BIOS data-access ordering within an instruction, DMA bus transitions, or general Thumb open bus. It does not model refill-time latches, BIOS data-access ordering within an instruction, or DMA bus transitions.
A subsequent [Thumb open-bus subset](../hardware/cpu.md#thumb-unused-memory-data-reads) adds region-dependent snapshots outside protected BIOS reads.
At BIOS boundaries, unavailable lookahead invalidates history under the existing diagnostic policy. At BIOS boundaries, unavailable lookahead invalidates history under the existing diagnostic policy.
A branch can still succeed; a later protected read reports unknown history rather than exposing stale bytes. A branch can still succeed; a later protected read reports unknown history rather than exposing stale bytes.
This boundary policy is conservative, not a hardware claim. This boundary policy is conservative, not a hardware claim.
@ -61,7 +62,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, unchanged general Thumb open-bus diagnostics, nominal costs, and device progression. - Separate host reads, retained IWRAM Thumb open-bus diagnostics, nominal costs, and device progression.
## Validation result ## Validation result

View file

@ -72,6 +72,7 @@ Update this file when a feature lands or a limit is removed.
- The original BIOS exposes documented boot, SoftReset, SWI, and IRQ protected-read words through its actual exit layout. - The original BIOS exposes documented boot, SoftReset, SWI, and IRQ protected-read words through its actual exit layout.
- 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.
- 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.
@ -130,7 +131,8 @@ 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).
This is not a fetch pipeline. General Thumb, DMA, and unused/write-only I/O open bus remain unmodeled. Thumb reads use [region-dependent snapshots](hardware/cpu.md#thumb-unused-memory-data-reads) where the documented value needs no prior bus history.
This is not a fetch pipeline. Thumb IWRAM, region-crossing fetches, DMA, and unused/write-only I/O open bus remain unsupported.
BIOS protection uses separate retained BIOS fetch snapshots as described in [BIOS behavior](hardware/bios.md#cpu-bios-read-protection). BIOS protection uses separate retained BIOS fetch snapshots as described in [BIOS behavior](hardware/bios.md#cpu-bios-read-protection).
Host inspection and instruction fetches remain strict. Other unmapped reads return errors. Host inspection and instruction fetches remain strict. Other unmapped reads return errors.
Reads beyond the supplied cartridge bytes also return errors. Reads beyond the supplied cartridge bytes also return errors.
@ -140,7 +142,8 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa
## Next steps ## Next steps
1. Extend general open-bus support with documented Thumb region/alignment rules and explicit limits for history-dependent cases. 1. Add validated prior-bus history for Thumb IWRAM open bus, including data-load overwrites and DMA transitions.
Keep unknown history diagnostic until those interactions 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.
The desktop runner still uses only the original BIOS. Keep protected reads image-derived for all supplied images. The desktop runner still uses only the original BIOS. Keep protected reads image-derived for all supplied images.
2. Refine nominal timing with a fetch pipeline, Game Pak prefetch, per-access device updates, and verified timer/IRQ delays. 2. Refine nominal timing with a fetch pipeline, Game Pak prefetch, per-access device updates, and verified timer/IRQ delays.

View file

@ -65,7 +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/Thumb/DMA diagnostics. They check branch-target resampling, missing lookahead, skipped conditions, nominal timing, device progress, and retained host/fetch/IWRAM-Thumb/DMA diagnostics.
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.
Short-ROM tests require only the fetched halfword in 16-bit regions. IWRAM and region-crossing tests retain explicit diagnostics.
Host/fetch isolation, separate BIOS history, unsupported stores/data regions, nominal device progression, and DMA diagnostics also have checks.
The pinned public ARM ROM now reaches its completion checkpoint with r12 = 0 in debug and release builds. The pinned public ARM ROM now reaches its completion checkpoint with r12 = 0 in debug and release builds.
The pinned public Thumb ROM also reaches its ARM-state checkpoint with r7 = 0 in both builds. The pinned public Thumb ROM also reaches its ARM-state checkpoint with r7 = 0 in both builds.
Empty-list Thumb regressions cover PC+6, all low bases and processor modes, banked stacks, both code alignments, and unaligned data. Empty-list Thumb regressions cover PC+6, all low bases and processor modes, banked stacks, both code alignments, and unaligned data.
@ -224,7 +228,7 @@ They cover all processor modes, load widths, lanes, rotation, both Thumb code al
They verify direct reads while executing inside BIOS, re-entry, SWI/IRQ returns, host-inspection isolation, and original-boot provenance. 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.
General Thumb unused-memory reads remain diagnostics; supporting Thumb BIOS snapshots does not enable general open bus. Thumb IWRAM unused-memory reads remain diagnostics. Separate tests cover supported 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.