use crate::nes::console::TimingMode;
#[derive(Debug, Clone, Copy, Default)]
pub struct MasterClock {
master_clock: u64,
ppu_clock: u64,
cpu_divider: u64,
ppu_divider: u64,
start_clock: u64,
}
impl MasterClock {
const READ_WRITE_SHIFT: u64 = 1;
pub fn new(tv_system: TimingMode) -> Self {
let (cpu_divider, ppu_divider, start_clock) = match tv_system {
TimingMode::Ntsc | TimingMode::MultiRegion | TimingMode::Unknown(_) => (12, 4, 6),
TimingMode::Pal => (16, 5, 8),
TimingMode::Dendy => (15, 5, 7),
};
Self {
master_clock: 0,
ppu_clock: 0,
cpu_divider,
ppu_divider,
start_clock,
}
}
pub fn master_cycles(&self) -> u64 {
self.master_clock
}
pub fn set_master_cycles(&mut self, cycles: u64) {
self.master_clock = cycles;
}
pub fn advance_cpu_cycles(&mut self, cpu_cycles: u64) {
self.master_clock += self.cpu_divider * cpu_cycles;
}
pub fn before_cpu_cycle(&mut self, is_write: bool) {
self.master_clock += if is_write {
self.start_clock + Self::READ_WRITE_SHIFT
} else {
self.start_clock - Self::READ_WRITE_SHIFT
};
}
pub fn after_cpu_cycle(&mut self, is_write: bool) {
let end_clock = self.cpu_divider - self.start_clock;
self.master_clock += if is_write {
end_clock - Self::READ_WRITE_SHIFT
} else {
end_clock + Self::READ_WRITE_SHIFT
};
}
pub fn ppu_cycles_since_last(&mut self) -> u64 {
let elapsed_master_ticks = self.master_clock - self.ppu_clock;
let ppu_cycles = elapsed_master_ticks / self.ppu_divider;
self.ppu_clock += ppu_cycles * self.ppu_divider;
ppu_cycles
}
pub fn ppu_cycles(&self) -> u64 {
self.ppu_clock
}
pub fn set_ppu_cycles(&mut self, cycles: u64) {
self.ppu_clock = cycles;
}
pub fn reset(&mut self) {
self.master_clock = 0;
self.ppu_clock = 0;
}
#[allow(dead_code)]
pub fn cpu_divider(&self) -> u64 {
self.cpu_divider
}
#[allow(dead_code)]
pub fn ppu_divider(&self) -> u64 {
self.ppu_divider
}
}
#[cfg(test)]
mod tests {
use super::MasterClock;
use crate::nes::console::TimingMode;
#[test]
fn test_ppu_cycles_since_last_ntsc_rounds_down_and_tracks_remainder() {
let mut clock = MasterClock::new(TimingMode::Ntsc);
assert_eq!(clock.ppu_cycles_since_last(), 0);
clock.set_master_cycles(3);
assert_eq!(clock.ppu_cycles_since_last(), 0);
clock.set_master_cycles(4);
assert_eq!(clock.ppu_cycles_since_last(), 1);
clock.set_master_cycles(7);
assert_eq!(clock.ppu_cycles_since_last(), 0);
clock.set_master_cycles(8);
assert_eq!(clock.ppu_cycles_since_last(), 1);
}
#[test]
fn test_ppu_cycles_since_last_ntsc_multiple_cycles_at_once() {
let mut clock = MasterClock::new(TimingMode::Ntsc);
clock.set_master_cycles(10);
assert_eq!(clock.ppu_cycles_since_last(), 2);
clock.set_master_cycles(11);
assert_eq!(clock.ppu_cycles_since_last(), 0);
clock.set_master_cycles(12);
assert_eq!(clock.ppu_cycles_since_last(), 1);
}
#[test]
fn test_ppu_cycles_since_last_pal_uses_divider_5() {
let mut clock = MasterClock::new(TimingMode::Pal);
clock.set_master_cycles(4);
assert_eq!(clock.ppu_cycles_since_last(), 0);
clock.set_master_cycles(5);
assert_eq!(clock.ppu_cycles_since_last(), 1);
clock.set_master_cycles(9);
assert_eq!(clock.ppu_cycles_since_last(), 0);
clock.set_master_cycles(10);
assert_eq!(clock.ppu_cycles_since_last(), 1);
}
#[test]
fn test_dendy_cpu_divider_is_15() {
let clock = MasterClock::new(TimingMode::Dendy);
assert_eq!(clock.cpu_divider(), 15);
}
#[test]
fn test_dendy_before_read_cycle_is_6_master_ticks() {
let mut clock = MasterClock::new(TimingMode::Dendy);
clock.before_cpu_cycle(false);
assert_eq!(clock.master_cycles(), 6);
}
#[test]
fn test_dendy_before_write_cycle_is_8_master_ticks() {
let mut clock = MasterClock::new(TimingMode::Dendy);
clock.before_cpu_cycle(true);
assert_eq!(clock.master_cycles(), 8);
}
#[test]
fn test_dendy_read_cycle_total_is_15_master_ticks() {
let mut clock = MasterClock::new(TimingMode::Dendy);
clock.before_cpu_cycle(false);
clock.after_cpu_cycle(false);
assert_eq!(clock.master_cycles(), 15);
}
#[test]
fn test_dendy_write_cycle_total_is_15_master_ticks() {
let mut clock = MasterClock::new(TimingMode::Dendy);
clock.before_cpu_cycle(true);
clock.after_cpu_cycle(true);
assert_eq!(clock.master_cycles(), 15);
}
#[test]
fn test_dendy_one_cpu_cycle_yields_3_ppu_cycles_with_no_remainder() {
let mut clock = MasterClock::new(TimingMode::Dendy);
clock.before_cpu_cycle(false);
clock.after_cpu_cycle(false);
assert_eq!(clock.ppu_cycles_since_last(), 3);
assert_eq!(
clock.ppu_cycles_since_last(),
0,
"ppu_clock must be aligned with no remainder after read"
);
let mut clock = MasterClock::new(TimingMode::Dendy);
clock.before_cpu_cycle(true);
clock.after_cpu_cycle(true);
assert_eq!(clock.ppu_cycles_since_last(), 3);
assert_eq!(
clock.ppu_cycles_since_last(),
0,
"ppu_clock must be aligned with no remainder after write"
);
}
#[test]
fn test_reset_restores_initial_state_but_preserves_dividers() {
let mut clock = MasterClock::new(TimingMode::Ntsc);
clock.set_master_cycles(37);
let _ = clock.ppu_cycles_since_last();
clock.before_cpu_cycle(false);
clock.after_cpu_cycle(true);
assert_ne!(clock.master_cycles(), 0);
let cpu_divider_before = clock.cpu_divider();
let ppu_divider_before = clock.ppu_divider();
clock.reset();
assert_eq!(clock.master_cycles(), 0);
assert_eq!(clock.ppu_cycles_since_last(), 0);
assert_eq!(clock.cpu_divider(), cpu_divider_before);
assert_eq!(clock.ppu_divider(), ppu_divider_before);
}
}