feat(memory): sample Thumb IWRAM refill targets

After successful refill instructions, sample the Thumb IWRAM target and target+2 halfwords to establish bus history. Share ARM/Thumb refill classification with nominal timing. Respect restored instruction state, completed data accesses, region limits, and diagnostic rollback; keep instruction execution and nominal costs unchanged.

Add original regressions for ARM/Thumb BX, Thumb branches and BL, PC writes, stack/block PC loads, status-restoring returns, real SWI/IRQ returns, both target alignments, mirrors/wrap, captured sample ordering, load lanes, failure retention, BIOS isolation, and DMA invalidation. Document official ARM7TDMI refill evidence and the distinction from a full pipeline.

Darwin arm64 validation: 917 workspace tests and 848 core/demo tests per debug/release profile; 39 preparation tests; native ROM windows in both profiles; ten graphics/window smoke modes; fmt, clippy, and rustdoc. Public ARM, Thumb, memory, and BIOS reports remain identical to prior passing results and between profiles. Cold startup, ARM-target refill history, and DMA-to-CPU ordering remain incomplete; no hardware-conformance or commercial-game claim.
This commit is contained in:
0xrsydn 2026-10-06 02:04:07 +07:00
commit 8b0f1a5ead
12 changed files with 566 additions and 35 deletions

View file

@ -24,6 +24,8 @@ mod instruction_tests;
#[cfg(test)]
mod iwram_open_bus_tests;
#[cfg(test)]
mod iwram_refill_tests;
#[cfg(test)]
mod load_alias_tests;
#[cfg(test)]
mod open_bus_tests;
@ -211,14 +213,17 @@ impl Cpu {
// Record the executing instruction, not an operand's pipelined PC value.
// Always clear the access context, including on diagnostic errors.
let pc = self.pc();
let sequential_thumb =
self.instruction_set == InstructionSet::Thumb && !self.thumb_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);
let result = self.execute_instruction(instruction, memory);
let sequential = sequential_thumb
&& self.instruction_set == InstructionSet::Thumb
&& self.pc() == pc.wrapping_add(2);
memory.end_cpu_access(result.is_ok(), sequential);
if result.is_ok() && refill {
memory.refill_cpu_bus_history(self.pc(), self.instruction_set);
}
result
}

View file

@ -295,7 +295,7 @@ fn failed_steps_discard_prefetch_history_before_retry() {
}
#[test]
fn taken_control_flow_invalidates_history_even_when_target_is_fallthrough() {
fn taken_control_flow_refills_history_even_when_target_is_fallthrough() {
for branch in [0x4720, 0xe7ff, 0x46a7, 0xd0ff, 0xbd00] {
// BX, B, MOV pc, BEQ, POP pc.
let (mut cpu, mut bus) = program(CODE, &[SEED, branch, PROBE, 0x5566, 0x99aa]);
@ -306,12 +306,8 @@ fn taken_control_flow_invalidates_history_even_when_target_is_fallthrough() {
cpu.step(&mut bus).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.pc(), CODE + 4);
let before = cpu.clone();
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
assert_eq!(cpu, before);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[6], 0x5566_99aa);
}
let (mut cpu, mut bus) = program(CODE, &[SEED, 0xd0ff, PROBE, 0x5566, 0x99aa]);
cpu.flags.zero = false; // Untaken BEQ has no refill.

View file

@ -0,0 +1,425 @@
//! Original control-flow programs for Thumb IWRAM refill-lane snapshots.
use super::*;
use crate::{
dma::{DMA_BASE, DMA_STRIDE},
io::{IE, IME, TIMER_BASE},
machine::{Machine, MachineError, StepKind},
memory::{BIOS_SIZE, ROM_START},
};
const SOURCE: u32 = 0x0300_0100;
const TARGET: u32 = 0x0300_0200;
const STACK: u32 = 0x0300_1000;
const UNUSED: u32 = 0x8000_0000;
const PROBE: u16 = 0x6806; // LDR r6,[r0]
const MARKER: u16 = 0x91ab;
const LOOKAHEAD: u16 = 0xd234;
fn target(bus: &mut Memory, pc: u32, instruction: u16) {
for (index, half) in [instruction, MARKER, LOOKAHEAD, 0x5678]
.into_iter()
.enumerate()
{
bus.write16(pc + index as u32 * 2, half).unwrap();
}
}
fn expected(pc: u32) -> u32 {
if pc & 2 == 0 {
0x91ab_d234
} else {
0xd234_91ab
}
}
fn branch(source: u32, thumb: bool, destination: u32) -> (Cpu, Memory) {
let instruction: u32 = if thumb { 0x46c0_4720 } else { 0xe12f_ff14 }; // BX r4
let mut bus = if source < BIOS_SIZE as u32 {
let mut bios = vec![0; BIOS_SIZE];
bios[source as usize..source as usize + 4].copy_from_slice(&instruction.to_le_bytes());
Memory::with_bios(vec![], bios).unwrap()
} else if source == ROM_START {
Memory::new(instruction.to_le_bytes().to_vec()).unwrap()
} else {
let mut bus = Memory::new(vec![]).unwrap();
bus.write32(source, instruction).unwrap();
bus
};
target(&mut bus, destination, PROBE);
let mut cpu = Cpu::new(source);
cpu.instruction_set = if thumb {
InstructionSet::Thumb
} else {
InstructionSet::Arm
};
cpu.registers[0] = UNUSED;
cpu.registers[4] = destination | 1;
(cpu, bus)
}
#[test]
fn arm_and_thumb_bx_establish_history_at_both_target_alignments_and_mirrors() {
for source in [0x100, 0x0200_0100, SOURCE, ROM_START] {
for thumb in [false, true] {
for destination in [
TARGET,
TARGET + 2,
TARGET + 0x8000,
0x0300_7ffc,
0x0300_7ffe,
] {
let (mut cpu, mut bus) = branch(source, thumb, destination);
let status = cpu.cpsr() | 0xb000_00c0;
cpu.apply_status(status, Mode::System);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.pc(), destination);
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[6], expected(destination));
assert_eq!(cpu.cpsr(), status | 0x20);
assert_eq!(cpu.registers[4], destination | 1);
}
}
}
}
#[test]
fn refill_samples_target_plus_two_at_branch_time_but_plus_four_on_arrival() {
for destination in [TARGET, TARGET + 2] {
let (mut cpu, mut bus) = branch(ROM_START, false, destination);
cpu.step(&mut bus).unwrap();
bus.write16(destination + 2, 0xeeee).unwrap();
bus.write16(destination + 4, 0xabcd).unwrap();
assert_eq!(bus.read16(destination + 2).unwrap(), 0xeeee);
cpu.step(&mut bus).unwrap();
assert_eq!(
cpu.registers[6],
if destination & 2 == 0 {
0x91ab_abcd
} else {
0xabcd_91ab
}
);
}
}
#[test]
fn refill_history_supports_normal_load_lanes_rotation_and_sign_extension() {
for destination in [TARGET, TARGET + 2] {
let word = expected(destination);
for (instruction, width, signed) in [
(PROBE, 4, false),
(0x7806, 1, false),
(0x8806, 2, false),
(0x5746, 1, true),
(0x5f46, 2, true), // r5=0 offset, r6 result.
] {
for lane in 0..4 {
let (mut cpu, mut bus) = branch(ROM_START, false, destination);
target(&mut bus, destination, instruction);
cpu.registers[0] += lane;
cpu.step(&mut bus).unwrap();
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 after = cpu.clone();
after.registers[6] = value;
after.registers[15] += 2;
cpu.step(&mut bus).unwrap();
assert_eq!(cpu, after);
}
}
}
}
#[test]
fn thumb_pc_writes_refill_after_data_loads_and_preserve_transfer_semantics() {
for destination in [TARGET, TARGET + 2] {
for instruction in [0x4720, 0x46a7, 0x44a7, 0xbd00, 0xbc00, 0xcc00] {
// BX r4, MOV pc,r4, ADD pc,r4, POP {pc}, empty POP, empty LDM r4!.
let (mut cpu, mut bus) = branch(SOURCE, true, destination);
bus.write16(SOURCE, instruction).unwrap();
bus.write32(STACK, destination).unwrap();
cpu.registers[13] = STACK;
cpu.registers[4] = match instruction {
0xcc00 => STACK,
0x44a7 => destination.wrapping_sub(SOURCE + 4),
_ => destination | 1,
};
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.pc(), destination);
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
if instruction == 0xbd00 {
assert_eq!(cpu.registers[13], STACK + 4);
}
if instruction == 0xbc00 {
assert_eq!(cpu.registers[13], STACK + 64);
}
if instruction == 0xcc00 {
assert_eq!(cpu.registers[4], STACK + 64);
}
cpu.step(&mut bus).unwrap();
assert_eq!(
cpu.registers[6],
expected(destination),
"instruction={instruction:#x}"
);
}
}
}
#[test]
fn thumb_taken_branches_and_bl_suffix_establish_target_history() {
for destination in [TARGET, TARGET + 2] {
for kind in 0..3 {
let (mut cpu, mut bus) = branch(SOURCE, true, destination);
if kind == 2 {
bus.write16(SOURCE, 0xf000).unwrap(); // BL prefix: high offset zero.
bus.write16(SOURCE + 2, 0xf800 | ((destination - SOURCE - 4) / 2) as u16)
.unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.pc(), SOURCE + 2);
} else {
let offset = ((destination - SOURCE - 4) / 2) as u16;
bus.write16(
SOURCE,
if kind == 0 {
0xe000 | offset
} else {
0xd000 | offset
},
)
.unwrap();
cpu.flags.zero = true;
}
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.pc(), destination);
if kind == 2 {
assert_eq!(cpu.registers[14], (SOURCE + 4) | 1);
}
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[6], expected(destination));
}
}
}
#[test]
fn arm_exception_returns_use_saved_thumb_state_and_restored_banks() {
for destination in [TARGET, TARGET + 2] {
for mode in [Mode::User, Mode::System] {
for instruction in [0xe1b0_f00e_u32, 0xe25e_f004, 0xe8fd_8004] {
// MOVS pc,lr; SUBS pc,lr,#4; LDMIA sp!,{r2,pc}^.
let mut bus = Memory::new(instruction.to_le_bytes().to_vec()).unwrap();
target(&mut bus, destination, PROBE);
bus.write32(STACK, 0xfeed_beef).unwrap();
bus.write32(STACK + 4, destination).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = UNUSED;
cpu.registers[13] = 0x0300_7000;
cpu.registers[14] = 0x1234;
cpu.enter_exception(Exception::SoftwareInterrupt);
cpu.registers[15] = ROM_START;
cpu.registers[13] = STACK;
cpu.registers[14] = destination + if instruction == 0xe25e_f004 { 4 } else { 0 };
let status = 0xa000_00e0 | mode as u32;
cpu.set_spsr(status);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.pc(), destination);
assert_eq!(cpu.cpsr(), status);
assert_eq!(cpu.registers[13], 0x0300_7000);
assert_eq!(cpu.registers[14], 0x1234);
if instruction == 0xe8fd_8004 {
assert_eq!(cpu.registers[2], 0xfeed_beef);
}
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[6], expected(destination));
cpu.switch_mode(Mode::Supervisor);
assert_eq!(
cpu.registers[13],
STACK + if instruction == 0xe8fd_8004 { 8 } else { 0 }
);
}
}
}
}
#[test]
fn real_swi_and_irq_returns_can_resume_thumb_iwram_with_known_lanes() {
for irq in [false, true] {
let destination = if irq { TARGET } else { TARGET + 2 };
let mut bios = vec![0; BIOS_SIZE];
bios[8..12].copy_from_slice(&0xe1b0_f00e_u32.to_le_bytes());
bios[0x18..0x1c].copy_from_slice(&0xe25e_f004_u32.to_le_bytes());
let mut bus = Memory::with_bios(vec![], bios).unwrap();
target(&mut bus, destination, PROBE);
if !irq {
bus.write16(TARGET, 0xdf00).unwrap();
}
let mut cpu = Cpu::new(TARGET);
cpu.instruction_set = InstructionSet::Thumb;
cpu.registers[0] = UNUSED;
let status = cpu.cpsr();
let mut machine = Machine::new(cpu, bus);
if irq {
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);
machine.memory_mut().write16(IME, 0).unwrap(); // Keep this test to one IRQ entry.
} else {
machine.step().unwrap();
}
machine.step().unwrap(); // Execute the original ARM return handler.
assert_eq!(machine.cpu().pc(), destination);
assert_eq!(machine.cpu().cpsr(), status);
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[6], expected(destination));
}
}
#[test]
fn failed_refill_instructions_preserve_cpu_and_preexisting_history() {
for instruction in [0xe1b0_f00e_u32, 0xe8fd_8004] {
let mut bus = Memory::new(instruction.to_le_bytes().to_vec()).unwrap();
for (index, half) in [0x680a_u16, PROBE, 0x1111, LOOKAHEAD]
.into_iter()
.enumerate()
{
bus.write16(SOURCE + index as u32 * 2, half).unwrap();
}
bus.write32(STACK, 0x1234_abcd).unwrap();
let mut paused = Cpu::new(SOURCE);
paused.instruction_set = InstructionSet::Thumb;
paused.registers[0] = UNUSED;
paused.registers[1] = STACK;
paused.step(&mut bus).unwrap(); // Seed sequential history with a word load.
let mut failing = Cpu::at_reset();
failing.registers[15] = ROM_START;
failing.registers[13] = STACK;
failing.registers[14] = TARGET;
failing.set_spsr(0); // Invalid return mode; no refill must commit.
let before = failing.clone();
assert!(failing.step_timed(&mut bus).is_err());
assert_eq!(failing, before);
paused.step(&mut bus).unwrap();
assert_eq!(paused.registers[6], 0xd234_abcd);
}
}
#[test]
fn cold_entry_region_crossings_and_unmapped_targets_do_not_invent_refill_history() {
let mut bus = Memory::new(vec![]).unwrap();
target(&mut bus, TARGET, PROBE);
let mut cpu = Cpu::new(TARGET);
cpu.instruction_set = InstructionSet::Thumb;
cpu.registers[0] = UNUSED;
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
for destination in [0x03ff_fffc, 0x03ff_fffe, 0x0e00_0000] {
let mut bus = Memory::new(0xe12f_ff14_u32.to_le_bytes().to_vec()).unwrap();
if destination >> 24 == 3 {
bus.write16(destination, PROBE).unwrap();
}
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = UNUSED;
cpu.registers[4] = destination | 1;
cpu.step(&mut bus).unwrap(); // An unavailable target/refill never fails BX early.
assert_eq!(cpu.pc(), destination);
let before = cpu.clone();
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(
if destination >> 24 == 3 {
UNUSED
} else {
destination
}
)))
);
assert_eq!(cpu, before);
}
}
#[test]
fn target_pair_refill_does_not_replace_the_separate_bios_retained_word() {
for destination in [TARGET, TARGET + 2] {
let mut bios = vec![0; BIOS_SIZE];
bios[0x100..0x104].copy_from_slice(&0xe12f_ff14_u32.to_le_bytes());
bios[0x108..0x10c].copy_from_slice(&0x89ab_cdef_u32.to_le_bytes());
let mut bus = Memory::with_bios(vec![], bios).unwrap();
target(&mut bus, destination, PROBE);
let mut cpu = Cpu::new(0x100);
cpu.registers[4] = destination | 1;
cpu.step(&mut bus).unwrap();
cpu.step(&mut bus).unwrap(); // r0=0: protected BIOS read, not unused memory.
assert_eq!(cpu.registers[6], 0x89ab_cdef);
assert_eq!(bus.read32(0x108).unwrap(), 0x89ab_cdef);
}
}
#[test]
fn successful_dma_between_refill_and_target_execution_invalidates_refill_history() {
let (cpu, mut bus) = branch(ROM_START, false, TARGET);
bus.write32(STACK, 0x1234_abcd).unwrap();
let mut machine = Machine::new(cpu, bus);
machine.step().unwrap();
let dma = DMA_BASE + 3 * DMA_STRIDE;
machine.memory_mut().write32(dma, STACK).unwrap();
machine.memory_mut().write32(dma + 4, 0x0200_0000).unwrap();
machine.memory_mut().write32(dma + 8, 0x8400_0001).unwrap();
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
let before = machine.cpu().clone();
let timing = machine.last_timing();
let cycles = machine.cycles();
assert_eq!(
machine.step(),
Err(MachineError::Cpu(CpuError::Memory(MemoryError::Unmapped(
UNUSED
))))
);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.last_timing(), timing);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.cpu().pc(), TARGET);
}
#[test]
fn refill_snapshots_keep_existing_nominal_costs_and_timed_untimed_results() {
for thumb in [false, true] {
for destination in [TARGET, TARGET + 2] {
let (cpu, mut bus) = branch(ROM_START, thumb, destination);
bus.write32(TIMER_BASE, 0x0080_0000).unwrap();
let mut machine = Machine::new(cpu, bus);
let (mut untimed, mut bus) = branch(ROM_START, thumb, destination);
for index in 0..2 {
untimed.step(&mut bus).unwrap();
machine.step().unwrap();
assert_eq!(machine.cpu(), &untimed);
assert_eq!(
machine.last_timing().code_cycles,
if index == 0 { 3 } else { 1 }
);
assert_eq!(machine.last_timing().data_cycles, index);
assert_eq!(machine.last_timing().internal_cycles, index);
}
assert_eq!(machine.cycles(), 6);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), 6);
assert_eq!(bus.cycles(), 0);
}
}
}

View file

@ -1,5 +1,5 @@
//! Nominal ARM7 instruction-cycle summaries, separate from instruction semantics.
//! Data costs come from actual bus accesses. No speculative pipeline reads occur.
//! Data costs come from actual bus accesses. Bus-history samples are separate from timing calculations.
//! PC writes use target-region code costs, following GBATEK's cycle summary.
use super::{Cpu, CpuError, InstructionSet};
@ -151,8 +151,11 @@ impl Cpu {
// Share the existing control-flow classification with bounded bus history.
// A PC write can refill even when its target equals the sequential address.
pub(super) fn thumb_refills(&self, instruction: u32) -> bool {
self.thumb_summary(instruction).refill
pub(super) fn instruction_refills(&self, instruction: u32) -> bool {
match self.instruction_set {
InstructionSet::Arm => self.arm_summary(instruction).refill,
InstructionSet::Thumb => self.thumb_summary(instruction).refill,
}
}
fn thumb_summary(&self, instruction: u32) -> Summary {

View file

@ -66,7 +66,8 @@ struct CpuAccess {
/// Unused-memory reads use ARM PC+8 or supported region-dependent Thumb snapshots.
/// Protected BIOS reads separately retain the snapshot from BIOS execution.
/// Sequential Thumb IWRAM accesses retain addressed bus lanes transactionally.
/// Refill history, a full fetch pipeline, and DMA bus latches are not modeled.
/// Successful refills into Thumb IWRAM sample the target pair to establish history.
/// A full fetch pipeline, ARM-target refill history, and DMA bus latches are not modeled.
pub struct Memory {
external_ram: Vec<u8>,
internal_ram: Vec<u8>,
@ -400,6 +401,24 @@ impl Memory {
}
}
/// Sample only the two target fetches of a completed refill into Thumb IWRAM.
/// This updates bus history, not the CPU's instruction buffer or nominal costs.
/// Incomplete or unsupported refills stay unknown without failing the branch early.
pub(crate) fn refill_cpu_bus_history(&mut self, pc: u32, instruction_set: InstructionSet) {
self.iwram_bus.invalidate();
if instruction_set == InstructionSet::Thumb
&& pc >> 24 == 3
&& pc.wrapping_add(2) >> 24 == 3
{
if let (Some(first), Some(second)) = (
self.snapshot_mapped_halfword(pc),
self.snapshot_mapped_halfword(pc + 2),
) {
self.iwram_bus.refill(pc, [first, second]);
}
}
}
pub(crate) fn invalidate_cpu_bus_history(&mut self) {
self.iwram_bus.invalidate();
}

View file

@ -1,5 +1,5 @@
//! Transactional lane history for consecutive Thumb instructions in IWRAM.
//! Unknown startup/refill/DMA history is not replaced with current memory bytes.
//! Transactional lane history for Thumb IWRAM execution and target-pair refills.
//! Unknown startup/DMA history is not replaced with guessed memory bytes.
use std::cell::Cell;
use crate::timing::AccessWidth;
@ -87,6 +87,20 @@ impl IwramBus {
}
}
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;