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;
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
pub struct ScanlinePos {
current_scanline: u16,
cycles_in_current_scanline: u16,
odd_frame: bool,
}
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
pub enum PpuState {
PreRenderScanline,
Rendering,
PostRenderScanline,
VBlank,
}
impl ScanlinePos {
pub fn start_frame() -> Self {
Self {
current_scanline: TOTAL_SCANLINES - 1,
cycles_in_current_scanline: 0,
odd_frame: true,
}
}
pub fn current_scanline(&self) -> u16 {
self.current_scanline
}
pub fn pos_at_scanline(&self) -> u16 {
self.cycles_in_current_scanline
}
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!(),
}
}
pub fn first_vblank_scanline(&self) -> bool {
self.current_scanline == FIRST_VBLANK_SCANLINE
}
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))
{
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;
}
}
}
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
}
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() {
let mut scanline_pos = ScanlinePos::start_frame();
scanline_pos.odd_frame = false;
for _ in 0..(PPU_CYCLE_COUNT_PER_SCANLINE + 257) {
scanline_pos.advance_cycle();
}
for _ in 0..4 {
assert_eq!(scanline_pos.current_vram_read_event(), VramReadEvent::None);
scanline_pos.advance_cycle();
}
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();
for _ in 0..4 {
assert_eq!(scanline_pos.current_vram_read_event(), VramReadEvent::None);
scanline_pos.advance_cycle();
}
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
}
);
}
}