feat(timing): implement nominal Game Pak opcode prefetch
This commit is contained in:
parent
7f3ebd3a5d
commit
8f22092e06
25 changed files with 1062 additions and 124 deletions
|
|
@ -229,10 +229,16 @@ impl Cpu {
|
|||
/// Execute one ARM instruction or one 16-bit Thumb instruction.
|
||||
/// Each half of a Thumb BL is a separate step.
|
||||
/// This CPU-only API does not execute DMA, advance clocks, or sample device interrupts.
|
||||
/// Enabled opcode prefetch still tracks nominal bus time, as in step_timed.
|
||||
/// It records HALTCNT writes but does not enforce HALT. Use Machine::step for integration.
|
||||
/// Unsupported operations and memory errors leave CPU state unchanged.
|
||||
/// These errors are development diagnostics, not emulated CPU exceptions.
|
||||
pub fn step(&mut self, memory: &mut Memory) -> Result<(), CpuError> {
|
||||
// Prefetch timing state must agree across CPU-only and timed calls.
|
||||
// Compute and discard nominal costs without advancing any device clocks.
|
||||
if memory.waitcnt() & 0x4000 != 0 {
|
||||
return self.step_timed(memory).map(|_| ());
|
||||
}
|
||||
let instruction = self.fetch(memory)?;
|
||||
self.execute_fetched(instruction, memory)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,17 +134,40 @@ fn check_pair(thumb: bool, instruction: u32, breaks: bool) {
|
|||
let extra = if thumb { 0 } else { s };
|
||||
let mut cpu = cpu.clone();
|
||||
// Cold startup keeps the existing nominal S policy.
|
||||
let first_timing = cpu.step_timed(&mut memory).unwrap();
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||
first_timing.code_cycles,
|
||||
s + extra,
|
||||
"first fetch for {instruction:08x}"
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.pipeline.as_ref().unwrap().next_access,
|
||||
if breaks {
|
||||
AccessKind::NonSequential
|
||||
} else {
|
||||
AccessKind::Sequential
|
||||
}
|
||||
);
|
||||
// RAM/internal work advances the opcode queue, independently of
|
||||
// the CPU's N request. The Thumb literal load instead cancels it.
|
||||
let expected = if prefetch != 0 && !(thumb && instruction == 0x4800) {
|
||||
(s + extra)
|
||||
.saturating_sub(first_timing.data_cycles + first_timing.internal_cycles)
|
||||
.max(1)
|
||||
} else if breaks {
|
||||
n + extra
|
||||
} else {
|
||||
s + extra
|
||||
};
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||
if breaks { n + extra } else { s + extra },
|
||||
expected,
|
||||
"following fetch for {instruction:08x}"
|
||||
);
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, s + extra);
|
||||
// Excess queued work can also accelerate a later fetch.
|
||||
if prefetch == 0 {
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, s + extra);
|
||||
}
|
||||
assert_eq!(memory.cycles(), 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,13 @@ impl Cpu {
|
|||
self.pipeline = None;
|
||||
}
|
||||
|
||||
/// Timing metadata cannot survive a cold or mismatched instruction pipeline.
|
||||
pub(super) fn has_retained_pipeline(&self) -> bool {
|
||||
self.pipeline.as_ref().is_some_and(|pipe| {
|
||||
pipe.pc == self.pc() && pipe.instruction_set == self.instruction_set
|
||||
})
|
||||
}
|
||||
|
||||
/// Cold/debugger entry keeps the existing nominal S startup policy.
|
||||
/// A mismatched PC/state cannot reuse another sequence's access kind.
|
||||
pub(super) fn next_fetch_kind(&self) -> AccessKind {
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ impl Cpu {
|
|||
let fetched = self.fetch(memory)?;
|
||||
let code_kind = memory.cpu_code_kind(self.next_fetch_kind());
|
||||
// Source timing precedes data accesses and their WAITCNT writes.
|
||||
memory.begin_cpu_timing();
|
||||
memory.begin_cpu_timing(fetched.fill.is_some());
|
||||
memory.record_code_access(
|
||||
fetched.lookahead.address(),
|
||||
self.instruction_set.access_width(),
|
||||
|
|
@ -75,12 +75,13 @@ impl Cpu {
|
|||
/// A missing discarded fetch cannot prevent entry or become a current-slot error.
|
||||
pub(crate) fn take_irq_timed(&mut self, memory: &mut Memory) -> Option<StepTiming> {
|
||||
let state = self.instruction_set;
|
||||
let cold = !self.has_retained_pipeline();
|
||||
let address = self.pc().wrapping_add(2 * state.width());
|
||||
let kind = memory.cpu_code_kind(self.next_fetch_kind());
|
||||
if !self.take_interrupt(memory.irq_pending(), false) {
|
||||
return None;
|
||||
}
|
||||
memory.begin_cpu_timing();
|
||||
memory.begin_cpu_timing(cold);
|
||||
let fetch = memory.fetch_instruction(address, state);
|
||||
memory.record_code_access(fetch.address(), state.access_width(), kind);
|
||||
memory.discarded_cpu_fetch(&fetch);
|
||||
|
|
|
|||
|
|
@ -156,7 +156,8 @@ impl Machine {
|
|||
/// generate FIQ. IRQ entry consumes a separate step and does not clear IF.
|
||||
/// CPU/DMA work uses nominal costs; HALT advances exactly to the next device event.
|
||||
/// CPU writes take effect before this bulk device update. Within-instruction
|
||||
/// register timing, prefetch, and IRQ synchronization delays are not modeled.
|
||||
/// register timing and IRQ synchronization delays are not modeled. Opcode
|
||||
/// prefetch follows ordered nominal CPU/DMA costs, not per-cycle arbitration.
|
||||
/// CPU/DMA diagnostics leave CPU, devices, and the clock unchanged for this step.
|
||||
/// Row-capture diagnostics are deferred to Memory::present_frame instead.
|
||||
pub fn step(&mut self) -> Result<StepKind, MachineError> {
|
||||
|
|
@ -173,7 +174,7 @@ impl Machine {
|
|||
idle_cycles: self.memory.next_event_cycles(),
|
||||
..StepTiming::default()
|
||||
};
|
||||
self.memory.advance_cycles(timing.total());
|
||||
self.memory.advance_halt_cycles(timing.total());
|
||||
self.last_timing = timing;
|
||||
return Ok(StepKind::HaltIdle);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ mod iwram_bus;
|
|||
#[cfg(test)]
|
||||
mod iwram_history_tests;
|
||||
#[cfg(test)]
|
||||
mod prefetch_tests;
|
||||
#[cfg(test)]
|
||||
mod timing_event_tests;
|
||||
pub(crate) use fetch::InstructionFetch;
|
||||
use iwram_bus::IwramBus;
|
||||
|
|
@ -19,7 +21,7 @@ use crate::{
|
|||
dma::{DmaError, DMA_BASE, DMA_END},
|
||||
input::Buttons,
|
||||
io::{Io, HALTCNT, KEYINPUT, POSTFLG},
|
||||
timing::{bus_cycles, AccessKind, AccessWidth, CpuTiming, StepTiming},
|
||||
timing::{AccessKind, AccessWidth, CpuTiming, Prefetch, StepTiming},
|
||||
video::{self, sprites::pipeline, Framebuffer, VideoError},
|
||||
};
|
||||
|
||||
|
|
@ -92,6 +94,7 @@ pub struct Memory {
|
|||
io: Io,
|
||||
cycles: u64,
|
||||
cpu_timing: Cell<Option<CpuTiming>>,
|
||||
gamepak_prefetch: Prefetch,
|
||||
#[cfg(test)]
|
||||
last_cpu_timing: Option<CpuTiming>,
|
||||
// A completed DMA unit breaks the CPU's next nominal code-access sequence.
|
||||
|
|
@ -119,6 +122,7 @@ impl Memory {
|
|||
io: Io::default(),
|
||||
cycles: 0,
|
||||
cpu_timing: Cell::new(None),
|
||||
gamepak_prefetch: Prefetch::default(),
|
||||
#[cfg(test)]
|
||||
last_cpu_timing: None,
|
||||
cpu_resume_nonsequential: false,
|
||||
|
|
@ -150,6 +154,8 @@ impl Memory {
|
|||
/// Advance devices and optional row capture. Multiple unserviced DMA requests
|
||||
/// coalesce. No CPU or DMA work is executed by this method. STOP freezes all
|
||||
/// progress: supplied cycles are ignored until an external wake condition.
|
||||
/// This device-only API does not advance opcode prefetch. CPU/DMA/HALT steps
|
||||
/// account for their own cartridge-bus time before updating device clocks.
|
||||
pub fn advance_cycles(&mut self, cycles: u32) {
|
||||
if !self.stopped() {
|
||||
self.advance_running_cycles(cycles);
|
||||
|
|
@ -337,11 +343,16 @@ impl Memory {
|
|||
} else {
|
||||
AccessKind::Sequential
|
||||
};
|
||||
let mut prefetch = self.gamepak_prefetch;
|
||||
prefetch.configure(self.waitcnt());
|
||||
let internal_cycles = if transfer.first { 2 } else { 0 };
|
||||
// Keep the existing whole-unit DMA startup model; no sub-instruction arbitration.
|
||||
prefetch.advance(internal_cycles);
|
||||
let timing = StepTiming {
|
||||
code_cycles: 0,
|
||||
data_cycles: bus_cycles(self.waitcnt(), transfer.source, transfer.width, kind)
|
||||
+ bus_cycles(self.waitcnt(), transfer.destination, transfer.width, kind),
|
||||
internal_cycles: if transfer.first { 2 } else { 0 },
|
||||
data_cycles: prefetch.data(transfer.source, transfer.width, kind)
|
||||
+ prefetch.data(transfer.destination, transfer.width, kind),
|
||||
internal_cycles,
|
||||
idle_cycles: 0,
|
||||
};
|
||||
match transfer.width {
|
||||
|
|
@ -350,6 +361,9 @@ impl Memory {
|
|||
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
||||
}
|
||||
.map_err(map_error)?;
|
||||
// Only a successful unit commits queue progress. A WAITCNT destination can reset it.
|
||||
prefetch.configure(self.waitcnt());
|
||||
self.gamepak_prefetch = prefetch;
|
||||
// DMA runs before the resumed CPU fetch. Only actual IWRAM accesses drive
|
||||
// its local lanes, in read/write order. Channel halfword duplication is not
|
||||
// a full IWRAM bus write. Failed units never reach these history commits.
|
||||
|
|
@ -500,7 +514,7 @@ impl Memory {
|
|||
)
|
||||
}
|
||||
|
||||
/// Current Game Pak wait-state control. Prefetch is not implemented.
|
||||
/// Current Game Pak wait-state control, including nominal opcode prefetch enable.
|
||||
pub fn waitcnt(&self) -> u16 {
|
||||
self.io.waitcnt()
|
||||
}
|
||||
|
|
@ -515,9 +529,21 @@ impl Memory {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn begin_cpu_timing(&self) {
|
||||
pub(crate) fn begin_cpu_timing(&self, cold: bool) {
|
||||
debug_assert!(self.cpu_timing.get().is_none());
|
||||
self.cpu_timing.set(Some(CpuTiming::new(self.waitcnt())));
|
||||
let mut prefetch = self.gamepak_prefetch;
|
||||
if cold {
|
||||
prefetch.clear();
|
||||
}
|
||||
self.cpu_timing
|
||||
.set(Some(CpuTiming::new(self.waitcnt(), prefetch)));
|
||||
}
|
||||
|
||||
pub(crate) fn advance_halt_cycles(&mut self, cycles: u32) {
|
||||
if !self.stopped() {
|
||||
self.gamepak_prefetch.advance(cycles);
|
||||
}
|
||||
self.advance_cycles(cycles);
|
||||
}
|
||||
|
||||
pub(crate) fn end_cpu_timing(&mut self, succeeded: bool) -> StepTiming {
|
||||
|
|
@ -528,6 +554,7 @@ impl Memory {
|
|||
if !succeeded {
|
||||
return StepTiming::default();
|
||||
}
|
||||
self.gamepak_prefetch = trace.prefetch;
|
||||
#[cfg(test)]
|
||||
{
|
||||
self.last_cpu_timing = Some(trace);
|
||||
|
|
@ -669,6 +696,15 @@ impl Memory {
|
|||
}
|
||||
self.io.write8(byte_address, byte);
|
||||
}
|
||||
if address & !3 == crate::io::WAITCNT {
|
||||
// CPU changes are speculative with their timing transaction; host/DMA changes are immediate.
|
||||
if let Some(mut trace) = self.cpu_timing.get() {
|
||||
trace.prefetch.configure(self.waitcnt());
|
||||
self.cpu_timing.set(Some(trace));
|
||||
} else {
|
||||
self.gamepak_prefetch.configure(self.waitcnt());
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if N == 1 && address >> 24 == 0x07 {
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ fn sampling_has_no_timing_or_history_side_effects_and_never_uses_data_fallback()
|
|||
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_cpu_timing();
|
||||
memory.begin_cpu_timing(false);
|
||||
let fetch = memory.fetch_instruction(8, InstructionSet::Arm);
|
||||
memory.fetch_instruction(0x0300_0000, InstructionSet::Thumb);
|
||||
assert_eq!(memory.end_cpu_timing(true), StepTiming::default());
|
||||
|
|
|
|||
304
crates/gba-core/src/memory/prefetch_tests.rs
Normal file
304
crates/gba-core/src/memory/prefetch_tests.rs
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
//! Original integration checks. Expected cycles are explicit, not computed by the queue.
|
||||
use super::{timing_event_tests::prepared, *};
|
||||
use crate::{
|
||||
cpu::Cpu,
|
||||
dma::DMA_STRIDE,
|
||||
io::{HALTCNT, IE, IME, TIMER_BASE, WAITCNT},
|
||||
machine::{Machine, StepKind},
|
||||
timing::TimingEvent,
|
||||
};
|
||||
|
||||
const NOP: u32 = 0xe1a0_0000;
|
||||
const RAM: u32 = 0x0300_1000;
|
||||
|
||||
fn enable(memory: &mut Memory) {
|
||||
memory.write16(WAITCNT, 0x4000).unwrap();
|
||||
}
|
||||
|
||||
fn dma(memory: &mut Memory, source: u32, destination: u32) {
|
||||
let base = DMA_BASE + 3 * DMA_STRIDE;
|
||||
memory.write32(base, source).unwrap();
|
||||
memory.write32(base + 4, destination).unwrap();
|
||||
memory.write32(base + 8, 0x8400_0001).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ram_loads_and_internal_work_reduce_the_following_fetch_despite_cpu_n_kind() {
|
||||
for (thumb, instruction, address, expected) in [
|
||||
(false, 0xe591_0000, RAM, 4), // 6 ROM cycles minus RAM 1 and load 1I.
|
||||
(false, 0xe591_0000, 0x0200_1000, 1), // EWRAM 6 plus load 1I.
|
||||
(true, 0x6808, RAM, 1), // 3 ROM cycles minus RAM 1 and load 1I.
|
||||
(false, 0xe1a0_0312, RAM, 5), // Register shift supplies 1I.
|
||||
(true, 0x4090, RAM, 2), // Thumb register shift supplies 1I.
|
||||
] {
|
||||
let (mut cpu, mut memory) = prepared(thumb, &[instruction, NOP], &[(1, address)]);
|
||||
enable(&mut memory);
|
||||
cpu.step_timed(&mut memory).unwrap();
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, expected);
|
||||
assert!(matches!(memory.last_cpu_timing.unwrap().events()[0],
|
||||
TimingEvent::Code { kind: AccessKind::NonSequential, cycles, .. } if cycles == expected));
|
||||
assert_eq!(memory.cycles(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rom_data_cancels_the_queue_and_charges_its_last_cycle_stall() {
|
||||
// An EWRAM load leaves seven free cycles. The following hit advances one more
|
||||
// cycle before ROM data cancels the active third halfword.
|
||||
let (mut cpu, mut memory) = prepared(
|
||||
false,
|
||||
&[0xe591_0000, 0xe592_0000, NOP],
|
||||
&[(1, 0x0200_1000), (2, ROM_START)],
|
||||
);
|
||||
enable(&mut memory);
|
||||
cpu.step_timed(&mut memory).unwrap(); // Seven idle-bus cycles: two entries and one cycle of the third.
|
||||
let timing = cpu.step_timed(&mut memory).unwrap(); // Hit adds one more cycle: remaining=1.
|
||||
assert_eq!(timing.code_cycles, 1);
|
||||
assert_eq!(timing.data_cycles, 9); // Raw N+S=8, cancellation=1.
|
||||
assert_eq!(timing.internal_cycles, 1);
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn branches_can_match_the_head_or_cancel_a_partial_fetch() {
|
||||
for (instruction, expected) in [(0xea00_0001, 18), (0xea00_0010, 20)] {
|
||||
let (mut cpu, mut memory) = prepared(false, &[instruction], &[]);
|
||||
enable(&mut memory);
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, expected);
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, 6);
|
||||
}
|
||||
let (mut cpu, mut memory) = prepared(false, &[0xe591_f000], &[(1, RAM)]);
|
||||
memory.write32(RAM, ROM_START + 0x300).unwrap();
|
||||
enable(&mut memory);
|
||||
let timing = cpu.step_timed(&mut memory).unwrap();
|
||||
assert_eq!(timing.code_cycles, 21); // Source 6, cancelled target 9, second target 6.
|
||||
assert_eq!(timing.data_cycles + timing.internal_cycles, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swi_return_keeps_queue_ownership_separate_from_saved_instruction_state() {
|
||||
for thumb in [false, true] {
|
||||
let swi = if thumb { 0xdf00 } else { 0xef00_0000 };
|
||||
let nop = if thumb { 0x46c0 } else { NOP };
|
||||
let (mut cpu, mut memory) = prepared(thumb, &[swi, nop], &[]);
|
||||
// Original one-instruction BIOS handler: MOVS pc,lr restores the saved state.
|
||||
memory.bios.as_mut().unwrap()[8..12].copy_from_slice(&0xe1b0_f00e_u32.to_le_bytes());
|
||||
enable(&mut memory);
|
||||
let state = cpu.instruction_set();
|
||||
let return_pc = cpu.pc() + if thumb { 2 } else { 4 };
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||
if thumb { 5 } else { 8 }
|
||||
);
|
||||
assert_eq!(cpu.pc(), 8);
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||
if thumb { 9 } else { 15 }
|
||||
);
|
||||
assert_eq!((cpu.pc(), cpu.instruction_set()), (return_pc, state));
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||
if thumb { 3 } else { 6 }
|
||||
);
|
||||
assert_eq!(memory.cycles(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timed_and_cpu_only_execution_commit_the_same_queue_without_device_clocks() {
|
||||
for thumb in [false, true] {
|
||||
let load = if thumb { 0x6808 } else { 0xe591_0000 };
|
||||
let nop = if thumb { 0x46c0 } else { NOP };
|
||||
let (mut timed, mut a) = prepared(thumb, &[load, nop, load, nop], &[(1, 0x0200_1000)]);
|
||||
let (mut plain, mut b) = prepared(thumb, &[load, nop, load, nop], &[(1, 0x0200_1000)]);
|
||||
enable(&mut a);
|
||||
enable(&mut b);
|
||||
for _ in 0..4 {
|
||||
let timing = timed.step_timed(&mut a).unwrap();
|
||||
plain.step(&mut b).unwrap();
|
||||
assert_eq!(timed, plain);
|
||||
assert_eq!(a.gamepak_prefetch, b.gamepak_prefetch);
|
||||
assert_eq!(timing, b.last_cpu_timing.unwrap().total);
|
||||
assert_eq!((a.cycles(), b.cycles()), (0, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failures_discard_source_and_partial_data_progress() {
|
||||
for (bad, address) in [(0xe591_0000, 0x0e00_0000), (0xe8b1_0005, IME)] {
|
||||
let (mut cpu, mut memory) = prepared(false, &[NOP, bad], &[(1, address)]);
|
||||
enable(&mut memory);
|
||||
cpu.step_timed(&mut memory).unwrap();
|
||||
let queue = memory.gamepak_prefetch;
|
||||
let previous = memory.last_cpu_timing.unwrap().events();
|
||||
let before = cpu.clone();
|
||||
for _ in 0..2 {
|
||||
assert!(cpu.step_timed(&mut memory).is_err());
|
||||
assert_eq!(cpu, before);
|
||||
assert_eq!(memory.gamepak_prefetch, queue);
|
||||
assert_eq!(memory.last_cpu_timing.unwrap().events(), previous);
|
||||
assert!(memory.cpu_timing.get().is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn background_work_never_reads_missing_rom_bytes_or_fails_a_branch_early() {
|
||||
let mut memory = Memory::new(0xea00_003e_u32.to_le_bytes().to_vec()).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
enable(&mut memory);
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, 20);
|
||||
memory.advance_halt_cycles(1000); // Background addresses are beyond the file.
|
||||
let queue = memory.gamepak_prefetch;
|
||||
assert!(cpu.step_timed(&mut memory).is_err());
|
||||
assert_eq!(memory.gamepak_prefetch, queue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_access_and_clock_only_updates_do_not_advance_or_consume_the_queue() {
|
||||
let (mut cpu, mut memory) = prepared(false, &[NOP, NOP], &[]);
|
||||
enable(&mut memory);
|
||||
cpu.step_timed(&mut memory).unwrap();
|
||||
let queue = memory.gamepak_prefetch;
|
||||
memory.read32(ROM_START).unwrap();
|
||||
memory.write32(RAM, NOP).unwrap();
|
||||
memory.advance_cycles(1000);
|
||||
assert_eq!(memory.gamepak_prefetch, queue);
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debugger_invalidation_discards_queued_timing_on_success() {
|
||||
let (mut cpu, mut memory) = prepared(false, &[0xe591_0000, NOP], &[(1, 0x0200_1000)]);
|
||||
enable(&mut memory);
|
||||
cpu.step_timed(&mut memory).unwrap();
|
||||
cpu.invalidate_pipeline();
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, 6); // Cold nominal S, not a hit.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpu_waitcnt_write_resets_after_the_old_source_and_data_costs() {
|
||||
for new in [0, 0x4018, 0x4001, 0x4800] {
|
||||
let (mut cpu, mut memory) = prepared(false, &[0xe581_0000, NOP], &[(0, new), (1, WAITCNT)]);
|
||||
enable(&mut memory);
|
||||
let old = cpu.step_timed(&mut memory).unwrap();
|
||||
assert_eq!((old.code_cycles, old.data_cycles), (6, 1));
|
||||
assert_eq!(memory.waitcnt(), new as u16);
|
||||
let expected = match new {
|
||||
0 => 8, // Disabled: CPU N request.
|
||||
0x4018 => 5, // Changed waits reset queue: new N+S.
|
||||
_ => 5, // SRAM/PHI keep queue: 6 minus the store's free cycle.
|
||||
};
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_byte_and_dma_waitcnt_writes_apply_queue_configuration() {
|
||||
let (mut cpu, mut memory) = prepared(false, &[NOP, NOP, NOP], &[]);
|
||||
enable(&mut memory);
|
||||
cpu.step_timed(&mut memory).unwrap();
|
||||
memory.write8(WAITCNT, 0x18).unwrap();
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, 4);
|
||||
memory.write32(RAM, 0).unwrap();
|
||||
dma(&mut memory, RAM, WAITCNT);
|
||||
memory.step_dma().unwrap().unwrap();
|
||||
assert_eq!(memory.waitcnt(), 0);
|
||||
assert_eq!(cpu.step_timed(&mut memory).unwrap().code_cycles, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ram_dma_advances_the_queue_and_rom_dma_cancels_it_before_cpu_resume() {
|
||||
for (source, destination, expected_dma, expected_cpu) in [
|
||||
(RAM, RAM + 4, 4, 2), // Startup 2 + RAM 1+1 gives four free cycles.
|
||||
(0x0200_1000, RAM, 9, 1), // EWRAM gives a complete word.
|
||||
(ROM_START, RAM, 12, 8), // Startup leaves remaining=1: ROM data stalls one cycle.
|
||||
] {
|
||||
let (mut cpu, mut memory) = prepared(false, &[NOP, NOP], &[]);
|
||||
enable(&mut memory);
|
||||
cpu.step_timed(&mut memory).unwrap();
|
||||
dma(&mut memory, source, destination);
|
||||
let (_, timing) = memory.step_dma().unwrap().unwrap();
|
||||
assert_eq!(timing.total(), expected_dma);
|
||||
assert_eq!(memory.cycles(), u64::from(expected_dma));
|
||||
assert_eq!(
|
||||
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||
expected_cpu
|
||||
);
|
||||
assert!(!memory.cpu_resume_nonsequential);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_dma_preserves_queue_and_clocks() {
|
||||
for (source, destination) in [(0x0e00_0000, RAM), (RAM, ROM_START)] {
|
||||
let (mut cpu, mut memory) = prepared(false, &[NOP], &[]);
|
||||
enable(&mut memory);
|
||||
cpu.step_timed(&mut memory).unwrap();
|
||||
let queue = memory.gamepak_prefetch;
|
||||
dma(&mut memory, source, destination);
|
||||
assert!(memory.step_dma().is_err());
|
||||
assert_eq!(memory.gamepak_prefetch, queue);
|
||||
assert_eq!(memory.cycles(), 0);
|
||||
assert!(!memory.cpu_resume_nonsequential);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn machine_advances_prefetch_once_and_halt_progresses_while_stop_freezes() {
|
||||
let (cpu, mut memory) = prepared(false, &[0xe591_0000, NOP], &[(1, RAM)]);
|
||||
enable(&mut memory);
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.last_timing().code_cycles, 4); // Bulk device advance must not fill queue again.
|
||||
assert_eq!(machine.cycles(), 12);
|
||||
|
||||
for stop in [false, true] {
|
||||
let (mut cpu, mut memory) = prepared(false, &[NOP], &[]);
|
||||
enable(&mut memory);
|
||||
cpu.step_timed(&mut memory).unwrap();
|
||||
memory.write8(HALTCNT, if stop { 0x80 } else { 0 }).unwrap();
|
||||
let queue = memory.gamepak_prefetch;
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
assert_eq!(
|
||||
machine.step().unwrap(),
|
||||
if stop {
|
||||
StepKind::StopIdle
|
||||
} else {
|
||||
StepKind::HaltIdle
|
||||
}
|
||||
);
|
||||
if stop {
|
||||
assert_eq!(machine.memory().gamepak_prefetch, queue);
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
} else {
|
||||
assert_ne!(machine.memory().gamepak_prefetch, queue);
|
||||
assert!(machine.cycles() > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn irq_source_uses_queued_code_and_bios_vector_fetches_advance_the_rom_stream() {
|
||||
let (mut cpu, mut memory) = prepared(false, &[0xe591_0000, NOP], &[(1, RAM)]);
|
||||
enable(&mut memory);
|
||||
cpu.step_timed(&mut memory).unwrap(); // Next word has two cycles of progress.
|
||||
memory.write16(IE, 8).unwrap();
|
||||
memory.write16(IME, 1).unwrap();
|
||||
memory.write32(TIMER_BASE, 0x00c0_ffff).unwrap();
|
||||
memory.advance_cycles(1);
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
|
||||
assert_eq!(machine.last_timing().code_cycles, 6); // Source 4 plus vector 1+1.
|
||||
let mut queue = machine.memory().gamepak_prefetch;
|
||||
let events = machine.memory().last_cpu_timing.unwrap().events();
|
||||
let TimingEvent::Code { address, .. } = events[0] else {
|
||||
panic!("missing source")
|
||||
};
|
||||
assert_eq!(
|
||||
queue.code(address + 4, AccessWidth::Word, AccessKind::Sequential),
|
||||
4
|
||||
);
|
||||
}
|
||||
|
|
@ -13,7 +13,11 @@ const THUMB: u32 = ROM_START + 0x180;
|
|||
const DATA: u32 = 0x0300_1000;
|
||||
const ARM_NOP: u32 = 0xe1a0_0000;
|
||||
|
||||
fn prepared(thumb: bool, instructions: &[u32], operands: &[(usize, u32)]) -> (Cpu, Memory) {
|
||||
pub(super) fn prepared(
|
||||
thumb: bool,
|
||||
instructions: &[u32],
|
||||
operands: &[(usize, u32)],
|
||||
) -> (Cpu, Memory) {
|
||||
let mut operands = operands.to_vec();
|
||||
if thumb {
|
||||
operands.push((12, THUMB | 1));
|
||||
|
|
@ -210,7 +214,7 @@ fn swaps_and_register_shifts_place_internal_cycles_after_their_bus_work() {
|
|||
#[test]
|
||||
fn waitcnt_store_records_old_source_settings_and_next_step_records_new_settings() {
|
||||
for waitcnt in [0x18, 0x4018] {
|
||||
// Prefetch enable is still stored only.
|
||||
// Enabling prefetch does not arm a stream before the next ROM code fetch.
|
||||
let (mut cpu, mut memory) = prepared(
|
||||
false,
|
||||
&[0xe581_0000, ARM_NOP],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
//! Nominal GBA bus costs. These calculations do not perform memory accesses.
|
||||
//! Game Pak prefetch, display-bus contention, and configurable EWRAM timing are not modeled.
|
||||
//! Game Pak prefetch has a separate nominal queue; display contention and configurable EWRAM timing remain unmodeled.
|
||||
|
||||
mod prefetch;
|
||||
pub(crate) use prefetch::Prefetch;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AccessWidth {
|
||||
|
|
@ -95,6 +98,8 @@ pub fn bus_cycles(waitcnt: u16, address: u32, width: AccessWidth, kind: AccessKi
|
|||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub(crate) struct CpuTiming {
|
||||
pub total: StepTiming,
|
||||
pub prefetch: Prefetch,
|
||||
#[cfg(test)]
|
||||
code_waitcnt: u16,
|
||||
next_data: Option<u32>,
|
||||
// The largest supported step is LDM with PC: source + 16 data + I + 2 targets.
|
||||
|
|
@ -106,15 +111,18 @@ pub(crate) struct CpuTiming {
|
|||
}
|
||||
|
||||
impl CpuTiming {
|
||||
pub fn new(code_waitcnt: u16) -> Self {
|
||||
pub fn new(code_waitcnt: u16, mut prefetch: Prefetch) -> Self {
|
||||
prefetch.configure(code_waitcnt);
|
||||
Self {
|
||||
prefetch,
|
||||
#[cfg(test)]
|
||||
code_waitcnt,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn code(&mut self, address: u32, width: AccessWidth, kind: AccessKind) {
|
||||
let cycles = bus_cycles(self.code_waitcnt, address, width, kind);
|
||||
let cycles = self.prefetch.code(address, width, kind);
|
||||
self.total.code_cycles += cycles;
|
||||
#[cfg(test)]
|
||||
self.record(TimingEvent::Code {
|
||||
|
|
@ -132,7 +140,8 @@ impl CpuTiming {
|
|||
} else {
|
||||
AccessKind::NonSequential
|
||||
};
|
||||
let cycles = bus_cycles(waitcnt, address, width, kind);
|
||||
self.prefetch.configure(waitcnt);
|
||||
let cycles = self.prefetch.data(address, width, kind);
|
||||
self.total.data_cycles += cycles;
|
||||
self.next_data = Some(address.wrapping_add(width.bytes()));
|
||||
#[cfg(test)]
|
||||
|
|
@ -147,6 +156,7 @@ impl CpuTiming {
|
|||
|
||||
pub fn internal(&mut self, cycles: u32) {
|
||||
self.total.internal_cycles += cycles;
|
||||
self.prefetch.advance(cycles);
|
||||
#[cfg(test)]
|
||||
if cycles != 0 {
|
||||
self.record(TimingEvent::Internal { cycles });
|
||||
|
|
|
|||
165
crates/gba-core/src/timing/prefetch.rs
Normal file
165
crates/gba-core/src/timing/prefetch.rs
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
//! Nominal Game Pak opcode prefetch timing. No ROM bytes or CPU instructions live here.
|
||||
//! Full-buffer pause/restart follows the independently reviewed ares/jgenesis model.
|
||||
use super::{bus_cycles, AccessKind, AccessWidth};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct Prefetch {
|
||||
waitcnt: u16,
|
||||
stream: Option<Stream>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct Stream {
|
||||
head: u32,
|
||||
ready: u8,
|
||||
remaining: u32,
|
||||
running: bool,
|
||||
}
|
||||
|
||||
impl Prefetch {
|
||||
pub fn configure(&mut self, waitcnt: u16) {
|
||||
// Changes to ROM waits or enable invalidate timing metadata. PHI/SRAM do not.
|
||||
// Exact hardware behavior of disabling a partially filled queue remains unverified.
|
||||
if (self.waitcnt ^ waitcnt) & 0x47fc != 0 {
|
||||
self.clear();
|
||||
}
|
||||
self.waitcnt = waitcnt;
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.stream = None;
|
||||
}
|
||||
|
||||
fn enabled(&self) -> bool {
|
||||
self.waitcnt & 0x4000 != 0
|
||||
}
|
||||
|
||||
fn can_produce(address: u32) -> bool {
|
||||
(0x08..=0x0d).contains(&(address >> 24)) && address & 0x1fffe != 0
|
||||
}
|
||||
|
||||
fn start(&mut self, address: u32) {
|
||||
self.stream = Some(Stream {
|
||||
head: address,
|
||||
ready: 0,
|
||||
remaining: bus_cycles(
|
||||
self.waitcnt,
|
||||
address,
|
||||
AccessWidth::Halfword,
|
||||
AccessKind::Sequential,
|
||||
),
|
||||
running: Self::can_produce(address),
|
||||
});
|
||||
}
|
||||
|
||||
/// Advance only while the cartridge bus is free. Work is bounded by queue capacity.
|
||||
pub fn advance(&mut self, mut cycles: u32) {
|
||||
if !self.enabled() {
|
||||
return;
|
||||
}
|
||||
let Some(stream) = &mut self.stream else {
|
||||
return;
|
||||
};
|
||||
while cycles != 0 && stream.running {
|
||||
if stream.ready == 8 {
|
||||
stream.running = false;
|
||||
stream.remaining = 0;
|
||||
break;
|
||||
}
|
||||
let elapsed = cycles.min(stream.remaining);
|
||||
cycles -= elapsed;
|
||||
stream.remaining -= elapsed;
|
||||
if stream.remaining == 0 {
|
||||
stream.ready += 1;
|
||||
let tail = stream.head.wrapping_add(u32::from(stream.ready) * 2);
|
||||
stream.running = Self::can_produce(tail);
|
||||
stream.remaining = bus_cycles(
|
||||
self.waitcnt,
|
||||
tail,
|
||||
AccessWidth::Halfword,
|
||||
AccessKind::Sequential,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancelling the last cycle of an active halfword adds one nominal stall cycle.
|
||||
fn cancel(&mut self) -> u32 {
|
||||
let stall = u32::from(
|
||||
self.stream
|
||||
.is_some_and(|s| s.running && s.ready < 8 && s.remaining == 1),
|
||||
);
|
||||
self.clear();
|
||||
stall
|
||||
}
|
||||
|
||||
pub fn code(&mut self, address: u32, width: AccessWidth, kind: AccessKind) -> u32 {
|
||||
let raw = bus_cycles(self.waitcnt, address, width, kind);
|
||||
if !(0x08..=0x0d).contains(&(address >> 24)) {
|
||||
self.advance(raw);
|
||||
return raw;
|
||||
}
|
||||
if !self.enabled() {
|
||||
return raw;
|
||||
}
|
||||
debug_assert!(matches!(width, AccessWidth::Halfword | AccessWidth::Word));
|
||||
let matches = self
|
||||
.stream
|
||||
.is_some_and(|s| s.head == address && (s.ready != 0 || s.running))
|
||||
&& Self::can_produce(address);
|
||||
if !matches {
|
||||
let drained = self
|
||||
.stream
|
||||
.is_some_and(|s| s.head == address && s.ready == 0 && !s.running);
|
||||
let stall = self.cancel();
|
||||
let cycles = if drained {
|
||||
bus_cycles(self.waitcnt, address, width, AccessKind::NonSequential)
|
||||
} else {
|
||||
raw
|
||||
};
|
||||
self.start(address.wrapping_add(width.bytes()));
|
||||
return cycles + stall;
|
||||
}
|
||||
|
||||
// One CPU-side cycle can deliver either a halfword or a fully queued ARM word.
|
||||
let mut cycles = 1;
|
||||
self.advance(1);
|
||||
for _ in 0..width.bytes() / 2 {
|
||||
let mut stream = self.stream.unwrap();
|
||||
if stream.ready == 0 {
|
||||
if !stream.running {
|
||||
// Full-buffer stop can also be reached between an ARM word's halves.
|
||||
stream.running = true;
|
||||
stream.remaining = bus_cycles(
|
||||
self.waitcnt,
|
||||
stream.head,
|
||||
AccessWidth::Halfword,
|
||||
AccessKind::NonSequential,
|
||||
);
|
||||
self.stream = Some(stream);
|
||||
}
|
||||
let wait = stream.remaining;
|
||||
self.advance(wait);
|
||||
cycles += wait;
|
||||
}
|
||||
let stream = self.stream.as_mut().unwrap();
|
||||
debug_assert!(stream.ready > 0);
|
||||
stream.ready -= 1;
|
||||
stream.head = stream.head.wrapping_add(2);
|
||||
}
|
||||
cycles
|
||||
}
|
||||
|
||||
pub fn data(&mut self, address: u32, width: AccessWidth, kind: AccessKind) -> u32 {
|
||||
let raw = bus_cycles(self.waitcnt, address, width, kind);
|
||||
if (0x08..=0x0f).contains(&(address >> 24)) {
|
||||
raw + self.cancel()
|
||||
} else {
|
||||
self.advance(raw);
|
||||
raw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
305
crates/gba-core/src/timing/prefetch/tests.rs
Normal file
305
crates/gba-core/src/timing/prefetch/tests.rs
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
//! Independent queue expectations. These tests do not call bus_cycles for expected costs.
|
||||
use super::*;
|
||||
|
||||
const PC: u32 = 0x0800_1000;
|
||||
const ENABLE: u16 = 0x4000;
|
||||
|
||||
fn queue(waitcnt: u16) -> Prefetch {
|
||||
let mut queue = Prefetch::default();
|
||||
queue.configure(waitcnt);
|
||||
queue
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ready_and_in_progress_fetches_follow_all_rom_wait_settings_and_widths() {
|
||||
for window in 0..3 {
|
||||
for first in 0..4 {
|
||||
for second in 0..2 {
|
||||
let waitcnt =
|
||||
ENABLE | (first << [2, 5, 8][window]) | (second << [4, 7, 10][window]);
|
||||
let n = [5, 4, 3, 9][first as usize];
|
||||
let s = if second == 1 { 2 } else { [3, 5, 9][window] };
|
||||
for width in [AccessWidth::Halfword, AccessWidth::Word] {
|
||||
let halves = width.bytes() / 2;
|
||||
let pc = PC + window as u32 * 0x0200_0000;
|
||||
for idle in 0..=2 * s + 2 {
|
||||
for kind in [AccessKind::Sequential, AccessKind::NonSequential] {
|
||||
let mut q = queue(waitcnt);
|
||||
assert_eq!(
|
||||
q.code(pc, width, AccessKind::NonSequential),
|
||||
n + (halves - 1) * s
|
||||
);
|
||||
q.advance(idle);
|
||||
// A live cartridge burst ignores the CPU's N request, including after I cycles.
|
||||
assert_eq!(
|
||||
q.code(pc + width.bytes(), width, kind),
|
||||
(halves * s).saturating_sub(idle).max(1),
|
||||
"window={window} waitcnt={waitcnt:#x} width={width:?} idle={idle}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sequential_fetches_without_spare_bus_time_keep_the_sequential_rom_cost() {
|
||||
for width in [AccessWidth::Halfword, AccessWidth::Word] {
|
||||
let mut q = queue(ENABLE);
|
||||
q.code(PC, width, AccessKind::Sequential);
|
||||
for index in 1..100 {
|
||||
assert_eq!(
|
||||
q.code(PC + index * width.bytes(), width, AccessKind::Sequential),
|
||||
if width == AccessWidth::Word { 6 } else { 3 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_queue_pauses_until_drained_then_restarts_nonsequentially() {
|
||||
for width in [AccessWidth::Halfword, AccessWidth::Word] {
|
||||
let mut q = queue(ENABLE);
|
||||
q.code(PC, width, AccessKind::Sequential);
|
||||
q.advance(25); // Eight halfwords at S=3, then one cycle observing full capacity.
|
||||
assert_eq!(q.stream.unwrap().ready, 8);
|
||||
assert!(!q.stream.unwrap().running);
|
||||
let mut huge = q;
|
||||
huge.advance(u32::MAX);
|
||||
assert_eq!(q, huge);
|
||||
let count = 16 / width.bytes();
|
||||
for index in 1..=count {
|
||||
assert_eq!(
|
||||
q.code(PC + index * width.bytes(), width, AccessKind::Sequential),
|
||||
1
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
q.code(
|
||||
PC + (count + 1) * width.bytes(),
|
||||
width,
|
||||
AccessKind::Sequential
|
||||
),
|
||||
if width == AccessWidth::Word { 8 } else { 5 }
|
||||
);
|
||||
assert_eq!(
|
||||
q.code(
|
||||
PC + (count + 2) * width.bytes(),
|
||||
width,
|
||||
AccessKind::Sequential
|
||||
),
|
||||
if width == AccessWidth::Word { 6 } else { 3 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arm_fetch_can_combine_a_ready_low_half_with_an_in_progress_high_half() {
|
||||
for (idle, cost) in [(0, 6), (1, 5), (2, 4), (3, 3), (4, 2), (5, 1), (6, 1)] {
|
||||
let mut q = queue(ENABLE);
|
||||
q.code(PC, AccessWidth::Word, AccessKind::NonSequential);
|
||||
q.advance(idle);
|
||||
assert_eq!(
|
||||
q.code(PC + 4, AccessWidth::Word, AccessKind::NonSequential),
|
||||
cost
|
||||
);
|
||||
}
|
||||
// Halfword ownership also permits a state change at the exact next queued address.
|
||||
let mut q = queue(ENABLE);
|
||||
q.code(PC + 2, AccessWidth::Halfword, AccessKind::NonSequential);
|
||||
q.advance(6);
|
||||
assert_eq!(
|
||||
q.code(PC + 4, AccessWidth::Word, AccessKind::NonSequential),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
q.code(PC + 8, AccessWidth::Halfword, AccessKind::Sequential),
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arm_word_restarts_between_halves_after_draining_an_odd_full_queue() {
|
||||
let mut q = queue(ENABLE);
|
||||
q.code(PC, AccessWidth::Halfword, AccessKind::Sequential);
|
||||
q.advance(25);
|
||||
assert_eq!(
|
||||
q.code(PC + 2, AccessWidth::Halfword, AccessKind::Sequential),
|
||||
1
|
||||
);
|
||||
for offset in [4, 8, 12] {
|
||||
assert_eq!(
|
||||
q.code(PC + offset, AccessWidth::Word, AccessKind::Sequential),
|
||||
1
|
||||
);
|
||||
}
|
||||
// The low half is the last ready entry. The missing high half restarts with N=5.
|
||||
assert_eq!(
|
||||
q.code(PC + 16, AccessWidth::Word, AccessKind::Sequential),
|
||||
6
|
||||
);
|
||||
assert_eq!(
|
||||
q.code(PC + 20, AccessWidth::Word, AccessKind::Sequential),
|
||||
6
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancellation_stalls_only_at_the_last_cycle_of_an_active_halfword() {
|
||||
for idle in 0..=26 {
|
||||
let stall = u32::from(idle < 24 && idle % 3 == 2);
|
||||
let mut q = queue(ENABLE);
|
||||
q.code(PC, AccessWidth::Halfword, AccessKind::Sequential);
|
||||
q.advance(idle);
|
||||
let mut branch = q;
|
||||
assert_eq!(
|
||||
q.data(PC + 0x100, AccessWidth::Word, AccessKind::NonSequential),
|
||||
8 + stall
|
||||
);
|
||||
assert!(q.stream.is_none());
|
||||
assert_eq!(
|
||||
branch.code(PC + 0x100, AccessWidth::Word, AccessKind::NonSequential),
|
||||
8 + stall
|
||||
);
|
||||
assert_eq!(branch.stream.unwrap().head, PC + 0x104);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_the_queue_head_can_hit_and_rom_data_never_consumes_opcode_entries() {
|
||||
let mut q = queue(ENABLE);
|
||||
q.code(PC, AccessWidth::Halfword, AccessKind::Sequential);
|
||||
q.advance(12);
|
||||
assert_eq!(
|
||||
q.code(PC + 6, AccessWidth::Halfword, AccessKind::NonSequential),
|
||||
5
|
||||
);
|
||||
q.advance(12);
|
||||
let head = q.stream.unwrap().head;
|
||||
assert_eq!(
|
||||
q.data(head, AccessWidth::Halfword, AccessKind::NonSequential),
|
||||
5
|
||||
);
|
||||
assert!(q.stream.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_memory_and_internal_work_advance_the_queue_without_arming_it() {
|
||||
let mut q = queue(ENABLE);
|
||||
assert_eq!(
|
||||
q.data(0x0200_0000, AccessWidth::Word, AccessKind::NonSequential),
|
||||
6
|
||||
);
|
||||
q.advance(10);
|
||||
assert!(q.stream.is_none());
|
||||
q.code(PC, AccessWidth::Word, AccessKind::Sequential);
|
||||
assert_eq!(
|
||||
q.data(0x0200_0000, AccessWidth::Word, AccessKind::NonSequential),
|
||||
6
|
||||
);
|
||||
assert_eq!(
|
||||
q.code(PC + 4, AccessWidth::Word, AccessKind::NonSequential),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
q.code(0x0300_0000, AccessWidth::Word, AccessKind::NonSequential),
|
||||
1
|
||||
);
|
||||
q.advance(1);
|
||||
assert_eq!(
|
||||
q.code(PC + 8, AccessWidth::Halfword, AccessKind::NonSequential),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_boundaries_pause_background_work_and_demand_uses_the_new_window() {
|
||||
for (boundary, word_n, word_s) in [
|
||||
(0x0802_0000, 8, 6),
|
||||
(0x0a00_0000, 10, 10),
|
||||
(0x0c00_0000, 14, 18),
|
||||
] {
|
||||
let mut q = queue(ENABLE);
|
||||
q.code(boundary - 8, AccessWidth::Word, AccessKind::Sequential);
|
||||
q.advance(u32::MAX);
|
||||
assert_eq!(q.stream.unwrap().ready, 2);
|
||||
assert!(!q.stream.unwrap().running);
|
||||
assert_eq!(
|
||||
q.code(boundary - 4, AccessWidth::Word, AccessKind::Sequential),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
q.code(boundary, AccessWidth::Word, AccessKind::Sequential),
|
||||
word_n
|
||||
);
|
||||
assert_eq!(
|
||||
q.code(boundary + 4, AccessWidth::Word, AccessKind::Sequential),
|
||||
word_s
|
||||
);
|
||||
}
|
||||
let mut q = queue(ENABLE);
|
||||
q.code(0x0dff_fffc, AccessWidth::Word, AccessKind::Sequential);
|
||||
q.advance(u32::MAX);
|
||||
assert_eq!(q.stream.unwrap().ready, 0); // Never prefetch into cartridge save space.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relevant_waitcnt_changes_clear_the_queue_but_phi_and_sram_fields_do_not() {
|
||||
for changed in [0, ENABLE | 4, ENABLE | 0x10, ENABLE | 0x80, ENABLE | 0x400] {
|
||||
let mut q = queue(ENABLE);
|
||||
q.code(PC, AccessWidth::Word, AccessKind::Sequential);
|
||||
q.advance(10);
|
||||
q.configure(changed);
|
||||
assert!(q.stream.is_none());
|
||||
}
|
||||
let mut q = queue(ENABLE);
|
||||
q.code(PC, AccessWidth::Word, AccessKind::Sequential);
|
||||
q.advance(10);
|
||||
let stream = q.stream;
|
||||
q.configure(ENABLE | 0x1803);
|
||||
assert_eq!(q.stream, stream);
|
||||
assert_eq!(q.code(PC + 4, AccessWidth::Word, AccessKind::Sequential), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_prefetch_retains_raw_costs_and_never_starts_background_work() {
|
||||
let mut q = queue(0);
|
||||
for _ in 0..3 {
|
||||
assert_eq!(q.code(PC, AccessWidth::Word, AccessKind::Sequential), 6);
|
||||
assert_eq!(
|
||||
q.code(PC + 4, AccessWidth::Word, AccessKind::NonSequential),
|
||||
8
|
||||
);
|
||||
q.advance(u32::MAX);
|
||||
assert!(q.stream.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batched_progress_matches_single_clock_progress_for_seeded_event_sequences() {
|
||||
let mut fast = queue(ENABLE);
|
||||
let mut slow = fast;
|
||||
let mut seed = 0x1234_5678_u32;
|
||||
let mut address = PC;
|
||||
for _ in 0..10_000 {
|
||||
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
|
||||
let idle = seed >> 24;
|
||||
fast.advance(idle);
|
||||
for _ in 0..idle {
|
||||
slow.advance(1);
|
||||
}
|
||||
assert_eq!(fast, slow);
|
||||
let width = if address & 3 == 0 && seed & 1 == 0 {
|
||||
AccessWidth::Word
|
||||
} else {
|
||||
AccessWidth::Halfword
|
||||
};
|
||||
assert_eq!(
|
||||
fast.code(address, width, AccessKind::Sequential),
|
||||
slow.code(address, width, AccessKind::Sequential)
|
||||
);
|
||||
address += width.bytes();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue