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.
|
/// Execute one ARM instruction or one 16-bit Thumb instruction.
|
||||||
/// Each half of a Thumb BL is a separate step.
|
/// Each half of a Thumb BL is a separate step.
|
||||||
/// This CPU-only API does not execute DMA, advance clocks, or sample device interrupts.
|
/// 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.
|
/// It records HALTCNT writes but does not enforce HALT. Use Machine::step for integration.
|
||||||
/// Unsupported operations and memory errors leave CPU state unchanged.
|
/// Unsupported operations and memory errors leave CPU state unchanged.
|
||||||
/// These errors are development diagnostics, not emulated CPU exceptions.
|
/// These errors are development diagnostics, not emulated CPU exceptions.
|
||||||
pub fn step(&mut self, memory: &mut Memory) -> Result<(), CpuError> {
|
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)?;
|
let instruction = self.fetch(memory)?;
|
||||||
self.execute_fetched(instruction, 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 extra = if thumb { 0 } else { s };
|
||||||
let mut cpu = cpu.clone();
|
let mut cpu = cpu.clone();
|
||||||
// Cold startup keeps the existing nominal S policy.
|
// Cold startup keeps the existing nominal S policy.
|
||||||
|
let first_timing = cpu.step_timed(&mut memory).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
first_timing.code_cycles,
|
||||||
s + extra,
|
s + extra,
|
||||||
"first fetch for {instruction:08x}"
|
"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!(
|
assert_eq!(
|
||||||
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
cpu.step_timed(&mut memory).unwrap().code_cycles,
|
||||||
if breaks { n + extra } else { s + extra },
|
expected,
|
||||||
"following fetch for {instruction:08x}"
|
"following fetch for {instruction:08x}"
|
||||||
);
|
);
|
||||||
|
// 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!(cpu.step_timed(&mut memory).unwrap().code_cycles, s + extra);
|
||||||
|
}
|
||||||
assert_eq!(memory.cycles(), 0);
|
assert_eq!(memory.cycles(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,13 @@ impl Cpu {
|
||||||
self.pipeline = None;
|
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.
|
/// Cold/debugger entry keeps the existing nominal S startup policy.
|
||||||
/// A mismatched PC/state cannot reuse another sequence's access kind.
|
/// A mismatched PC/state cannot reuse another sequence's access kind.
|
||||||
pub(super) fn next_fetch_kind(&self) -> AccessKind {
|
pub(super) fn next_fetch_kind(&self) -> AccessKind {
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ impl Cpu {
|
||||||
let fetched = self.fetch(memory)?;
|
let fetched = self.fetch(memory)?;
|
||||||
let code_kind = memory.cpu_code_kind(self.next_fetch_kind());
|
let code_kind = memory.cpu_code_kind(self.next_fetch_kind());
|
||||||
// Source timing precedes data accesses and their WAITCNT writes.
|
// 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(
|
memory.record_code_access(
|
||||||
fetched.lookahead.address(),
|
fetched.lookahead.address(),
|
||||||
self.instruction_set.access_width(),
|
self.instruction_set.access_width(),
|
||||||
|
|
@ -75,12 +75,13 @@ impl Cpu {
|
||||||
/// A missing discarded fetch cannot prevent entry or become a current-slot error.
|
/// 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> {
|
pub(crate) fn take_irq_timed(&mut self, memory: &mut Memory) -> Option<StepTiming> {
|
||||||
let state = self.instruction_set;
|
let state = self.instruction_set;
|
||||||
|
let cold = !self.has_retained_pipeline();
|
||||||
let address = self.pc().wrapping_add(2 * state.width());
|
let address = self.pc().wrapping_add(2 * state.width());
|
||||||
let kind = memory.cpu_code_kind(self.next_fetch_kind());
|
let kind = memory.cpu_code_kind(self.next_fetch_kind());
|
||||||
if !self.take_interrupt(memory.irq_pending(), false) {
|
if !self.take_interrupt(memory.irq_pending(), false) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
memory.begin_cpu_timing();
|
memory.begin_cpu_timing(cold);
|
||||||
let fetch = memory.fetch_instruction(address, state);
|
let fetch = memory.fetch_instruction(address, state);
|
||||||
memory.record_code_access(fetch.address(), state.access_width(), kind);
|
memory.record_code_access(fetch.address(), state.access_width(), kind);
|
||||||
memory.discarded_cpu_fetch(&fetch);
|
memory.discarded_cpu_fetch(&fetch);
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,8 @@ impl Machine {
|
||||||
/// generate FIQ. IRQ entry consumes a separate step and does not clear IF.
|
/// 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/DMA work uses nominal costs; HALT advances exactly to the next device event.
|
||||||
/// CPU writes take effect before this bulk device update. Within-instruction
|
/// 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.
|
/// CPU/DMA diagnostics leave CPU, devices, and the clock unchanged for this step.
|
||||||
/// Row-capture diagnostics are deferred to Memory::present_frame instead.
|
/// Row-capture diagnostics are deferred to Memory::present_frame instead.
|
||||||
pub fn step(&mut self) -> Result<StepKind, MachineError> {
|
pub fn step(&mut self) -> Result<StepKind, MachineError> {
|
||||||
|
|
@ -173,7 +174,7 @@ impl Machine {
|
||||||
idle_cycles: self.memory.next_event_cycles(),
|
idle_cycles: self.memory.next_event_cycles(),
|
||||||
..StepTiming::default()
|
..StepTiming::default()
|
||||||
};
|
};
|
||||||
self.memory.advance_cycles(timing.total());
|
self.memory.advance_halt_cycles(timing.total());
|
||||||
self.last_timing = timing;
|
self.last_timing = timing;
|
||||||
return Ok(StepKind::HaltIdle);
|
return Ok(StepKind::HaltIdle);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ mod iwram_bus;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod iwram_history_tests;
|
mod iwram_history_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
mod prefetch_tests;
|
||||||
|
#[cfg(test)]
|
||||||
mod timing_event_tests;
|
mod timing_event_tests;
|
||||||
pub(crate) use fetch::InstructionFetch;
|
pub(crate) use fetch::InstructionFetch;
|
||||||
use iwram_bus::IwramBus;
|
use iwram_bus::IwramBus;
|
||||||
|
|
@ -19,7 +21,7 @@ use crate::{
|
||||||
dma::{DmaError, DMA_BASE, DMA_END},
|
dma::{DmaError, DMA_BASE, DMA_END},
|
||||||
input::Buttons,
|
input::Buttons,
|
||||||
io::{Io, HALTCNT, KEYINPUT, POSTFLG},
|
io::{Io, HALTCNT, KEYINPUT, POSTFLG},
|
||||||
timing::{bus_cycles, AccessKind, AccessWidth, CpuTiming, StepTiming},
|
timing::{AccessKind, AccessWidth, CpuTiming, Prefetch, StepTiming},
|
||||||
video::{self, sprites::pipeline, Framebuffer, VideoError},
|
video::{self, sprites::pipeline, Framebuffer, VideoError},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -92,6 +94,7 @@ pub struct Memory {
|
||||||
io: Io,
|
io: Io,
|
||||||
cycles: u64,
|
cycles: u64,
|
||||||
cpu_timing: Cell<Option<CpuTiming>>,
|
cpu_timing: Cell<Option<CpuTiming>>,
|
||||||
|
gamepak_prefetch: Prefetch,
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
last_cpu_timing: Option<CpuTiming>,
|
last_cpu_timing: Option<CpuTiming>,
|
||||||
// A completed DMA unit breaks the CPU's next nominal code-access sequence.
|
// A completed DMA unit breaks the CPU's next nominal code-access sequence.
|
||||||
|
|
@ -119,6 +122,7 @@ impl Memory {
|
||||||
io: Io::default(),
|
io: Io::default(),
|
||||||
cycles: 0,
|
cycles: 0,
|
||||||
cpu_timing: Cell::new(None),
|
cpu_timing: Cell::new(None),
|
||||||
|
gamepak_prefetch: Prefetch::default(),
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
last_cpu_timing: None,
|
last_cpu_timing: None,
|
||||||
cpu_resume_nonsequential: false,
|
cpu_resume_nonsequential: false,
|
||||||
|
|
@ -150,6 +154,8 @@ impl Memory {
|
||||||
/// Advance devices and optional row capture. Multiple unserviced DMA requests
|
/// Advance devices and optional row capture. Multiple unserviced DMA requests
|
||||||
/// coalesce. No CPU or DMA work is executed by this method. STOP freezes all
|
/// coalesce. No CPU or DMA work is executed by this method. STOP freezes all
|
||||||
/// progress: supplied cycles are ignored until an external wake condition.
|
/// 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) {
|
pub fn advance_cycles(&mut self, cycles: u32) {
|
||||||
if !self.stopped() {
|
if !self.stopped() {
|
||||||
self.advance_running_cycles(cycles);
|
self.advance_running_cycles(cycles);
|
||||||
|
|
@ -337,11 +343,16 @@ impl Memory {
|
||||||
} else {
|
} else {
|
||||||
AccessKind::Sequential
|
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 {
|
let timing = StepTiming {
|
||||||
code_cycles: 0,
|
code_cycles: 0,
|
||||||
data_cycles: bus_cycles(self.waitcnt(), transfer.source, transfer.width, kind)
|
data_cycles: prefetch.data(transfer.source, transfer.width, kind)
|
||||||
+ bus_cycles(self.waitcnt(), transfer.destination, transfer.width, kind),
|
+ prefetch.data(transfer.destination, transfer.width, kind),
|
||||||
internal_cycles: if transfer.first { 2 } else { 0 },
|
internal_cycles,
|
||||||
idle_cycles: 0,
|
idle_cycles: 0,
|
||||||
};
|
};
|
||||||
match transfer.width {
|
match transfer.width {
|
||||||
|
|
@ -350,6 +361,9 @@ impl Memory {
|
||||||
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
AccessWidth::Byte => unreachable!("DMA is never byte-wide"),
|
||||||
}
|
}
|
||||||
.map_err(map_error)?;
|
.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
|
// DMA runs before the resumed CPU fetch. Only actual IWRAM accesses drive
|
||||||
// its local lanes, in read/write order. Channel halfword duplication is not
|
// its local lanes, in read/write order. Channel halfword duplication is not
|
||||||
// a full IWRAM bus write. Failed units never reach these history commits.
|
// 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 {
|
pub fn waitcnt(&self) -> u16 {
|
||||||
self.io.waitcnt()
|
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());
|
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 {
|
pub(crate) fn end_cpu_timing(&mut self, succeeded: bool) -> StepTiming {
|
||||||
|
|
@ -528,6 +554,7 @@ impl Memory {
|
||||||
if !succeeded {
|
if !succeeded {
|
||||||
return StepTiming::default();
|
return StepTiming::default();
|
||||||
}
|
}
|
||||||
|
self.gamepak_prefetch = trace.prefetch;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
{
|
{
|
||||||
self.last_cpu_timing = Some(trace);
|
self.last_cpu_timing = Some(trace);
|
||||||
|
|
@ -669,6 +696,15 @@ impl Memory {
|
||||||
}
|
}
|
||||||
self.io.write8(byte_address, byte);
|
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(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if N == 1 && address >> 24 == 0x07 {
|
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());
|
bios[8..12].copy_from_slice(&WORD.to_le_bytes());
|
||||||
let mut memory = Memory::with_bios(vec![], bios).unwrap();
|
let mut memory = Memory::with_bios(vec![], bios).unwrap();
|
||||||
memory.cpu_resume_nonsequential = true;
|
memory.cpu_resume_nonsequential = true;
|
||||||
memory.begin_cpu_timing();
|
memory.begin_cpu_timing(false);
|
||||||
let fetch = memory.fetch_instruction(8, InstructionSet::Arm);
|
let fetch = memory.fetch_instruction(8, InstructionSet::Arm);
|
||||||
memory.fetch_instruction(0x0300_0000, InstructionSet::Thumb);
|
memory.fetch_instruction(0x0300_0000, InstructionSet::Thumb);
|
||||||
assert_eq!(memory.end_cpu_timing(true), StepTiming::default());
|
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 DATA: u32 = 0x0300_1000;
|
||||||
const ARM_NOP: u32 = 0xe1a0_0000;
|
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();
|
let mut operands = operands.to_vec();
|
||||||
if thumb {
|
if thumb {
|
||||||
operands.push((12, THUMB | 1));
|
operands.push((12, THUMB | 1));
|
||||||
|
|
@ -210,7 +214,7 @@ fn swaps_and_register_shifts_place_internal_cycles_after_their_bus_work() {
|
||||||
#[test]
|
#[test]
|
||||||
fn waitcnt_store_records_old_source_settings_and_next_step_records_new_settings() {
|
fn waitcnt_store_records_old_source_settings_and_next_step_records_new_settings() {
|
||||||
for waitcnt in [0x18, 0x4018] {
|
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(
|
let (mut cpu, mut memory) = prepared(
|
||||||
false,
|
false,
|
||||||
&[0xe581_0000, ARM_NOP],
|
&[0xe581_0000, ARM_NOP],
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
//! Nominal GBA bus costs. These calculations do not perform memory accesses.
|
//! 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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum AccessWidth {
|
pub enum AccessWidth {
|
||||||
|
|
@ -95,6 +98,8 @@ pub fn bus_cycles(waitcnt: u16, address: u32, width: AccessWidth, kind: AccessKi
|
||||||
#[derive(Debug, Default, Clone, Copy)]
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
pub(crate) struct CpuTiming {
|
pub(crate) struct CpuTiming {
|
||||||
pub total: StepTiming,
|
pub total: StepTiming,
|
||||||
|
pub prefetch: Prefetch,
|
||||||
|
#[cfg(test)]
|
||||||
code_waitcnt: u16,
|
code_waitcnt: u16,
|
||||||
next_data: Option<u32>,
|
next_data: Option<u32>,
|
||||||
// The largest supported step is LDM with PC: source + 16 data + I + 2 targets.
|
// The largest supported step is LDM with PC: source + 16 data + I + 2 targets.
|
||||||
|
|
@ -106,15 +111,18 @@ pub(crate) struct CpuTiming {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
Self {
|
||||||
|
prefetch,
|
||||||
|
#[cfg(test)]
|
||||||
code_waitcnt,
|
code_waitcnt,
|
||||||
..Self::default()
|
..Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn code(&mut self, address: u32, width: AccessWidth, kind: AccessKind) {
|
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;
|
self.total.code_cycles += cycles;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
self.record(TimingEvent::Code {
|
self.record(TimingEvent::Code {
|
||||||
|
|
@ -132,7 +140,8 @@ impl CpuTiming {
|
||||||
} else {
|
} else {
|
||||||
AccessKind::NonSequential
|
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.total.data_cycles += cycles;
|
||||||
self.next_data = Some(address.wrapping_add(width.bytes()));
|
self.next_data = Some(address.wrapping_add(width.bytes()));
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -147,6 +156,7 @@ impl CpuTiming {
|
||||||
|
|
||||||
pub fn internal(&mut self, cycles: u32) {
|
pub fn internal(&mut self, cycles: u32) {
|
||||||
self.total.internal_cycles += cycles;
|
self.total.internal_cycles += cycles;
|
||||||
|
self.prefetch.advance(cycles);
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
if cycles != 0 {
|
if cycles != 0 {
|
||||||
self.record(TimingEvent::Internal { cycles });
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -52,6 +52,9 @@ The repository is a Cargo workspace with three crates:
|
||||||
| `crates/gba-core/src/dma.rs` | DMA registers, internal pointers, retained channel data, trigger state, priority, and completion IRQs |
|
| `crates/gba-core/src/dma.rs` | DMA registers, internal pointers, retained channel data, trigger state, priority, and completion IRQs |
|
||||||
| `crates/gba-core/src/machine.rs` | Timed CPU/DMA/device stepping and IRQ delivery |
|
| `crates/gba-core/src/machine.rs` | Timed CPU/DMA/device stepping and IRQ delivery |
|
||||||
| `crates/gba-core/src/timing.rs` | Bus widths, wait-state costs, ordered CPU timing transactions, and timing breakdowns |
|
| `crates/gba-core/src/timing.rs` | Bus widths, wait-state costs, ordered CPU timing transactions, and timing breakdowns |
|
||||||
|
| `crates/gba-core/src/timing/prefetch.rs` | Nominal eight-halfword opcode queue, partial transfers, capacity stops, and cancellation |
|
||||||
|
| `crates/gba-core/src/timing/prefetch/tests.rs` | Wait-state matrices, partial words, cancellation phases, boundaries, configuration, and progress consistency |
|
||||||
|
| `crates/gba-core/src/memory/prefetch_tests.rs` | CPU/DMA/idle queue ownership, WAITCNT writes, branch/IRQ timing, API isolation, and rollback |
|
||||||
| `crates/gba-core/src/cpu/pipeline.rs` | CPU-owned ARM/Thumb instructions, PC/state tags, next fetch kind, deferred diagnostics, refills, and invalidation |
|
| `crates/gba-core/src/cpu/pipeline.rs` | CPU-owned ARM/Thumb instructions, PC/state tags, next fetch kind, deferred diagnostics, refills, and invalidation |
|
||||||
| `crates/gba-core/src/cpu/arm_pipeline_tests.rs` | ARM self-modifying code, branches, target pairs, DMA isolation, exceptions, errors, and timing equivalence |
|
| `crates/gba-core/src/cpu/arm_pipeline_tests.rs` | ARM self-modifying code, branches, target pairs, DMA isolation, exceptions, errors, and timing equivalence |
|
||||||
| `crates/gba-core/src/cpu/thumb_pipeline_tests.rs` | Thumb retained halfwords, BL prefix/suffix, state-tagged refills, DMA isolation, rollback, and timing equivalence |
|
| `crates/gba-core/src/cpu/thumb_pipeline_tests.rs` | Thumb retained halfwords, BL prefix/suffix, state-tagged refills, DMA isolation, rollback, and timing equivalence |
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,7 @@ Other commands:
|
||||||
direnv exec . cargo run --locked -- --cpu-demo
|
direnv exec . cargo run --locked -- --cpu-demo
|
||||||
|
|
||||||
# Run a timer-generated interrupt through an original test handler.
|
# Run a timer-generated interrupt through an original test handler.
|
||||||
# Uses nominal instruction/bus costs; prefetch and sub-instruction timing are not modeled.
|
# Uses nominal instruction/bus costs and opcode prefetch; sub-instruction device scheduling is not modeled.
|
||||||
direnv exec . cargo run --locked -- --timer-demo
|
direnv exec . cargo run --locked -- --timer-demo
|
||||||
|
|
||||||
# Verify 60 CPU-driven frames with scripted movement and color changes.
|
# Verify 60 CPU-driven frames with scripted movement and color changes.
|
||||||
|
|
|
||||||
|
|
@ -360,13 +360,39 @@ Timed CPU execution uses an ordered `CpuTiming` transaction: source fetch, actua
|
||||||
A failed instruction discards the entire transaction. Target samples do not add duplicate fetch or data costs.
|
A failed instruction discards the entire transaction. Target samples do not add duplicate fetch or data costs.
|
||||||
IRQ entry records its source fetch and vector pair in the same path.
|
IRQ entry records its source fetch and vector pair in the same path.
|
||||||
Device clocks still advance once after the successful step; this is not per-cycle scheduling.
|
Device clocks still advance once after the successful step; this is not per-cycle scheduling.
|
||||||
See [Game Pak prefetch research](../research/gamepak-prefetch.md) for this prerequisite and unresolved queue behavior.
|
The transaction also stages the nominal Game Pak prefetch queue described below.
|
||||||
|
|
||||||
|
### Game Pak opcode prefetch
|
||||||
|
|
||||||
|
WAITCNT bit 14 enables an eight-halfword timing queue, separate from retained CPU instructions and bus-history values.
|
||||||
|
ROM opcode misses start a stream. Internal work and non-cartridge accesses advance an active stream.
|
||||||
|
Matching queued ARM words or Thumb halfwords cost one cycle; partial words wait for the remaining halfword work.
|
||||||
|
The CPU-side delivery cycle also advances active background work. A CPU N request can still hit the queue.
|
||||||
|
ROM data accesses and nonmatching ROM code requests cancel the stream.
|
||||||
|
Cancellation adds one cycle when an active halfword has one cycle remaining.
|
||||||
|
Full-buffer production stops until the entries drain; the next empty demand restarts with N timing.
|
||||||
|
Background production pauses before 128 KiB boundaries, where demand uses forced-N timing.
|
||||||
|
|
||||||
|
The queue stores no instruction bytes and performs no background memory reads.
|
||||||
|
Missing lookahead and branch targets therefore keep their existing deferred diagnostic behavior.
|
||||||
|
Successful CPU transactions commit queue progress; failures discard it.
|
||||||
|
`Cpu::step` maintains the same queue as `Cpu::step_timed` when prefetch is enabled, without advancing device clocks.
|
||||||
|
Cold or invalidated CPU pipelines clear staged queue metadata and retain nominal S startup timing.
|
||||||
|
|
||||||
|
Changes to ROM wait fields or prefetch enable clear queue metadata after the WAITCNT write.
|
||||||
|
PHI/SRAM-only changes preserve it. Exact hardware live-reconfiguration behavior remains unverified.
|
||||||
|
RAM DMA and HALT supply idle cartridge time; cartridge DMA cancels the stream. STOP freezes queue progress.
|
||||||
|
Host reads and `Memory::advance_cycles` do not advance the queue; machine steps account for progress separately.
|
||||||
|
|
||||||
|
These are source-backed nominal rules, not a hardware conformance claim.
|
||||||
|
See [queue evidence, regression coverage, and limits](../research/gamepak-prefetch.md).
|
||||||
|
|
||||||
### Nominal CPU resume after DMA
|
### Nominal CPU resume after DMA
|
||||||
|
|
||||||
A successful DMA unit marks the next CPU instruction's nominal code access non-sequential.
|
A successful DMA unit marks the next CPU instruction's nominal code access non-sequential.
|
||||||
This applies even when DMA only accesses RAM. Consecutive units retain one pending resume, not multiple penalties.
|
This applies even when DMA only accesses RAM. Consecutive units retain one pending resume, not multiple penalties.
|
||||||
The source fetch uses its actual lookahead address, incoming width, and current WAITCNT with N instead of S.
|
The source request uses its actual lookahead address, incoming width, and current WAITCNT with N instead of S.
|
||||||
|
A matching enabled prefetch stream can satisfy that request without paying the raw N cost.
|
||||||
This includes branches and accepted IRQ entry; target pairs still use N then S.
|
This includes branches and accepted IRQ entry; target pairs still use N then S.
|
||||||
ARM word accesses retain an S cost for their second halfword.
|
ARM word accesses retain an S cost for their second halfword.
|
||||||
CPU history and ROM boundaries that already require N do not receive an additional access.
|
CPU history and ROM boundaries that already require N do not receive an additional access.
|
||||||
|
|
@ -390,7 +416,7 @@ Important timing limits:
|
||||||
Cold entry, debugger invalidation, and PC/state mismatches use nominal S, not hardware-validated startup timing.
|
Cold entry, debugger invalidation, and PC/state mismatches use nominal S, not hardware-validated startup timing.
|
||||||
- Refill cost calculation does not read target bytes. ARM/Thumb instruction-buffer and local IWRAM bus-history samples add no extra cycles.
|
- Refill cost calculation does not read target bytes. ARM/Thumb instruction-buffer and local IWRAM bus-history samples add no extra cycles.
|
||||||
- Target-pair sampling cannot fail the branch early. An invalid branch target still fails on the following instruction fetch.
|
- Target-pair sampling cannot fail the branch early. An invalid branch target still fails on the following instruction fetch.
|
||||||
- Game Pak prefetch is not implemented. WAITCNT bit 14 is stored but does not accelerate execution.
|
- Game Pak prefetch is nominal. Full-buffer restart, page boundaries, cancellation, and live WAITCNT changes need independent hardware validation.
|
||||||
- PHI and SRAM wait fields are stored; PHI output and SRAM mapping are not implemented.
|
- PHI and SRAM wait fields are stored; PHI output and SRAM mapping are not implemented.
|
||||||
- External work RAM timing is fixed. The undocumented memory-control register is not implemented.
|
- External work RAM timing is fixed. The undocumented memory-control register is not implemented.
|
||||||
- Exact DMA startup/resumption delays, display-bus contention, shared timer prescaler phase, and timer startup delays remain unmodeled.
|
- Exact DMA startup/resumption delays, display-bus contention, shared timer prescaler phase, and timer startup delays remain unmodeled.
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,11 @@ Timing uses independent source and destination bus costs, including WAITCNT and
|
||||||
The first unit uses non-sequential accesses plus two internal cycles. Later units use sequential accesses.
|
The first unit uses non-sequential accesses plus two internal cycles. Later units use sequential accesses.
|
||||||
Each step reports zero code cycles. Overlapping transfers read each unit after the preceding write; they are not bulk host copies.
|
Each step reports zero code cycles. Overlapping transfers read each unit after the preceding write; they are not bulk host copies.
|
||||||
A successful unit breaks the next nominal CPU code-access sequence, even if DMA accessed only RAM.
|
A successful unit breaks the next nominal CPU code-access sequence, even if DMA accessed only RAM.
|
||||||
The resumed non-refill instruction uses N code timing once; later instructions return to their normal summaries.
|
The resumed instruction requests N once; enabled opcode prefetch can still satisfy a matching request.
|
||||||
|
RAM accesses and the nominal startup cycles advance an active prefetch stream; cartridge accesses cancel it.
|
||||||
|
Cancellation can add one data cycle at the final halfword phase. DMA never consumes queued opcodes.
|
||||||
|
Only successful units commit queue progress; WAITCNT destinations apply configuration changes after the transfer.
|
||||||
|
Exact startup/completion placement remains nominal, not independently verified prefetch arbitration.
|
||||||
See [CPU resume timing](cpu.md#nominal-cpu-resume-after-dma) for failure, idle, and refill rules.
|
See [CPU resume timing](cpu.md#nominal-cpu-resume-after-dma) for failure, idle, and refill rules.
|
||||||
Startup scheduling delays, channel-resumption costs, a full CPU fetch pipeline, and display-bus contention remain unmodeled.
|
Startup scheduling delays, channel-resumption costs, a full CPU fetch pipeline, and display-bus contention remain unmodeled.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,7 @@ Display edges include every HBlank entry and scanline start. Cascaded timers rec
|
||||||
These bounds prevent idle advances from skipping DMA triggers or wake-up events.
|
These bounds prevent idle advances from skipping DMA triggers or wake-up events.
|
||||||
They also keep frame execution bounded when no interrupt can wake the CPU.
|
They also keep frame execution bounded when no interrupt can wake the CPU.
|
||||||
Idle steps neither fetch instructions nor change CPU registers; code, data, and internal cycle counts remain zero.
|
Idle steps neither fetch instructions nor change CPU registers; code, data, and internal cycle counts remain zero.
|
||||||
|
HALT idle cycles advance an already active Game Pak opcode prefetch stream, without starting one.
|
||||||
Exact hardware entry/exit delays and pipeline effects are not modeled.
|
Exact hardware entry/exit delays and pipeline effects are not modeled.
|
||||||
|
|
||||||
CPU writes to POSTFLG and HALTCNT take effect only when the executing instruction address is within the 16 KiB BIOS region.
|
CPU writes to POSTFLG and HALTCNT take effect only when the executing instruction address is within the 16 KiB BIOS region.
|
||||||
|
|
@ -171,6 +172,7 @@ CPU-only stepping can record HALT/STOP but does not enforce either pause. Use `M
|
||||||
Writing HALTCNT with bit 7 set requests STOP. Bits 0–6 are ignored.
|
Writing HALTCNT with bit 7 set requests STOP. Bits 0–6 are ignored.
|
||||||
The instruction that writes STOP finishes and pays its full nominal cycle cost.
|
The instruction that writes STOP finishes and pays its full nominal cycle cost.
|
||||||
Later machine steps perform no CPU fetch, DMA transfer, timer tick, or display update.
|
Later machine steps perform no CPU fetch, DMA transfer, timer tick, or display update.
|
||||||
|
The nominal Game Pak prefetch queue also remains frozen.
|
||||||
`StepKind::StopIdle` reports zero timing; it does not represent elapsed host wall time.
|
`StepKind::StopIdle` reports zero timing; it does not represent elapsed host wall time.
|
||||||
`Memory::stopped()` and `Machine::stopped()` report this state. The corresponding `halted()` methods return false in STOP.
|
`Memory::stopped()` and `Machine::stopped()` report this state. The corresponding `halted()` methods return false in STOP.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -206,8 +206,13 @@ Game Pak prefetch research:
|
||||||
- The pinned NanoBoyAdvance bus timing above, for queued/in-progress/miss cases and completion-edge cancellation stalls.
|
- The pinned NanoBoyAdvance bus timing above, for queued/in-progress/miss cases and completion-edge cancellation stalls.
|
||||||
- [ares prefetch at 6f6786e0](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/prefetch.cpp), compared with its pinned bus dispatcher above, for halfword ownership, full-buffer stopping, and page-boundary behavior.
|
- [ares prefetch at 6f6786e0](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/prefetch.cpp), compared with its pinned bus dispatcher above, for halfword ownership, full-buffer stopping, and page-boundary behavior.
|
||||||
|
|
||||||
See [prefetch evidence and timing prerequisites](research/gamepak-prefetch.md).
|
- [Jgenesis prefetch at fab6e2cc](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/prefetch.rs) and its [bus integration](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/bus.rs), for independent halfword capacity, full-buffer pause/restart, CPU delivery, and cancellation rules.
|
||||||
The source differences remain validation questions. No Game Pak prefetch implementation or hardware conformance result is claimed yet.
|
- [NanoBoyAdvance boundary test at cc3f4a28](https://codeberg.org/nba-emu/hw-test/src/commit/cc3f4a286cdef823980d9b353bd70befc9927d28/bus/128kb-boundary/source/main.c), reviewed for data LDM/DMA boundaries, not active opcode-queue conformance.
|
||||||
|
- [PrefetchAbuse](https://github.com/zaydlang/PrefetchAbuse), a search-excerpt-only cancellation-test lead. Its source and ROM were not used.
|
||||||
|
|
||||||
|
See [prefetch behavior and evidence](research/gamepak-prefetch.md).
|
||||||
|
The nominal queue is implemented. Source differences and live WAITCNT transitions remain validation questions.
|
||||||
|
No hardware conformance result is claimed.
|
||||||
|
|
||||||
Use [GBATEK](https://problemkaputt.de/gbatek.htm) and ARM7TDMI documentation for further hardware work.
|
Use [GBATEK](https://problemkaputt.de/gbatek.htm) and ARM7TDMI documentation for further hardware work.
|
||||||
Only use game ROMs that you may lawfully use. Do not commit game ROMs, BIOS files, or game assets.
|
Only use game ROMs that you may lawfully use. Do not commit game ROMs, BIOS files, or game assets.
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ No extra access is appended. A resumed ROM branch can therefore cost more than i
|
||||||
The later [sequencing extension](fetch-access-sequencing.md) applies each instruction's data/internal effects to the following source fetch.
|
The later [sequencing extension](fetch-access-sequencing.md) applies each instruction's data/internal effects to the following source fetch.
|
||||||
CPU history, DMA resume, and ROM page boundaries can all require N without duplicating an access.
|
CPU history, DMA resume, and ROM page boundaries can all require N without duplicating an access.
|
||||||
A completed transfer after DMA can establish a new N requirement for the following fetch.
|
A completed transfer after DMA can establish a new N requirement for the following fetch.
|
||||||
Game Pak prefetch remains unimplemented; WAITCNT bit 14 does not alter this nominal timing path.
|
This baseline preceded active prefetch. The later [nominal queue](gamepak-prefetch.md) can satisfy an N resume request from queued code.
|
||||||
|
|
||||||
This change does not select a DMA value for CPU open-bus reads.
|
This change does not select a DMA value for CPU open-bus reads.
|
||||||
DMA channel data, persistent local IWRAM lanes, and protected BIOS history remain separate.
|
DMA channel data, persistent local IWRAM lanes, and protected BIOS history remain separate.
|
||||||
|
|
|
||||||
|
|
@ -123,8 +123,10 @@ The timer therefore runs two additional cycles before the handler stops it.
|
||||||
- Cold-start costs and state-changing test/compare quirks are not hardware-validated.
|
- Cold-start costs and state-changing test/compare quirks are not hardware-validated.
|
||||||
- Data timing still uses an instruction-local access trace. This is not a complete bus-event scheduler.
|
- Data timing still uses an instruction-local access trace. This is not a complete bus-event scheduler.
|
||||||
- Device clocks still update once per instruction, IRQ entry, or DMA unit.
|
- Device clocks still update once per instruction, IRQ entry, or DMA unit.
|
||||||
- Game Pak prefetch, configurable EWRAM timing, display contention, and DMA during internal cycles remain unimplemented.
|
- Configurable EWRAM timing, display contention, and DMA during internal cycles remain unimplemented.
|
||||||
- IRQ synchronization delays and complete BIOS/general-bus latch history remain incomplete.
|
- IRQ synchronization delays and complete BIOS/general-bus latch history remain incomplete.
|
||||||
|
|
||||||
The later [prefetch prerequisite](gamepak-prefetch.md) records source, data, internal, and refill costs in order without changing totals.
|
The later ordered timing transaction preserved these baseline totals.
|
||||||
|
The subsequent [nominal prefetch queue](gamepak-prefetch.md) uses those events and can accelerate code despite a CPU N request.
|
||||||
|
CPU access-kind rules remain separate from the queue's cartridge-bus timing.
|
||||||
The next timing work remains an active Game Pak queue, with verified startup and invalidation rules and independent tests.
|
The next timing work remains an active Game Pak queue, with verified startup and invalidation rules and independent tests.
|
||||||
|
|
|
||||||
|
|
@ -72,10 +72,11 @@ The later [refill timing extension](refill-fetch-timing.md) also charges source
|
||||||
- Thumb BL prefixes use their lookahead address; suffixes add the target pair.
|
- Thumb BL prefixes use their lookahead address; suffixes add the target pair.
|
||||||
- Cold fills have no separate startup charge, even though their samples update supported bus history.
|
- Cold fills have no separate startup charge, even though their samples update supported bus history.
|
||||||
- Devices advance once after the whole successful instruction. No per-access timer/IRQ update is introduced.
|
- Devices advance once after the whole successful instruction. No per-access timer/IRQ update is introduced.
|
||||||
- Game Pak prefetch, display contention, configurable EWRAM timing, and sub-instruction DMA arbitration remain unimplemented.
|
- Display contention, configurable EWRAM timing, and sub-instruction DMA arbitration remain unimplemented.
|
||||||
|
- The later [nominal prefetch queue](gamepak-prefetch.md) changes code costs when bit 14 is enabled.
|
||||||
|
|
||||||
This is a fetch-address correction within the existing timing model, not cycle-accurate emulation.
|
This is a fetch-address correction within the existing timing model, not cycle-accurate emulation.
|
||||||
The next timing step is Game Pak prefetch with verified startup and invalidation rules.
|
Game Pak prefetch followed this baseline; independent startup and invalidation timing validation remains necessary.
|
||||||
|
|
||||||
## Original regression coverage
|
## Original regression coverage
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,115 +1,139 @@
|
||||||
# Game Pak prefetch: evidence and timing prerequisites
|
# Game Pak prefetch: nominal queue and evidence
|
||||||
|
|
||||||
## Current result
|
## Current result
|
||||||
|
|
||||||
Game Pak prefetch is **not enabled in the core yet**.
|
WAITCNT bit 14 enables a nominal Game Pak opcode prefetch queue.
|
||||||
WAITCNT bit 14 remains stored without accelerating instruction fetches.
|
The queue tracks eight halfwords, partial transfers, full-buffer stops, and cancellation stalls.
|
||||||
This change prepares ordered CPU timing transactions without changing cycle totals or instruction results.
|
It follows ordered CPU timing events and the existing instruction-boundary DMA scheduler.
|
||||||
|
This is source-backed timing behavior, not hardware-verified accuracy.
|
||||||
|
|
||||||
The previous timing path collected data costs during execution, then calculated source and refill costs afterward.
|
The queue stores addresses and timing metadata, not ROM bytes.
|
||||||
That produced correct existing totals but could not directly advance a prefetch queue in bus-access order.
|
CPU instruction retention, BIOS protected-read history, and IWRAM local lanes remain separate.
|
||||||
A prefetch implementation needs to distinguish cartridge-bus occupation from time available for background fetching.
|
Background progress cannot cause an early ROM-file lookup or change instruction values.
|
||||||
An aggregate instruction cost cannot identify when to fill or invalidate a queue.
|
|
||||||
|
|
||||||
## Sources reviewed
|
## Sources reviewed
|
||||||
|
|
||||||
- [GBATEK GamePak Prefetch](https://www.problemkaputt.de/gbatek-gba-gamepak-prefetch.htm) describes eight 16-bit entries, opcode-only service, and prefetch during internal or other-memory accesses.
|
- [GBATEK GamePak Prefetch](https://www.problemkaputt.de/gbatek-gba-gamepak-prefetch.htm) describes eight 16-bit entries, opcode-only service, and idle-bus progress.
|
||||||
- [mGBA's cycle-counting article](https://mgba.io/2015/06/27/cycle-counting-prefetch/) explains independent cartridge-bus progress, partially completed fetches, and instruction-pair examples.
|
- [mGBA's cycle-counting article](https://mgba.io/2015/06/27/cycle-counting-prefetch/) explains independent cartridge progress and partially completed fetches.
|
||||||
Its examples and linked timing suite provide leads for independent validation, not a local hardware-test pass.
|
Its examples and linked timing suite provide validation leads, not a local hardware-test pass.
|
||||||
- [NanoBoyAdvance bus timing at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/bus/timing.cc) distinguishes queue hits, in-progress fetches, misses, and cancellation stalls.
|
- [NanoBoyAdvance bus timing at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/bus/timing.cc) distinguishes hits, partial fetches, misses, and cancellation stalls.
|
||||||
- [ares prefetch at 6f6786e0](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/prefetch.cpp) provides an independent halfword-queue implementation.
|
- [ares prefetch at 6f6786e0](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/prefetch.cpp) uses halfword entries and stops at capacity or page boundaries.
|
||||||
Its [bus dispatcher](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/bus.cpp) separates opcode requests, data requests, and other-region progress.
|
Its [bus dispatcher](https://github.com/ares-emulator/ares/blob/6f6786e04f0822a3475463df284f313ab8518d51/ares/gba/cpu/bus.cpp) separates opcode requests, data requests, and other-memory progress.
|
||||||
|
- [Jgenesis prefetch at fab6e2cc](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/prefetch.rs) independently supports halfword entries, full-buffer stopping, and last-cycle cancellation stalls.
|
||||||
|
Its [bus integration](https://github.com/jsgroth/jgenesis/blob/fab6e2ccc60e492dd68b7f1e927b0829a6d80195/backend/gba-core/src/bus.rs) separates CPU delivery cycles from background transfers and DMA ownership.
|
||||||
|
- [NanoBoyAdvance 128 KiB boundary test at cc3f4a28](https://codeberg.org/nba-emu/hw-test/src/commit/cc3f4a286cdef823980d9b353bd70befc9927d28/bus/128kb-boundary/source/main.c) tests data LDM and DMA boundaries.
|
||||||
|
Reading this test does not validate active opcode-queue crossings. It was not run here.
|
||||||
|
- [PrefetchAbuse](https://github.com/zaydlang/PrefetchAbuse) appeared in search results as a cartridge-access cancellation test lead.
|
||||||
|
Only its search excerpt was reviewed. No expectations or implementation code were taken from it.
|
||||||
|
|
||||||
No external implementation or ROM was added to the repository.
|
No external implementation or ROM was added to the repository.
|
||||||
No physical-hardware measurement or external-emulator differential run was performed.
|
No physical-hardware measurement or external-emulator differential run was performed.
|
||||||
|
|
||||||
## Agreement and unresolved details
|
## Implemented queue rules
|
||||||
|
|
||||||
The sources agree on the main model:
|
|
||||||
|
|
||||||
- Prefetch is controlled by WAITCNT bit 14.
|
|
||||||
- The buffer serves Game Pak opcodes, not arbitrary ROM data loads.
|
|
||||||
- Internal CPU work and accesses to other memory can give the cartridge bus time to fetch ahead.
|
|
||||||
- A matching ready entry avoids ordinary ROM waits.
|
|
||||||
- A request for an in-progress fetch waits for the remaining work rather than restarting the full access.
|
|
||||||
- Cartridge data traffic and nonmatching code requests interact with the active prefetch sequence.
|
|
||||||
|
|
||||||
However, the implementation details are not interchangeable:
|
|
||||||
|
|
||||||
| Area | Source observation | Required follow-up |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Entry granularity | NanoBoyAdvance groups ARM words; ares tracks halfwords | Verify partial ARM fetches and ARM/Thumb transitions |
|
|
||||||
| Full buffer | ares stops production and restarts after draining; NanoBoyAdvance uses an instruction-sized countdown/count model | Establish capacity, pause, drain, and restart timing independently |
|
|
||||||
| ROM page boundary | ares prevents background reads across the forced-N boundary; NanoBoyAdvance's queue step uses its retained duty | Verify 128 KiB boundaries without assuming a normal ready-entry hit |
|
|
||||||
| Cancellation | Both contain completion-edge stall handling | Verify the exact halfword phase and which code/data requests trigger a stall |
|
|
||||||
| DMA and idle | Prefetch shares cartridge time with other bus owners | Verify RAM-only DMA, cartridge DMA, HALT/STOP, and resume separately |
|
|
||||||
| WAITCNT changes | Queue state and current transfer timing depend on register changes | Define enable, disable, and changed-wait behavior before integration |
|
|
||||||
|
|
||||||
These differences do not establish that either implementation is wrong.
|
|
||||||
They show why copying one queue policy is not sufficient evidence for the entire subsystem.
|
|
||||||
The first implementation should state its supported cases and retain explicit limits for unresolved behavior.
|
|
||||||
|
|
||||||
## Ordered CPU timing transactions
|
|
||||||
|
|
||||||
`CpuTiming` replaces the data-only accumulator.
|
|
||||||
A timed CPU instruction now records these events in order:
|
|
||||||
|
|
||||||
1. Source instruction fetch, using the incoming access kind and instruction width.
|
|
||||||
2. Actual data reads/writes, using their aligned bus addresses and widths.
|
|
||||||
3. Internal CPU cycles from the incoming instruction classification.
|
|
||||||
4. Refill target N and target+width S accesses, using the resulting instruction state.
|
|
||||||
|
|
||||||
S means sequential access; N means non-sequential access.
|
S means sequential access; N means non-sequential access.
|
||||||
An instruction without data, internal work, or a refill omits those events.
|
Raw ROM costs include one clock plus the configured wait states for each halfword.
|
||||||
The two refill events occur in `Cpu::refill_pipeline`, alongside its existing mapped target samples.
|
The public `bus_cycles` helper remains stateless and does not apply prefetch.
|
||||||
No separate refill-cost formula runs after execution.
|
|
||||||
IRQ entry records its discarded incoming-state source fetch before its ARM vector pair, without a data or internal event.
|
|
||||||
|
|
||||||
Code timing retains the entry WAITCNT snapshot.
|
1. A ROM code miss pays the requested raw cost, plus any cancellation stall.
|
||||||
Data timing retains the settings observed at each actual bus access, including pre-write settings for a WAITCNT store.
|
It starts background fetching at the following halfword address.
|
||||||
The next instruction uses the new WAITCNT settings.
|
2. Internal cycles and non-cartridge memory accesses advance an already active stream.
|
||||||
The existing data N/S rule and persistent CPU source-fetch kind are unchanged.
|
They cannot start a stream by themselves.
|
||||||
|
3. A code request must match the queue head. CPU N requests can still match queued code.
|
||||||
|
A queued Thumb halfword or complete ARM word costs one CPU-side cycle.
|
||||||
|
4. That CPU-side cycle also advances background work when production is active.
|
||||||
|
A partial ARM word waits separately for its missing halfwords.
|
||||||
|
5. Background production stops when an advance observes all eight entries occupied.
|
||||||
|
It remains stopped while the queue drains. The next empty demand restarts with N timing.
|
||||||
|
6. ROM data accesses cancel the stream instead of consuming opcode entries.
|
||||||
|
A nonmatching ROM code request also cancels it before starting a new stream.
|
||||||
|
7. Cancellation costs one additional cycle when an active halfword has one cycle remaining.
|
||||||
|
A paused or fully occupied queue does not add that stall.
|
||||||
|
8. Background production pauses before a 128 KiB ROM boundary.
|
||||||
|
Demand at that boundary uses raw forced-N timing and starts the next stream.
|
||||||
|
No background transfer crosses beyond the final ROM window.
|
||||||
|
|
||||||
The transaction returns the same `StepTiming` fields as before.
|
The full-buffer policy follows ares and Jgenesis.
|
||||||
A diagnostic discards the entire transaction, including any data events recorded before a late failure.
|
The page-boundary policy follows ares conservatively; the reviewed sources differ in this area.
|
||||||
Missing lookahead or target bytes retain their existing deferred diagnostic policy and nominal attempted-fetch cost.
|
The cancellation cycle is included in the requesting code or data field of `StepTiming`.
|
||||||
Device clocks still advance once after a successful machine step.
|
These rules have original regression tests, not a hardware conformance result.
|
||||||
This is ordered cost accounting, not a cycle-by-cycle bus scheduler.
|
|
||||||
|
|
||||||
Untimed execution, host inspection, cold-fill sampling, and DMA do not join a CPU timing transaction.
|
## Timing transactions and ownership
|
||||||
They retain their existing behavior and do not create extra code/data charges.
|
|
||||||
Untimed CPU execution still updates instruction retention and the next fetch kind, but does not compute a timing transaction.
|
`Memory` owns the committed queue because CPU, DMA, and HALT share cartridge-bus time.
|
||||||
A future active prefetch model must define timing-state progression for this API explicitly.
|
Each `CpuTiming` transaction stages a copy and applies events in order:
|
||||||
|
|
||||||
|
1. Source instruction fetch at ARM P+8 or Thumb P+4.
|
||||||
|
2. Actual data accesses, using aligned bus addresses and widths.
|
||||||
|
3. Internal cycles from the incoming instruction classification.
|
||||||
|
4. Refill target N and target+width S accesses, using the resulting instruction state.
|
||||||
|
|
||||||
|
IRQ entry records the discarded incoming-state source, then the ARM vector pair.
|
||||||
|
Non-ROM code fetches advance the existing ROM stream without consuming it.
|
||||||
|
A branch can therefore preserve the stream while executing a BIOS handler.
|
||||||
|
A later ROM fetch still needs an exact head match to use queued timing.
|
||||||
|
|
||||||
|
Success commits queue progress. A diagnostic discards all staged progress, including earlier successful data accesses.
|
||||||
|
Missing lookahead or target bytes retain deferred diagnostics and nominal attempted-fetch costs.
|
||||||
|
Device clocks still advance once after a successful machine step, not after each timing event.
|
||||||
|
|
||||||
|
`Cpu::step_timed` updates the queue and returns costs without advancing devices.
|
||||||
|
`Cpu::step` uses the same path when prefetch is enabled, but discards returned costs.
|
||||||
|
With prefetch disabled, the CPU-only path retains its previous untimed behavior.
|
||||||
|
Host reads, setup writes, and `Memory::advance_cycles` do not supply queue progress.
|
||||||
|
Host WAITCNT writes do apply queue configuration.
|
||||||
|
|
||||||
|
Cold entry, debugger invalidation, and PC/state mismatches clear staged queue metadata.
|
||||||
|
They preserve the existing nominal S startup policy and add no current/decode fill charge.
|
||||||
|
Accepted IRQ entry uses the interrupted pipeline's cold/retained state before replacing that pipeline.
|
||||||
|
Failed CPU steps preserve the previously committed queue.
|
||||||
|
|
||||||
|
### WAITCNT changes
|
||||||
|
|
||||||
|
A store pays source and data costs using the previous settings.
|
||||||
|
After the write, changing ROM wait fields or bit 14 clears timing metadata immediately.
|
||||||
|
Changing only SRAM or PHI fields preserves the stream.
|
||||||
|
CPU, host byte/halfword/word, and DMA writes use this configuration policy.
|
||||||
|
|
||||||
|
This reset policy is deliberate, not a verified model of live hardware reconfiguration.
|
||||||
|
Jgenesis models retained entries after disable; that behavior is not implemented here.
|
||||||
|
Exact enable/disable transitions and changes during a partial transfer need independent tests.
|
||||||
|
|
||||||
|
### DMA and idle
|
||||||
|
|
||||||
|
A DMA unit stages queue progress independently of the CPU transaction.
|
||||||
|
Its existing two startup cycles precede source and destination accesses in the nominal model.
|
||||||
|
RAM-only DMA supplies idle cartridge time. Cartridge DMA cancels the stream and can incur the completion-edge stall.
|
||||||
|
A failed unit preserves queue state and clocks. A successful WAITCNT destination applies the new configuration.
|
||||||
|
DMA's CPU-resume N request remains separate; a matching queue entry can still satisfy that request.
|
||||||
|
|
||||||
|
`HaltIdle` advances an active queue with its idle cycles. `StopIdle` freezes it.
|
||||||
|
Machine device updates do not advance the queue a second time.
|
||||||
|
Exact DMA startup/completion placement, bus arbitration, and hardware HALT/STOP edges remain unverified.
|
||||||
|
|
||||||
## Original regression coverage
|
## Original regression coverage
|
||||||
|
|
||||||
Test builds retain events from the last successful CPU timing transaction.
|
Queue tests cover all ROM windows, all N/S settings, ARM/Thumb widths, partial words, capacity, and restart.
|
||||||
Production builds retain only totals, the code WAITCNT snapshot, and data-sequence state; they allocate no event log.
|
They check every cancellation phase around a fill, exact-head matching, data cancellation, and page/window boundaries.
|
||||||
The test log covers the maximum supported block load, including PC, without truncation.
|
Configuration tests distinguish relevant fields from PHI/SRAM fields.
|
||||||
Tests inspect event order and metadata independently of total cycle assertions.
|
A seeded batched-versus-single-clock comparison checks progression consistency, not independent hardware correctness.
|
||||||
|
|
||||||
Coverage includes:
|
Integration tests cover RAM loads, register shifts, ROM data stalls, branch hits/misses, and PC-load refills.
|
||||||
|
They check timed/CPU-only equivalence, debugger invalidation, IRQ entry, WAITCNT writes, DMA, HALT, and STOP.
|
||||||
|
Failure tests cover late block loads, missing ROM bytes, failed DMA, retries, and host isolation.
|
||||||
|
Instruction-pair matrices separately assert CPU N/S kinds and prefetch-adjusted costs.
|
||||||
|
Existing ordered-event tests retain source/data/internal/refill ordering checks, including a full-register LDM with PC.
|
||||||
|
Test builds retain an event log; production builds allocate no event log or queue storage on the heap.
|
||||||
|
|
||||||
- Word PC loads and the full register-list LDM sequence.
|
## Validation and remaining limits
|
||||||
- Thumb POP with word data and halfword target fetches.
|
|
||||||
- ARM-to-Thumb BX, taken branches to fallthrough, and skipped conditions.
|
|
||||||
- Swaps, register-shift internal cycles, and absence of duplicate source/refill events.
|
|
||||||
- WAITCNT stores with old source settings and new settings on the next fetch.
|
|
||||||
- IRQ entry after a Thumb load, including the retained non-sequential source kind.
|
|
||||||
- Failed data accesses, late block-load failure, missing current/lookahead slots, and retry isolation.
|
|
||||||
- Host reads, untimed CPU execution, and DMA isolation from the CPU trace.
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
Workspace and core/demo tests pass in debug and release on Darwin arm64.
|
Workspace and core/demo tests pass in debug and release on Darwin arm64.
|
||||||
Formatting, clippy, rustdoc, Python preparation tests, native ROM windows, and all graphics smoke modes pass.
|
Formatting, clippy, rustdoc, Python preparation tests, native ROM windows, and all graphics smoke modes pass.
|
||||||
The release executable is native Mach-O arm64.
|
The release executable is native Mach-O arm64.
|
||||||
Public ARM, Thumb, memory, and BIOS reports match the preceding sequencing change exactly, including cycles and step counts.
|
Public ARM, Thumb, memory, and BIOS reports match the preceding ordered-timing change exactly, including cycles and step counts.
|
||||||
Debug and release reports also match. Original CPU and timer demo traces are unchanged.
|
Debug and release reports also match. Original CPU and timer demo traces are unchanged.
|
||||||
|
These public checkpoints do not independently validate active prefetch timing.
|
||||||
|
|
||||||
## Next implementation work
|
Next, validate this queue against independent timing tests, especially full-buffer restart, cancellation, and page boundaries.
|
||||||
|
Cold-start timing, live WAITCNT reconfiguration, per-access device scheduling, and sub-instruction DMA arbitration remain incomplete.
|
||||||
Implement and test queue progress against these ordered timing events before enabling WAITCNT acceleration.
|
Exact bus history, display contention, configurable EWRAM timing, and timer/IRQ delays remain separate work.
|
||||||
Keep the cartridge queue separate from CPU instruction retention, BIOS protected-read history, and IWRAM local lanes.
|
|
||||||
Resolve startup, capacity, page boundaries, partial ARM fetches, cancellation, and WAITCNT changes with independent expectations.
|
|
||||||
DMA/idle progression and diagnostic rollback need separate tests before claiming those interactions are supported.
|
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@ The stopped counter therefore increases by five even though the final machine-cy
|
||||||
- Cold current/decode fills have no separate startup timing charge.
|
- Cold current/decode fills have no separate startup timing charge.
|
||||||
- BIOS retained history is not a complete refill/data-access bus latch.
|
- BIOS retained history is not a complete refill/data-access bus latch.
|
||||||
- Missing instruction bytes remain diagnostics, not hardware prefetch aborts.
|
- Missing instruction bytes remain diagnostics, not hardware prefetch aborts.
|
||||||
- Game Pak prefetch, per-access device updates, display contention, configurable EWRAM timing, and sub-instruction DMA arbitration remain unimplemented.
|
- Per-access device updates, display contention, configurable EWRAM timing, and sub-instruction DMA arbitration remain unimplemented.
|
||||||
|
- The later [nominal prefetch queue](gamepak-prefetch.md) can change source and target costs when bit 14 is enabled.
|
||||||
|
|
||||||
The next timing work is Game Pak prefetch with verified startup and invalidation rules.
|
Game Pak prefetch followed this baseline; independent startup and invalidation timing validation remains necessary.
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ Update this file when a feature lands or a limit is removed.
|
||||||
- IRQ entry samples its discarded incoming-state fetch before the ARM vector pair, with local IWRAM history updates.
|
- IRQ entry samples its discarded incoming-state fetch before the ARM vector pair, with local IWRAM history updates.
|
||||||
- Persistent fetch access kinds follow CPU data/internal work, compose with DMA resume, and reset after target refills.
|
- Persistent fetch access kinds follow CPU data/internal work, compose with DMA resume, and reset after target refills.
|
||||||
- Game Pak wait-state control (`WAITCNT`) for all three ROM windows.
|
- Game Pak wait-state control (`WAITCNT`) for all three ROM windows.
|
||||||
|
- Bit 14 enables a nominal eight-halfword opcode prefetch queue, with partial fetches, cancellation, and DMA/idle integration.
|
||||||
- Sequential/non-sequential data accesses, branch refill costs, and variable multiply timing.
|
- Sequential/non-sequential data accesses, branch refill costs, and variable multiply timing.
|
||||||
- Optional caller-supplied 16 KiB BIOS mapping, with CPU read protection based on retained ARM PC+8 or aligned Thumb PC+4 words.
|
- Optional caller-supplied 16 KiB BIOS mapping, with CPU read protection based on retained ARM PC+8 or aligned Thumb PC+4 words.
|
||||||
- Optional original BIOS replacement: minimal boot, SoftReset, selective RegisterRamReset, IRQ dispatch, interrupt waits, memory copy/fill, integer/fixed-point arithmetic, affine matrices, bit unpacking, LZ77/run-length/Huffman decompression, and differential filters.
|
- Optional original BIOS replacement: minimal boot, SoftReset, selective RegisterRamReset, IRQ dispatch, interrupt waits, memory copy/fill, integer/fixed-point arithmetic, affine matrices, bit unpacking, LZ77/run-length/Huffman decompression, and differential filters.
|
||||||
|
|
@ -117,6 +118,8 @@ This diagnostic policy does not reproduce partial transfers during hardware data
|
||||||
CPU instructions, including swaps, finish before a newly requested DMA unit runs. Sub-instruction bus arbitration is not modeled.
|
CPU instructions, including swaps, finish before a newly requested DMA unit runs. Sub-instruction bus arbitration is not modeled.
|
||||||
CPU/bus costs are nominal estimates, not a cycle-accurate implementation.
|
CPU/bus costs are nominal estimates, not a cycle-accurate implementation.
|
||||||
The instruction buffer retains the next fetch kind; cold startup costs and per-access device scheduling remain unmodeled.
|
The instruction buffer retains the next fetch kind; cold startup costs and per-access device scheduling remain unmodeled.
|
||||||
|
Game Pak prefetch uses source-backed queue rules, not hardware-verified timing.
|
||||||
|
Live WAITCNT transitions, page-boundary behavior, and exact DMA/idle ordering still need independent timing validation.
|
||||||
Graphics support includes Mode 0–5 debug snapshots and HBlank row capture with sprites, mosaic, windows, and color effects.
|
Graphics support includes Mode 0–5 debug snapshots and HBlank row capture with sprites, mosaic, windows, and color effects.
|
||||||
Internal affine origins, vertical window flags, and vertical mosaic counters track line boundaries.
|
Internal affine origins, vertical window flags, and vertical mosaic counters track line boundaries.
|
||||||
Horizontal window comparators retain four-cycle event history.
|
Horizontal window comparators retain four-cycle event history.
|
||||||
|
|
@ -167,9 +170,9 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa
|
||||||
Keep unknown history diagnostic until fetch ordering and latch ownership have independent coverage.
|
Keep unknown history diagnostic until fetch ordering and latch ownership have independent coverage.
|
||||||
Validate BIOS snapshots with independent hardware tests before claiming exact refill or data-access history.
|
Validate BIOS snapshots with independent hardware tests before claiming exact refill or data-access history.
|
||||||
The desktop runner still uses only the original BIOS. Keep protected reads image-derived for all supplied images.
|
The desktop runner still uses only the original BIOS. Keep protected reads image-derived for all supplied images.
|
||||||
2. Add Game Pak prefetch using the ordered CPU timing path; bit 14 still does not accelerate execution.
|
2. Validate the active nominal Game Pak prefetch queue against independent timing tests.
|
||||||
Verify queue startup, full-buffer restart, page boundaries, partial ARM fetches, and invalidation before enabling it.
|
Prioritize full-buffer restart, cancellation, page boundaries, and live WAITCNT transitions.
|
||||||
See [prefetch research and prerequisites](research/gamepak-prefetch.md).
|
See [prefetch behavior and evidence](research/gamepak-prefetch.md).
|
||||||
Validate cold-start costs, then extend per-access device updates, bus arbitration, and timer/IRQ delays.
|
Validate cold-start costs, then extend per-access device updates, bus arbitration, and timer/IRQ delays.
|
||||||
3. Validate keypad retrigger behavior with hardware tests and add remaining device registers; extend BIOS reset coverage as sound/serial support becomes available.
|
3. Validate keypad retrigger behavior with hardware tests and add remaining device registers; extend BIOS reset coverage as sound/serial support becomes available.
|
||||||
Validate STOP entry/wake edges and add external wake sources and remaining DMA device modes.
|
Validate STOP entry/wake edges and add external wake sources and remaining DMA device modes.
|
||||||
|
|
|
||||||
|
|
@ -267,9 +267,14 @@ Page/wait-window crossings, masked IRQs, host/HALT isolation, and pre-write WAIT
|
||||||
See [persistent fetch access kinds](research/fetch-access-sequencing.md) for evidence and public-report changes.
|
See [persistent fetch access kinds](research/fetch-access-sequencing.md) for evidence and public-report changes.
|
||||||
Timing-event tests inspect source/data/internal/refill order and metadata independently of aggregate cycle costs.
|
Timing-event tests inspect source/data/internal/refill order and metadata independently of aggregate cycle costs.
|
||||||
They cover the full LDM register list, PC loads, Thumb POP, state changes, skipped branches, swaps, shifts, and IRQ entry.
|
They cover the full LDM register list, PC loads, Thumb POP, state changes, skipped branches, swaps, shifts, and IRQ entry.
|
||||||
WAITCNT snapshots, late failure rollback, deferred fetch errors, and host/untimed/DMA isolation have separate checks.
|
WAITCNT snapshots, late failure rollback, deferred fetch errors, and host/DMA isolation have separate checks.
|
||||||
|
CPU-only execution joins the timing path when prefetch is enabled.
|
||||||
Test-only event logs do not add a production allocation or a public trace API.
|
Test-only event logs do not add a production allocation or a public trace API.
|
||||||
See [prefetch prerequisites](research/gamepak-prefetch.md); these tests do not establish an active Game Pak prefetch implementation.
|
Prefetch queue tests cover every ROM wait setting and width, partial ARM words, full-buffer stops, restart, and cancellation phases.
|
||||||
|
They also check page/window boundaries, configuration changes, and batched-versus-single-clock consistency.
|
||||||
|
Integration tests check RAM/internal progress, ROM data stalls, branch and IRQ fetches, CPU-only equivalence, and debugger invalidation.
|
||||||
|
WAITCNT CPU/host/DMA writes, RAM/cart DMA, HALT/STOP, failed steps, and missing ROM bytes have separate checks.
|
||||||
|
See [prefetch evidence and limits](research/gamepak-prefetch.md). These tests validate the nominal model, not physical-hardware accuracy.
|
||||||
Instruction timing tests cover arithmetic, shifts, branches, transfers, block operations, multiply, and exception returns.
|
Instruction timing tests cover arithmetic, shifts, branches, transfers, block operations, multiply, and exception returns.
|
||||||
Timed and untimed execution are compared across all 65,536 Thumb encodings with fixed initial registers.
|
Timed and untimed execution are compared across all 65,536 Thumb encodings with fixed initial registers.
|
||||||
Timer/IRQ tests verify that changing WAITCNT changes when an interrupt is sampled.
|
Timer/IRQ tests verify that changing WAITCNT changes when an interrupt is sampled.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue