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

@ -238,19 +238,16 @@ impl Cpu {
instruction,
continuation,
lookahead,
fill,
} = fetched;
// Record the executing instruction, not an operand's pipelined PC value.
// Always clear the access context, including on diagnostic errors.
let pc = self.pc();
let instruction_set = self.instruction_set;
let refill = self.instruction_refills(instruction);
let sequential_thumb = self.instruction_set == InstructionSet::Thumb && !refill;
memory.begin_cpu_access(pc, self.instruction_set, &lookahead);
memory.begin_cpu_access(pc, self.instruction_set, fill.as_ref(), &lookahead);
let result = self.execute_instruction(instruction, memory);
let sequential = sequential_thumb
&& self.instruction_set == InstructionSet::Thumb
&& self.pc() == pc.wrapping_add(2);
memory.end_cpu_access(result.is_ok(), sequential);
memory.end_cpu_access(result.is_ok());
if result.is_ok() {
if refill {
self.refill_pipeline(memory);

View file

@ -205,8 +205,8 @@ fn failed_dma_and_failed_resumed_instruction_preserve_last_completed_dma_history
}
#[test]
fn partial_continuation_can_gain_known_lanes_but_cold_direct_entry_stays_unknown() {
let (mut cpu, mut bus) = program(CODE, 0x46c0); // NOP fills only the low lane.
fn dma_updates_retained_lanes_but_a_cold_pipeline_fill_drives_them_again() {
let (mut cpu, mut bus) = program(CODE, 0x46c0); // Cold NOP fills both lanes.
cpu.step(&mut bus).unwrap();
bus.write32(EWRAM, DATA).unwrap();
configure(&mut bus, 3, EWRAM, IWRAM, 1, true);
@ -214,19 +214,17 @@ fn partial_continuation_can_gain_known_lanes_but_cold_direct_entry_stays_unknown
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[6], resumed(DATA.to_le_bytes(), CODE));
// No executed instruction or refill established an expected continuation PC.
// DMA establishes the latch, but the later cold fill drives both instruction lanes.
let (mut cpu, mut bus) = program(CODE, PROBE);
bus.write32(EWRAM, DATA).unwrap();
configure(&mut bus, 3, EWRAM, IWRAM, 1, true);
bus.step_dma().unwrap().unwrap();
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[6], 0x6806_4321);
}
#[test]
fn irq_and_pc_discontinuity_do_not_reuse_dma_updated_continuations() {
fn irq_preserves_dma_history_while_pc_discontinuity_performs_a_new_fill() {
for irq in [false, true] {
let mut machine = ready(CODE);
machine.memory_mut().write32(EWRAM, DATA).unwrap();
@ -245,9 +243,14 @@ fn irq_and_pc_discontinuity_do_not_reuse_dma_updated_continuations() {
cpu.registers[15] = CODE + 0x100;
machine.memory_mut().write16(cpu.pc(), PROBE).unwrap();
}
cpu.step(machine.memory_mut()).unwrap();
assert_eq!(
cpu.step(machine.memory_mut()),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
cpu.registers[6],
if irq {
resumed(DATA.to_le_bytes(), CODE)
} else {
0
}
);
}
}
@ -257,7 +260,7 @@ fn arm_and_other_thumb_regions_keep_their_fetch_snapshot_not_dma_data() {
for thumb in [false, true] {
for pc in [0x0200_0100, 0x0300_0100, 0x0700_0100] {
if thumb && pc >> 24 == 3 {
continue; // The bounded IWRAM continuation is tested separately.
continue; // Persistent IWRAM lanes are tested separately.
}
let mut bus = Memory::new(vec![]).unwrap();
let mut cpu = Cpu::new(pc);

View file

@ -249,17 +249,18 @@ fn block_transfers_leave_the_last_iwram_word_and_keep_writeback() {
}
#[test]
fn cold_history_is_unknown_but_successful_fetches_or_word_accesses_can_establish_it() {
fn cold_pipeline_fills_establish_history_before_the_first_thumb_data_read() {
for pc in [CODE, CODE + 2] {
let (mut cpu, mut bus) = program(pc, &[PROBE, PROBE, 0x1111, 0x2222]);
let before = cpu.clone();
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
assert_eq!(cpu, before);
bus.write16(pc, NOP).unwrap();
cpu.step(&mut bus).unwrap();
assert_eq!(
cpu.registers[6],
if pc & 2 == 0 {
0x6806_1111
} else {
0x1111_6806
}
);
cpu.step(&mut bus).unwrap();
assert_eq!(
cpu.registers[6],
@ -288,7 +289,12 @@ fn failed_steps_discard_prefetch_history_before_retry() {
cpu.step(&mut bus).unwrap();
assert_eq!(
cpu.registers[6],
drive(DATA, pc + 6, &0x9bcd_u16.to_le_bytes())
// Debugger invalidation starts a fresh fill: PC+2 is now also fetched.
if pc & 2 == 0 {
0x9bcd_1111
} else {
0x1111_9bcd
}
);
assert_eq!(bus.cycles(), 0);
}
@ -319,23 +325,19 @@ fn taken_control_flow_refills_history_even_when_target_is_fallthrough() {
}
#[test]
fn pc_discontinuities_and_non_iwram_execution_do_not_reuse_old_history() {
fn pc_discontinuities_fill_from_actual_target_bytes_not_stale_instruction_slots() {
let (mut cpu, mut bus) = program(CODE, &[SEED, PROBE, 0x1111, 0x2222]);
cpu.step(&mut bus).unwrap();
bus.write16(CODE + 0x100, PROBE).unwrap();
cpu.registers[15] = CODE + 0x100;
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[6], 0); // New target's next two halfwords are zero.
bus.write16(0x0200_0000, NOP).unwrap();
cpu.registers[15] = 0x0200_0000;
cpu.step(&mut bus).unwrap();
cpu.registers[15] = CODE + 2;
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[6], 0x2222_1111);
}
#[test]
@ -399,7 +401,7 @@ fn history_updates_add_no_data_accesses_or_device_cycles() {
}
#[test]
fn accepted_irq_invalidates_history_before_any_handler_instruction() {
fn accepted_irq_preserves_local_history_before_any_handler_iwram_access() {
let (cpu, bus) = program(CODE, &[SEED, PROBE, 0x1111, 0x9bcd]);
let mut machine = Machine::new(cpu, bus);
machine.step().unwrap();
@ -413,8 +415,6 @@ fn accepted_irq_invalidates_history_before_any_handler_instruction() {
machine.memory_mut().advance_cycles(1);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
// Probe the paused instruction with a separate CPU, without executing BIOS.
assert_eq!(
interrupted.step(machine.memory_mut()),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
interrupted.step(machine.memory_mut()).unwrap();
assert_eq!(interrupted.registers[6], 0x9bcd_80a5);
}

View file

@ -319,16 +319,14 @@ fn failed_refill_instructions_preserve_cpu_and_preexisting_history() {
}
#[test]
fn cold_entry_region_crossings_and_unmapped_targets_do_not_invent_refill_history() {
fn cold_fill_establishes_lanes_but_region_crossings_and_unmapped_targets_stay_diagnostic() {
let mut bus = Memory::new(vec![]).unwrap();
target(&mut bus, TARGET, PROBE);
let mut cpu = Cpu::new(TARGET);
cpu.instruction_set = InstructionSet::Thumb;
cpu.registers[0] = UNUSED;
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[6], expected(TARGET));
for destination in [0x03ff_fffc, 0x03ff_fffe, 0x0e00_0000] {
let mut bus = Memory::new(0xe12f_ff14_u32.to_le_bytes().to_vec()).unwrap();

View file

@ -247,12 +247,8 @@ fn cold_iwram_thumb_reads_and_unused_fetches_do_not_reuse_arm_context() {
bus.write16(0x0300_0100, 0x6801).unwrap(); // IWRAM Thumb LDR r1,[r0]
cpu.registers[15] = 0x0300_0100;
cpu.instruction_set = InstructionSet::Thumb;
let before = cpu.clone();
assert_eq!(
cpu.step(&mut bus),
Err(CpuError::Memory(MemoryError::Unmapped(UNUSED)))
);
assert_eq!(cpu, before);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[1], 0); // Actual cold-fill lanes, not the prior ARM snapshot.
}
#[test]

View file

@ -15,6 +15,7 @@ pub(super) struct Fetched {
pub instruction: u32,
pub continuation: Pipeline,
pub lookahead: InstructionFetch,
pub fill: Option<[InstructionFetch; 2]>,
}
impl Cpu {
@ -33,17 +34,17 @@ impl Cpu {
.pipeline
.as_ref()
.filter(|pipe| pipe.pc == pc && pipe.instruction_set == instruction_set);
let instruction = match retained {
Some(pipe) => pipe.instructions[0].clone(),
None => memory.fetch_instruction(pc, instruction_set).instruction,
}?;
let decode = match retained {
Some(pipe) => pipe.instructions[1].clone(),
None => {
memory
.fetch_instruction(pc.wrapping_add(width), instruction_set)
.instruction
}
let (instruction, decode, fill) = if let Some(pipe) = retained {
(
pipe.instructions[0].clone()?,
pipe.instructions[1].clone(),
None,
)
} else {
let current = memory.fetch_instruction(pc, instruction_set);
let instruction = current.instruction.clone()?;
let next = memory.fetch_instruction(pc.wrapping_add(width), instruction_set);
(instruction, next.instruction.clone(), Some([current, next]))
};
let fetched = memory.fetch_instruction(pc.wrapping_add(2 * width), instruction_set);
Ok(Fetched {
@ -54,6 +55,7 @@ impl Cpu {
instructions: [decode, fetched.instruction.clone()],
},
lookahead: fetched,
fill,
})
}

View file

@ -242,20 +242,18 @@ fn failed_thumb_steps_keep_previous_history_and_clear_access_context() {
}
#[test]
fn outside_thumb_execution_keeps_bios_history_without_seeding_iwram_history() {
fn outside_thumb_execution_keeps_bios_history_separate_from_cold_iwram_fills() {
let mut memory = bus(0x6801, thumb_image(THUMB_ENTRY, BX_R5));
let mut cpu = enter_thumb(&mut memory, THUMB_ENTRY, true);
cpu.step(&mut memory).unwrap();
assert_eq!(protected_word(&mut memory), OTHER);
memory.write16(0x0300_0100, 0x6801).unwrap(); // No preceding sequential IWRAM history.
memory.write16(0x0300_0100, 0x6801).unwrap(); // Cold IWRAM entry.
memory.write16(0x0300_0102, 0x6801).unwrap(); // Next protected BIOS read.
memory.write16(0x0300_0104, 0x1357).unwrap();
cpu.registers[15] = 0x0300_0100;
cpu.registers[0] = 0x4000;
let before = cpu.clone();
assert_eq!(
cpu.step(&mut memory),
Err(CpuError::Memory(MemoryError::Unmapped(0x4000)))
);
assert_eq!(cpu, before);
cpu.step(&mut memory).unwrap();
assert_eq!(cpu.registers[1], 0x6801_1357);
assert_eq!(memory.read32(0x4000), Err(MemoryError::Unmapped(0x4000)));
assert_eq!(protected_word(&mut memory), OTHER);
cpu.registers[0] = 0;

View file

@ -294,12 +294,21 @@ fn short_rom_needs_only_the_fetched_halfword_and_errors_only_when_used() {
}
#[test]
fn cold_iwram_history_and_region_crossing_fetches_remain_diagnostics() {
fn cold_iwram_fills_establish_lanes_but_region_crossing_fetches_remain_diagnostics() {
for base in [0x0300_0100, 0x0300_8100, 0x02ff_fffc, 0x05ff_fffc] {
for offset in [0, 2] {
let pc = base + offset;
let mut bus = program(pc, LDR);
let mut cpu = cpu(pc);
if pc >> 24 == 3 {
cpu.step(&mut bus).unwrap();
assert_eq!(
cpu.registers[1],
if offset == 0 { 0x46c0_80a5 } else { DATA }
);
assert_eq!(bus.read32(UNUSED), Err(MemoryError::Unmapped(UNUSED)));
continue;
}
let before = cpu.clone();
assert_eq!(
cpu.step(&mut bus),

View file

@ -4,6 +4,8 @@ mod fetch;
#[cfg(test)]
mod fetch_tests;
mod iwram_bus;
#[cfg(test)]
mod iwram_history_tests;
pub(crate) use fetch::InstructionFetch;
use iwram_bus::IwramBus;
@ -69,9 +71,9 @@ struct CpuAccess {
/// Work/video/palette RAM, OAM, cartridge ROM, optional BIOS, and supported I/O.
/// Unused-memory reads use ARM PC+8 or supported region-dependent Thumb snapshots.
/// Protected BIOS reads separately retain the snapshot from BIOS execution.
/// Sequential Thumb IWRAM accesses retain addressed bus lanes transactionally.
/// Successful refills into Thumb IWRAM sample the target pair to establish history.
/// DMA accesses drive existing Thumb IWRAM continuation lanes before the next fetch.
/// ARM/Thumb IWRAM fetches and data accesses retain local bus lanes transactionally.
/// Cold fills and target pairs drive the same latch, independently of executing region.
/// Successful DMA accesses drive local lanes even before the first CPU instruction.
/// Channel data remains separate; general and sub-instruction CPU bus handoff is unmodeled.
/// Instruction buffering and supported bus history consume shared fetch samples.
/// Complete bus ownership and per-access timing remain unmodeled.
@ -374,19 +376,27 @@ impl Memory {
&mut self,
pc: u32,
instruction_set: InstructionSet,
fill: Option<&[InstructionFetch; 2]>,
fetch: &InstructionFetch,
) {
debug_assert!(self.cpu_access.is_none());
debug_assert_eq!(fetch.state, instruction_set);
debug_assert_eq!(fetch.address, pc.wrapping_add(2 * instruction_set.width()));
// Consume the pipeline's new fetch, never a retained opcode or a second read.
// Missing lookahead must not fail an instruction that never uses it.
// Cold fills drive current/decode fetches; retained instructions never replay them.
// Stage the new lookahead last, before data accesses and the entry snapshot.
self.iwram_bus.begin();
if let Some(fill) = fill {
for (index, sample) in fill.iter().enumerate() {
debug_assert_eq!(sample.state, instruction_set);
debug_assert_eq!(
sample.address,
pc.wrapping_add(index as u32 * instruction_set.width())
);
self.drive_iwram_fetch(sample);
}
}
self.drive_iwram_fetch(fetch);
let same_region = fetch.address >> 24 == pc >> 24;
let iwram_pc = (instruction_set == InstructionSet::Thumb && pc >> 24 == 3).then_some(pc);
let fetched = same_region
.then(|| fetch.instruction.as_ref().ok().map(|value| *value as u16))
.flatten();
self.iwram_bus.begin(iwram_pc, fetched);
let prefetch = match instruction_set {
InstructionSet::Arm => fetch.bus_word,
InstructionSet::Thumb if !same_region => None,
@ -396,12 +406,12 @@ impl Memory {
self.cpu_access = Some(CpuAccess { pc, prefetch });
}
pub(crate) fn end_cpu_access(&mut self, succeeded: bool, sequential: bool) {
pub(crate) fn end_cpu_access(&mut self, succeeded: bool) {
if succeeded {
// Timed and untimed CPU execution both consume a resume. Errors do not.
self.cpu_resume_nonsequential = false;
}
self.iwram_bus.finish(succeeded, sequential);
self.iwram_bus.finish(succeeded);
if let Some(access) = self.cpu_access.take() {
if succeeded && access.pc < BIOS_SIZE as u32 {
// Commit only successful instruction snapshots under our diagnostic policy.
@ -411,25 +421,30 @@ impl Memory {
}
}
/// Consume the same target samples that fill the CPU's instruction buffer.
/// Incomplete or unsupported refills stay unknown without failing the branch early.
fn drive_iwram_fetch(&self, fetch: &InstructionFetch) {
if let Ok(value) = fetch.instruction {
let width = match fetch.state {
InstructionSet::Arm => AccessWidth::Word,
InstructionSet::Thumb => AccessWidth::Halfword,
};
self.iwram_bus.access(fetch.address, width, value);
}
}
/// Successful IWRAM target fetches drive their lanes in order, in either CPU state.
/// Other-region and unavailable target fetches leave the local latch unchanged.
pub(crate) fn refill_cpu_bus_history(&mut self, fetches: &[InstructionFetch; 2]) {
self.iwram_bus.invalidate();
self.iwram_bus.begin();
let [first, second] = fetches;
debug_assert_eq!(first.state, second.state);
debug_assert_eq!(
second.address,
first.address.wrapping_add(first.state.width())
);
if first.state == InstructionSet::Thumb
&& first.address >> 24 == 3
&& second.address >> 24 == 3
{
if let (Ok(first_value), Ok(second_value)) = (&first.instruction, &second.instruction) {
self.iwram_bus
.refill(first.address, [*first_value as u16, *second_value as u16]);
}
for fetch in fetches {
self.drive_iwram_fetch(fetch);
}
self.iwram_bus.finish(true);
}
fn can_write_power_control(&self) -> bool {

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));
}