refactor(cpu): share instruction fetch samples with bus history

This commit is contained in:
0xrsydn 2026-10-06 14:12:03 +07:00
commit e630d7a0ea
18 changed files with 538 additions and 117 deletions

View file

@ -38,6 +38,8 @@ mod dma_resume_tests;
#[cfg(test)]
mod exception_tests;
#[cfg(test)]
mod fetch_history_tests;
#[cfg(test)]
mod instruction_tests;
#[cfg(test)]
mod iwram_dma_tests;
@ -155,7 +157,7 @@ pub enum InstructionSet {
}
impl InstructionSet {
fn width(self) -> u32 {
pub(crate) fn width(self) -> u32 {
match self {
Self::Arm => 4,
Self::Thumb => 2,
@ -235,6 +237,7 @@ impl Cpu {
let pipeline::Fetched {
instruction,
continuation,
lookahead,
} = fetched;
// Record the executing instruction, not an operand's pipelined PC value.
// Always clear the access context, including on diagnostic errors.
@ -242,7 +245,7 @@ impl Cpu {
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);
memory.begin_cpu_access(pc, self.instruction_set, &lookahead);
let result = self.execute_instruction(instruction, memory);
let sequential = sequential_thumb
&& self.instruction_set == InstructionSet::Thumb
@ -250,7 +253,6 @@ impl Cpu {
memory.end_cpu_access(result.is_ok(), sequential);
if result.is_ok() {
if refill {
memory.refill_cpu_bus_history(self.pc(), self.instruction_set);
self.refill_pipeline(memory);
} else {
// A state change without an ordinary refill starts a cold sequence.

View file

@ -0,0 +1,114 @@
//! Original sample-consumption tests. The split fetch/execute is a test-only mutation seam,
//! not a claim that DMA can interrupt an instruction in the current scheduler.
use super::*;
const UNUSED: u32 = 0x8000_0000;
const DATA: u32 = 0x0300_1000;
const OLD: u32 = 0x1234_5678;
const NEW: u32 = 0x9abc_def0;
#[test]
fn arm_open_bus_uses_the_sample_already_taken_for_the_pipeline() {
for pc in [0x0200_0100, 0x0300_0100, 0x0700_0100] {
let mut bus = Memory::new(vec![]).unwrap();
bus.write32(pc, 0xe590_6000).unwrap(); // LDR r6,[r0].
bus.write32(pc + 8, OLD).unwrap();
let mut cpu = Cpu::new(pc);
cpu.registers[0] = UNUSED;
let fetched = cpu.fetch(&bus).unwrap();
bus.write32(pc + 8, NEW).unwrap();
cpu.execute_fetched(fetched, &mut bus).unwrap();
assert_eq!(cpu.registers[6], OLD);
assert_eq!(bus.read32(pc + 8).unwrap(), NEW);
assert_eq!(bus.cycles(), 0);
}
}
#[test]
fn thumb_narrow_open_bus_uses_the_sample_already_taken_for_the_pipeline() {
for pc in [0x0200_0100, 0x0500_0100, 0x0600_0100] {
for alignment in [0, 2] {
let pc = pc + alignment;
let mut bus = Memory::new(vec![]).unwrap();
bus.write16(pc, 0x6806).unwrap(); // LDR r6,[r0].
bus.write16(pc + 4, OLD as u16).unwrap();
let mut cpu = Cpu::new(pc);
cpu.instruction_set = InstructionSet::Thumb;
cpu.registers[0] = UNUSED;
let fetched = cpu.fetch(&bus).unwrap();
bus.write16(pc + 4, NEW as u16).unwrap();
cpu.execute_fetched(fetched, &mut bus).unwrap();
assert_eq!(cpu.registers[6], 0x5678_5678);
}
}
}
#[test]
fn thumb_wide_open_bus_retains_both_lanes_of_the_fetch_sample() {
for pc in [0x0700_0100, 0x0700_0102] {
let mut bus = Memory::new(vec![]).unwrap();
bus.write16(pc, 0x6806).unwrap();
bus.write32((pc + 4) & !3, OLD).unwrap();
let mut cpu = Cpu::new(pc);
cpu.instruction_set = InstructionSet::Thumb;
cpu.registers[0] = UNUSED;
let fetched = cpu.fetch(&bus).unwrap();
bus.write32((pc + 4) & !3, NEW).unwrap();
cpu.execute_fetched(fetched, &mut bus).unwrap();
assert_eq!(cpu.registers[6], OLD);
}
}
#[test]
fn iwram_lane_history_uses_new_fetch_data_not_retained_opcodes_or_reread_bytes() {
for pc in [0x0300_0100, 0x0300_0102] {
let mut bus = Memory::new(vec![]).unwrap();
for (index, half) in [0x680a, 0x6806, 0x46c0, 0x5678].into_iter().enumerate() {
bus.write16(pc + index as u32 * 2, half).unwrap();
}
bus.write32(DATA, 0x1122_3344).unwrap();
let mut cpu = Cpu::new(pc);
cpu.instruction_set = InstructionSet::Thumb;
cpu.registers[0] = UNUSED;
cpu.registers[1] = DATA;
cpu.step(&mut bus).unwrap(); // Data read establishes all local bus lanes.
let fetched = cpu.fetch(&bus).unwrap();
bus.write16(pc + 6, 0xdef0).unwrap();
cpu.execute_fetched(fetched, &mut bus).unwrap();
let expected = if pc & 2 == 0 {
0x5678_3344
} else {
0x1122_5678
};
assert_eq!(cpu.registers[6], expected);
}
}
#[test]
fn failed_execution_discards_its_sample_and_retry_takes_a_new_one() {
let pc = 0x0300_0100;
let mut bus = Memory::new(vec![]).unwrap();
for (index, half) in [0x680a, 0x6823, 0x6806, 0x5678, 0x9abc]
.into_iter()
.enumerate()
{
bus.write16(pc + index as u32 * 2, half).unwrap();
}
bus.write32(DATA, 0x1122_3344).unwrap();
let mut cpu = Cpu::new(pc);
cpu.instruction_set = InstructionSet::Thumb;
cpu.registers[0] = UNUSED;
cpu.registers[1] = DATA;
cpu.registers[4] = 0x0e00_0000;
cpu.step(&mut bus).unwrap();
let before = cpu.clone();
let fetched = cpu.fetch(&bus).unwrap();
assert!(cpu.execute_fetched(fetched, &mut bus).is_err());
assert_eq!(cpu, before);
bus.write16(pc + 6, 0xdef0).unwrap();
cpu.registers[4] = UNUSED; // Retry reads open bus instead of unsupported save memory.
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[3], 0xdef0_3344);
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[6], 0xdef0_9abc);
}

View file

@ -1,6 +1,6 @@
//! ARM/Thumb instruction buffering. Timing and bus ownership remain separate.
use super::{Cpu, CpuError, InstructionSet};
use crate::memory::{Memory, MemoryError};
use crate::memory::{InstructionFetch, Memory, MemoryError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Pipeline {
@ -14,6 +14,7 @@ pub(super) struct Pipeline {
pub(super) struct Fetched {
pub instruction: u32,
pub continuation: Pipeline,
pub lookahead: InstructionFetch,
}
impl Cpu {
@ -34,11 +35,15 @@ impl Cpu {
.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),
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),
None => {
memory
.fetch_instruction(pc.wrapping_add(width), instruction_set)
.instruction
}
};
let fetched = memory.fetch_instruction(pc.wrapping_add(2 * width), instruction_set);
Ok(Fetched {
@ -46,23 +51,26 @@ impl Cpu {
continuation: Pipeline {
pc: pc.wrapping_add(width),
instruction_set,
instructions: [decode, fetched],
instructions: [decode, fetched.instruction.clone()],
},
lookahead: fetched,
})
}
/// Capture the target pair using the resulting state, without early errors.
/// Explicit exception entry without Memory leaves a cold buffer instead.
pub(crate) fn refill_pipeline(&mut self, memory: &Memory) {
pub(crate) fn refill_pipeline(&mut self, memory: &mut Memory) {
let pc = self.pc();
let instruction_set = self.instruction_set;
let fetches = [
memory.fetch_instruction(pc, instruction_set),
memory.fetch_instruction(pc.wrapping_add(instruction_set.width()), instruction_set),
];
memory.refill_cpu_bus_history(&fetches);
self.pipeline = Some(Pipeline {
pc,
instruction_set,
instructions: [
memory.fetch_instruction(pc, instruction_set),
memory.fetch_instruction(pc.wrapping_add(instruction_set.width()), instruction_set),
],
instructions: fetches.map(|fetch| fetch.instruction),
});
}
}

View file

@ -178,8 +178,7 @@ impl Machine {
return Ok(StepKind::HaltIdle);
}
let (kind, timing) = if self.cpu.take_interrupt(self.memory.irq_pending(), false) {
self.memory.invalidate_cpu_bus_history();
self.cpu.refill_pipeline(&self.memory);
self.cpu.refill_pipeline(&mut self.memory);
(StepKind::IrqEntry, self.cpu.exception_timing(&self.memory))
} else {
(

View file

@ -1,6 +1,10 @@
use std::{cell::Cell, error::Error, fmt};
mod fetch;
#[cfg(test)]
mod fetch_tests;
mod iwram_bus;
pub(crate) use fetch::InstructionFetch;
use iwram_bus::IwramBus;
use crate::{
@ -69,7 +73,8 @@ struct CpuAccess {
/// Successful refills into Thumb IWRAM sample the target pair to establish history.
/// 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.
/// CPU-owned ARM/Thumb instruction buffering does not yet drive complete bus history or timing.
/// Instruction buffering and supported bus history consume shared fetch samples.
/// Complete bus ownership and per-access timing remain unmodeled.
pub struct Memory {
external_ram: Vec<u8>,
internal_ram: Vec<u8>,
@ -365,80 +370,32 @@ impl Memory {
self.io.next_event_cycles()
}
pub(crate) fn begin_cpu_access(&mut self, pc: u32, instruction_set: InstructionSet) {
pub(crate) fn begin_cpu_access(
&mut self,
pc: u32,
instruction_set: InstructionSet,
fetch: &InstructionFetch,
) {
debug_assert!(self.cpu_access.is_none());
// Sample before execution, without data timing or open-bus recursion.
// Missing lookahead bytes must not fail an instruction that never uses them.
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.
let same_region = fetch.address >> 24 == pc >> 24;
let iwram_pc = (instruction_set == InstructionSet::Thumb && pc >> 24 == 3).then_some(pc);
let fetched = iwram_pc
.filter(|pc| pc.wrapping_add(4) >> 24 == 3)
.and_then(|pc| self.snapshot_mapped_halfword(pc + 4));
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 => self.snapshot_mapped_word(pc.wrapping_add(8)),
InstructionSet::Thumb => self.snapshot_thumb_prefetch(pc),
InstructionSet::Arm => fetch.bus_word,
InstructionSet::Thumb if !same_region => None,
InstructionSet::Thumb if pc >> 24 == 3 => self.iwram_bus.snapshot(),
InstructionSet::Thumb => fetch.bus_word,
};
self.cpu_access = Some(CpuAccess { pc, prefetch });
}
fn snapshot_thumb_prefetch(&self, pc: u32) -> Option<u32> {
let address = pc.wrapping_add(4);
// Region-crossing fetches need pipeline history, not a rule chosen from the old PC.
if address >> 24 != pc >> 24 {
return None;
}
match pc >> 24 {
// BIOS and OAM drive a full word even for a halfword instruction fetch.
0x00 | 0x07 => self.snapshot_mapped_word(address & !3),
// The IWRAM halfword fetch replaces one lane of its retained bus word.
0x03 => self.iwram_bus.snapshot(),
// These 16-bit regions repeat the fetched halfword in both word lanes.
0x02 | 0x05 | 0x06 | 0x08..=0x0d => {
Some(u32::from(self.snapshot_mapped_halfword(address)?) * 0x0001_0001)
}
// Unsupported code regions must not manufacture an open-bus snapshot.
_ => None,
}
}
fn snapshot_mapped_halfword(&self, address: u32) -> Option<u16> {
Some(u16::from_le_bytes([
self.read_mapped_byte(address).ok()?,
self.read_mapped_byte(address + 1).ok()?,
]))
}
/// Strict instruction fetch without data context, timing, or bus-history changes.
/// Thumb reads exactly two bytes, even on a region with a wider data-bus latch.
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));
}
let mut bytes = [0; 4];
for (offset, byte) in bytes[..width].iter_mut().enumerate() {
*byte = self.read_mapped_byte(address.wrapping_add(offset as u32))?;
}
Ok(u32::from_le_bytes(bytes))
}
fn snapshot_mapped_word(&self, address: u32) -> Option<u32> {
let mut bytes = [0; 4];
for (offset, byte) in bytes.iter_mut().enumerate() {
*byte = self
.read_mapped_byte(address.wrapping_add(offset as u32))
.ok()?;
}
Some(u32::from_le_bytes(bytes))
}
pub(crate) fn end_cpu_access(&mut self, succeeded: bool, sequential: bool) {
if succeeded {
// Timed and untimed CPU execution both consume a resume. Errors do not.
@ -454,28 +411,27 @@ impl Memory {
}
}
/// Sample only the two target fetches of a completed refill into Thumb IWRAM.
/// This updates bus history, not the CPU's instruction buffer or nominal costs.
/// Consume the same target samples that fill the CPU's instruction buffer.
/// Incomplete or unsupported refills stay unknown without failing the branch early.
pub(crate) fn refill_cpu_bus_history(&mut self, pc: u32, instruction_set: InstructionSet) {
pub(crate) fn refill_cpu_bus_history(&mut self, fetches: &[InstructionFetch; 2]) {
self.iwram_bus.invalidate();
if instruction_set == InstructionSet::Thumb
&& pc >> 24 == 3
&& pc.wrapping_add(2) >> 24 == 3
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 (Some(first), Some(second)) = (
self.snapshot_mapped_halfword(pc),
self.snapshot_mapped_halfword(pc + 2),
) {
self.iwram_bus.refill(pc, [first, second]);
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]);
}
}
}
pub(crate) fn invalidate_cpu_bus_history(&mut self) {
self.iwram_bus.invalidate();
}
fn can_write_power_control(&self) -> bool {
// Bare memory writes are host/debug setup. CPU code must execute in BIOS.
self.cpu_access

View file

@ -0,0 +1,65 @@
//! One mapped sample supplies instruction bits and the supported bus observation.
//! Sampling is side-effect-free; execution commits history under the diagnostic policy.
use super::{InstructionSet, Memory, MemoryError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct InstructionFetch {
pub(super) address: u32,
pub(super) state: InstructionSet,
pub(crate) instruction: Result<u32, MemoryError>,
// None for unsupported regions, failed reads, or lane-based Thumb IWRAM history.
pub(super) bus_word: Option<u32>,
}
impl Memory {
pub(crate) fn fetch_instruction(
&self,
address: u32,
state: InstructionSet,
) -> InstructionFetch {
let instruction = self.read_instruction(address, state);
let bus_word = instruction.as_ref().ok().copied().and_then(|value| {
if state == InstructionSet::Arm {
return Some(value);
}
match address >> 24 {
// BIOS/OAM drive the aligned word. Read only the other halfword;
// do not sample the instruction bytes a second time.
0x00 | 0x07 => {
let other = self
.read_instruction(address ^ 2, InstructionSet::Thumb)
.ok()?;
Some(if address & 2 == 0 {
value | (other << 16)
} else {
other | (value << 16)
})
}
// IWRAM updates its existing addressed lanes at execution entry.
0x03 => None,
0x02 | 0x05 | 0x06 | 0x08..=0x0d => Some(value * 0x0001_0001),
_ => None,
}
});
InstructionFetch {
address,
state,
instruction,
bus_word,
}
}
/// Strict mapped bytes, without data context, timing, or bus-history changes.
/// An unavailable wider bus observation must not invalidate a valid instruction.
fn read_instruction(&self, address: u32, state: InstructionSet) -> Result<u32, MemoryError> {
let width = state.width() as usize;
if address & (width as u32 - 1) != 0 {
return Err(MemoryError::Unaligned(address));
}
let mut bytes = [0; 4];
for (offset, byte) in bytes[..width].iter_mut().enumerate() {
*byte = self.read_mapped_byte(address.wrapping_add(offset as u32))?;
}
Ok(u32::from_le_bytes(bytes))
}
}

View file

@ -0,0 +1,178 @@
//! Independent fetch-observation checks. Mutation between sample and consumption
//! checks ownership of captured values, not sub-instruction hardware arbitration.
use super::*;
const WORD: u32 = 0x9abc_1234;
const CHANGED: u32 = 0x5678_def0;
const UNUSED: u32 = 0x8000_0000;
#[test]
fn mapped_fetches_separate_instruction_width_from_driven_bus_lanes() {
let mut bios = vec![0; BIOS_SIZE];
bios[..4].copy_from_slice(&WORD.to_le_bytes());
let mut memory = Memory::with_bios(WORD.to_le_bytes().to_vec(), bios).unwrap();
// Explicit expected bus words for each Thumb alignment; IWRAM needs prior lanes.
for (address, expected) in [
(0, [Some(WORD), Some(WORD)]),
(0x0200_0000, [Some(0x1234_1234), Some(0x9abc_9abc)]),
(0x0300_0000, [None, None]),
(0x0500_0000, [Some(0x1234_1234), Some(0x9abc_9abc)]),
(0x0600_0000, [Some(0x1234_1234), Some(0x9abc_9abc)]),
(0x0700_0000, [Some(WORD), Some(WORD)]),
(0x0800_0000, [Some(0x1234_1234), Some(0x9abc_9abc)]),
(0x0a00_0000, [Some(0x1234_1234), Some(0x9abc_9abc)]),
(0x0c00_0000, [Some(0x1234_1234), Some(0x9abc_9abc)]),
] {
if (0x0200_0000..0x0800_0000).contains(&address) {
memory.write32(address, WORD).unwrap();
}
let arm = memory.fetch_instruction(address, InstructionSet::Arm);
assert_eq!(arm.instruction, Ok(WORD));
assert_eq!(arm.bus_word, Some(WORD));
for (lane, expected) in expected.into_iter().enumerate() {
let thumb = memory.fetch_instruction(address + lane as u32 * 2, InstructionSet::Thumb);
assert_eq!(
thumb.instruction,
Ok(if lane == 0 { 0x1234 } else { 0x9abc })
);
assert_eq!(thumb.bus_word, expected);
}
}
}
#[test]
fn fetch_errors_remain_strict_and_narrow_rom_fetches_need_only_two_bytes() {
let memory = Memory::new(vec![0x34, 0x12]).unwrap();
let thumb = memory.fetch_instruction(ROM_START, InstructionSet::Thumb);
assert_eq!(thumb.instruction, Ok(0x1234));
assert_eq!(thumb.bus_word, Some(0x1234_1234));
for (address, state, error) in [
(
ROM_START,
InstructionSet::Arm,
MemoryError::Unmapped(ROM_START + 2),
),
(
ROM_START + 2,
InstructionSet::Thumb,
MemoryError::Unmapped(ROM_START + 2),
),
(0, InstructionSet::Thumb, MemoryError::Unmapped(0)),
(UNUSED, InstructionSet::Arm, MemoryError::Unmapped(UNUSED)),
(
ROM_START + 1,
InstructionSet::Thumb,
MemoryError::Unaligned(ROM_START + 1),
),
(
ROM_START + 2,
InstructionSet::Arm,
MemoryError::Unaligned(ROM_START + 2),
),
] {
let fetch = memory.fetch_instruction(address, state);
assert_eq!(fetch.instruction, Err(error));
assert_eq!(fetch.bus_word, None);
}
}
#[test]
fn refill_history_consumes_captured_target_halfwords_without_rereading_memory() {
for target in [0x0300_0100, 0x0300_0102] {
let mut memory = Memory::new(vec![]).unwrap();
memory.write16(target, 0x1234).unwrap();
memory.write16(target + 2, 0x9abc).unwrap();
memory.write16(target + 4, 0x5678).unwrap();
let fetches = [
memory.fetch_instruction(target, InstructionSet::Thumb),
memory.fetch_instruction(target + 2, InstructionSet::Thumb),
];
memory.write16(target, 0xffff).unwrap();
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);
assert_eq!(
memory.read32(UNUSED).unwrap(),
if target & 2 == 0 {
0x9abc_5678
} else {
0x5678_9abc
}
);
memory.end_cpu_access(true, true);
}
}
#[test]
fn bios_history_commits_the_captured_bus_word_not_later_image_bytes() {
for (pc, state, fetch_address) in [
(0x100, InstructionSet::Arm, 0x108),
(0x100, InstructionSet::Thumb, 0x104),
(0x102, InstructionSet::Thumb, 0x106),
] {
let mut bios = vec![0; BIOS_SIZE];
let word_address = (fetch_address & !3) as usize;
bios[word_address..word_address + 4].copy_from_slice(&WORD.to_le_bytes());
let mut memory = Memory::with_bios(vec![], bios).unwrap();
let fetch = memory.fetch_instruction(fetch_address, state);
// 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);
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);
assert_eq!(memory.read32(word_address as u32).unwrap(), WORD);
// Instruction reads still bypass the protected data value.
assert_eq!(
memory
.fetch_instruction(word_address as u32, InstructionSet::Arm)
.instruction,
Ok(CHANGED)
);
memory.end_cpu_access(true, false);
}
}
#[test]
fn sampling_has_no_timing_or_history_side_effects_and_never_uses_data_fallback() {
let mut bios = vec![0; BIOS_SIZE];
bios[8..12].copy_from_slice(&WORD.to_le_bytes());
let mut memory = Memory::with_bios(vec![], bios).unwrap();
memory.cpu_resume_nonsequential = true;
memory.begin_data_timing();
let fetch = memory.fetch_instruction(8, InstructionSet::Arm);
memory.fetch_instruction(0x0300_0000, InstructionSet::Thumb);
assert_eq!(memory.end_data_timing(), 0);
assert_eq!(memory.bios_prefetch, None);
assert!(memory.cpu_resume_nonsequential);
assert_eq!(memory.cycles(), 0);
memory.begin_cpu_access(0, InstructionSet::Arm, &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);
assert_eq!(memory.bios_prefetch, None);
assert!(memory.cpu_resume_nonsequential);
}
#[test]
fn missing_fetch_observation_invalidates_bios_history_only_on_success() {
let mut memory = Memory::with_bios(vec![], vec![0; BIOS_SIZE]).unwrap();
memory.bios_prefetch = Some(WORD);
for state in [InstructionSet::Arm, InstructionSet::Thumb] {
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);
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);
assert_eq!(memory.bios_prefetch, None);
}