refactor(cpu): account for timing in bus-access order

This commit is contained in:
0xrsydn 2026-10-06 17:16:08 +07:00
commit 7f3ebd3a5d
14 changed files with 626 additions and 65 deletions

View file

@ -257,6 +257,7 @@ impl Cpu {
let result = self.execute_instruction(instruction, memory);
memory.end_cpu_access(result.is_ok());
if result.is_ok() {
memory.record_internal_cycles(summary.internal);
if summary.refill {
self.refill_pipeline(memory);
} else {

View file

@ -83,6 +83,12 @@ impl Cpu {
memory.fetch_instruction(pc, instruction_set),
memory.fetch_instruction(pc.wrapping_add(instruction_set.width()), instruction_set),
];
for (sample, kind) in fetches
.iter()
.zip([AccessKind::NonSequential, AccessKind::Sequential])
{
memory.record_code_access(sample.address(), instruction_set.access_width(), kind);
}
memory.refill_cpu_bus_history(&fetches);
self.pipeline = Some(Pipeline {
pc,

View file

@ -5,14 +5,14 @@
use super::{Cpu, CpuError, InstructionSet};
use crate::{
memory::Memory,
timing::{bus_cycles, refill_cycles, AccessKind, AccessWidth, StepTiming},
timing::{AccessKind, AccessWidth, StepTiming},
};
#[derive(Default)]
pub(super) struct Summary {
data: bool,
pub(super) refill: bool,
internal: u32,
pub(super) internal: u32,
}
impl Summary {
@ -43,7 +43,7 @@ impl Summary {
}
impl InstructionSet {
fn access_width(self) -> AccessWidth {
pub(super) fn access_width(self) -> AccessWidth {
match self {
Self::Arm => AccessWidth::Word,
Self::Thumb => AccessWidth::Halfword,
@ -57,27 +57,18 @@ impl Cpu {
/// Failed instructions discard their timing trace and retain existing diagnostics.
pub fn step_timed(&mut self, memory: &mut Memory) -> Result<StepTiming, CpuError> {
let fetched = self.fetch(memory)?;
let instruction = fetched.instruction;
let summary = self.instruction_summary(instruction);
let fetch_address = fetched.lookahead.address();
let width = self.instruction_set.access_width();
// A WAITCNT store affects subsequent instructions, not this code access.
let waitcnt = memory.waitcnt();
let code_kind = memory.cpu_code_kind(self.next_fetch_kind());
memory.begin_data_timing();
// Source timing precedes data accesses and their WAITCNT writes.
memory.begin_cpu_timing();
memory.record_code_access(
fetched.lookahead.address(),
self.instruction_set.access_width(),
code_kind,
);
let result = self.execute_fetched(fetched, memory);
let data_cycles = memory.end_data_timing();
let timing = memory.end_cpu_timing(result.is_ok());
result?;
let mut code_cycles = bus_cycles(waitcnt, fetch_address, width, code_kind);
if summary.refill {
code_cycles += refill_cycles(waitcnt, self.pc(), self.instruction_set.access_width());
}
Ok(StepTiming {
code_cycles,
data_cycles,
internal_cycles: summary.internal,
idle_cycles: 0,
})
Ok(timing)
}
/// Accept IRQ between instructions, discard the old-state fetch, then refill ARM.
@ -89,15 +80,12 @@ impl Cpu {
if !self.take_interrupt(memory.irq_pending(), false) {
return None;
}
let waitcnt = memory.waitcnt();
memory.begin_cpu_timing();
let fetch = memory.fetch_instruction(address, state);
memory.record_code_access(fetch.address(), state.access_width(), kind);
memory.discarded_cpu_fetch(&fetch);
self.refill_pipeline(memory);
Some(StepTiming {
code_cycles: bus_cycles(waitcnt, fetch.address(), state.access_width(), kind)
+ refill_cycles(waitcnt, self.pc(), self.instruction_set.access_width()),
..StepTiming::default()
})
Some(memory.end_cpu_timing(true))
}
fn arm_summary(&self, instruction: u32) -> Summary {

View file

@ -8,6 +8,8 @@ mod irq_fetch_tests;
mod iwram_bus;
#[cfg(test)]
mod iwram_history_tests;
#[cfg(test)]
mod timing_event_tests;
pub(crate) use fetch::InstructionFetch;
use iwram_bus::IwramBus;
@ -17,7 +19,7 @@ use crate::{
dma::{DmaError, DMA_BASE, DMA_END},
input::Buttons,
io::{Io, HALTCNT, KEYINPUT, POSTFLG},
timing::{bus_cycles, AccessKind, AccessWidth, DataTiming, StepTiming},
timing::{bus_cycles, AccessKind, AccessWidth, CpuTiming, StepTiming},
video::{self, sprites::pipeline, Framebuffer, VideoError},
};
@ -89,7 +91,9 @@ pub struct Memory {
bios: Option<Vec<u8>>,
io: Io,
cycles: u64,
data_timing: Cell<Option<DataTiming>>,
cpu_timing: Cell<Option<CpuTiming>>,
#[cfg(test)]
last_cpu_timing: Option<CpuTiming>,
// A completed DMA unit breaks the CPU's next nominal code-access sequence.
cpu_resume_nonsequential: bool,
cpu_access: Option<CpuAccess>,
@ -114,7 +118,9 @@ impl Memory {
bios: None,
io: Io::default(),
cycles: 0,
data_timing: Cell::new(None),
cpu_timing: Cell::new(None),
#[cfg(test)]
last_cpu_timing: None,
cpu_resume_nonsequential: false,
cpu_access: None,
bios_prefetch: None,
@ -509,19 +515,47 @@ impl Memory {
}
}
pub(crate) fn begin_data_timing(&self) {
debug_assert!(self.data_timing.get().is_none());
self.data_timing.set(Some(DataTiming::default()));
pub(crate) fn begin_cpu_timing(&self) {
debug_assert!(self.cpu_timing.get().is_none());
self.cpu_timing.set(Some(CpuTiming::new(self.waitcnt())));
}
pub(crate) fn end_data_timing(&self) -> u32 {
self.data_timing.take().map_or(0, |trace| trace.cycles)
pub(crate) fn end_cpu_timing(&mut self, succeeded: bool) -> StepTiming {
let trace = self
.cpu_timing
.take()
.expect("CPU timing transaction is active");
if !succeeded {
return StepTiming::default();
}
#[cfg(test)]
{
self.last_cpu_timing = Some(trace);
}
trace.total
}
pub(crate) fn record_code_access(&self, address: u32, width: AccessWidth, kind: AccessKind) {
if let Some(mut trace) = self.cpu_timing.get() {
trace.code(address, width, kind);
self.cpu_timing.set(Some(trace));
}
}
pub(crate) fn record_internal_cycles(&self, cycles: u32) {
if cycles == 0 {
return;
}
if let Some(mut trace) = self.cpu_timing.get() {
trace.internal(cycles);
self.cpu_timing.set(Some(trace));
}
}
fn record_access(&self, address: u32, width: AccessWidth) {
if let Some(mut trace) = self.data_timing.get() {
trace.access(self.waitcnt(), address, width);
self.data_timing.set(Some(trace));
if let Some(mut trace) = self.cpu_timing.get() {
trace.data(self.waitcnt(), address, width);
self.cpu_timing.set(Some(trace));
}
}

View file

@ -142,10 +142,10 @@ fn sampling_has_no_timing_or_history_side_effects_and_never_uses_data_fallback()
bios[8..12].copy_from_slice(&WORD.to_le_bytes());
let mut memory = Memory::with_bios(vec![], bios).unwrap();
memory.cpu_resume_nonsequential = true;
memory.begin_data_timing();
memory.begin_cpu_timing();
let fetch = memory.fetch_instruction(8, InstructionSet::Arm);
memory.fetch_instruction(0x0300_0000, InstructionSet::Thumb);
assert_eq!(memory.end_data_timing(), 0);
assert_eq!(memory.end_cpu_timing(true), StepTiming::default());
assert_eq!(memory.bios_prefetch, None);
assert!(memory.cpu_resume_nonsequential);
assert_eq!(memory.cycles(), 0);

View file

@ -0,0 +1,321 @@
//! Original checks of timing-event order, independent of aggregate cycle totals.
use super::*;
use crate::{
cpu::Cpu,
dma::DMA_STRIDE,
io::{IE, IME, TIMER_BASE, WAITCNT},
machine::{Machine, StepKind},
timing::TimingEvent,
};
const BASE: u32 = ROM_START + 0x100;
const THUMB: u32 = ROM_START + 0x180;
const DATA: u32 = 0x0300_1000;
const ARM_NOP: u32 = 0xe1a0_0000;
fn prepared(thumb: bool, instructions: &[u32], operands: &[(usize, u32)]) -> (Cpu, Memory) {
let mut operands = operands.to_vec();
if thumb {
operands.push((12, THUMB | 1));
}
let mut rom = ARM_NOP.to_le_bytes().repeat(256);
for (index, &(register, value)) in operands.iter().enumerate() {
let at = 0x100 + 4 * index;
let literal = 0x200 + 4 * index;
let ldr = 0xe59f_0000 | (register as u32) << 12 | (literal - at - 8) as u32;
rom[at..at + 4].copy_from_slice(&ldr.to_le_bytes());
rom[literal..literal + 4].copy_from_slice(&value.to_le_bytes());
}
// A NOP after operand setup establishes an incoming S kind.
let entry = 0x100 + 4 * (operands.len() + 1);
let (entry, width) = if thumb {
rom[entry..entry + 4].copy_from_slice(&0xe12f_ff1c_u32.to_le_bytes());
rom[0x180..0x1c0].copy_from_slice(&0x46c0_u16.to_le_bytes().repeat(32));
(0x180, 2)
} else {
(entry, 4)
};
for (index, instruction) in instructions.iter().enumerate() {
rom[entry + index * width..entry + (index + 1) * width]
.copy_from_slice(&instruction.to_le_bytes()[..width]);
}
let mut memory = Memory::with_bios(rom, ARM_NOP.to_le_bytes().repeat(BIOS_SIZE / 4)).unwrap();
let mut cpu = Cpu::new(BASE);
for _ in 0..operands.len() + 1 + usize::from(thumb) {
cpu.step(&mut memory).unwrap();
}
assert!(memory.last_cpu_timing.is_none()); // Untimed setup does not create a trace.
(cpu, memory)
}
fn code(address: u32, width: AccessWidth, kind: AccessKind, cycles: u32) -> TimingEvent {
TimingEvent::Code {
address,
width,
kind,
waitcnt: 0,
cycles,
}
}
fn data(address: u32, width: AccessWidth, kind: AccessKind, cycles: u32) -> TimingEvent {
TimingEvent::Data {
address,
width,
kind,
waitcnt: 0,
cycles,
}
}
fn events(memory: &Memory) -> Vec<TimingEvent> {
assert!(memory.cpu_timing.get().is_none());
memory.last_cpu_timing.unwrap().events()
}
#[test]
fn pc_load_records_source_data_internal_and_target_pair_in_order() {
let target = 0x0200_0100;
let (mut cpu, mut memory) = prepared(false, &[0xe591_f000], &[(1, DATA)]);
memory.write32(DATA, target).unwrap();
let source = cpu.pc() + 8;
let timing = cpu.step_timed(&mut memory).unwrap();
assert_eq!(
events(&memory),
vec![
code(source, AccessWidth::Word, AccessKind::Sequential, 6),
data(DATA, AccessWidth::Word, AccessKind::NonSequential, 1),
TimingEvent::Internal { cycles: 1 },
code(target, AccessWidth::Word, AccessKind::NonSequential, 6),
code(target + 4, AccessWidth::Word, AccessKind::Sequential, 6),
]
);
assert_eq!(
timing,
StepTiming {
code_cycles: 18,
data_cycles: 1,
internal_cycles: 1,
idle_cycles: 0
}
);
assert_eq!(memory.cycles(), 0);
}
#[test]
fn largest_block_load_records_all_data_before_internal_and_refill_events() {
let target = 0x0200_0200;
let (mut cpu, mut memory) = prepared(false, &[0xe890_ffff], &[(0, DATA)]);
memory.write32(DATA + 60, target).unwrap();
let mut expected = vec![code(
cpu.pc() + 8,
AccessWidth::Word,
AccessKind::Sequential,
6,
)];
for index in 0..16 {
expected.push(data(
DATA + index * 4,
AccessWidth::Word,
if index == 0 {
AccessKind::NonSequential
} else {
AccessKind::Sequential
},
1,
));
}
expected.extend([
TimingEvent::Internal { cycles: 1 },
code(target, AccessWidth::Word, AccessKind::NonSequential, 6),
code(target + 4, AccessWidth::Word, AccessKind::Sequential, 6),
]);
let timing = cpu.step_timed(&mut memory).unwrap();
assert_eq!(events(&memory), expected);
assert_eq!(timing.total(), 35);
}
#[test]
fn thumb_pop_records_word_data_then_halfword_targets_without_duplicate_fetch_costs() {
let target = 0x0300_2000;
let (mut cpu, mut memory) = prepared(true, &[0xbd03], &[(13, DATA)]);
memory.write32(DATA + 8, target).unwrap();
let timing = cpu.step_timed(&mut memory).unwrap();
assert_eq!(
events(&memory),
vec![
code(THUMB + 4, AccessWidth::Halfword, AccessKind::Sequential, 3),
data(DATA, AccessWidth::Word, AccessKind::NonSequential, 1),
data(DATA + 4, AccessWidth::Word, AccessKind::Sequential, 1),
data(DATA + 8, AccessWidth::Word, AccessKind::Sequential, 1),
TimingEvent::Internal { cycles: 1 },
code(target, AccessWidth::Halfword, AccessKind::NonSequential, 1),
code(target + 2, AccessWidth::Halfword, AccessKind::Sequential, 1),
]
);
assert_eq!(timing.total(), 9);
}
#[test]
fn state_change_and_taken_or_skipped_branches_have_only_the_required_code_events() {
let target = 0x0200_0100;
let (mut cpu, mut memory) = prepared(false, &[0xe12f_ff10], &[(0, target | 1)]);
let source = cpu.pc() + 8;
cpu.step_timed(&mut memory).unwrap();
assert_eq!(
events(&memory),
vec![
code(source, AccessWidth::Word, AccessKind::Sequential, 6),
code(target, AccessWidth::Halfword, AccessKind::NonSequential, 3),
code(target + 2, AccessWidth::Halfword, AccessKind::Sequential, 3),
]
);
for (instruction, taken) in [(0xeaff_ffff, true), (0x0aff_ffff, false)] {
let (mut cpu, mut memory) = prepared(false, &[instruction], &[]);
let pc = cpu.pc();
let mut expected = vec![code(pc + 8, AccessWidth::Word, AccessKind::Sequential, 6)];
if taken {
expected.extend([
code(pc + 4, AccessWidth::Word, AccessKind::NonSequential, 8),
code(pc + 8, AccessWidth::Word, AccessKind::Sequential, 6),
]);
}
cpu.step_timed(&mut memory).unwrap();
assert_eq!(events(&memory), expected);
}
}
#[test]
fn swaps_and_register_shifts_place_internal_cycles_after_their_bus_work() {
for (instruction, swap) in [(0xe101_0092, true), (0xe1a0_0312, false)] {
let (mut cpu, mut memory) = prepared(false, &[instruction], &[(1, DATA)]);
let mut expected = vec![code(
cpu.pc() + 8,
AccessWidth::Word,
AccessKind::Sequential,
6,
)];
if swap {
expected.extend([
data(DATA, AccessWidth::Word, AccessKind::NonSequential, 1),
data(DATA, AccessWidth::Word, AccessKind::NonSequential, 1),
]);
}
expected.push(TimingEvent::Internal { cycles: 1 });
cpu.step_timed(&mut memory).unwrap();
assert_eq!(events(&memory), expected);
}
}
#[test]
fn waitcnt_store_records_old_source_settings_and_next_step_records_new_settings() {
for waitcnt in [0x18, 0x4018] {
// Prefetch enable is still stored only.
let (mut cpu, mut memory) = prepared(
false,
&[0xe581_0000, ARM_NOP],
&[(0, waitcnt), (1, WAITCNT)],
);
let pc = cpu.pc();
cpu.step_timed(&mut memory).unwrap();
assert_eq!(
events(&memory),
vec![
code(pc + 8, AccessWidth::Word, AccessKind::Sequential, 6),
data(WAITCNT, AccessWidth::Word, AccessKind::NonSequential, 1),
]
);
cpu.step_timed(&mut memory).unwrap();
assert_eq!(
events(&memory),
vec![TimingEvent::Code {
address: pc + 12,
width: AccessWidth::Word,
kind: AccessKind::NonSequential,
waitcnt: waitcnt as u16,
cycles: 5,
}]
);
}
}
#[test]
fn irq_entry_records_the_incoming_source_and_vector_pair_without_executing_code() {
let (mut cpu, mut memory) = prepared(true, &[0x6808], &[(1, DATA)]);
cpu.step_timed(&mut memory).unwrap(); // Load makes the next source N.
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);
let start = machine.cycles();
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(
events(machine.memory()),
vec![
code(
THUMB + 6,
AccessWidth::Halfword,
AccessKind::NonSequential,
5
),
code(0x18, AccessWidth::Word, AccessKind::NonSequential, 1),
code(0x1c, AccessWidth::Word, AccessKind::Sequential, 1),
]
);
assert_eq!(machine.cycles() - start, 7);
}
#[test]
fn failed_instructions_discard_events_and_preserve_the_last_successful_trace() {
// The second LDR reads an unsupported data region; the first retains normal events.
let (mut cpu, mut memory) = prepared(false, &[0xe591_1000, 0xe591_0000], &[(1, DATA)]);
memory.write32(DATA, 0x0e00_0000).unwrap();
cpu.step_timed(&mut memory).unwrap();
let previous = events(&memory);
let before = cpu.clone();
for _ in 0..2 {
assert!(cpu.step_timed(&mut memory).is_err());
assert_eq!(cpu, before);
assert_eq!(events(&memory), previous);
assert_eq!(memory.cycles(), 0);
}
// A late block-load error must also discard already recorded data accesses.
{
let (mut cpu, mut memory) = prepared(false, &[ARM_NOP, 0xe8b1_0005], &[(1, IME)]);
cpu.step_timed(&mut memory).unwrap();
let previous = events(&memory);
let before = cpu.clone();
assert!(cpu.step_timed(&mut memory).is_err());
assert_eq!(cpu, before);
assert_eq!(events(&memory), previous);
}
// Host reads and untimed code neither join nor replace a completed trace.
memory.read32(DATA).unwrap();
let mut probe = Cpu::new(0x0300_0000);
probe.step(&mut memory).unwrap();
assert_eq!(events(&memory), previous);
}
#[test]
fn deferred_source_errors_and_dma_steps_do_not_add_hidden_cpu_events() {
let mut memory = Memory::new(ARM_NOP.to_le_bytes().to_vec()).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.step_timed(&mut memory).unwrap(); // Missing P+8 is deferred but still costs a fetch.
let previous = vec![code(
ROM_START + 8,
AccessWidth::Word,
AccessKind::Sequential,
6,
)];
assert_eq!(events(&memory), previous);
assert!(cpu.step_timed(&mut memory).is_err());
assert_eq!(events(&memory), previous);
let dma = DMA_BASE + 3 * DMA_STRIDE;
memory.write32(dma, DATA).unwrap();
memory.write32(dma + 4, DATA + 4).unwrap();
memory.write32(dma + 8, 0x8400_0001).unwrap();
memory.step_dma().unwrap().unwrap();
assert_eq!(events(&memory), previous);
assert_eq!(memory.cycles(), 4); // IWRAM read/write plus DMA startup, no CPU timing.
}

View file

@ -89,33 +89,103 @@ pub fn bus_cycles(waitcnt: u16, address: u32, width: AccessWidth, kind: AccessKi
}
}
/// Target/decode pair only. The caller separately charges the old-state source fetch.
pub(crate) fn refill_cycles(waitcnt: u16, target: u32, width: AccessWidth) -> u32 {
bus_cycles(waitcnt, target, width, AccessKind::NonSequential)
+ bus_cycles(
waitcnt,
target.wrapping_add(width.bytes()),
width,
AccessKind::Sequential,
)
}
/// A data-only trace. Instruction fetches and branch refills are charged by the
/// CPU timing summary, not here. Actual CPU bus calls supply addresses and widths.
/// One speculative instruction/IRQ timing transaction. Events arrive in source,
/// data, internal, then target-pair order. Device clocks still advance in bulk.
/// Code uses the entry WAITCNT snapshot; data uses settings at each bus access.
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct DataTiming {
pub cycles: u32,
next: Option<u32>,
pub(crate) struct CpuTiming {
pub total: StepTiming,
code_waitcnt: u16,
next_data: Option<u32>,
// The largest supported step is LDM with PC: source + 16 data + I + 2 targets.
// Production builds keep only costs and sequencing state, not an event log.
#[cfg(test)]
events: [Option<TimingEvent>; 20],
#[cfg(test)]
event_count: usize,
}
impl DataTiming {
pub fn access(&mut self, waitcnt: u16, address: u32, width: AccessWidth) {
let kind = if self.next == Some(address) {
impl CpuTiming {
pub fn new(code_waitcnt: u16) -> Self {
Self {
code_waitcnt,
..Self::default()
}
}
pub fn code(&mut self, address: u32, width: AccessWidth, kind: AccessKind) {
let cycles = bus_cycles(self.code_waitcnt, address, width, kind);
self.total.code_cycles += cycles;
#[cfg(test)]
self.record(TimingEvent::Code {
address,
width,
kind,
waitcnt: self.code_waitcnt,
cycles,
});
}
pub fn data(&mut self, waitcnt: u16, address: u32, width: AccessWidth) {
let kind = if self.next_data == Some(address) {
AccessKind::Sequential
} else {
AccessKind::NonSequential
};
self.cycles += bus_cycles(waitcnt, address, width, kind);
self.next = Some(address.wrapping_add(width.bytes()));
let cycles = bus_cycles(waitcnt, address, width, kind);
self.total.data_cycles += cycles;
self.next_data = Some(address.wrapping_add(width.bytes()));
#[cfg(test)]
self.record(TimingEvent::Data {
address,
width,
kind,
waitcnt,
cycles,
});
}
pub fn internal(&mut self, cycles: u32) {
self.total.internal_cycles += cycles;
#[cfg(test)]
if cycles != 0 {
self.record(TimingEvent::Internal { cycles });
}
}
#[cfg(test)]
fn record(&mut self, event: TimingEvent) {
self.events[self.event_count] = Some(event);
self.event_count += 1;
}
#[cfg(test)]
pub fn events(&self) -> Vec<TimingEvent> {
self.events[..self.event_count]
.iter()
.map(|event| event.unwrap())
.collect()
}
}
#[cfg(test)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TimingEvent {
Code {
address: u32,
width: AccessWidth,
kind: AccessKind,
waitcnt: u16,
cycles: u32,
},
Data {
address: u32,
width: AccessWidth,
kind: AccessKind,
waitcnt: u16,
cycles: u32,
},
Internal {
cycles: u32,
},
}