gameboy-rust/crates/gba-core/src/timing.rs

191 lines
5.6 KiB
Rust

//! Nominal GBA bus costs. These calculations do not perform memory accesses.
//! Game Pak prefetch, display-bus contention, and configurable EWRAM timing are not modeled.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccessWidth {
Byte,
Halfword,
Word,
}
impl AccessWidth {
pub(crate) fn bytes(self) -> u32 {
match self {
Self::Byte => 1,
Self::Halfword => 2,
Self::Word => 4,
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum AccessKind {
NonSequential,
#[default]
Sequential,
}
/// Timing breakdown for one instruction, exception entry, DMA unit, or HALT batch.
/// DMA has zero code cycles and charges two internal cycles on its first unit.
/// This is a nominal estimate, not a cycle-accurate bus trace.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct StepTiming {
pub code_cycles: u32,
pub data_cycles: u32,
pub internal_cycles: u32,
/// Device-clock cycles while the CPU is halted, without CPU or DMA bus work.
pub idle_cycles: u32,
}
impl StepTiming {
pub fn total(self) -> u32 {
self.code_cycles + self.data_cycles + self.internal_cycles + self.idle_cycles
}
}
/// Compute an access cost from a WAITCNT snapshot. This does not validate the
/// address against the memory map. Unmapped regions retain a one-cycle fallback
/// so a branch can finish before the next instruction fetch reports a diagnostic.
/// WAITCNT's prefetch and PHI bits are stored but do not affect this calculation.
pub fn bus_cycles(waitcnt: u16, address: u32, width: AccessWidth, kind: AccessKind) -> u32 {
match address >> 24 {
0x02 => {
if width == AccessWidth::Word {
6
} else {
3
}
}
0x05 | 0x06 => {
if width == AccessWidth::Word {
2
} else {
1
}
}
0x08..=0x0d => {
let window = ((address >> 25) - 4) as usize;
let first_shift = [2, 5, 8][window];
let second_shift = [4, 7, 10][window];
let first = 1 + [4, 3, 2, 8][usize::from((waitcnt >> first_shift) & 3)];
let second = 1 + if waitcnt & (1 << second_shift) != 0 {
1
} else {
[2, 4, 8][window]
};
// The Game Pak bus forces N timing at each 128 KiB boundary.
let sequential = kind == AccessKind::Sequential && address & 0x1fffe != 0;
let cycles = if sequential { second } else { first };
// Each word is two halfwords. The second halfword always uses S timing.
cycles
+ if width == AccessWidth::Word {
second
} else {
0
}
}
// BIOS, IWRAM, I/O, and OAM use a 32-bit bus. Other areas remain unmapped.
_ => 1,
}
}
/// 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 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 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
};
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,
},
}