feat(cpu): retain Thumb instruction pipeline halfwords

This commit is contained in:
0xrsydn 2026-10-06 04:10:20 +07:00
commit ea055270f4
21 changed files with 535 additions and 97 deletions

View file

@ -58,6 +58,8 @@ mod thumb_empty_tests;
#[cfg(test)] #[cfg(test)]
mod thumb_open_bus_tests; mod thumb_open_bus_tests;
#[cfg(test)] #[cfg(test)]
mod thumb_pipeline_tests;
#[cfg(test)]
mod thumb_tests; mod thumb_tests;
#[cfg(test)] #[cfg(test)]
mod timing_tests; mod timing_tests;
@ -165,8 +167,8 @@ impl InstructionSet {
} }
} }
/// ARM/Thumb interpreter with register banks, ARM instruction buffering, and nominal costs. /// ARM/Thumb interpreter with register banks, instruction buffering, and nominal costs.
/// Clone/equality include buffered fetch results. Thumb instruction buffering is not modeled. /// Clone/equality include buffered fetch results and their instruction-set tag.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cpu { pub struct Cpu {
registers: [u32; 16], registers: [u32; 16],
@ -176,7 +178,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>, pipeline: Option<pipeline::Pipeline>,
} }
impl Cpu { impl Cpu {
@ -193,7 +195,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, pipeline: None,
} }
} }
@ -237,6 +239,7 @@ impl Cpu {
// 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();
let instruction_set = self.instruction_set;
let refill = self.instruction_refills(instruction); let refill = self.instruction_refills(instruction);
let sequential_thumb = self.instruction_set == InstructionSet::Thumb && !refill; let sequential_thumb = self.instruction_set == InstructionSet::Thumb && !refill;
memory.begin_cpu_access(pc, self.instruction_set); memory.begin_cpu_access(pc, self.instruction_set);
@ -248,13 +251,13 @@ impl Cpu {
if result.is_ok() { if result.is_ok() {
if refill { 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); self.refill_pipeline(memory);
} else { } else {
// Never carry an ARM buffer through a Thumb-state transition. // A state change without an ordinary refill starts a cold sequence.
self.arm_pipeline = if self.instruction_set == InstructionSet::Arm self.pipeline = if self.instruction_set == instruction_set
&& self.pc() == pc.wrapping_add(4) && self.pc() == pc.wrapping_add(instruction_set.width())
{ {
continuation Some(continuation)
} else { } else {
None None
}; };

View file

@ -208,7 +208,7 @@ fn swi_and_machine_irq_refill_arm_vectors_before_handler_execution() {
StepKind::Instruction StepKind::Instruction
} }
); );
assert!(machine.cpu().arm_pipeline.is_some()); // Entry itself captured the ARM vector pair. assert!(machine.cpu().pipeline.is_some()); // Entry itself captured the ARM vector pair.
machine.step().unwrap(); // Vector BX refills target pair. machine.step().unwrap(); // Vector BX refills target pair.
machine.memory_mut().write32(TARGET, NEW).unwrap(); machine.memory_mut().write32(TARGET, NEW).unwrap();
machine.step().unwrap(); machine.step().unwrap();
@ -244,21 +244,21 @@ fn explicit_exception_and_pc_discontinuity_discard_the_old_execution_sequence()
cpu.step(&mut bus).unwrap(); cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 2); assert_eq!(cpu.registers[0], 2);
cpu.enter_exception(Exception::Irq); cpu.enter_exception(Exception::Irq);
assert!(cpu.arm_pipeline.is_none()); // This API has no Memory to sample. assert!(cpu.pipeline.is_none()); // This API has no Memory to sample.
let before = cpu.clone(); let before = cpu.clone();
assert!(cpu.step(&mut bus).is_err()); // Missing BIOS remains a strict error. assert!(cpu.step(&mut bus).is_err()); // Missing BIOS remains a strict error.
assert_eq!(cpu, before); assert_eq!(cpu, before);
} }
#[test] #[test]
fn leaving_arm_discards_its_buffer_and_returning_refills_from_current_memory() { fn state_changes_replace_the_buffer_and_returning_refills_from_current_memory() {
let (mut cpu, mut bus) = program(&[0xe12f_ff11, OLD, OLD]); let (mut cpu, mut bus) = program(&[0xe12f_ff11, OLD, OLD]);
bus.write16(TARGET, 0x4710).unwrap(); // Thumb BX r2. bus.write16(TARGET, 0x4710).unwrap(); // Thumb BX r2.
cpu.registers[1] = TARGET | 1; cpu.registers[1] = TARGET | 1;
cpu.registers[2] = CODE + 4; cpu.registers[2] = CODE + 4;
cpu.step(&mut bus).unwrap(); cpu.step(&mut bus).unwrap();
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb); assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
assert!(cpu.arm_pipeline.is_none()); assert!(cpu.pipeline.is_some()); // BX captured the Thumb target pair.
bus.write32(CODE + 4, NEW).unwrap(); bus.write32(CODE + 4, NEW).unwrap();
cpu.step(&mut bus).unwrap(); cpu.step(&mut bus).unwrap();
bus.write32(CODE + 4, OLD).unwrap(); bus.write32(CODE + 4, OLD).unwrap();

View file

@ -163,7 +163,10 @@ fn completed_units_and_priority_changes_use_access_order_not_channel_number() {
#[test] #[test]
fn failed_dma_and_failed_resumed_instruction_preserve_last_completed_dma_history() { fn failed_dma_and_failed_resumed_instruction_preserve_last_completed_dma_history() {
for fail_destination in [false, true] { for fail_destination in [false, true] {
let mut machine = ready(CODE); let (mut cpu, bus) = program(CODE, 0x680a);
cpu.registers[0] = 0x0e00_0000; // Buffered PROBE will fail on unsupported save memory.
let mut machine = Machine::new(cpu, bus);
machine.step().unwrap();
machine.memory_mut().write32(EWRAM, DATA).unwrap(); machine.memory_mut().write32(EWRAM, DATA).unwrap();
transfer(&mut machine, EWRAM, IWRAM, true); transfer(&mut machine, EWRAM, IWRAM, true);
// A valid IWRAM source must not drive history if its destination is invalid. // A valid IWRAM source must not drive history if its destination is invalid.
@ -189,16 +192,15 @@ fn failed_dma_and_failed_resumed_instruction_preserve_last_completed_dma_history
.memory_mut() .memory_mut()
.write16(DMA_BASE + 3 * DMA_STRIDE + 10, 0) .write16(DMA_BASE + 3 * DMA_STRIDE + 10, 0)
.unwrap(); .unwrap();
machine.memory_mut().write16(CODE + 2, 0xde00).unwrap(); // Unsupported instruction.
assert!(machine.step().is_err()); assert!(machine.step().is_err());
assert_eq!(machine.cpu(), &before); assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles); assert_eq!(machine.cycles(), cycles);
machine.memory_mut().write16(CODE + 2, PROBE).unwrap(); assert_eq!(machine.last_timing(), timing);
machine.step().unwrap(); // Repair the data address, not the retained instruction or DMA history.
assert_eq!( let mut cpu = machine.cpu().clone();
machine.cpu().registers()[6], cpu.registers[0] = UNUSED;
resumed(DATA.to_le_bytes(), CODE) cpu.step_timed(machine.memory_mut()).unwrap();
); assert_eq!(cpu.registers()[6], resumed(DATA.to_le_bytes(), CODE));
} }
} }

View file

@ -133,7 +133,7 @@ fn known_history_supports_widths_sign_extension_and_unaligned_loads_in_all_modes
expected.registers[6] = value; expected.registers[6] = value;
expected.registers[15] += 2; expected.registers[15] += 2;
cpu.step(&mut bus).unwrap(); cpu.step(&mut bus).unwrap();
assert_eq!(cpu, expected); assert_cpu_arch_eq!(cpu, expected);
} }
} }
} }
@ -284,6 +284,7 @@ fn failed_steps_discard_prefetch_history_before_retry() {
assert!(cpu.step_timed(&mut bus).is_err()); assert!(cpu.step_timed(&mut bus).is_err());
assert_eq!(cpu, before); assert_eq!(cpu, before);
bus.write16(pc + 2, PROBE).unwrap(); bus.write16(pc + 2, PROBE).unwrap();
cpu.invalidate_pipeline(); // Debugger repair leaves separate bus history intact.
cpu.step(&mut bus).unwrap(); cpu.step(&mut bus).unwrap();
assert_eq!( assert_eq!(
cpu.registers[6], cpu.registers[6],

View file

@ -134,7 +134,7 @@ fn refill_history_supports_normal_load_lanes_rotation_and_sign_extension() {
after.registers[6] = value; after.registers[6] = value;
after.registers[15] += 2; after.registers[15] += 2;
cpu.step(&mut bus).unwrap(); cpu.step(&mut bus).unwrap();
assert_eq!(cpu, after); assert_cpu_arch_eq!(cpu, after);
} }
} }
} }

View file

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

View file

@ -66,8 +66,9 @@ fn empty_thumb_stores_use_pc_plus_six_for_all_bases_banks_and_alignments() {
} else { } else {
cpu.step(&mut memory).unwrap(); cpu.step(&mut memory).unwrap();
} }
assert_eq!( assert_cpu_arch_eq!(
cpu, after, cpu,
after,
"{instruction:#06x} {mode:?} code={code_low} data={data_low}" "{instruction:#06x} {mode:?} code={code_low} data={data_low}"
); );
assert_eq!(memory.read32(DATA).unwrap(), pc + 6); assert_eq!(memory.read32(DATA).unwrap(), pc + 6);

View file

@ -127,8 +127,9 @@ fn loads_select_lanes_rotate_and_sign_extend_without_changing_flags() {
after.registers[1] = value; after.registers[1] = value;
after.registers[15] += 2; after.registers[15] += 2;
cpu.step(&mut bus).unwrap(); cpu.step(&mut bus).unwrap();
assert_eq!( assert_cpu_arch_eq!(
cpu, after, cpu,
after,
"pc={pc:#x} instruction={instruction:#x} lane={lane}" "pc={pc:#x} instruction={instruction:#x} lane={lane}"
); );
} }

View file

@ -0,0 +1,321 @@
//! Original Thumb instruction retention tests; bus history and timing remain separate.
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: u16 = 0x46c0;
const OLD: u16 = 0x2001; // MOV r0,#1.
const NEW: u16 = 0x2002; // MOV r0,#2.
fn program(pc: u32, code: &[u16]) -> (Cpu, Memory) {
let mut bus = Memory::new(vec![]).unwrap();
for (index, half) in code.iter().enumerate() {
bus.write16(pc + index as u32 * 2, *half).unwrap();
}
let mut cpu = Cpu::new(pc);
cpu.instruction_set = InstructionSet::Thumb;
(cpu, bus)
}
#[test]
fn thumb_stores_retain_the_two_prefetched_halfwords_but_not_the_next_one() {
for instruction in [0x800a, 0x700a] {
// STRH/STRB r2,[r1].
for pc in [CODE, CODE + 2] {
for offset in [2, 4, 6] {
let (mut cpu, mut bus) = program(pc, &[instruction, OLD, OLD, OLD]);
cpu.registers[1] = pc + offset;
cpu.registers[2] = u32::from(NEW);
cpu.step(&mut bus).unwrap();
assert_eq!(bus.read16(pc + offset).unwrap(), NEW);
for _ in 0..offset / 2 {
cpu.step(&mut bus).unwrap();
}
assert_eq!(cpu.registers[0], if offset <= 4 { 1 } else { 2 });
}
}
}
}
#[test]
fn host_writes_and_memory_mirrors_do_not_replace_buffered_halfwords() {
for pc in [
CODE,
CODE + 2,
0x0300_fffe,
0x0203_fffe,
0x0500_03fe,
0x0600_fffe,
0x0700_03fe,
] {
let (mut cpu, mut bus) = program(pc, &[NOP, OLD, OLD, OLD]);
cpu.step(&mut bus).unwrap();
for offset in [2, 4, 6] {
bus.write16(pc + offset, NEW).unwrap();
}
for expected in [1, 1, 2] {
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], expected);
}
}
}
#[test]
fn pc_writes_to_fallthrough_refill_the_thumb_target_pair() {
for instruction in [
0xe7ff, 0xd0ff, 0x468f, 0x448f, 0x4708, 0xbd00, 0xbc00, 0xc900, 0xf800,
] {
// B, BEQ, MOV pc,r1, ADD pc,r1, BX r1, POP pc, empty POP/LDM, BL suffix.
let (mut cpu, mut bus) = program(CODE, &[NOP, instruction, OLD, OLD]);
cpu.flags.zero = true;
cpu.registers[1] = match instruction {
0x448f => u32::MAX - 1, // Visible PC=CODE+6, target=CODE+4.
0xc900 => DATA,
_ => (CODE + 4) | 1,
};
cpu.registers[13] = DATA;
cpu.registers[14] = CODE + 4;
bus.write32(DATA, CODE + 4).unwrap(); // POP keeps Thumb even with bit0 clear.
cpu.step(&mut bus).unwrap();
bus.write16(CODE + 4, NEW).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.pc(), CODE + 4);
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
bus.write16(CODE + 4, OLD).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 2, "{instruction:#06x}");
}
}
#[test]
fn untaken_branch_keeps_the_old_pair() {
let (mut cpu, mut bus) = program(CODE, &[NOP, 0xd0ff, OLD]);
cpu.step(&mut bus).unwrap();
bus.write16(CODE + 4, NEW).unwrap();
cpu.step(&mut bus).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 1);
}
#[test]
fn bl_prefix_retains_the_suffix_and_only_the_suffix_refills() {
let (mut cpu, mut bus) = program(CODE, &[0xf000, 0xf800, OLD, OLD]);
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 1);
assert_eq!(cpu.registers[14], CODE + 4);
bus.write16(CODE + 2, NOP).unwrap(); // Does not replace the retained suffix.
bus.write16(CODE + 4, NEW).unwrap();
assert_eq!(cpu.step_timed(&mut bus).unwrap().code_cycles, 3);
assert_eq!(cpu.registers[14], CODE + 5);
bus.write16(CODE + 4, OLD).unwrap(); // Suffix already captured the updated target.
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 2);
}
#[test]
fn arm_to_thumb_refill_captures_target_and_plus_two_but_not_plus_four() {
for target in [TARGET, TARGET + 2] {
let (mut cpu, mut bus) = program(CODE, &[0, 0]);
cpu.instruction_set = InstructionSet::Arm;
bus.write32(CODE, 0xe12f_ff11).unwrap(); // BX r1.
cpu.registers[1] = target | 1;
for offset in [0, 2, 4] {
bus.write16(target + offset, OLD).unwrap();
}
cpu.step(&mut bus).unwrap();
for offset in [0, 2, 4] {
bus.write16(target + offset, NEW).unwrap();
}
for expected in [1, 1, 2] {
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], expected);
}
}
}
#[test]
fn dma_halfword_and_word_writes_do_not_replace_thumb_instruction_slots() {
for word in [false, true] {
let (cpu, mut bus) = program(CODE, &[NOP, OLD, OLD, OLD]);
for offset in [0, 2, 4] {
bus.write16(DATA + offset, 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 + if word { 4 } else { 2 })
.unwrap();
machine
.memory_mut()
.write32(dma + 8, if word { 0x8400_0001 } else { 0x8000_0003 })
.unwrap();
for _ in 0..if word { 1 } else { 3 } {
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 3 });
assert_eq!(machine.cpu(), &before);
}
for expected in [1, 1, 2] {
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[0], expected);
}
}
}
#[test]
fn failed_thumb_load_rolls_back_buffer_progress_not_earlier_retention() {
let (mut cpu, mut bus) = program(CODE, &[NOP, 0x680a, OLD, OLD]); // LDR r2,[r1].
cpu.registers[1] = 0x0e00_0000;
cpu.step(&mut bus).unwrap();
let before = cpu.clone();
assert!(cpu.step_timed(&mut bus).is_err());
assert_eq!(cpu, before);
bus.write16(CODE + 2, NOP).unwrap();
bus.write16(CODE + 4, NEW).unwrap();
bus.write16(CODE + 6, NEW).unwrap();
bus.write32(DATA, 0x1234).unwrap();
cpu.registers[1] = DATA;
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[2], 0x1234);
for expected in [1, 2] {
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], expected);
}
}
#[test]
fn short_rom_and_refill_errors_are_deferred_and_never_use_open_bus_instructions() {
for half in [NOP, 0x4708, 0xf000] {
// NOP, BX r1, BL prefix.
let mut bus = Memory::new(half.to_le_bytes().to_vec()).unwrap();
bus.write16(TARGET, OLD).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.instruction_set = InstructionSet::Thumb;
cpu.registers[1] = TARGET | 1;
cpu.step(&mut bus).unwrap();
if half == 0x4708 {
bus.write16(TARGET, NEW).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 1);
} else {
let before = cpu.clone();
for _ in 0..2 {
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(ROM_START + 2)))
);
assert_eq!(cpu, before);
}
}
}
let (mut cpu, mut bus) = program(CODE, &[0x4708]);
cpu.registers[1] = 0x8000_0001;
cpu.step(&mut bus).unwrap();
let before = cpu.clone();
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(0x8000_0000)))
);
assert_eq!(cpu, before);
}
#[test]
fn explicit_invalidation_repairs_buffered_thumb_code_and_clones_stay_independent() {
let (mut cpu, mut bus) = program(CODE, &[NOP, 0xde00, OLD]);
cpu.step(&mut bus).unwrap();
let before = cpu.clone();
let mut fork = cpu.clone();
bus.write16(CODE + 2, NEW).unwrap();
assert!(cpu.step(&mut bus).is_err());
assert_eq!(cpu, before);
cpu.invalidate_pipeline();
assert_cpu_arch_eq!(cpu, before);
assert_ne!(cpu, before);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 2);
assert!(fork.step(&mut bus).is_err());
}
#[test]
fn instruction_state_and_pc_both_select_the_retained_buffer() {
let (mut cpu, mut bus) = program(CODE, &[NOP, OLD, OLD]);
cpu.step(&mut bus).unwrap();
// A PC discontinuity starts a new Thumb sequence from mapped bytes.
cpu.registers[15] = TARGET + 2;
bus.write16(TARGET + 2, NOP).unwrap();
bus.write16(TARGET + 4, OLD).unwrap();
cpu.step(&mut bus).unwrap();
// Expected next Thumb PC is now TARGET+4. Keep that PC but change state without a refill.
cpu.instruction_set = InstructionSet::Arm;
bus.write32(TARGET + 4, 0xe3a0_0007).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], 7); // Must not decode the old Thumb halfword as ARM.
cpu.registers[15] = CODE + 1;
cpu.instruction_set = InstructionSet::Thumb;
let before = cpu.clone();
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unaligned(CODE + 1)))
);
assert_eq!(cpu, before);
}
#[test]
fn swi_and_irq_returns_refill_thumb_targets_using_saved_state() {
for irq in [false, true] {
let mut bios = vec![0; BIOS_SIZE];
let vector = if irq { 0x18 } else { 8 };
let instruction: u32 = if irq { 0xe25e_f004 } else { 0xe1b0_f00e };
bios[vector..vector + 4].copy_from_slice(&instruction.to_le_bytes());
let mut bus = Memory::with_bios(vec![], bios).unwrap();
bus.write16(CODE, if irq { NOP } else { 0xdf00 }).unwrap();
for offset in [2, 4] {
bus.write16(CODE + offset, OLD).unwrap();
}
let mut cpu = Cpu::new(CODE);
cpu.instruction_set = InstructionSet::Thumb;
let mut machine = Machine::new(cpu, bus);
machine.step().unwrap();
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();
}
machine.memory_mut().write16(CODE + 2, NEW).unwrap();
machine.step().unwrap(); // ARM exception return captures updated Thumb target.
machine.memory_mut().write16(CODE + 2, OLD).unwrap();
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[0], 2);
assert_eq!(machine.cpu().instruction_set(), InstructionSet::Thumb);
}
}
#[test]
fn timed_and_untimed_thumb_buffering_has_no_additional_nominal_costs() {
let code = [NOP, 0xe7ff, OLD, NOP];
let (mut timed, mut timed_bus) = program(CODE, &code);
let (mut untimed, mut bus) = program(CODE, &code);
for cost in [1, 3, 1] {
let timing = timed.step_timed(&mut timed_bus).unwrap();
untimed.step(&mut bus).unwrap();
assert_eq!(timed, untimed);
assert_eq!(timing.code_cycles, cost);
assert_eq!(timing.data_cycles, 0);
assert_eq!(timing.internal_cycles, 0);
assert_eq!(timed_bus.cycles(), 0);
}
}

View file

@ -179,7 +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); self.cpu.refill_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.
/// CPU-owned ARM instruction buffering does not yet drive complete bus history or timing. /// CPU-owned ARM/Thumb 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,13 +408,22 @@ impl Memory {
])) ]))
} }
/// Strict ARM fetch without CPU data context, timing trace, or bus-history changes. /// Strict instruction fetch without data context, timing, or bus-history changes.
pub(crate) fn fetch_arm_word(&self, address: u32) -> Result<u32, MemoryError> { /// Thumb reads exactly two bytes, even on a region with a wider data-bus latch.
if address & 3 != 0 { pub(crate) fn fetch_instruction(
&self,
address: u32,
state: InstructionSet,
) -> Result<u32, MemoryError> {
let width = match state {
InstructionSet::Arm => 4,
InstructionSet::Thumb => 2,
};
if address & (width as u32 - 1) != 0 {
return Err(MemoryError::Unaligned(address)); return Err(MemoryError::Unaligned(address));
} }
let mut bytes = [0; 4]; let mut bytes = [0; 4];
for (offset, byte) in bytes.iter_mut().enumerate() { for (offset, byte) in bytes[..width].iter_mut().enumerate() {
*byte = self.read_mapped_byte(address.wrapping_add(offset as u32))?; *byte = self.read_mapped_byte(address.wrapping_add(offset as u32))?;
} }
Ok(u32::from_le_bytes(bytes)) Ok(u32::from_le_bytes(bytes))

View file

@ -47,8 +47,9 @@ 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/pipeline.rs` | CPU-owned ARM/Thumb instructions, PC/state tags, 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/arm_pipeline_tests.rs` | ARM self-modifying code, branches, target pairs, DMA isolation, exceptions, errors, and timing equivalence |
| `crates/gba-core/src/cpu/thumb_pipeline_tests.rs` | Thumb retained halfwords, BL prefix/suffix, state-tagged refills, DMA isolation, rollback, 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,7 @@ 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 BIOS readback model remains separate from the CPU's ARM instruction buffer. This BIOS readback model remains separate from the CPU's ARM/Thumb instruction buffer.
It does not model exact refill, BIOS data-access, or DMA bus history. 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.

View file

@ -76,19 +76,21 @@ 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 and Thumb instruction buffering
ARM execution retains fetched instructions in a CPU-owned buffer, independent of later RAM contents. ARM and Thumb execution retain 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. Let W be the instruction width: four bytes for ARM, two for Thumb.
During sequential execution, the current and next words come from retained results; only P+8 is newly sampled. At cold entry, a successful step reads the current instruction P and samples P+W and P+2W before instruction effects.
During sequential execution, the current and next instructions come from retained results; only P+2W is newly sampled.
Success advances the buffer. CPU/DMA stores and host writes do not replace already buffered instructions. 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. 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. A successful branch or PC-writing refill samples target T and T+W after instruction effects, using the resulting instruction state.
This includes taken branches, BX, PC loads, status-restoring returns, and software interrupts. 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. 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. The first target instruction samples T+2W before execution. State transitions replace the buffer with the destination pair.
Leaving ARM discards its buffer. Thumb still fetches directly and uses its separate bus-history snapshots. Thumb BL's prefix advances sequentially; only its suffix refills. Ordinary Thumb PC writes preserve Thumb state.
The buffer records both expected PC and instruction state. A state mismatch cannot reinterpret retained ARM words as Thumb halfwords.
Machine IRQ entry captures the ARM vector pair before any handler instruction runs. 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. `Cpu::enter_exception` and `Cpu::take_interrupt` have no memory parameter: they invalidate the buffer and leave vector sampling to the next step.
@ -101,14 +103,14 @@ A failed instruction preserves all CPU state, including the old buffer; its spec
A successful branch discards abandoned-path errors and replaces them with target fetch results. 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 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. `Cpu::invalidate_pipeline()` explicitly discards buffered instructions without changing registers, flags, or device clocks.
Use it after debugger code repair or when attaching different memory to a retained CPU. 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. 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. 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. 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. 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). See the [ARM evidence and diagnostic policy](../research/arm-instruction-buffer.md) and [Thumb extension](../research/thumb-instruction-buffer.md).
## ARM unused-memory data reads ## ARM unused-memory data reads
@ -137,8 +139,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.
- ARM instruction buffering is persistent, but these data snapshots remain a separate bounded model. - ARM/Thumb instruction buffering is persistent, but these data snapshots remain a separate bounded model.
Thumb IWRAM refills sample their target pair without buffering Thumb instructions. Thumb IWRAM bus-history samples do not share storage with retained 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.
@ -220,13 +222,13 @@ Stack and block-load returns finish their data accesses before the target sample
Failed instructions do not sample a target or replace committed history. Failed instructions do not sample a target or replace committed history.
Snapshots use strict mapped reads and add no nominal data or device cycles. Snapshots use strict mapped reads and add no nominal data or device cycles.
They update bus history only; the interpreter does not cache these instructions for execution. They update bus history only; a separate CPU buffer retains the target instructions for execution.
Host inspection does not change captured lanes. A failed target fetch remains a later instruction diagnostic. Host inspection does not change captured lanes. A failed target fetch remains a later instruction diagnostic.
Cold direct startup, unsupported target states/regions, and region-crossing lookahead still have no inferred refill history. Cold direct startup, unsupported target states/regions, and region-crossing lookahead still have no inferred refill history.
DMA between refill and arrival updates this continuation's actual IWRAM access lanes before the target+4 sample. DMA between refill and arrival updates this continuation's actual IWRAM access lanes before the target+4 sample.
The [refill evidence and scope](../research/iwram-bus-history.md#thumb-iwram-refill-extension) distinguish these samples from a full pipeline. The [refill evidence and scope](../research/iwram-bus-history.md#thumb-iwram-refill-extension) distinguish these samples from a full pipeline.
ARM-target refill history, sub-instruction DMA ordering, self-modifying instruction execution, and exact refill timing remain incomplete. ARM-target refill history, sub-instruction DMA ordering, and exact refill timing remain incomplete.
### DMA effects on Thumb IWRAM continuations ### DMA effects on Thumb IWRAM continuations
@ -273,7 +275,7 @@ 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.
ARM instruction buffering is discarded on a state change without an ordinary refill. Instruction buffering is discarded on a state change without an ordinary refill.
Instruction-state-changing compare forms and exact pipeline behavior remain hardware-unverified. 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.

View file

@ -147,13 +147,16 @@ 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: ARM/Thumb 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. - [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/Thumb PC offsets.
- The pinned NanoBoyAdvance ARM7TDMI `Run` and `ReloadPipeline32` helpers above, for retained words, pre-execution fetching, and target-pair refill order. - The pinned NanoBoyAdvance ARM7TDMI `Run`, `ReloadPipeline32`, and `ReloadPipeline16` helpers above, for retained instructions, pre-execution fetching, and target-pair refill order.
- The ARM branch cycle sequence above, for target and target+instruction-width fetches. - 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. - [ARM Thumb branch-with-link timing](https://support.arm.com/documentation/ddi0210/c/Instruction-Cycle-Timings/Thumb-branch-with-link), for sequential prefix execution and suffix target refill.
See [persistent ARM instruction buffering](research/arm-instruction-buffer.md) and its [Thumb extension](research/thumb-instruction-buffer.md).
Instruction retention is implemented; unified fetch-driven bus history and per-access timing 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.

View file

@ -24,7 +24,8 @@ No external source code or test ROM was imported. No physical-hardware or extern
## Implementation ## Implementation
`cpu/pipeline.rs` owns a private two-slot ARM continuation with its expected execution PC. `cpu/pipeline.rs` owns a private two-slot continuation with its expected execution PC and instruction state.
The later [Thumb extension](thumb-instruction-buffer.md) uses this same structure for halfwords.
Each slot retains either a mapped word or its strict fetch error. Each slot retains either a mapped word or its strict fetch error.
`Cpu` owns and clones this state; full CPU equality includes it. `Cpu` owns and clones this state; full CPU equality includes it.
@ -37,13 +38,13 @@ 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. 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. 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. 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. Untaken branches retain the pair. A transition to Thumb now replaces it with a Thumb target pair.
Machine IRQ entry captures its vector pair immediately after exception entry. 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. 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. 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. `Memory::fetch_instruction` reads aligned mapped instructions without CPU data context, timing traces, or bus-history changes.
Normal memory mirrors apply. Unmapped, truncated, and misaligned fetches retain their existing errors. 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. No BIOS-protected data value or general open-bus value can become a synthetic instruction through this path.
@ -74,7 +75,7 @@ The current instruction address still selects nominal code cost rather than a se
ARM PC+8 open bus and protected BIOS readback remain separate snapshots. 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 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. 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. 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. DMA still runs between whole instructions, not during their data accesses or internal cycles.
## Original regression coverage ## Original regression coverage

View file

@ -45,7 +45,7 @@ A completed DMA WAITCNT store changes the settings before CPU resume.
## Limits ## Limits
The timing model has no per-access fetch scheduling or 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. The later [ARM](arm-instruction-buffer.md) and [Thumb](thumb-instruction-buffer.md) instruction buffers retain instructions without changing 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

@ -56,7 +56,7 @@ An accepted machine IRQ invalidates history. Failed DMA units preserve it.
The DMA extension below updates existing continuation lanes from successful IWRAM accesses. The DMA extension below updates existing continuation lanes from successful IWRAM accesses.
This is not a full IWRAM bus model across all CPU states. ARM-target history and DMA-to-CPU ordering remain incomplete. This is not a full IWRAM bus model across all CPU states. ARM-target history and DMA-to-CPU ordering remain incomplete.
It is not a persistent instruction pipeline. Self-modifying instruction execution and exact per-access device timing remain unverified. It is not a persistent instruction pipeline. Exact per-access device timing remains unverified.
## Thumb IWRAM refill extension ## Thumb IWRAM refill extension
@ -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 Thumb instructions when it executes them; ARM now has separate instruction buffering. ARM and Thumb execution now use a separate instruction buffer. Cold buffer filling does not seed this bus history.
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,8 +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.
This extension does not fix self-modifying Thumb instruction-buffer behavior. This extension models bus lanes, not self-modifying instruction execution.
The later [ARM instruction buffer](arm-instruction-buffer.md) separately retains ARM words across DMA. The later [ARM](arm-instruction-buffer.md) and [Thumb](thumb-instruction-buffer.md) instruction buffers separately retain instructions 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

@ -0,0 +1,85 @@
# Persistent Thumb instruction buffering
## Evidence and scope
The [ARM7TDMI pipeline description](https://developer.arm.com/documentation/dvi0027/b/arm7tdmi/instruction-pipeline) identifies fetch, decode, and execute stages.
The [technical reference manual](https://documentation-service.arm.com/static/5e8e1323fd977155116a3129) describes Thumb's two-byte instruction width and visible PC offset of four bytes.
The [Thumb BL cycle sequence](https://support.arm.com/documentation/ddi0210/c/Instruction-Cycle-Timings/Thumb-branch-with-link) distinguishes the sequential prefix from the branching suffix.
[NanoBoyAdvance at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/arm/arm7tdmi.hh) provides an implementation cross-check:
- `Run` takes the retained current instruction and shifts the next instruction forward.
- In Thumb state, it reads the next halfword before executing the current instruction.
- `ReloadPipeline16` reads target T, then T+2. The target instruction later fetches T+4.
- `ReloadPipeline32` uses ARM's four-byte width instead.
Thus, execution at P retains the halfwords at P+2 and P+4 before instruction effects.
A store cannot replace these retained instructions. It can affect P+6, which is fetched later.
A taken PC write discards the sequential pair, even when its destination equals fallthrough.
The BL prefix retains its suffix; only the suffix refills the target pair.
This extends the [ARM instruction buffer](arm-instruction-buffer.md), not the bus scheduler.
No external source code or test ROM was imported. No physical-hardware or external-emulator differential run was performed.
## Implementation
`cpu/pipeline.rs` shares one two-slot buffer between ARM and Thumb.
Each buffer records its expected execution PC and instruction state.
Both must match before retained instructions can be reused.
Thumb halfwords are zero-extended in the slots; upper bits do not hold another instruction.
Cold execution reads the current instruction and samples the next two instructions.
Sequential execution uses retained results and samples only P+4 in Thumb state.
The prospective continuation commits only after successful execution.
Refills sample the target pair after data transfers and status restoration, using the resulting instruction state.
ARM-to-Thumb BX and saved-state exception returns therefore capture T and T+2, not T and T+4.
Thumb-to-ARM transitions and SWI/IRQ vectors capture ARM pairs.
`Memory::fetch_instruction` uses strict mapped reads with the selected instruction width.
A Thumb fetch reads exactly two bytes, even in a region whose separate bus snapshot uses a full word.
Memory mirrors apply. Unmapped and misaligned instruction reads retain diagnostics rather than using data open bus.
Lookahead errors remain deferred until their slot executes; a branch can discard them.
Failed execution preserves the previous buffer exactly. Newly sampled lookahead does not commit.
CPU stores, DMA transfers, and host writes never replace retained instructions automatically.
`Cpu::invalidate_pipeline()` explicitly discards retained instructions for debugger repair or attachment to different memory.
It does not clear separate BIOS or IWRAM bus history.
CPU clones own independent buffers; full CPU equality includes them.
## Separation from bus history and timing
Instruction retention and data-bus snapshots remain separate models.
Cold instruction-buffer filling does not make unknown IWRAM bus lanes known.
Successful DMA can update existing IWRAM bus lanes without changing retained CPU instructions.
Refill bus-history sampling and instruction sampling currently perform separate strict mapped reads.
Buffer samples add no nominal cycles or data accesses.
Timed and untimed execution use the same buffer logic.
Destination-based refill summaries and one-shot non-sequential CPU resume costs remain unchanged.
Unified fetch-driven bus history, exact region crossings, per-access timing, Game Pak prefetch, and sub-instruction DMA arbitration remain incomplete.
Instruction-state-changing compare quirks also remain hardware-unverified.
## Original regression coverage
Original programs cover:
- CPU byte/halfword stores to P+2, P+4, and the later P+6 instruction.
- Host writes, RAM/video mirrors, and halfword/word DMA writes.
- Taken and untaken branches, PC writes to fallthrough, and standalone BL suffixes.
- BL prefix retention and suffix refill, with unchanged nominal costs.
- ARM-to-Thumb target pairs at both halfword alignments and SWI/IRQ saved-state returns.
- Failed data loads, discarded speculative advances, deferred short-ROM errors, and strict target diagnostics.
- Debugger invalidation, independent clones, PC/state mismatches, and alignment errors.
- Equal timed/untimed buffers without additional nominal cycles.
The retention regressions failed with the previous direct-fetch Thumb interpreter before implementation.
Existing bus-history tests now invalidate deliberately repaired code or repair a data address without changing the retained instruction.
Successful instruction-semantic tests compare architectural state separately; failed-step tests still compare complete CPU state.
## 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 ARM-buffer reports exactly, including nominal cycle counts.
Debug and release reports also match for the same fixture paths.
These checks do not establish full hardware pipeline or timing conformance.

View file

@ -27,7 +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. - Persistent ARM/Thumb 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.
@ -141,8 +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.
ARM instruction buffering now retains sequential words and refill target pairs, independently of these bus snapshots. ARM/Thumb instruction buffering retains sequential instructions 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. 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.
@ -153,7 +153,7 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa
## Next steps ## Next steps
1. Extend persistent ARM instruction buffering to Thumb, then connect fetches to bus history with independent per-access tests. 1. Connect ARM/Thumb instruction 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.

View file

@ -85,7 +85,10 @@ They cover refill target pairs, PC writes to fallthrough, conditional branches,
Full CPU equality checks buffer rollback and cloning. Successful instruction-semantic tests compare architectural state separately. 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. 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. 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. Thumb instruction-buffer tests verify retained P+2/P+4 halfwords, BL prefix/suffix behavior, and state-tagged target pairs.
They also cover stores, host/DMA writes, saved-state returns, deferred errors, mirrors, debugger repair, and nominal timing.
Bus-history repair tests explicitly invalidate buffered code or repair a data address; failure checks retain full CPU equality.
See [ARM instruction buffering](research/arm-instruction-buffer.md) and its [Thumb extension](research/thumb-instruction-buffer.md) for remaining 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.