260. Ppu vblank generation from timing
Implement VBlank generation from PPU timing.
Lesson 260 of 356 · tests/chapter_04_ppu_timing_and_vblank/test_260_ppu_vblank_generation_from_timing.py
Reference
https://www.nesdev.org/wiki/PPU_rendering#Line-by-line_timing
https://www.nesdev.org/wiki/PPU_registers#PPUSTATUSFile to update
emulator/ppu/ppu.pyConstants to add
PPU_VBLANK_START_SCANLINE = 241
PPU_PRE_RENDER_SCANLINE = 261Why this step exists
Many NES programs wait for VBlank before writing PPU memory. They commonly poll PPUSTATUS ($2002) until bit 7 becomes set.
Conceptual behavior for this tutorial step
when the PPU enters scanline 241:
set PPUSTATUS bit 7, VBLANK_STARTED
when the PPU enters scanline 261, the pre-render scanline:
clear PPUSTATUS bit 7This gives ROMs a basic frame signal without implementing full rendering yet.
Suggested implementation example
PPU_VBLANK_START_SCANLINE = 241
PPU_PRE_RENDER_SCANLINE = 261
def step(self, cycles: int = 1) -> None:
for _ in range(cycles):
self.cycle += 1
if self.cycle >= PPU_CYCLES_PER_SCANLINE:
self.cycle = 0
self.scanline += 1
if self.scanline >= PPU_SCANLINES_PER_FRAME:
self.scanline = 0
self.frame += 1
# Simplified scanline-level timing event.
if self.scanline == PPU_VBLANK_START_SCANLINE:
self.status |= VBLANK_STARTED
if self.scanline == PPU_PRE_RENDER_SCANLINE:
self.status &= ~VBLANK_STARTEDImportant simplification
This is not dot-accurate PPU timing. On real hardware, status changes happen at specific PPU dots/cycles, commonly modeled around dot/cycle 1 of the relevant scanline. For now, this tutorial uses scanline-entry behavior because the goal is to teach the frame/VBlank concept before full cycle accuracy.
Out of tutorial objectives for now
- dot/cycle-accurate VBlank timing
- odd-frame cycle skip
- rendering pixels during visible scanlines
- sprite 0 hit
- sprite overflow
- NMI generation/CPU interrupt handling
Run this lesson
uv run pytest tests/chapter_04_ppu_timing_and_vblank/test_260_ppu_vblank_generation_from_timing.py -v