feat(cpu): retain Thumb instruction pipeline halfwords
This commit is contained in:
parent
47df9d05a7
commit
ea055270f4
21 changed files with 535 additions and 97 deletions
|
|
@ -58,6 +58,8 @@ mod thumb_empty_tests;
|
|||
#[cfg(test)]
|
||||
mod thumb_open_bus_tests;
|
||||
#[cfg(test)]
|
||||
mod thumb_pipeline_tests;
|
||||
#[cfg(test)]
|
||||
mod thumb_tests;
|
||||
#[cfg(test)]
|
||||
mod timing_tests;
|
||||
|
|
@ -165,8 +167,8 @@ impl InstructionSet {
|
|||
}
|
||||
}
|
||||
|
||||
/// ARM/Thumb interpreter with register banks, ARM instruction buffering, and nominal costs.
|
||||
/// Clone/equality include buffered fetch results. Thumb instruction buffering is not modeled.
|
||||
/// ARM/Thumb interpreter with register banks, instruction buffering, and nominal costs.
|
||||
/// Clone/equality include buffered fetch results and their instruction-set tag.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Cpu {
|
||||
registers: [u32; 16],
|
||||
|
|
@ -176,7 +178,7 @@ pub struct Cpu {
|
|||
irq_disabled: bool,
|
||||
fiq_disabled: bool,
|
||||
banks: status::Banks,
|
||||
arm_pipeline: Option<pipeline::ArmPipeline>,
|
||||
pipeline: Option<pipeline::Pipeline>,
|
||||
}
|
||||
|
||||
impl Cpu {
|
||||
|
|
@ -193,7 +195,7 @@ impl Cpu {
|
|||
irq_disabled: false,
|
||||
fiq_disabled: false,
|
||||
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.
|
||||
// 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);
|
||||
|
|
@ -248,13 +251,13 @@ impl Cpu {
|
|||
if result.is_ok() {
|
||||
if refill {
|
||||
memory.refill_cpu_bus_history(self.pc(), self.instruction_set);
|
||||
self.refill_arm_pipeline(memory);
|
||||
self.refill_pipeline(memory);
|
||||
} else {
|
||||
// Never carry an ARM buffer through a Thumb-state transition.
|
||||
self.arm_pipeline = if self.instruction_set == InstructionSet::Arm
|
||||
&& self.pc() == pc.wrapping_add(4)
|
||||
// A state change without an ordinary refill starts a cold sequence.
|
||||
self.pipeline = if self.instruction_set == instruction_set
|
||||
&& self.pc() == pc.wrapping_add(instruction_set.width())
|
||||
{
|
||||
continuation
|
||||
Some(continuation)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ fn swi_and_machine_irq_refill_arm_vectors_before_handler_execution() {
|
|||
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.memory_mut().write32(TARGET, NEW).unwrap();
|
||||
machine.step().unwrap();
|
||||
|
|
@ -244,21 +244,21 @@ fn explicit_exception_and_pc_discontinuity_discard_the_old_execution_sequence()
|
|||
cpu.step(&mut bus).unwrap();
|
||||
assert_eq!(cpu.registers[0], 2);
|
||||
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();
|
||||
assert!(cpu.step(&mut bus).is_err()); // Missing BIOS remains a strict error.
|
||||
assert_eq!(cpu, before);
|
||||
}
|
||||
|
||||
#[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]);
|
||||
bus.write16(TARGET, 0x4710).unwrap(); // Thumb BX r2.
|
||||
cpu.registers[1] = TARGET | 1;
|
||||
cpu.registers[2] = CODE + 4;
|
||||
cpu.step(&mut bus).unwrap();
|
||||
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();
|
||||
cpu.step(&mut bus).unwrap();
|
||||
bus.write32(CODE + 4, OLD).unwrap();
|
||||
|
|
|
|||
|
|
@ -163,7 +163,10 @@ fn completed_units_and_priority_changes_use_access_order_not_channel_number() {
|
|||
#[test]
|
||||
fn failed_dma_and_failed_resumed_instruction_preserve_last_completed_dma_history() {
|
||||
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();
|
||||
transfer(&mut machine, EWRAM, IWRAM, true);
|
||||
// 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()
|
||||
.write16(DMA_BASE + 3 * DMA_STRIDE + 10, 0)
|
||||
.unwrap();
|
||||
machine.memory_mut().write16(CODE + 2, 0xde00).unwrap(); // Unsupported instruction.
|
||||
assert!(machine.step().is_err());
|
||||
assert_eq!(machine.cpu(), &before);
|
||||
assert_eq!(machine.cycles(), cycles);
|
||||
machine.memory_mut().write16(CODE + 2, PROBE).unwrap();
|
||||
machine.step().unwrap();
|
||||
assert_eq!(
|
||||
machine.cpu().registers()[6],
|
||||
resumed(DATA.to_le_bytes(), CODE)
|
||||
);
|
||||
assert_eq!(machine.last_timing(), timing);
|
||||
// Repair the data address, not the retained instruction or DMA history.
|
||||
let mut cpu = machine.cpu().clone();
|
||||
cpu.registers[0] = UNUSED;
|
||||
cpu.step_timed(machine.memory_mut()).unwrap();
|
||||
assert_eq!(cpu.registers()[6], resumed(DATA.to_le_bytes(), CODE));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ fn known_history_supports_widths_sign_extension_and_unaligned_loads_in_all_modes
|
|||
expected.registers[6] = value;
|
||||
expected.registers[15] += 2;
|
||||
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_eq!(cpu, before);
|
||||
bus.write16(pc + 2, PROBE).unwrap();
|
||||
cpu.invalidate_pipeline(); // Debugger repair leaves separate bus history intact.
|
||||
cpu.step(&mut bus).unwrap();
|
||||
assert_eq!(
|
||||
cpu.registers[6],
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ fn refill_history_supports_normal_load_lanes_rotation_and_sign_extension() {
|
|||
after.registers[6] = value;
|
||||
after.registers[15] += 2;
|
||||
cpu.step(&mut bus).unwrap();
|
||||
assert_eq!(cpu, after);
|
||||
assert_cpu_arch_eq!(cpu, after);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 crate::memory::{Memory, MemoryError};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) struct ArmPipeline {
|
||||
pub(super) struct Pipeline {
|
||||
pc: u32,
|
||||
instruction_set: InstructionSet,
|
||||
// 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 instruction: u32,
|
||||
pub continuation: Option<ArmPipeline>,
|
||||
pub continuation: Pipeline,
|
||||
}
|
||||
|
||||
impl Cpu {
|
||||
/// Discard buffered instructions after debugger code edits or rebinding memory.
|
||||
/// 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) {
|
||||
self.arm_pipeline = None;
|
||||
self.pipeline = None;
|
||||
}
|
||||
|
||||
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 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 {
|
||||
Some(pipe) => pipe.words[0].clone(),
|
||||
None => memory.fetch_arm_word(pc),
|
||||
Some(pipe) => pipe.instructions[0].clone(),
|
||||
None => memory.fetch_instruction(pc, instruction_set),
|
||||
}?;
|
||||
let decode = match retained {
|
||||
Some(pipe) => pipe.words[1].clone(),
|
||||
None => memory.fetch_arm_word(pc.wrapping_add(4)),
|
||||
Some(pipe) => pipe.instructions[1].clone(),
|
||||
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 {
|
||||
instruction,
|
||||
continuation: Some(ArmPipeline {
|
||||
pc: pc.wrapping_add(4),
|
||||
words: [decode, fetched],
|
||||
}),
|
||||
continuation: Pipeline {
|
||||
pc: pc.wrapping_add(width),
|
||||
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.
|
||||
pub(crate) fn refill_arm_pipeline(&mut self, memory: &Memory) {
|
||||
self.arm_pipeline = (self.instruction_set == InstructionSet::Arm).then(|| ArmPipeline {
|
||||
pc: self.pc(),
|
||||
words: [
|
||||
memory.fetch_arm_word(self.pc()),
|
||||
memory.fetch_arm_word(self.pc().wrapping_add(4)),
|
||||
pub(crate) fn refill_pipeline(&mut self, memory: &Memory) {
|
||||
let pc = self.pc();
|
||||
let instruction_set = self.instruction_set;
|
||||
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),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,8 +66,9 @@ fn empty_thumb_stores_use_pc_plus_six_for_all_bases_banks_and_alignments() {
|
|||
} else {
|
||||
cpu.step(&mut memory).unwrap();
|
||||
}
|
||||
assert_eq!(
|
||||
cpu, after,
|
||||
assert_cpu_arch_eq!(
|
||||
cpu,
|
||||
after,
|
||||
"{instruction:#06x} {mode:?} code={code_low} data={data_low}"
|
||||
);
|
||||
assert_eq!(memory.read32(DATA).unwrap(), pc + 6);
|
||||
|
|
|
|||
|
|
@ -127,8 +127,9 @@ fn loads_select_lanes_rotate_and_sign_extend_without_changing_flags() {
|
|||
after.registers[1] = value;
|
||||
after.registers[15] += 2;
|
||||
cpu.step(&mut bus).unwrap();
|
||||
assert_eq!(
|
||||
cpu, after,
|
||||
assert_cpu_arch_eq!(
|
||||
cpu,
|
||||
after,
|
||||
"pc={pc:#x} instruction={instruction:#x} lane={lane}"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
321
crates/gba-core/src/cpu/thumb_pipeline_tests.rs
Normal file
321
crates/gba-core/src/cpu/thumb_pipeline_tests.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -179,7 +179,7 @@ impl Machine {
|
|||
}
|
||||
let (kind, timing) = if self.cpu.take_interrupt(self.memory.irq_pending(), false) {
|
||||
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))
|
||||
} else {
|
||||
(
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ 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 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 {
|
||||
external_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.
|
||||
pub(crate) fn fetch_arm_word(&self, address: u32) -> Result<u32, MemoryError> {
|
||||
if address & 3 != 0 {
|
||||
/// 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.iter_mut().enumerate() {
|
||||
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))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue