feat(cpu): retain IWRAM bus history across execution states

This commit is contained in:
0xrsydn 2026-10-06 14:30:38 +07:00
commit f2c532f8f6
24 changed files with 522 additions and 359 deletions

View file

@ -91,7 +91,7 @@ fn refill_history_consumes_captured_target_halfwords_without_rereading_memory()
memory.write16(target + 2, 0xffff).unwrap();
memory.refill_cpu_bus_history(&fetches);
let lookahead = memory.fetch_instruction(target + 4, InstructionSet::Thumb);
memory.begin_cpu_access(target, InstructionSet::Thumb, &lookahead);
memory.begin_cpu_access(target, InstructionSet::Thumb, None, &lookahead);
assert_eq!(
memory.read32(UNUSED).unwrap(),
if target & 2 == 0 {
@ -100,7 +100,7 @@ fn refill_history_consumes_captured_target_halfwords_without_rereading_memory()
0x5678_9abc
}
);
memory.end_cpu_access(true, true);
memory.end_cpu_access(true);
}
}
@ -119,11 +119,11 @@ fn bios_history_commits_the_captured_bus_word_not_later_image_bytes() {
// Test-only image mutation: BIOS remains read-only through normal APIs.
memory.bios.as_mut().unwrap()[word_address..word_address + 4]
.copy_from_slice(&CHANGED.to_le_bytes());
memory.begin_cpu_access(pc, state, &fetch);
memory.end_cpu_access(true, false);
memory.begin_cpu_access(pc, state, None, &fetch);
memory.end_cpu_access(true);
assert_eq!(memory.bios_prefetch, Some(WORD));
let outside = memory.fetch_instruction(0x0200_0008, InstructionSet::Arm);
memory.begin_cpu_access(0x0200_0000, InstructionSet::Arm, &outside);
memory.begin_cpu_access(0x0200_0000, InstructionSet::Arm, None, &outside);
assert_eq!(memory.read32(word_address as u32).unwrap(), WORD);
// Instruction reads still bypass the protected data value.
assert_eq!(
@ -132,7 +132,7 @@ fn bios_history_commits_the_captured_bus_word_not_later_image_bytes() {
.instruction,
Ok(CHANGED)
);
memory.end_cpu_access(true, false);
memory.end_cpu_access(true);
}
}
@ -149,12 +149,12 @@ fn sampling_has_no_timing_or_history_side_effects_and_never_uses_data_fallback()
assert_eq!(memory.bios_prefetch, None);
assert!(memory.cpu_resume_nonsequential);
assert_eq!(memory.cycles(), 0);
memory.begin_cpu_access(0, InstructionSet::Arm, &fetch);
memory.begin_cpu_access(0, InstructionSet::Arm, None, &fetch);
assert_eq!(memory.read32(UNUSED).unwrap(), WORD);
let missing = memory.fetch_instruction(UNUSED, InstructionSet::Arm);
assert_eq!(missing.instruction, Err(MemoryError::Unmapped(UNUSED)));
assert_eq!(missing.bus_word, None);
memory.end_cpu_access(false, false);
memory.end_cpu_access(false);
assert_eq!(memory.bios_prefetch, None);
assert!(memory.cpu_resume_nonsequential);
}
@ -167,12 +167,12 @@ fn missing_fetch_observation_invalidates_bios_history_only_on_success() {
let pc = BIOS_SIZE as u32 - state.width();
let fetch = memory.fetch_instruction(pc + 2 * state.width(), state);
assert!(fetch.instruction.is_err());
memory.begin_cpu_access(pc, state, &fetch);
memory.end_cpu_access(false, false);
memory.begin_cpu_access(pc, state, None, &fetch);
memory.end_cpu_access(false);
assert_eq!(memory.bios_prefetch, Some(WORD));
}
let fetch = memory.fetch_instruction(BIOS_SIZE as u32, InstructionSet::Thumb);
memory.begin_cpu_access(BIOS_SIZE as u32 - 4, InstructionSet::Thumb, &fetch);
memory.end_cpu_access(true, false);
memory.begin_cpu_access(BIOS_SIZE as u32 - 4, InstructionSet::Thumb, None, &fetch);
memory.end_cpu_access(true);
assert_eq!(memory.bios_prefetch, None);
}

View file

@ -1,6 +1,5 @@
//! Transactional lane history for Thumb IWRAM execution and target-pair refills.
//! Completed DMA accesses update existing continuation lanes before the next fetch.
//! Unknown startup history is not replaced with guessed memory bytes.
//! Persistent local IWRAM lanes, independent of CPU state and executing region.
//! CPU instructions stage changes; successful DMA units commit accesses in order.
use std::cell::Cell;
use crate::timing::AccessWidth;
@ -13,6 +12,9 @@ struct Lanes {
impl Lanes {
fn drive(&mut self, address: u32, width: AccessWidth, value: u32) {
if address >> 24 != 3 {
return;
}
debug_assert_eq!(address & (width.bytes() - 1), 0);
let mask = match width {
AccessWidth::Byte => 0xff,
@ -30,93 +32,47 @@ impl Lanes {
}
}
#[derive(Clone, Copy)]
struct History {
next_pc: u32,
lanes: Lanes,
}
#[derive(Default)]
pub(super) struct IwramBus {
committed: Option<History>,
committed: Lanes,
// Reads use &Memory. Stage their lane changes without changing committed history.
pending: Cell<Option<History>>,
pending: Cell<Option<Lanes>>,
}
impl IwramBus {
pub(super) fn begin(&mut self, pc: Option<u32>, fetched: Option<u16>) {
#[cfg(test)]
pub(super) fn committed_word(&self) -> Option<u32> {
self.committed.word()
}
pub(super) fn begin(&mut self) {
debug_assert!(self.pending.get().is_none());
self.pending.set(pc.map(|pc| {
let mut lanes = self
.committed
.filter(|history| history.next_pc == pc)
.map_or_else(Lanes::default, |history| history.lanes);
if let Some(half) = fetched {
lanes.drive(pc + 4, AccessWidth::Halfword, u32::from(half));
} else {
lanes = Lanes::default(); // Unavailable or region-crossing prefetch.
}
History {
next_pc: pc.wrapping_add(2),
lanes,
}
}));
self.pending.set(Some(self.committed));
}
pub(super) fn snapshot(&self) -> Option<u32> {
self.pending.get().and_then(|history| history.lanes.word())
self.pending.get().and_then(Lanes::word)
}
pub(super) fn access(&self, address: u32, width: AccessWidth, value: u32) {
if address >> 24 != 3 {
return;
}
if let Some(mut history) = self.pending.get() {
history.lanes.drive(address, width, value);
self.pending.set(Some(history));
if let Some(mut lanes) = self.pending.get() {
lanes.drive(address, width, value);
self.pending.set(Some(lanes));
}
}
/// DMA runs between instructions. Commit only after the entire unit succeeds.
/// Keep the expected CPU continuation PC; do not invent one at cold startup.
/// DMA runs between instructions. Call only after the entire unit succeeds.
pub(super) fn dma_access(&mut self, address: u32, width: AccessWidth, value: u32) {
debug_assert!(self.pending.get().is_none());
if address >> 24 == 3 {
if let Some(history) = &mut self.committed {
history.lanes.drive(address, width, value);
}
}
self.committed.drive(address, width, value);
}
pub(super) fn finish(&mut self, succeeded: bool, sequential: bool) {
pub(super) fn finish(&mut self, succeeded: bool) {
let pending = self.pending.take();
if succeeded {
self.committed = if sequential {
pending.filter(|history| history.next_pc >> 24 == 3)
} else {
None
};
self.committed = pending.expect("IWRAM transaction must have started");
}
}
pub(super) fn refill(&mut self, pc: u32, fetched: [u16; 2]) {
debug_assert!(self.pending.get().is_none());
debug_assert_eq!(pc & 1, 0);
let mut lanes = Lanes::default();
for (index, half) in fetched.into_iter().enumerate() {
lanes.drive(
pc + index as u32 * 2,
AccessWidth::Halfword,
u32::from(half),
);
}
self.committed = Some(History { next_pc: pc, lanes });
}
pub(super) fn invalidate(&mut self) {
debug_assert!(self.pending.get().is_none());
self.committed = None;
}
}
#[cfg(test)]
@ -126,31 +82,36 @@ mod tests {
#[test]
fn incomplete_lanes_stay_unknown_until_all_bytes_have_been_driven() {
let mut bus = IwramBus::default();
bus.begin(Some(0x0300_0100), Some(0x1234));
bus.begin();
bus.access(0x0300_0104, AccessWidth::Halfword, 0x1234);
assert_eq!(bus.snapshot(), None);
bus.access(0x0300_2002, AccessWidth::Byte, 0x56);
assert_eq!(bus.snapshot(), None);
bus.access(0x0200_2003, AccessWidth::Byte, 0xff); // Other memory cannot fill a lane.
bus.access(0x0200_2003, AccessWidth::Byte, 0xff);
assert_eq!(bus.snapshot(), None);
bus.access(0x0300_2003, AccessWidth::Byte, 0x78);
assert_eq!(bus.snapshot(), Some(0x7856_1234));
bus.finish(true, true);
bus.begin(Some(0x0300_0102), Some(0xabcd));
bus.finish(true);
bus.begin();
bus.access(0x0300_0106, AccessWidth::Halfword, 0xabcd);
assert_eq!(bus.snapshot(), Some(0xabcd_1234));
}
#[test]
fn failed_transaction_discards_all_staged_accesses_not_only_prefetch() {
let mut bus = IwramBus::default();
bus.begin(Some(0x0300_0100), Some(0x1234));
bus.begin();
bus.access(0x0300_0104, AccessWidth::Halfword, 0x1234);
bus.access(0x0300_2000, AccessWidth::Word, 0x1122_3344);
bus.finish(true, true);
bus.begin(Some(0x0300_0102), Some(0xabcd));
bus.finish(true);
bus.begin();
bus.access(0x0300_0106, AccessWidth::Halfword, 0xabcd);
bus.access(0x0300_2000, AccessWidth::Word, 0xdead_beef);
bus.access(0x0300_2001, AccessWidth::Byte, 0x55);
assert_eq!(bus.snapshot(), Some(0xdead_55ef));
bus.finish(false, false);
bus.begin(Some(0x0300_0102), Some(0x5678));
bus.finish(false);
bus.begin();
bus.access(0x0300_0106, AccessWidth::Halfword, 0x5678);
assert_eq!(bus.snapshot(), Some(0x5678_3344));
}
}

View file

@ -0,0 +1,192 @@
//! Original local-bus tests. Expected lanes are independent of the CPU instruction buffer.
use super::*;
use crate::{
cpu::Cpu,
dma::DMA_STRIDE,
io::{IE, IME, TIMER_BASE},
machine::{Machine, StepKind},
};
const CODE: u32 = 0x0200_0100;
const IWRAM: u32 = 0x0300_0000;
const WORD: u32 = 0x9abc_1234;
const NOP: u32 = 0xe1a0_0000;
fn arm(code: &[u32]) -> (Cpu, Memory) {
let mut memory = Memory::new(vec![]).unwrap();
for (i, word) in code.iter().enumerate() {
memory.write32(CODE + i as u32 * 4, *word).unwrap();
}
memory.write32(IWRAM, WORD).unwrap();
(Cpu::new(CODE), memory)
}
#[test]
fn arm_data_accesses_from_other_code_regions_drive_only_addressed_iwram_lanes() {
for (instruction, expected) in [
(0xe5d0_1000, None), // LDRB low byte, not a complete word.
(0xe1d0_10b2, None), // LDRH high half, not a complete word.
(0xe590_1000, Some(WORD)), // LDR word.
] {
let (mut cpu, mut memory) = arm(&[0xe3a0_0403, instruction, NOP, NOP]); // MOV r0,#0x03000000.
cpu.step(&mut memory).unwrap();
assert_eq!(memory.iwram_bus.committed_word(), None);
cpu.step(&mut memory).unwrap();
assert_eq!(memory.iwram_bus.committed_word(), expected);
cpu.step(&mut memory).unwrap();
assert_eq!(memory.iwram_bus.committed_word(), expected); // EWRAM code does not erase the local latch.
}
}
#[test]
fn arm_byte_and_halfword_reads_and_stores_preserve_other_lanes_across_nonlocal_code() {
for (instruction, expected) in [
(0xe5d0_1001, 0x9abc_5634), // LDRB r1,[r0,#1].
(0xe1d0_10b2, 0x5678_1234), // LDRH r1,[r0,#2].
(0xe5c0_2001, 0x9abc_0034), // STRB r2,[r0,#1], r2=0.
(0xe1c0_20b2, 0x0000_1234), // STRH r2,[r0,#2].
] {
let (mut cpu, mut memory) = arm(&[0xe3a0_0403, 0xe590_1000, NOP, instruction, NOP, NOP]);
cpu.step(&mut memory).unwrap();
cpu.step(&mut memory).unwrap();
memory.write32(IWRAM, 0x5678_5678).unwrap(); // Host writes do not drive the latch.
cpu.step(&mut memory).unwrap();
assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));
cpu.step(&mut memory).unwrap();
assert_eq!(memory.iwram_bus.committed_word(), Some(expected));
}
}
#[test]
fn arm_and_thumb_target_pairs_drive_the_same_local_bus_in_fetch_order() {
// EWRAM ARM -> IWRAM ARM -> IWRAM Thumb -> unavailable BIOS ARM.
let (mut cpu, mut memory) = arm(&[0xe3a0_0403, 0xe12f_ff10, NOP, NOP]);
memory.write32(IWRAM, 0xe280_1011).unwrap(); // ADD r1,r0,#0x11.
memory.write32(IWRAM + 4, 0xe12f_ff11).unwrap(); // BX r1.
memory.write32(IWRAM + 8, WORD).unwrap();
memory.write32(IWRAM + 12, 0x8765_4321).unwrap();
memory.write16(IWRAM + 16, 0x46c0).unwrap(); // Thumb NOP.
memory.write16(IWRAM + 18, 0x4710).unwrap(); // BX r2 (zero -> BIOS; target error deferred).
memory.write32(IWRAM + 20, 0x5678_1234).unwrap();
cpu.step(&mut memory).unwrap();
cpu.step(&mut memory).unwrap();
assert_eq!(memory.iwram_bus.committed_word(), Some(0xe12f_ff11));
cpu.step(&mut memory).unwrap();
assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
assert_eq!(memory.iwram_bus.committed_word(), Some(0x4710_46c0));
cpu.step(&mut memory).unwrap();
assert_eq!(memory.iwram_bus.committed_word(), Some(0x4710_1234));
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.instruction_set(), InstructionSet::Arm);
assert_eq!(memory.iwram_bus.committed_word(), Some(0x5678_1234));
let before = cpu.clone();
assert!(cpu.step(&mut memory).is_err());
assert_eq!(cpu, before);
assert_eq!(memory.iwram_bus.committed_word(), Some(0x5678_1234));
}
#[test]
fn thumb_data_access_from_ewram_drives_local_iwram_history() {
let (mut cpu, mut memory) = arm(&[
0xe3a0_0403, // MOV r0,#0x03000000.
0xe28f_1001, // ADD r1,pc,#1 -> CODE+13.
0xe12f_ff11, // BX r1.
0x46c0_6802, // Thumb LDR r2,[r0]; NOP.
0x46c0_46c0,
]);
for _ in 0..3 {
cpu.step(&mut memory).unwrap();
}
assert_eq!(cpu.instruction_set(), InstructionSet::Thumb);
assert_eq!(memory.iwram_bus.committed_word(), None);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers()[2], WORD);
assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));
memory.write32(IWRAM, 0).unwrap();
cpu.step(&mut memory).unwrap();
assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));
}
#[test]
fn dma_can_establish_lanes_before_any_cpu_instruction() {
for word in [false, true] {
let (_, mut memory) = arm(&[]);
let dma = DMA_BASE + 3 * DMA_STRIDE;
memory.write32(dma, IWRAM).unwrap();
memory.write32(dma + 4, CODE).unwrap();
memory
.write32(dma + 8, if word { 0x8400_0001 } else { 0x8000_0002 })
.unwrap();
memory.step_dma().unwrap().unwrap();
assert_eq!(
memory.iwram_bus.committed_word(),
if word { Some(WORD) } else { None }
);
if !word {
memory.step_dma().unwrap().unwrap();
assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));
}
}
}
#[test]
fn accepted_irq_and_bios_fetches_preserve_local_lanes_until_actual_iwram_access() {
let mut bios = vec![0; BIOS_SIZE];
bios[0x18..0x1c].copy_from_slice(&NOP.to_le_bytes());
bios[0x1c..0x20].copy_from_slice(&0xe590_1000_u32.to_le_bytes()); // LDR r1,[r0].
let mut memory = Memory::with_bios(vec![], bios).unwrap();
for (i, word) in [0xe3a0_0403, 0xe590_1000, NOP, NOP].into_iter().enumerate() {
memory.write32(CODE + i as u32 * 4, word).unwrap();
}
memory.write32(IWRAM, WORD).unwrap();
let mut machine = Machine::new(Cpu::new(CODE), memory);
machine.step().unwrap();
machine.step().unwrap();
assert_eq!(machine.memory().iwram_bus.committed_word(), Some(WORD));
machine.memory_mut().write32(IWRAM, 0x8765_4321).unwrap();
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);
assert_eq!(machine.memory().iwram_bus.committed_word(), Some(WORD));
machine.step().unwrap();
assert_eq!(machine.memory().iwram_bus.committed_word(), Some(WORD));
machine.step().unwrap();
assert_eq!(
machine.memory().iwram_bus.committed_word(),
Some(0x8765_4321)
);
}
#[test]
fn failed_instruction_discards_its_iwram_fetch_and_failed_dma_preserves_committed_lanes() {
let (mut cpu, mut memory) = arm(&[0xe3a0_0403, 0xe590_1000, 0xe12f_ff10, NOP, NOP]);
cpu.step(&mut memory).unwrap();
cpu.step(&mut memory).unwrap();
memory.write32(IWRAM, 0xe7f0_00f0).unwrap(); // Unsupported ARM instruction.
memory.write32(IWRAM + 4, WORD).unwrap();
memory.write32(IWRAM + 8, 0x8765_4321).unwrap();
cpu.step(&mut memory).unwrap(); // Target pair ends with WORD.
let before = cpu.clone();
assert!(cpu.step_timed(&mut memory).is_err());
assert_eq!(cpu, before);
assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));
// A failing cold entry also discards its current/decode/lookahead lane updates.
let mut cold = Cpu::new(IWRAM);
let before = cold.clone();
assert!(cold.step(&mut memory).is_err());
assert_eq!(cold, before);
assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));
let dma = DMA_BASE + 3 * DMA_STRIDE;
memory.write32(dma, IWRAM + 8).unwrap();
memory.write32(dma + 4, ROM_START).unwrap(); // Read-only destination.
memory.write32(dma + 8, 0x8400_0001).unwrap();
assert!(memory.step_dma().is_err());
assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));
}