use crate::nes::console::TimingMode;
pub(crate) const PIXELS_PER_SCANLINE: u16 = 341;
pub(crate) const VBLANK_START_SCANLINE: u16 = 241;
pub(crate) const VBLANK_NMI_LATCH_PIXEL: u16 = 2;
pub(crate) const FIRST_VISIBLE_SCANLINE: u16 = 0;
pub(crate) const LAST_VISIBLE_SCANLINE_PLUS_ONE: u16 = 240;
pub(crate) const NTSC_PRERENDER_SCANLINE: u16 = 261;
pub(crate) const PAL_PRERENDER_SCANLINE: u16 = 311;
pub(crate) const LAST_DOT: u16 = 340;
pub(crate) const FIRST_DOT: u16 = 0;
const ODD_FRAME_SKIP_DOT: u16 = 339;
pub(crate) const FIRST_VISIBLE_PIXEL: u16 = 1;
pub(crate) const LAST_VISIBLE_PIXEL: u16 = 256;
pub(crate) const FINE_Y_INCREMENT_PIXEL: u16 = 256;
pub(crate) const HORIZONTAL_BITS_COPY_PIXEL: u16 = 257;
pub(crate) const SPRITE_TILE_LOAD_START: u16 = 257;
pub(crate) const SPRITE_TILE_LOAD_END: u16 = 320;
pub(crate) const BG_PREFETCH_START: u16 = 321;
pub(crate) const BG_PREFETCH_END: u16 = 336;
pub(crate) const BG_PREFETCH_SHIFT_START: u16 = 329;
#[cfg(test)]
const RENDERING_CYCLE_START: u16 = 328;
#[cfg(test)]
const RENDERING_CYCLE_END: u16 = 336;
pub(crate) const DUMMY_NT_FETCH_1: u16 = 337;
pub(crate) const DUMMY_NT_FETCH_2: u16 = 339;
pub(crate) const VERTICAL_BITS_COPY_START: u16 = 280;
pub(crate) const VERTICAL_BITS_COPY_END: u16 = 304;
pub struct Timing {
total_cycles: u64,
tv_system: TimingMode,
scanline: u16,
pixel: u16,
frame_count: u64,
rendering_enabled_d1: bool,
rendering_enabled_d2: bool,
}
impl Timing {
pub fn new(tv_system: TimingMode) -> Self {
Self {
total_cycles: 0,
tv_system,
scanline: 0,
pixel: 0,
frame_count: 0,
rendering_enabled_d1: false,
rendering_enabled_d2: false,
}
}
pub fn reset(&mut self) {
self.total_cycles = 0;
self.scanline = 0;
self.pixel = 0;
self.frame_count = 0;
self.rendering_enabled_d1 = false;
self.rendering_enabled_d2 = false;
}
fn rendering_enabled_for_odd_frame_skip(&mut self, rendering_enabled: bool) -> bool {
let rendering_enabled_for_odd_skip = self.rendering_enabled_d2;
self.rendering_enabled_d2 = self.rendering_enabled_d1;
self.rendering_enabled_d1 = rendering_enabled;
rendering_enabled_for_odd_skip
}
pub fn tick(&mut self, rendering_enabled: bool) -> bool {
self.total_cycles += 1;
let rendering_enabled_for_odd_skip =
self.rendering_enabled_for_odd_frame_skip(rendering_enabled);
let should_skip_odd_frame = self.tv_system == TimingMode::Ntsc
&& (self.frame_count & 1) == 1 && rendering_enabled_for_odd_skip
&& self.scanline == NTSC_PRERENDER_SCANLINE
&& self.pixel == ODD_FRAME_SKIP_DOT;
if should_skip_odd_frame {
self.pixel = FIRST_DOT;
self.scanline = FIRST_VISIBLE_SCANLINE;
self.frame_count += 1;
true
} else {
self.pixel += 1;
if self.pixel >= PIXELS_PER_SCANLINE {
self.pixel = 0;
self.scanline += 1;
let scanlines_per_frame = self.tv_system.scanlines_per_frame();
if self.scanline >= scanlines_per_frame {
self.scanline = 0;
self.frame_count += 1;
}
}
false
}
}
pub fn total_cycles(&self) -> u64 {
self.total_cycles
}
pub fn scanline(&self) -> u16 {
self.scanline
}
pub fn pixel(&self) -> u16 {
self.pixel
}
pub fn frame_count(&self) -> u64 {
self.frame_count
}
pub fn restore_state(
&mut self,
scanline: u16,
pixel: u16,
total_cycles: u64,
frame_count: u64,
) {
self.scanline = scanline;
self.pixel = pixel;
self.total_cycles = total_cycles;
self.frame_count = frame_count;
self.rendering_enabled_d1 = false;
self.rendering_enabled_d2 = false;
}
pub fn rendering_enabled_delays(&self) -> (bool, bool) {
(self.rendering_enabled_d1, self.rendering_enabled_d2)
}
pub fn set_rendering_enabled_delays(&mut self, d1: bool, d2: bool) {
self.rendering_enabled_d1 = d1;
self.rendering_enabled_d2 = d2;
}
pub fn tv_system(&self) -> TimingMode {
self.tv_system
}
#[cfg(test)]
pub fn is_rendering_cycle(&self) -> bool {
let is_visible_scanline = self.scanline < LAST_VISIBLE_SCANLINE_PLUS_ONE;
let is_prerender_scanline = match self.tv_system {
TimingMode::Ntsc => self.scanline == NTSC_PRERENDER_SCANLINE,
TimingMode::Pal | TimingMode::Dendy => self.scanline == PAL_PRERENDER_SCANLINE,
TimingMode::MultiRegion | TimingMode::Unknown(_) => {
self.scanline == NTSC_PRERENDER_SCANLINE
}
};
if is_visible_scanline || is_prerender_scanline {
self.pixel <= LAST_VISIBLE_PIXEL
|| (self.pixel >= RENDERING_CYCLE_START && self.pixel <= RENDERING_CYCLE_END)
} else {
false
}
}
#[cfg(test)]
pub fn is_visible_pixel(&self) -> bool {
self.scanline < LAST_VISIBLE_SCANLINE_PLUS_ONE
&& self.pixel >= FIRST_VISIBLE_PIXEL
&& self.pixel <= LAST_VISIBLE_PIXEL
}
}
#[cfg(test)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TimingDebugState {
pub total_cycles: u64,
pub tv_system: TimingMode,
pub scanline: u16,
pub pixel: u16,
pub frame_count: u64,
pub rendering_enabled_d1: bool,
pub rendering_enabled_d2: bool,
}
#[cfg(test)]
impl Timing {
pub fn debug_state(&self) -> TimingDebugState {
TimingDebugState {
total_cycles: self.total_cycles,
tv_system: self.tv_system,
scanline: self.scanline,
pixel: self.pixel,
frame_count: self.frame_count,
rendering_enabled_d1: self.rendering_enabled_d1,
rendering_enabled_d2: self.rendering_enabled_d2,
}
}
pub fn set_debug_state(&mut self, state: TimingDebugState) {
self.total_cycles = state.total_cycles;
self.tv_system = state.tv_system;
self.scanline = state.scanline;
self.pixel = state.pixel;
self.frame_count = state.frame_count;
self.rendering_enabled_d1 = state.rendering_enabled_d1;
self.rendering_enabled_d2 = state.rendering_enabled_d2;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_timing_new() {
let timing = Timing::new(TimingMode::Ntsc);
assert_eq!(timing.scanline(), 0);
assert_eq!(timing.pixel(), 0);
assert_eq!(timing.total_cycles(), 0);
assert_eq!(timing.frame_count(), 0);
}
#[test]
fn test_timing_reset() {
let mut timing = Timing::new(TimingMode::Ntsc);
timing.tick(false);
timing.reset();
assert_eq!(timing.scanline(), 0);
assert_eq!(timing.pixel(), 0);
assert_eq!(timing.total_cycles(), 0);
}
#[test]
fn test_timing_tick_increments_pixel() {
let mut timing = Timing::new(TimingMode::Ntsc);
timing.tick(false);
assert_eq!(timing.pixel(), 1);
assert_eq!(timing.total_cycles(), 1);
}
#[test]
fn test_timing_scanline_wraps() {
let mut timing = Timing::new(TimingMode::Ntsc);
for _ in 0..PIXELS_PER_SCANLINE {
timing.tick(false);
}
assert_eq!(timing.scanline(), 1);
assert_eq!(timing.pixel(), 0);
}
#[test]
fn test_timing_frame_wraps() {
let mut timing = Timing::new(TimingMode::Ntsc);
for _ in 0..(262 * (PIXELS_PER_SCANLINE as usize)) {
timing.tick(false);
}
assert_eq!(timing.scanline(), 0);
assert_eq!(timing.pixel(), 0);
assert_eq!(timing.frame_count(), 1);
}
#[test]
fn test_timing_odd_frame_skip() {
let mut timing = Timing::new(TimingMode::Ntsc);
for _ in 0..(262 * (PIXELS_PER_SCANLINE as usize)) {
timing.tick(false);
}
assert_eq!(timing.frame_count(), 1);
for _ in 0..(261 * (PIXELS_PER_SCANLINE as usize) + 339) {
timing.tick(true);
}
assert_eq!(timing.scanline(), 261);
assert_eq!(timing.pixel(), 339);
let skipped = timing.tick(true);
assert!(skipped);
assert_eq!(timing.scanline(), 0);
assert_eq!(timing.pixel(), 0);
}
#[test]
fn test_is_rendering_cycle() {
let mut timing = Timing::new(TimingMode::Ntsc);
timing.scanline = FIRST_VISIBLE_SCANLINE;
timing.pixel = 100;
assert!(timing.is_rendering_cycle());
timing.scanline = VBLANK_START_SCANLINE;
timing.pixel = 100;
assert!(!timing.is_rendering_cycle());
timing.scanline = NTSC_PRERENDER_SCANLINE;
timing.pixel = 100;
assert!(timing.is_rendering_cycle());
}
#[test]
fn test_is_rendering_cycle_pal_prerender_scanline() {
let mut timing = Timing::new(TimingMode::Pal);
timing.scanline = PAL_PRERENDER_SCANLINE;
timing.pixel = 100;
assert!(timing.is_rendering_cycle());
timing.scanline = NTSC_PRERENDER_SCANLINE;
timing.pixel = 100;
assert!(!timing.is_rendering_cycle());
}
#[test]
fn test_is_visible_pixel() {
let mut timing = Timing::new(TimingMode::Ntsc);
timing.scanline = 100;
timing.pixel = 100;
assert!(timing.is_visible_pixel());
timing.pixel = FIRST_DOT;
assert!(!timing.is_visible_pixel());
timing.scanline = VBLANK_START_SCANLINE;
timing.pixel = 100;
assert!(!timing.is_visible_pixel());
}
#[test]
fn test_ntsc_scanline_count() {
let mut timing = Timing::new(TimingMode::Ntsc);
timing.scanline = NTSC_PRERENDER_SCANLINE;
timing.pixel = LAST_DOT;
timing.tick(false); assert_eq!(
timing.scanline(),
FIRST_VISIBLE_SCANLINE,
"NTSC should wrap from scanline 261 to 0"
);
assert_eq!(timing.frame_count(), 1, "Frame count should increment");
}
#[test]
fn test_pal_scanline_count() {
let mut timing = Timing::new(TimingMode::Pal);
timing.scanline = PAL_PRERENDER_SCANLINE;
timing.pixel = LAST_DOT;
timing.tick(false); assert_eq!(
timing.scanline(),
FIRST_VISIBLE_SCANLINE,
"PAL should wrap from scanline 311 to 0"
);
assert_eq!(timing.frame_count(), 1, "Frame count should increment");
}
#[test]
fn test_dots_per_scanline() {
let _timing = Timing::new(TimingMode::Ntsc);
assert_eq!(
PIXELS_PER_SCANLINE, 341,
"Should have 341 pixels per scanline"
);
}
#[test]
fn test_ntsc_odd_frame_skip() {
let mut timing = Timing::new(TimingMode::Ntsc);
timing.frame_count = 1; timing.scanline = NTSC_PRERENDER_SCANLINE;
timing.pixel = ODD_FRAME_SKIP_DOT;
timing.rendering_enabled_d2 = true;
let skipped = timing.tick(true); assert!(
skipped,
"Should skip dot 340 on odd NTSC frame with rendering enabled"
);
assert_eq!(
timing.scanline(),
FIRST_VISIBLE_SCANLINE,
"Should jump to scanline 0"
);
assert_eq!(timing.pixel(), FIRST_DOT, "Should jump to pixel 0");
assert_eq!(timing.frame_count(), 2, "Frame count should increment");
}
#[test]
fn test_ntsc_even_frame_no_skip() {
let mut timing = Timing::new(TimingMode::Ntsc);
timing.frame_count = 0; timing.scanline = NTSC_PRERENDER_SCANLINE;
timing.pixel = ODD_FRAME_SKIP_DOT;
timing.rendering_enabled_d2 = true;
let skipped = timing.tick(true);
assert!(!skipped, "Should not skip on even NTSC frame");
assert_eq!(timing.pixel(), LAST_DOT, "Should advance to pixel 340");
}
#[test]
fn test_pal_no_frame_skip() {
let mut timing = Timing::new(TimingMode::Pal);
timing.frame_count = 1; timing.scanline = PAL_PRERENDER_SCANLINE;
timing.pixel = ODD_FRAME_SKIP_DOT;
let skipped = timing.tick(true);
assert!(!skipped, "PAL should never skip frames");
assert_eq!(timing.pixel(), LAST_DOT, "Should advance to pixel 340");
}
#[test]
fn test_ntsc_cycles_per_frame() {
let mut timing = Timing::new(TimingMode::Ntsc);
let start_cycles = timing.total_cycles();
for _ in 0..262 {
for _ in 0..PIXELS_PER_SCANLINE {
timing.tick(false);
}
}
let cycles_elapsed = timing.total_cycles() - start_cycles;
assert_eq!(
cycles_elapsed, 89342,
"NTSC even frame should have 89342 cycles (262 * 341)"
);
assert_eq!(timing.scanline(), 0, "Should wrap back to scanline 0");
assert_eq!(timing.pixel(), 0, "Should wrap back to pixel 0");
assert_eq!(timing.frame_count(), 1, "Frame count should be 1");
}
#[test]
fn test_ntsc_odd_frame_cycles() {
let mut timing = Timing::new(TimingMode::Ntsc);
timing.frame_count = 1;
let start_cycles = timing.total_cycles();
for scanline in 0..262 {
let dots = if scanline == NTSC_PRERENDER_SCANLINE {
LAST_DOT
} else {
PIXELS_PER_SCANLINE
};
for _ in 0..dots {
timing.tick(true);
}
}
let cycles_elapsed = timing.total_cycles() - start_cycles;
assert_eq!(
cycles_elapsed, 89341,
"NTSC odd frame with rendering should have 89341 cycles (89342 - 1)"
);
assert_eq!(
timing.scanline(),
FIRST_VISIBLE_SCANLINE,
"Should wrap back to scanline 0"
);
assert_eq!(timing.pixel(), FIRST_DOT, "Should wrap back to pixel 0");
assert_eq!(timing.frame_count(), 2, "Frame count should be 2");
}
#[test]
fn test_pal_cycles_per_frame() {
let mut timing = Timing::new(TimingMode::Pal);
let start_cycles = timing.total_cycles();
for _ in 0..312 {
for _ in 0..PIXELS_PER_SCANLINE {
timing.tick(false);
}
}
let cycles_elapsed = timing.total_cycles() - start_cycles;
assert_eq!(
cycles_elapsed, 106392,
"PAL frame should have 106392 cycles (312 * 341)"
);
assert_eq!(timing.scanline(), 0, "Should wrap back to scanline 0");
assert_eq!(timing.pixel(), 0, "Should wrap back to pixel 0");
assert_eq!(timing.frame_count(), 1, "Frame count should be 1");
}
#[test]
fn test_frame_counter_wraparound() {
let mut timing = Timing::new(TimingMode::Ntsc);
assert_eq!(timing.frame_count(), 0);
for _ in 0..(262 * (PIXELS_PER_SCANLINE as usize)) {
timing.tick(false);
}
assert_eq!(timing.frame_count(), 1);
for _ in 0..(262 * (PIXELS_PER_SCANLINE as usize)) {
timing.tick(false);
}
assert_eq!(timing.frame_count(), 2);
timing.frame_count = u64::MAX - 1;
for _ in 0..(262 * (PIXELS_PER_SCANLINE as usize)) {
timing.tick(false);
}
assert_eq!(timing.frame_count(), u64::MAX);
}
}