feat(cpu): retain IWRAM bus history across execution states
This commit is contained in:
parent
e630d7a0ea
commit
f2c532f8f6
24 changed files with 522 additions and 359 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
192
crates/gba-core/src/memory/iwram_history_tests.rs
Normal file
192
crates/gba-core/src/memory/iwram_history_tests.rs
Normal 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));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue