feat(cpu): retain ARM instruction pipeline words

Buffer ARM sequential fetch results and refill target pairs after branches, PC writes, and machine exception entry. Stage advances transactionally, defer speculative fetch diagnostics, and preserve buffered words across CPU/DMA stores and host edits. Add explicit CPU pipeline invalidation for debugger repair and memory rebinding.

Add 15 original instruction-buffer regressions. Keep full-state rollback equality while distinguishing architectural comparisons in semantic tests. Update the display error test for an already buffered branch. Document evidence and separate remaining Thumb, bus-history, and timing work.

Validated on Darwin arm64: 967 workspace and 898 core/demo tests in each debug/release profile, 39 Python tests, native ROM windows, ten window smoke modes, fmt, clippy, and rustdoc. Public ARM/Thumb/memory/BIOS reports and nominal cycles remain unchanged.
This commit is contained in:
0xrsydn 2026-10-06 03:57:57 +07:00
commit 47df9d05a7
24 changed files with 629 additions and 37 deletions

View file

@ -2,9 +2,23 @@ use std::{error::Error, fmt};
use crate::memory::{Memory, MemoryError}; use crate::memory::{Memory, MemoryError};
// Semantic tests compare architectural state after successful instructions.
// Error/rollback tests keep full Cpu equality, including buffered fetch results.
#[cfg(test)]
macro_rules! assert_cpu_arch_eq {
($actual:expr, $expected:expr $(, $($message:tt)+)?) => {{
let mut actual = $actual.clone();
let mut expected = $expected.clone();
actual.invalidate_pipeline();
expected.invalidate_pipeline();
assert_eq!(actual, expected $(, $($message)+)?);
}};
}
mod alu; mod alu;
mod arm; mod arm;
mod exception; mod exception;
mod pipeline;
mod status; mod status;
mod thumb; mod thumb;
mod timing; mod timing;
@ -13,6 +27,8 @@ pub use exception::Exception;
pub use status::Mode; pub use status::Mode;
mod transfer; mod transfer;
#[cfg(test)]
mod arm_pipeline_tests;
#[cfg(test)] #[cfg(test)]
mod bios_access_tests; mod bios_access_tests;
#[cfg(test)] #[cfg(test)]
@ -149,7 +165,8 @@ impl InstructionSet {
} }
} }
/// ARM/Thumb interpreter with register banks and optional nominal cycle costs. /// ARM/Thumb interpreter with register banks, ARM instruction buffering, and nominal costs.
/// Clone/equality include buffered fetch results. Thumb instruction buffering is not modeled.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cpu { pub struct Cpu {
registers: [u32; 16], registers: [u32; 16],
@ -159,6 +176,7 @@ pub struct Cpu {
irq_disabled: bool, irq_disabled: bool,
fiq_disabled: bool, fiq_disabled: bool,
banks: status::Banks, banks: status::Banks,
arm_pipeline: Option<pipeline::ArmPipeline>,
} }
impl Cpu { impl Cpu {
@ -175,6 +193,7 @@ impl Cpu {
irq_disabled: false, irq_disabled: false,
fiq_disabled: false, fiq_disabled: false,
banks: status::Banks::default(), banks: status::Banks::default(),
arm_pipeline: None,
} }
} }
@ -206,14 +225,15 @@ impl Cpu {
self.execute_fetched(instruction, memory) self.execute_fetched(instruction, memory)
} }
fn fetch(&self, memory: &Memory) -> Result<u32, CpuError> { fn execute_fetched(
Ok(match self.instruction_set { &mut self,
InstructionSet::Arm => memory.read32(self.pc())?, fetched: pipeline::Fetched,
InstructionSet::Thumb => u32::from(memory.read16(self.pc())?), memory: &mut Memory,
}) ) -> Result<(), CpuError> {
} let pipeline::Fetched {
instruction,
fn execute_fetched(&mut self, instruction: u32, memory: &mut Memory) -> Result<(), CpuError> { continuation,
} = 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();
@ -225,8 +245,20 @@ impl Cpu {
&& self.instruction_set == InstructionSet::Thumb && self.instruction_set == InstructionSet::Thumb
&& self.pc() == pc.wrapping_add(2); && self.pc() == pc.wrapping_add(2);
memory.end_cpu_access(result.is_ok(), sequential); memory.end_cpu_access(result.is_ok(), sequential);
if result.is_ok() && refill { if result.is_ok() {
if refill {
memory.refill_cpu_bus_history(self.pc(), self.instruction_set); memory.refill_cpu_bus_history(self.pc(), self.instruction_set);
self.refill_arm_pipeline(memory);
} else {
// Never carry an ARM buffer through a Thumb-state transition.
self.arm_pipeline = if self.instruction_set == InstructionSet::Arm
&& self.pc() == pc.wrapping_add(4)
{
continuation
} else {
None
};
}
} }
result result
} }

View file

@ -0,0 +1,321 @@
//! Original ARM instruction-buffer tests. Bus timing remains nominal.
use super::*;
use crate::{
dma::{DMA_BASE, DMA_STRIDE},
io::{IE, IME, TIMER_BASE},
machine::{Machine, StepKind},
memory::{BIOS_SIZE, ROM_START},
};
const CODE: u32 = 0x0300_0100;
const TARGET: u32 = 0x0300_0200;
const DATA: u32 = 0x0200_1000;
const NOP: u32 = 0xe1a0_0000;
const OLD: u32 = 0xe3a0_0001; // MOV r0,#1.
const NEW: u32 = 0xe3a0_0002; // MOV r0,#2.
fn program(code: &[u32]) -> (Cpu, Memory) {
let mut bus = Memory::new(vec![]).unwrap();
for (index, word) in code.iter().enumerate() {
bus.write32(CODE + index as u32 * 4, *word).unwrap();
}
(Cpu::new(CODE), bus)
}
#[test]
fn cpu_stores_do_not_replace_the_two_words_fetched_before_execution() {
for offset in [4, 8, 12] {
let (mut cpu, mut bus) = program(&[0xe581_2000, OLD, OLD, OLD]); // STR r2,[r1].
cpu.registers[1] = CODE + offset;
cpu.registers[2] = NEW;
cpu.step(&mut bus).unwrap();
assert_eq!(bus.read32(CODE + offset).unwrap(), NEW);
for _ in 0..offset / 4 {
cpu.step(&mut bus).unwrap();
}
assert_eq!(cpu.registers[0], if offset <= 8 { 1 } else { 2 });
}
}
#[test]
fn host_writes_leave_buffered_words_but_future_fetches_observe_changes() {
let (mut cpu, mut bus) = program(&[NOP, OLD, OLD, OLD]);
cpu.step(&mut bus).unwrap();
for offset in [4, 8, 12] {
bus.write32(CODE + offset, NEW).unwrap();
}
assert_eq!(bus.read32(CODE + 4).unwrap(), NEW);
for expected in [1, 1, 2] {
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], expected);
}
}
#[test]
fn branch_to_fallthrough_refills_even_when_the_pc_value_is_sequential() {
for instruction in [0xeaff_ffff, 0xe1a0_f001, 0xe12f_ff11, 0xe591_f000] {
// B P+4, MOV pc,r1, BX r1, LDR pc,[r1].
let (mut cpu, mut bus) = program(&[NOP, instruction, OLD, OLD]);
cpu.registers[1] = if instruction == 0xe591_f000 {
DATA
} else {
CODE + 8
};
bus.write32(DATA, CODE + 8).unwrap();
cpu.step(&mut bus).unwrap();
bus.write32(CODE + 8, NEW).unwrap(); // Was fetched by NOP.
cpu.step(&mut bus).unwrap();
bus.write32(CODE + 8, OLD).unwrap(); // Was refetched by the branch.
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 2);
}
}
#[test]
fn untaken_branch_keeps_buffered_instructions() {
let (mut cpu, mut bus) = program(&[NOP, 0x0aff_ffff, OLD]); // BEQ, Z is clear.
cpu.step(&mut bus).unwrap();
bus.write32(CODE + 8, NEW).unwrap();
cpu.step(&mut bus).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 1);
}
#[test]
fn refill_captures_target_pair_but_not_target_plus_eight() {
for thumb_source in [false, true] {
let (mut cpu, mut bus) = program(&[if thumb_source { 0x4708 } else { 0xe12f_ff11 }]);
if thumb_source {
cpu.instruction_set = InstructionSet::Thumb;
}
cpu.registers[1] = TARGET;
for offset in [0, 4, 8] {
bus.write32(TARGET + offset, OLD).unwrap();
}
cpu.step(&mut bus).unwrap();
for offset in [0, 4, 8] {
bus.write32(TARGET + offset, NEW).unwrap();
}
for expected in [1, 1, 2] {
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], expected);
}
}
}
#[test]
fn dma_writes_do_not_snoop_buffered_arm_instructions() {
let (cpu, mut bus) = program(&[NOP, OLD, OLD, OLD]);
bus.write32(DATA, NEW).unwrap();
bus.write32(DATA + 4, NEW).unwrap();
let mut machine = Machine::new(cpu, bus);
machine.step().unwrap();
let before = machine.cpu().clone();
let dma = DMA_BASE + 3 * DMA_STRIDE;
machine.memory_mut().write32(dma, DATA).unwrap();
machine.memory_mut().write32(dma + 4, CODE + 4).unwrap();
machine.memory_mut().write32(dma + 8, 0x8400_0002).unwrap();
for _ in 0..2 {
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
assert_eq!(machine.cpu(), &before);
}
for _ in 0..2 {
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[0], 1);
}
}
#[test]
fn speculative_errors_do_not_fail_a_valid_instruction_or_an_escaping_branch() {
for first in [NOP, 0xe12f_ff11] {
let mut bus = Memory::new(first.to_le_bytes().to_vec()).unwrap();
bus.write32(TARGET, OLD).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[1] = TARGET;
cpu.step(&mut bus).unwrap();
if first == NOP {
let before = cpu.clone();
for _ in 0..2 {
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(ROM_START + 4)))
);
assert_eq!(cpu, before);
}
} else {
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 1);
}
}
let (mut cpu, mut bus) = program(&[0xe12f_ff11]);
cpu.registers[1] = 0x1000_0000;
cpu.step(&mut bus).unwrap(); // Refill failure is deferred until target execution.
let before = cpu.clone();
assert!(cpu.step(&mut bus).is_err());
assert_eq!(cpu, before);
}
#[test]
fn failed_data_access_keeps_the_old_buffer_and_discards_speculative_progress() {
let (mut cpu, mut bus) = program(&[NOP, 0xe591_2000, OLD, OLD]);
cpu.registers[1] = 0x0e00_0000;
cpu.step(&mut bus).unwrap();
let before = cpu.clone();
assert!(cpu.step(&mut bus).is_err());
assert_eq!(cpu, before);
// Current LDR and next MOV were buffered. P+8 sampled by the failed step was not committed.
bus.write32(CODE + 4, NOP).unwrap();
bus.write32(CODE + 8, NEW).unwrap();
bus.write32(CODE + 12, NEW).unwrap();
bus.write32(DATA, 0x1234).unwrap();
cpu.registers[1] = DATA;
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[2], 0x1234);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 1);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 2);
}
#[test]
fn swi_and_machine_irq_refill_arm_vectors_before_handler_execution() {
for irq in [false, true] {
let mut bios = vec![0; BIOS_SIZE];
let vector = if irq { 0x18 } else { 8 };
bios[vector..vector + 4].copy_from_slice(&0xe12f_ff11_u32.to_le_bytes()); // BX r1.
let rom = if irq { NOP } else { 0xef00_0000_u32 }
.to_le_bytes()
.to_vec();
let mut bus = Memory::with_bios(rom, bios).unwrap();
bus.write32(TARGET, OLD).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[1] = TARGET;
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(),
if irq {
StepKind::IrqEntry
} else {
StepKind::Instruction
}
);
assert!(machine.cpu().arm_pipeline.is_some()); // Entry itself captured the ARM vector pair.
machine.step().unwrap(); // Vector BX refills target pair.
machine.memory_mut().write32(TARGET, NEW).unwrap();
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[0], 1);
}
}
#[test]
fn explicit_invalidation_repairs_buffered_unsupported_code_without_changing_registers() {
let (mut cpu, mut bus) = program(&[NOP, 0xffff_ffff, OLD]);
cpu.step(&mut bus).unwrap();
let before = cpu.clone();
let mut fork = cpu.clone();
bus.write32(CODE + 4, NEW).unwrap();
for _ in 0..2 {
assert!(cpu.step(&mut bus).is_err());
assert_eq!(cpu, before); // Includes the retained unsupported word.
}
cpu.invalidate_pipeline();
assert_cpu_arch_eq!(cpu, before);
assert_ne!(cpu, before); // Full Cpu equality includes the buffer.
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 2);
assert!(fork.step(&mut bus).is_err()); // Cloning owns an independent retained buffer.
}
#[test]
fn explicit_exception_and_pc_discontinuity_discard_the_old_execution_sequence() {
let (mut cpu, mut bus) = program(&[NOP, OLD, OLD]);
cpu.step(&mut bus).unwrap();
bus.write32(TARGET, NEW).unwrap();
cpu.registers[15] = TARGET;
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 2);
cpu.enter_exception(Exception::Irq);
assert!(cpu.arm_pipeline.is_none()); // This API has no Memory to sample.
let before = cpu.clone();
assert!(cpu.step(&mut bus).is_err()); // Missing BIOS remains a strict error.
assert_eq!(cpu, before);
}
#[test]
fn leaving_arm_discards_its_buffer_and_returning_refills_from_current_memory() {
let (mut cpu, mut bus) = program(&[0xe12f_ff11, OLD, OLD]);
bus.write16(TARGET, 0x4710).unwrap(); // Thumb BX r2.
cpu.registers[1] = TARGET | 1;
cpu.registers[2] = CODE + 4;
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
assert!(cpu.arm_pipeline.is_none());
bus.write32(CODE + 4, NEW).unwrap();
cpu.step(&mut bus).unwrap();
bus.write32(CODE + 4, OLD).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 2);
}
#[test]
fn arm_exception_return_refills_after_status_and_target_restore() {
let (mut cpu, mut bus) = program(&[0xe1b0_f00e]); // MOVS pc,lr.
cpu.enter_exception(Exception::SoftwareInterrupt);
cpu.registers[15] = CODE;
cpu.registers[14] = TARGET;
bus.write32(TARGET, OLD).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.mode(), Mode::System);
assert_eq!(cpu.pc(), TARGET);
bus.write32(TARGET, NEW).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 1);
}
#[test]
fn ram_mirrors_and_word_alignment_apply_to_buffered_fetches() {
for pc in [0x0300_7ffc, 0x0300_fffc, 0x0203_fffc] {
let mut bus = Memory::new(vec![]).unwrap();
bus.write32(pc, NOP).unwrap();
bus.write32(pc + 4, OLD).unwrap();
bus.write32(pc + 8, OLD).unwrap();
let mut cpu = Cpu::new(pc);
cpu.step(&mut bus).unwrap();
bus.write32(pc + 4, NEW).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 1);
}
let (mut cpu, mut bus) = program(&[NOP]);
cpu.registers[15] += 2;
let before = cpu.clone();
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unaligned(CODE + 2)))
);
assert_eq!(cpu, before);
}
#[test]
fn timed_and_untimed_buffers_match_without_extra_fetch_or_refill_cycles() {
let code = [NOP, 0xeaff_ffff, OLD, NOP];
let (mut timed, mut bus_timed) = program(&code);
let (mut untimed, mut bus_untimed) = program(&code);
for expected_cost in [1, 3, 1] {
let timing = timed.step_timed(&mut bus_timed).unwrap();
untimed.step(&mut bus_untimed).unwrap();
assert_eq!(timed, untimed);
assert_eq!(timing.code_cycles, expected_cost);
assert_eq!(timing.data_cycles, 0);
assert_eq!(timing.internal_cycles, 0);
assert_eq!(bus_timed.cycles(), 0);
}
}

View file

@ -78,8 +78,9 @@ fn protected_bios_loads_use_latched_lanes_for_arm_and_thumb_callers() {
expected.registers[1] = value; expected.registers[1] = value;
expected.registers[15] += if is_thumb { 2 } else { 4 }; expected.registers[15] += if is_thumb { 2 } else { 4 };
let timing = cpu.step_timed(&mut memory).unwrap(); let timing = cpu.step_timed(&mut memory).unwrap();
assert_eq!( assert_cpu_arch_eq!(
cpu, expected, cpu,
expected,
"arm={arm:#010x} thumb={is_thumb} address={address:#x} lane={low}" "arm={arm:#010x} thumb={is_thumb} address={address:#x} lane={low}"
); );
assert_eq!(timing.data_cycles, 1); assert_eq!(timing.data_cycles, 1);

View file

@ -129,7 +129,7 @@ fn failed_conditions_do_not_restore_status_or_validate_spsr() {
expected.registers[15] += 4; expected.registers[15] += 4;
cpu.step(&mut memory(instruction(opcode, 1) & 0x0fff_ffff)) cpu.step(&mut memory(instruction(opcode, 1) & 0x0fff_ffff))
.unwrap(); // EQ .unwrap(); // EQ
assert_eq!(cpu, expected); assert_cpu_arch_eq!(cpu, expected);
} }
} }

View file

@ -29,6 +29,7 @@ impl Cpu {
/// For IRQ/FIQ, PC must identify the next instruction, between CPU steps. /// For IRQ/FIQ, PC must identify the next instruction, between CPU steps.
/// This does not execute a BIOS service or fetch the vector instruction. /// This does not execute a BIOS service or fetch the vector instruction.
pub fn enter_exception(&mut self, exception: Exception) { pub fn enter_exception(&mut self, exception: Exception) {
self.invalidate_pipeline();
let (mode, vector, offset) = match exception { let (mode, vector, offset) = match exception {
Exception::UndefinedInstruction => { Exception::UndefinedInstruction => {
(Mode::Undefined, 0x04, self.instruction_set.width()) (Mode::Undefined, 0x04, self.instruction_set.width())

View file

@ -90,7 +90,7 @@ fn conditional_swi_can_be_skipped_without_entering_supervisor() {
let mut expected = cpu.clone(); let mut expected = cpu.clone();
expected.registers[15] += 4; expected.registers[15] += 4;
cpu.step(&mut program(0x0fff_ffff)).unwrap(); cpu.step(&mut program(0x0fff_ffff)).unwrap();
assert_eq!(cpu, expected); assert_cpu_arch_eq!(cpu, expected);
} }
#[test] #[test]

View file

@ -99,7 +99,7 @@ fn load_aliases_keep_loaded_values_for_all_widths_offsets_and_index_modes() {
after.registers[0] = expected(kind, low); after.registers[0] = expected(kind, low);
after.registers[15] += 4; after.registers[15] += 4;
let timing = cpu.step_timed(&mut bus).unwrap(); let timing = cpu.step_timed(&mut bus).unwrap();
assert_eq!(cpu, after, "{kind:?} {index:?} up={up} register_offset={register_offset} low={low}"); assert_cpu_arch_eq!(cpu, after, "{kind:?} {index:?} up={up} register_offset={register_offset} low={low}");
assert_eq!(bus.read32(RAM).unwrap(), DATA); assert_eq!(bus.read32(RAM).unwrap(), DATA);
assert_eq!(bus.cycles(), 0); assert_eq!(bus.cycles(), 0);
assert_eq!( assert_eq!(
@ -152,7 +152,7 @@ fn load_aliases_modify_only_the_active_register_in_every_cpu_bank() {
register as u32, register as u32,
))) )))
.unwrap(); .unwrap();
assert_eq!(cpu, after, "{kind:?} {mode:?} r{register}"); assert_cpu_arch_eq!(cpu, after, "{kind:?} {mode:?} r{register}");
} }
} }
} }
@ -263,7 +263,7 @@ fn skipped_alias_loads_do_not_access_memory_or_apply_writeback() {
let mut after = cpu.clone(); let mut after = cpu.clone();
after.registers[15] += 4; after.registers[15] += 4;
let timing = cpu.step_timed(&mut bus).unwrap(); let timing = cpu.step_timed(&mut bus).unwrap();
assert_eq!(cpu, after); assert_cpu_arch_eq!(cpu, after);
assert_eq!(timing.data_cycles, 0); assert_eq!(timing.data_cycles, 0);
assert_eq!(timing.internal_cycles, 0); assert_eq!(timing.internal_cycles, 0);
} }

View file

@ -38,7 +38,7 @@ fn arm_unused_ranges_return_pc_plus_eight_not_address_mirrors() {
expected.registers[1] = DATA; expected.registers[1] = DATA;
expected.registers[15] += 4; expected.registers[15] += 4;
cpu.step(&mut bus).unwrap(); cpu.step(&mut bus).unwrap();
assert_eq!(cpu, expected, "{address:#010x}"); assert_cpu_arch_eq!(cpu, expected, "{address:#010x}");
// Inspection outside CPU execution remains strict, including every lane. // Inspection outside CPU execution remains strict, including every lane.
for low in 0..4 { for low in 0..4 {
assert_eq!( assert_eq!(
@ -73,7 +73,7 @@ fn open_bus_loads_select_lanes_rotate_and_sign_extend_normally() {
expected.registers[1] = value; expected.registers[1] = value;
expected.registers[15] += 4; expected.registers[15] += 4;
let timing = cpu.step_timed(&mut bus).unwrap(); let timing = cpu.step_timed(&mut bus).unwrap();
assert_eq!(cpu, expected, "{instruction:#010x}, lane {low}"); assert_cpu_arch_eq!(cpu, expected, "{instruction:#010x}, lane {low}");
assert_eq!(timing.data_cycles, 1); assert_eq!(timing.data_cycles, 1);
assert_eq!(timing.internal_cycles, 1); assert_eq!(timing.internal_cycles, 1);
assert_eq!(timing.code_cycles, 8); // ROM boundary; no extra lookahead cost assert_eq!(timing.code_cycles, 8); // ROM boundary; no extra lookahead cost

View file

@ -0,0 +1,63 @@
//! ARM instruction buffering only. Timing and bus ownership remain separate.
use super::{Cpu, CpuError, InstructionSet};
use crate::memory::{Memory, MemoryError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct ArmPipeline {
pc: u32,
// Deferred strict-fetch results for the next execute and decode positions.
words: [Result<u32, MemoryError>; 2],
}
pub(super) struct Fetched {
pub instruction: u32,
pub continuation: Option<ArmPipeline>,
}
impl Cpu {
/// Discard buffered instructions after debugger code edits or rebinding memory.
/// Ordinary CPU/DMA stores and host inspection must not call this automatically.
/// The next ARM step fills from its current PC without adding nominal cycles.
pub fn invalidate_pipeline(&mut self) {
self.arm_pipeline = None;
}
pub(super) fn fetch(&self, memory: &Memory) -> Result<Fetched, CpuError> {
if self.instruction_set == InstructionSet::Thumb {
return Ok(Fetched {
instruction: u32::from(memory.read16(self.pc())?),
continuation: None,
});
}
let pc = self.pc();
let retained = self.arm_pipeline.as_ref().filter(|pipe| pipe.pc == pc);
let instruction = match retained {
Some(pipe) => pipe.words[0].clone(),
None => memory.fetch_arm_word(pc),
}?;
let decode = match retained {
Some(pipe) => pipe.words[1].clone(),
None => memory.fetch_arm_word(pc.wrapping_add(4)),
};
let fetched = memory.fetch_arm_word(pc.wrapping_add(8));
Ok(Fetched {
instruction,
continuation: Some(ArmPipeline {
pc: pc.wrapping_add(4),
words: [decode, fetched],
}),
})
}
/// Capture target and target+4 after a successful refill, without early errors.
/// Explicit exception entry without Memory leaves a cold buffer instead.
pub(crate) fn refill_arm_pipeline(&mut self, memory: &Memory) {
self.arm_pipeline = (self.instruction_set == InstructionSet::Arm).then(|| ArmPipeline {
pc: self.pc(),
words: [
memory.fetch_arm_word(self.pc()),
memory.fetch_arm_word(self.pc().wrapping_add(4)),
],
});
}
}

View file

@ -190,6 +190,6 @@ fn conditional_status_transfers_do_not_execute_when_condition_fails() {
let mut expected = cpu.clone(); let mut expected = cpu.clone();
expected.registers[15] += 4; expected.registers[15] += 4;
cpu.step(&mut program(instruction)).unwrap(); cpu.step(&mut program(instruction)).unwrap();
assert_eq!(cpu, expected); assert_cpu_arch_eq!(cpu, expected);
} }
} }

View file

@ -80,7 +80,11 @@ fn thumb_exit_retains_both_word_lanes_for_each_code_alignment_and_load_width() {
after.registers[1] = value; after.registers[1] = value;
after.registers[15] += if target_thumb { 2 } else { 4 }; after.registers[15] += if target_thumb { 2 } else { 4 };
let timing = cpu.step_timed(&mut memory).unwrap(); let timing = cpu.step_timed(&mut memory).unwrap();
assert_eq!(cpu, after, "pc={pc:#x} lane={lane} thumb={target_thumb}"); assert_cpu_arch_eq!(
cpu,
after,
"pc={pc:#x} lane={lane} thumb={target_thumb}"
);
assert_eq!(timing.data_cycles, 1); assert_eq!(timing.data_cycles, 1);
assert_eq!(timing.internal_cycles, 1); assert_eq!(timing.internal_cycles, 1);
assert_eq!(memory.cycles(), 0); assert_eq!(memory.cycles(), 0);

View file

@ -50,7 +50,8 @@ impl Cpu {
/// this does not advance device time or sample interrupts. Machine does that. /// this does not advance device time or sample interrupts. Machine does that.
/// Failed instructions discard their timing trace and retain existing diagnostics. /// Failed instructions discard their timing trace and retain existing diagnostics.
pub fn step_timed(&mut self, memory: &mut Memory) -> Result<StepTiming, CpuError> { pub fn step_timed(&mut self, memory: &mut Memory) -> Result<StepTiming, CpuError> {
let instruction = self.fetch(memory)?; let fetched = self.fetch(memory)?;
let instruction = fetched.instruction;
let summary = match self.instruction_set { let summary = match self.instruction_set {
InstructionSet::Arm => self.arm_summary(instruction), InstructionSet::Arm => self.arm_summary(instruction),
InstructionSet::Thumb => self.thumb_summary(instruction), InstructionSet::Thumb => self.thumb_summary(instruction),
@ -61,7 +62,7 @@ impl Cpu {
let waitcnt = memory.waitcnt(); let waitcnt = memory.waitcnt();
let code_kind = memory.cpu_code_kind(summary.code_kind); let code_kind = memory.cpu_code_kind(summary.code_kind);
memory.begin_data_timing(); memory.begin_data_timing();
let result = self.execute_fetched(instruction, memory); let result = self.execute_fetched(fetched, memory);
let data_cycles = memory.end_data_timing(); let data_cycles = memory.end_data_timing();
result?; result?;
let code_cycles = if summary.refill { let code_cycles = if summary.refill {

View file

@ -179,6 +179,7 @@ impl Machine {
} }
let (kind, timing) = if self.cpu.take_interrupt(self.memory.irq_pending(), false) { let (kind, timing) = if self.cpu.take_interrupt(self.memory.irq_pending(), false) {
self.memory.invalidate_cpu_bus_history(); self.memory.invalidate_cpu_bus_history();
self.cpu.refill_arm_pipeline(&self.memory);
(StepKind::IrqEntry, self.cpu.exception_timing(&self.memory)) (StepKind::IrqEntry, self.cpu.exception_timing(&self.memory))
} else { } else {
( (

View file

@ -69,7 +69,7 @@ struct CpuAccess {
/// Successful refills into Thumb IWRAM sample the target pair to establish history. /// Successful refills into Thumb IWRAM sample the target pair to establish history.
/// DMA accesses drive existing Thumb IWRAM continuation lanes before the next fetch. /// DMA accesses drive existing Thumb IWRAM continuation lanes before the next fetch.
/// 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.
/// A full fetch pipeline and ARM-target refill history are not modeled. /// CPU-owned ARM instruction buffering does not yet drive complete bus history or timing.
pub struct Memory { pub struct Memory {
external_ram: Vec<u8>, external_ram: Vec<u8>,
internal_ram: Vec<u8>, internal_ram: Vec<u8>,
@ -408,6 +408,18 @@ impl Memory {
])) ]))
} }
/// Strict ARM fetch without CPU data context, timing trace, or bus-history changes.
pub(crate) fn fetch_arm_word(&self, address: u32) -> Result<u32, MemoryError> {
if address & 3 != 0 {
return Err(MemoryError::Unaligned(address));
}
let mut bytes = [0; 4];
for (offset, byte) in bytes.iter_mut().enumerate() {
*byte = self.read_mapped_byte(address.wrapping_add(offset as u32))?;
}
Ok(u32::from_le_bytes(bytes))
}
fn snapshot_mapped_word(&self, address: u32) -> Option<u32> { fn snapshot_mapped_word(&self, address: u32) -> Option<u32> {
let mut bytes = [0; 4]; let mut bytes = [0; 4];
for (offset, byte) in bytes.iter_mut().enumerate() { for (offset, byte) in bytes.iter_mut().enumerate() {

View file

@ -444,6 +444,11 @@ fn frame_runner_limits_and_cpu_errors_preserve_completed_progress_only() {
machine.run_until_vblank(10), machine.run_until_vblank(10),
Err(FrameRunError::Cpu(_)) Err(FrameRunError::Cpu(_))
)); ));
assert_eq!(machine.memory().display_position(), position); // The already buffered branch executes once and refills from the patched word.
assert_eq!(machine.cycles(), 6); // The unsupported instruction then fails without further device progress.
assert_eq!(
machine.memory().display_position().line_cycle,
position.line_cycle + 3
);
assert_eq!(machine.cycles(), 9);
} }

View file

@ -47,6 +47,8 @@ The repository is a Cargo workspace with three crates:
| `crates/gba-core/src/dma.rs` | DMA registers, internal pointers, retained channel data, trigger state, priority, and completion IRQs | | `crates/gba-core/src/dma.rs` | DMA registers, internal pointers, retained channel data, trigger state, priority, and completion IRQs |
| `crates/gba-core/src/machine.rs` | Timed CPU/DMA/device stepping and IRQ delivery | | `crates/gba-core/src/machine.rs` | Timed CPU/DMA/device stepping and IRQ delivery |
| `crates/gba-core/src/timing.rs` | Bus widths, wait-state costs, and timing breakdowns | | `crates/gba-core/src/timing.rs` | Bus widths, wait-state costs, and timing breakdowns |
| `crates/gba-core/src/cpu/pipeline.rs` | CPU-owned ARM instruction words, deferred fetch diagnostics, refill sampling, and explicit invalidation |
| `crates/gba-core/src/cpu/arm_pipeline_tests.rs` | ARM self-modifying code, branches, target pairs, DMA isolation, exceptions, errors, and timing equivalence |
| `crates/gba-core/src/cpu/timing.rs` | ARM/Thumb cycle summaries, nominal DMA resume, and data-access accounting | | `crates/gba-core/src/cpu/timing.rs` | ARM/Thumb cycle summaries, nominal DMA resume, and data-access accounting |
| `crates/gba-core/src/cpu/dma_resume_tests.rs` | One-shot non-sequential DMA resume costs, wait settings, failures, idle, IRQs, and API isolation | | `crates/gba-core/src/cpu/dma_resume_tests.rs` | One-shot non-sequential DMA resume costs, wait settings, failures, idle, IRQs, and API isolation |
| `crates/gba-core/src/cpu/timing_tests.rs` | Instruction timing and semantic-equivalence checks | | `crates/gba-core/src/cpu/timing_tests.rs` | Instruction timing and semantic-equivalence checks |

View file

@ -134,7 +134,8 @@ Missing BIOS images remain unmapped. The unused-memory ARM open-bus snapshot is
The Thumb rule follows documented BIOS bus lanes and a source comparison with NanoBoyAdvance. The Thumb rule follows documented BIOS bus lanes and a source comparison with NanoBoyAdvance.
See [Thumb BIOS prefetch research](../research/thumb-bios-prefetch.md) for evidence, emulator differences, and test scope. See [Thumb BIOS prefetch research](../research/thumb-bios-prefetch.md) for evidence, emulator differences, and test scope.
This models neither a persistent fetch pipeline nor exact refill, BIOS data-access, or DMA bus history. This BIOS readback model remains separate from the CPU's ARM instruction buffer.
It does not model exact refill, BIOS data-access, or DMA bus history.
The boundary diagnostic policy and precise bus timing remain hardware-unverified. The boundary diagnostic policy and precise bus timing remain hardware-unverified.
Original regressions cover synthetic images, boot, exception returns, access widths, alignment, modes, and diagnostics. Original regressions cover synthetic images, boot, exception returns, access widths, alignment, modes, and diagnostics.
The [public BIOS ROM](../public-bios-tests.md) passes its unchanged protected-read assertions in debug and release builds. The [public BIOS ROM](../public-bios-tests.md) passes its unchanged protected-read assertions in debug and release builds.

View file

@ -76,6 +76,40 @@ These forms retain nominal single-word access costs and the existing atomic-erro
With writeback enabled, `STM` stores the old base only when that register is first in the list. With writeback enabled, `STM` stores the old base only when that register is first in the list.
Otherwise, `STM` stores the updated base. Otherwise, `STM` stores the updated base.
## ARM instruction buffering
ARM execution retains fetched instructions in a CPU-owned buffer, independent of later RAM contents.
At cold entry, a successful step reads the current word P and samples P+4 and P+8 before instruction effects.
During sequential execution, the current and next words come from retained results; only P+8 is newly sampled.
Success advances the buffer. CPU/DMA stores and host writes do not replace already buffered instructions.
The existing r15 convention is unchanged: it stores the next executing address, not the hardware fetch address.
A successful branch or PC-writing refill into ARM samples target T and T+4 after instruction effects.
This includes taken branches, BX, PC loads, status-restoring returns, and software interrupts.
PC writes refill even when the destination equals fallthrough. Untaken conditional branches retain sequential buffering.
The first target instruction samples T+8 before execution. Thumb-to-ARM transitions also capture the ARM target pair.
Leaving ARM discards its buffer. Thumb still fetches directly and uses its separate bus-history snapshots.
Machine IRQ entry captures the ARM vector pair before any handler instruction runs.
`Cpu::enter_exception` and `Cpu::take_interrupt` have no memory parameter: they invalidate the buffer and leave vector sampling to the next step.
An unexpected PC discontinuity starts from current mapped bytes rather than reusing a mismatched buffer.
Fetch samples use strict mapped reads, without protected-data or unused-memory fallback.
Unavailable lookahead is retained as a deferred diagnostic; it cannot fail an otherwise valid current instruction or branch.
A buffered fetch error fails only if execution reaches that slot. No automatic hardware abort is generated.
A failed instruction preserves all CPU state, including the old buffer; its speculative advance is discarded.
A successful branch discards abandoned-path errors and replaces them with target fetch results.
CPU clones and equality include the buffer. Architectural state checks are separate from full-state rollback checks in tests.
`Cpu::invalidate_pipeline()` explicitly discards buffered words without changing registers, flags, or device clocks.
Use it after debugger code repair or when attaching different memory to a retained CPU.
Do not call it for ordinary CPU/DMA stores: that would hide self-modifying-code behavior.
This is instruction buffering, not a complete timed fetch pipeline.
Samples add no nominal data accesses or device cycles. Existing refill costs and one-shot DMA resume costs remain unchanged.
Thumb buffering, fetch-driven bus history, exact refill costs, per-access device updates, and DMA arbitration remain incomplete.
See [the evidence, diagnostic policy, and tests](../research/arm-instruction-buffer.md).
## ARM unused-memory data reads ## ARM unused-memory data reads
During ARM execution, data reads from `0x00004000..0x01ffffff` and `0x10000000..0xffffffff` return an open-bus value. During ARM execution, data reads from `0x00004000..0x01ffffff` and `0x10000000..0xffffffff` return an open-bus value.
@ -103,7 +137,8 @@ Limits remain explicit:
[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.
- 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.
- 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.
- There is no persistent instruction pipeline. Thumb IWRAM refills sample their target pair. - ARM instruction buffering is persistent, but these data snapshots remain a separate bounded model.
Thumb IWRAM refills sample their target pair without buffering Thumb instructions.
DMA updates existing IWRAM continuation lanes at instruction boundaries, not during CPU accesses. DMA updates existing IWRAM continuation 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.
@ -238,7 +273,8 @@ In User/System modes, they perform ordinary test/compare flag updates without a
Invalid saved modes still produce an atomic diagnostic. Failed conditions do not restore or validate status. Invalid saved modes still produce an atomic diagnostic. Failed conditions do not restore or validate status.
Nominal timing remains one sequential code access, plus an internal cycle for register-specified shifts; there is no refill cost. Nominal timing remains one sequential code access, plus an internal cycle for register-specified shifts; there is no refill cost.
This fixes the mode-switch expectation in public `jsmolka/gba-tests` ARM test 234 and agrees with mGBA's shared ALU flag handling. This fixes the mode-switch expectation in public `jsmolka/gba-tests` ARM test 234 and agrees with mGBA's shared ALU flag handling.
The interpreter has no fetch pipeline; instruction-state-changing forms and exact pipeline behavior remain hardware-unverified. ARM instruction buffering is discarded on a state change without an ordinary refill.
Instruction-state-changing compare forms and exact pipeline behavior remain hardware-unverified.
Other S-bit block transfers access User registers while using the current mode's base register. Other S-bit block transfers access User registers while using the current mode's base register.
User-bank writeback, S-bit empty lists, and User-mode S-bit transfers return diagnostics. User-bank writeback, S-bit empty lists, and User-mode S-bit transfers return diagnostics.
@ -311,7 +347,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. Separate Thumb IWRAM target-pair samples update bus history without extra cycles. - Refill cost calculation does not read target bytes. ARM instruction-buffer and Thumb 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

@ -147,6 +147,14 @@ DMA resume-timing research:
See [non-sequential CPU resume](research/dma-resume-timing.md) for the nominal subset and unmodeled refill/prefetch details. See [non-sequential CPU resume](research/dma-resume-timing.md) for the nominal subset and unmodeled refill/prefetch details.
The public test was reviewed, not executed. No hardware timing pass is claimed. The public test was reviewed, not executed. No hardware timing pass is claimed.
ARM instruction-buffer research:
- [ARM7TDMI instruction pipeline](https://developer.arm.com/documentation/dvi0027/b/arm7tdmi/instruction-pipeline) and [technical reference manual](https://documentation-service.arm.com/static/5e8e1323fd977155116a3129), for fetch/decode/execute stages and the visible ARM PC offset.
- The pinned NanoBoyAdvance ARM7TDMI `Run` and `ReloadPipeline32` helpers above, for retained words, pre-execution fetching, and target-pair refill order.
- The ARM branch cycle sequence above, for target and target+instruction-width fetches.
See [persistent ARM instruction buffering](research/arm-instruction-buffer.md). Instruction retention is implemented; per-access timing and Thumb buffering remain incomplete.
Use [GBATEK](https://problemkaputt.de/gbatek.htm) and ARM7TDMI documentation for further hardware work. Use [GBATEK](https://problemkaputt.de/gbatek.htm) and ARM7TDMI documentation for further hardware work.
Only use game ROMs that you may lawfully use. Do not commit game ROMs, BIOS files, or game assets. Only use game ROMs that you may lawfully use. Do not commit game ROMs, BIOS files, or game assets.
The ignore file excludes `roms/`, common game ROM extensions, and save files. The ignore file excludes `roms/`, common game ROM extensions, and save files.

View file

@ -0,0 +1,94 @@
# Persistent ARM instruction buffering
## Evidence and scope
The [ARM7TDMI instruction-pipeline description](https://developer.arm.com/documentation/dvi0027/b/arm7tdmi/instruction-pipeline) identifies fetch, decode, and execute stages.
The [ARM7TDMI technical reference manual](https://documentation-service.arm.com/static/5e8e1323fd977155116a3129) describes simultaneous execution, decoding, and fetching.
For ARM state, the executing instruction's visible PC is eight bytes ahead.
The [branch cycle sequence](https://support.arm.com/documentation/ddi0029/g/instruction-cycle-timings/branch-and-branch-with-link) fetches the destination, then destination plus instruction width.
[NanoBoyAdvance at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/arm7tdmi.hh) makes the retained-word ordering explicit:
- `ReloadPipeline32` reads target T, then T+4.
- `Run` takes the current instruction from its retained first slot.
- It shifts the second slot forward and reads the new fetch word before executing the current instruction.
- Taken PC writes reload the target pair instead of retaining the sequential continuation.
The resulting ARM sequence fetches P+8 before the instruction at P changes memory.
Consequently, a store to P+4 or P+8 cannot change those already fetched instructions without a refill.
A later store-visible word, such as P+12, is fetched on a subsequent step.
DMA writes likewise do not replace words already inside the CPU buffer.
These are source-backed instruction-buffer rules, not a claim of complete ARM7 bus timing.
No external source code or test ROM was imported. No physical-hardware or external-emulator differential run was performed.
## Implementation
`cpu/pipeline.rs` owns a private two-slot ARM continuation with its expected execution PC.
Each slot retains either a mapped word or its strict fetch error.
`Cpu` owns and clones this state; full CPU equality includes it.
At a cold or discontinuous ARM entry, fetch the current word and sample P+4/P+8.
At a matching continuation, use its first word for execution and retain its second word for the next step.
Sample P+8 before instruction effects. Keep that prospective continuation local until execution succeeds.
A failed current fetch or instruction preserves the previous buffer exactly.
Successful non-refill ARM execution commits the next pair.
A successful refill into ARM instead samples T and T+4 after data transfers and status restoration.
This includes ARM/Thumb BX, ARM PC writes and loads, taken branches, exception returns, and SWI vectors.
A refill to the sequential address still discards the previous sequential pair.
Untaken branches retain the pair. Leaving ARM clears it; Thumb continues to use direct instruction reads.
Machine IRQ entry captures its vector pair immediately after exception entry.
The standalone exception APIs have no Memory argument, so they invalidate the old pair and leave the vector cold.
This distinction is explicit rather than retaining unrelated words at a coincident PC.
`Memory::fetch_arm_word` reads aligned mapped words without CPU data context, timing traces, or bus-history changes.
Normal memory mirrors apply. Unmapped, truncated, and misaligned fetches retain their existing errors.
No BIOS-protected data value or general open-bus value can become a synthetic instruction through this path.
## Diagnostics and debugger behavior
An unavailable lookahead word is a deferred fetch diagnostic, not an immediate failure of the current instruction.
A valid branch can discard an unavailable sequential word. An unavailable target does not fail the branch early.
Execution reports a buffered error only when that slot becomes current.
Unsupported instruction encodings remain words until decode; they also do not fail preceding instructions.
The atomic-error policy preserves the entire old CPU buffer on failure.
A host repair of a buffered unsupported instruction does not silently change that retained word.
`Cpu::invalidate_pipeline()` explicitly clears the buffer for debugger repair or attachment to different memory.
It changes no architectural registers, memory, clocks, or separate BIOS/IWRAM bus history.
Ordinary CPU/DMA stores and host inspection never invoke it automatically.
A copied CPU retains its own instruction-buffer state. A copied CPU attached to unrelated memory must be invalidated explicitly.
Existing successful-instruction tests now distinguish architectural equality from full-state equality.
Failed-instruction tests continue to assert full-state preservation.
## Remaining limits
This implements persistent ARM instruction words, not a complete cycle-by-cycle pipeline.
Cold fill and target-pair samples do not add nominal cycles or data accesses.
The existing destination-based refill summaries and one-shot DMA resume timing remain unchanged.
The current instruction address still selects nominal code cost rather than a separately scheduled fetch address.
ARM PC+8 open bus and protected BIOS readback remain separate snapshots.
The instruction-buffer sample and those snapshots see mapped memory before instruction effects, but no unified per-access bus model exists yet.
The target samples do not establish complete ARM IWRAM or BIOS bus history.
Thumb buffering, 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.
## Original regression coverage
Original programs test stores to the two prefetched words and the next unfetched word.
Other tests cover host/DMA writes, taken and untaken branches, PC writes to fallthrough, target-pair sample order, and ARM/Thumb transitions.
SWI/IRQ entry and status-restoring returns verify refill integration.
Further tests check deferred fetch errors, discarded paths, failed-instruction rollback, explicit invalidation, CPU cloning, mirrors, and alignment.
Timed and untimed execution retain identical buffers and nominal costs.
The self-modifying cases failed under the previous direct-fetch interpreter before implementation.
## Validation result
Workspace and core/demo tests pass in debug and release on Darwin arm64.
Formatting, lint checks, rustdoc, preparation tests, native ROM windows, and graphics smoke modes pass.
Public ARM, Thumb, memory, and BIOS reports match the previous passing reports exactly, including nominal cycle counts.
These suites remain regression checks, not complete instruction-pipeline or hardware-timing conformance tests.

View file

@ -44,7 +44,8 @@ A completed DMA WAITCNT store changes the settings before CPU resume.
## Limits ## Limits
The model has no persistent fetch pipeline or per-access DMA arbitration. The timing model has no per-access fetch scheduling or DMA arbitration.
The later [ARM instruction buffer](arm-instruction-buffer.md) retains instruction words but does not change these nominal costs.
It charges the current instruction address, not a separately scheduled PC+4/PC+8 pipeline fetch. It charges the current instruction address, not a separately scheduled PC+4/PC+8 pipeline fetch.
DMA still executes only between instructions, not between data accesses or during CPU internal cycles. DMA still executes only between instructions, not between data accesses or during CPU internal cycles.

View file

@ -77,7 +77,7 @@ Unmapped instruction targets still fail on the following fetch.
Cold direct startup and arbitrary PC changes do not manufacture a refill. Cold direct startup and arbitrary PC changes do not manufacture a refill.
These are bus-history samples, not instruction-buffer contents or extra emulated cycles. These are bus-history samples, not instruction-buffer contents or extra emulated cycles.
The interpreter still reads instructions when it executes them. The interpreter still reads Thumb instructions when it executes them; ARM now has separate instruction buffering.
Changing T+2 after the branch does not replace its captured bus value; changing T+4 before arrival affects the later sample. 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. Those original tests verify sample ordering, not hardware-accurate self-modifying instruction execution.
@ -111,7 +111,8 @@ Only complete successful units commit history, preserving the project's atomic d
Cold direct entry still has no expected continuation PC. DMA does not manufacture one, even after a word access. 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. 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. General DMA open-bus reads, cross-state history, sub-instruction arbitration, and DMA during CPU internal cycles remain outside this subset.
The interpreter still fetches instructions directly; this extension does not fix self-modifying instruction-buffer behavior. This extension does not fix self-modifying Thumb instruction-buffer behavior.
The later [ARM instruction buffer](arm-instruction-buffer.md) separately retains ARM words across DMA.
No physical-hardware measurements or external emulator differential runs were performed. No physical-hardware measurements or external emulator differential runs were performed.
All regression programs are original, with a separate byte-array lane reference in the DMA matrix tests. All regression programs are original, with a separate byte-array lane reference in the DMA matrix tests.

View file

@ -27,6 +27,7 @@ Update this file when a feature lands or a limit is removed.
- `KEYCNT` selection and OR/AND matching, keypad IRQ latching, and HALT wake-up through IE bit 12. - `KEYCNT` selection and OR/AND matching, keypad IRQ latching, and HALT wake-up through IE bit 12.
- 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 instruction buffering, sequential fetch retention, branch/vector target pairs, and deferred fetch diagnostics.
- 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.
@ -140,7 +141,8 @@ Cold or invalidated IWRAM history remains diagnostic until all lanes are known.
Refills into Thumb IWRAM establish new target-pair history after the instruction succeeds. Refills into Thumb IWRAM establish new target-pair history after the instruction succeeds.
Accepted machine IRQs, unsupported refills, and other execution states/regions invalidate continuation history. Accepted machine IRQs, unsupported refills, and other execution states/regions invalidate continuation history.
Successful DMA updates existing Thumb IWRAM continuation lanes for actual IWRAM accesses, before the resumed PC+4 sample. Successful DMA updates existing Thumb IWRAM continuation lanes for actual IWRAM accesses, before the resumed PC+4 sample.
This is not a fetch pipeline. ARM-target history, region-crossing fetches, general DMA handoff, and unused/write-only I/O open bus remain unsupported. ARM instruction buffering now retains sequential words and refill target pairs, independently of these bus snapshots.
Thumb buffering, complete ARM bus history, exact region-crossing bus behavior, general DMA handoff, and unused/write-only I/O open bus remain incomplete.
[DMA channel data latches](hardware/dma.md#retained-channel-data) support blocked-source reuse, but never replace CPU bus history. [DMA channel data latches](hardware/dma.md#retained-channel-data) support blocked-source reuse, but never replace CPU bus history.
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.
@ -151,12 +153,12 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa
## Next steps ## Next steps
1. Extend bounded IWRAM DMA history and nominal non-sequential resume timing with independent per-access bus tests. 1. Extend persistent ARM instruction buffering to Thumb, then connect fetches to bus history with independent per-access tests.
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.
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. Connect instruction buffering to fetch timing, Game Pak prefetch, per-access device updates, and verified timer/IRQ delays.
3. Validate keypad retrigger behavior with hardware tests and add remaining device registers; extend BIOS reset coverage as sound/serial support becomes available. 3. Validate keypad retrigger behavior with hardware tests and add remaining device registers; extend BIOS reset coverage as sound/serial support becomes available.
Validate STOP entry/wake edges and add external wake sources and remaining DMA device modes. Validate STOP entry/wake edges and add external wake sources and remaining DMA device modes.
4. Replace nominal sprite work limits with verified individual fetch timing; add background fetch timing and per-pixel composition. 4. Replace nominal sprite work limits with verified individual fetch timing; add background fetch timing and per-pixel composition.

View file

@ -80,6 +80,12 @@ DMA tests compare source/destination lane updates against a byte-array reference
They cover channel preemption, blocked sources, failures, mirrors, partial/cold continuations, and other-region snapshot isolation. They cover channel preemption, blocked sources, failures, mirrors, partial/cold continuations, and other-region snapshot isolation.
IRQ and PC-discontinuity tests retain conservative invalidation; timed/untimed tests verify CPU results and nominal clock costs. IRQ and PC-discontinuity tests retain conservative invalidation; 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.
They cover refill target pairs, PC writes to fallthrough, conditional branches, ARM/Thumb transitions, exception entry/return, and deferred fetch errors.
Full CPU equality checks buffer rollback and cloning. Successful instruction-semantic tests compare architectural state separately.
Explicit invalidation tests cover debugger repair. Timed/untimed tests verify equal buffers without added nominal cycles.
The display-runner error test allows an already buffered branch to complete before the patched unsupported word executes.
See [ARM instruction buffering](research/arm-instruction-buffer.md) for scope and remaining timing limits.
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.