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
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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