cge_nes 0.1.2

Cycle-accurate NES (Nintendo Entertainment System) emulator library: CPU, PPU, cartridge, input, and iNES ROM loading.
Documentation
//! PPU scanline position tracking and timing.
//!
//! This module handles the PPU's scanline-based timing system, including:
//! * Tracking the current scanline and cycle position
//! * Managing odd/even frame timing
//! * Determining the PPU's current state (rendering, vblank, etc)
//! * Generating frame events for synchronization

use crate::ppu::{FrameEvent, VramReadEvent, SCREEN_HEIGHT};

const PPU_CYCLE_COUNT_PER_SCANLINE: u16 = 341;
const VISIBLE_SCANLINE_COUNT: u16 = SCREEN_HEIGHT;
const VBLANK_SCANLINE_COUNT: u16 = 20;
const TOTAL_SCANLINES: u16 = VISIBLE_SCANLINE_COUNT + VBLANK_SCANLINE_COUNT + 2;
const LAST_RENDER_SCANLINE: u16 = VISIBLE_SCANLINE_COUNT - 1;
const POST_RENDER_SCANLINE: u16 = VISIBLE_SCANLINE_COUNT;
const FIRST_VBLANK_SCANLINE: u16 = VISIBLE_SCANLINE_COUNT + 1;
const LAST_VBLANK_SCANLINE: u16 = VISIBLE_SCANLINE_COUNT + VBLANK_SCANLINE_COUNT;
const PRE_RENDER_SCANLINE: u16 = LAST_VBLANK_SCANLINE + 1;

/// Represents the PPU's position within the current frame.
///
/// Tracks both the current scanline number and the cycle count within that scanline.
/// Also handles odd/even frame timing differences.
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
pub struct ScanlinePos {
    current_scanline: u16,
    cycles_in_current_scanline: u16,
    odd_frame: bool,
}

/// The current operational state of the PPU.
///
/// Represents the different phases of frame rendering that determine
/// what operations the PPU performs.
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
pub enum PpuState {
    /// Last scanline of the frame (261), prepares for next frame
    PreRenderScanline,
    /// Visible scanlines (0-239), actively rendering pixels
    Rendering,
    /// First non-visible scanline (240), post-render idle
    PostRenderScanline,
    /// VBlank period (scanlines 241-260)
    VBlank,
}

impl ScanlinePos {
    /// Creates a new ScanlinePos at the start of a frame.
    ///
    /// Initializes position to the pre-render scanline (261) at cycle 0,
    /// starting with an odd frame.
    pub fn start_frame() -> Self {
        Self {
            current_scanline: TOTAL_SCANLINES - 1,
            cycles_in_current_scanline: 0,
            odd_frame: true,
        }
    }

    /// Returns the current scanline number (0-261).
    pub fn current_scanline(&self) -> u16 {
        self.current_scanline
    }

    /// Returns the current cycle position within the scanline (0-340).
    pub fn pos_at_scanline(&self) -> u16 {
        self.cycles_in_current_scanline
    }

    /// Returns the current operational state of the PPU.
    pub fn current_state(&self) -> PpuState {
        match self.current_scanline {
            0..=LAST_RENDER_SCANLINE => PpuState::Rendering,
            POST_RENDER_SCANLINE => PpuState::PostRenderScanline,
            FIRST_VBLANK_SCANLINE..=LAST_VBLANK_SCANLINE => PpuState::VBlank,
            PRE_RENDER_SCANLINE => PpuState::PreRenderScanline,
            TOTAL_SCANLINES..=u16::MAX => unreachable!(),
        }
    }

    /// Returns true if this is the first scanline of VBlank (241).
    pub fn first_vblank_scanline(&self) -> bool {
        self.current_scanline == FIRST_VBLANK_SCANLINE
    }

    /// Advances the PPU position by one cycle.
    ///
    /// Handles wrapping between scanlines and frames, including the
    /// special case of skipping the last cycle on odd frames.
    pub fn advance_cycle(&mut self) {
        self.cycles_in_current_scanline += 1;

        if (self.odd_frame)
            && (self.current_scanline == PRE_RENDER_SCANLINE)
            && (self.cycles_in_current_scanline == (PPU_CYCLE_COUNT_PER_SCANLINE - 1))
        {
            // Second to last cycle in prerender scanline during odd frames
            // Skip last cycle
            self.cycles_in_current_scanline += 1;
        }

        if self.cycles_in_current_scanline == PPU_CYCLE_COUNT_PER_SCANLINE {
            self.cycles_in_current_scanline = 0;
            self.current_scanline += 1;

            if self.current_scanline == TOTAL_SCANLINES {
                self.current_scanline = 0;
                self.odd_frame = !self.odd_frame;
            }
        }
    }

    /// Returns the frame event that occurs at the current position, if any.
    ///
    /// Frame events are used to synchronize PPU operations like frame
    /// presentation and vblank handling.
    pub fn current_frame_event(&self) -> FrameEvent {
        if self.odd_frame
            && (self.current_scanline == PRE_RENDER_SCANLINE)
            && (self.cycles_in_current_scanline == (PPU_CYCLE_COUNT_PER_SCANLINE - 2))
        {
            return FrameEvent::EndOfScanline;
        }

        if self.pos_at_scanline() == (PPU_CYCLE_COUNT_PER_SCANLINE - 1) {
            if self.current_scanline == LAST_RENDER_SCANLINE {
                return FrameEvent::ReadyToPresent;
            } else if self.current_scanline == LAST_VBLANK_SCANLINE {
                return FrameEvent::EndOfFrame;
            }

            return FrameEvent::EndOfScanline;
        }

        FrameEvent::None
    }

    /// Returns the VRAM fetch event for the current cycle, if any.
    ///
    /// Returns background tile-fetch events during visible dots 1..=256 and
    /// prefetch dots 321..=336, sprite-fetch events during the second-half garbage
    /// fetch on cycles 257..=320 (eight sprites, eight cycles each, with the
    /// pattern-table read happening on cycles 4 and 7 of each sprite's window),
    /// and [`VramReadEvent::None`] otherwise.
    pub fn current_vram_read_event(&self) -> VramReadEvent {
        let cycle_index = self.pos_at_scanline();
        if ((cycle_index >= 1) && (cycle_index <= 256))
            || ((cycle_index >= 321) && (cycle_index <= 336))
        {
            return match cycle_index % 8 {
                1 => VramReadEvent::TileAddrSet,
                2 => VramReadEvent::TileDataRead,
                3 => VramReadEvent::AttrAddrSet,
                4 => VramReadEvent::AttrDataRead,
                5 => VramReadEvent::BackgroundLowAddrSet,
                6 => VramReadEvent::BackgroundLowDataRead,
                7 => VramReadEvent::BackgroundHighAddrSet,
                0 => VramReadEvent::BackgroundHighDataRead,
                _ => unreachable!(),
            };
        } else if (cycle_index >= 257) && (cycle_index <= 320) {
            let sprite_fetch_cycle = cycle_index - 257;
            let sprite_index = (sprite_fetch_cycle / 8) as u8;
            let cycle_in_current_sprite = sprite_fetch_cycle % 8;

            return match cycle_in_current_sprite {
                0 | 1 | 2 | 3 => VramReadEvent::None,
                4 => VramReadEvent::SpriteAddrSet {
                    sprite_index,
                    is_high_byte: false,
                },
                5 => VramReadEvent::SpriteDataRead {
                    sprite_index,
                    is_high_byte: false,
                },
                6 => VramReadEvent::SpriteAddrSet {
                    sprite_index,
                    is_high_byte: true,
                },
                7 => VramReadEvent::SpriteDataRead {
                    sprite_index,
                    is_high_byte: true,
                },
                _ => unreachable!(),
            };
        }

        VramReadEvent::None
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn evenframe_render_scanlines_correct_cycle_count() {
        let mut scanline_pos = ScanlinePos::start_frame();
        scanline_pos.odd_frame = false;

        for _ in 0..PPU_CYCLE_COUNT_PER_SCANLINE {
            assert_eq!(scanline_pos.current_state(), PpuState::PreRenderScanline);

            scanline_pos.advance_cycle();
        }

        let render_cycles = (PPU_CYCLE_COUNT_PER_SCANLINE as u32) * (VISIBLE_SCANLINE_COUNT as u32);

        for _ in 0..render_cycles {
            assert_eq!(scanline_pos.current_state(), PpuState::Rendering);
            assert!(scanline_pos.odd_frame);

            scanline_pos.advance_cycle();
        }

        for _ in 0..PPU_CYCLE_COUNT_PER_SCANLINE {
            assert_eq!(scanline_pos.current_state(), PpuState::PostRenderScanline);
            assert!(scanline_pos.odd_frame);

            scanline_pos.advance_cycle();
        }

        let vblank_cycles = (PPU_CYCLE_COUNT_PER_SCANLINE as u32) * (VBLANK_SCANLINE_COUNT as u32);

        for _ in 0..vblank_cycles {
            assert_eq!(scanline_pos.current_state(), PpuState::VBlank);
            assert!(scanline_pos.odd_frame);

            scanline_pos.advance_cycle();
        }

        assert_eq!(scanline_pos.current_state(), PpuState::PreRenderScanline);
        assert_eq!(scanline_pos.cycles_in_current_scanline, 0);
        assert!(scanline_pos.odd_frame);
    }

    #[test]
    fn oddframe_render_scanlines_correct_cycle_count() {
        let mut scanline_pos = ScanlinePos::start_frame();
        scanline_pos.odd_frame = true;

        for _ in 0..(PPU_CYCLE_COUNT_PER_SCANLINE - 1) {
            assert_eq!(scanline_pos.current_state(), PpuState::PreRenderScanline);
            assert!(scanline_pos.odd_frame);

            scanline_pos.advance_cycle();
        }

        let render_cycles = (PPU_CYCLE_COUNT_PER_SCANLINE as u32) * (VISIBLE_SCANLINE_COUNT as u32);

        for _ in 0..render_cycles {
            assert_eq!(scanline_pos.current_state(), PpuState::Rendering);
            assert!(!scanline_pos.odd_frame);

            scanline_pos.advance_cycle();
        }

        for _ in 0..PPU_CYCLE_COUNT_PER_SCANLINE {
            assert_eq!(scanline_pos.current_state(), PpuState::PostRenderScanline);
            assert!(!scanline_pos.odd_frame);

            scanline_pos.advance_cycle();
        }

        let vblank_cycles = (PPU_CYCLE_COUNT_PER_SCANLINE as u32) * (VBLANK_SCANLINE_COUNT as u32);

        for _ in 0..vblank_cycles {
            assert_eq!(scanline_pos.current_state(), PpuState::VBlank);
            assert!(!scanline_pos.odd_frame);

            scanline_pos.advance_cycle();
        }

        assert_eq!(scanline_pos.current_state(), PpuState::PreRenderScanline);
        assert_eq!(scanline_pos.cycles_in_current_scanline, 0);
        assert!(!scanline_pos.odd_frame);
    }

    #[test]
    fn sprite_fetch_events_timing() {
        // Cycles 257-320 host the second-half garbage nametable reads (cycles
        // 0-3 of each sprite's window) and the actual sprite pattern reads
        // (cycles 4-7). Verify that `current_vram_read_event` returns the
        // expected events for the first two sprites.
        let mut scanline_pos = ScanlinePos::start_frame();
        scanline_pos.odd_frame = false;
        // Advance to cycle 257 of the first visible scanline.
        for _ in 0..(PPU_CYCLE_COUNT_PER_SCANLINE + 257) {
            scanline_pos.advance_cycle();
        }

        // Cycles 257-260 of the first sprite window: garbage nametable reads.
        for _ in 0..4 {
            assert_eq!(scanline_pos.current_vram_read_event(), VramReadEvent::None);
            scanline_pos.advance_cycle();
        }

        // Cycles 261-264 of sprite 0: addr/data reads for both bitplanes.
        assert_eq!(
            scanline_pos.current_vram_read_event(),
            VramReadEvent::SpriteAddrSet {
                sprite_index: 0,
                is_high_byte: false
            }
        );
        scanline_pos.advance_cycle();
        assert_eq!(
            scanline_pos.current_vram_read_event(),
            VramReadEvent::SpriteDataRead {
                sprite_index: 0,
                is_high_byte: false
            }
        );
        scanline_pos.advance_cycle();
        assert_eq!(
            scanline_pos.current_vram_read_event(),
            VramReadEvent::SpriteAddrSet {
                sprite_index: 0,
                is_high_byte: true
            }
        );
        scanline_pos.advance_cycle();
        assert_eq!(
            scanline_pos.current_vram_read_event(),
            VramReadEvent::SpriteDataRead {
                sprite_index: 0,
                is_high_byte: true
            }
        );
        scanline_pos.advance_cycle();

        // Cycles 265-268 of sprite 1: garbage nametable reads again.
        for _ in 0..4 {
            assert_eq!(scanline_pos.current_vram_read_event(), VramReadEvent::None);
            scanline_pos.advance_cycle();
        }

        // Cycles 269-272 of sprite 1: addr/data reads for both bitplanes.
        assert_eq!(
            scanline_pos.current_vram_read_event(),
            VramReadEvent::SpriteAddrSet {
                sprite_index: 1,
                is_high_byte: false
            }
        );
        scanline_pos.advance_cycle();
        assert_eq!(
            scanline_pos.current_vram_read_event(),
            VramReadEvent::SpriteDataRead {
                sprite_index: 1,
                is_high_byte: false
            }
        );
        scanline_pos.advance_cycle();
        assert_eq!(
            scanline_pos.current_vram_read_event(),
            VramReadEvent::SpriteAddrSet {
                sprite_index: 1,
                is_high_byte: true
            }
        );
        scanline_pos.advance_cycle();
        assert_eq!(
            scanline_pos.current_vram_read_event(),
            VramReadEvent::SpriteDataRead {
                sprite_index: 1,
                is_high_byte: true
            }
        );
    }
}