259. Ppu timing counters

Implement basic PPU timing counters.

Lesson 259 of 356 · tests/chapter_04_ppu_timing_and_vblank/test_259_ppu_timing_counters.py

Reference

https://www.nesdev.org/wiki/PPU_rendering#Line-by-line_timing

File to update

emulator/ppu/ppu.py

Constants to add

PPU_CYCLES_PER_SCANLINE = 341
PPU_SCANLINES_PER_FRAME = 262

State to add

cycle: int = 0
scanline: int = 0
frame: int = 0

Method to add

PPU.step(cycles: int = 1) -> None

Why this step exists

The PPU is a time-based device. Later, VBlank, NMI, rendering, and frame pacing will depend on knowing where the PPU is inside the current frame.

For this step, only add counters

cycle    -> position inside the current scanline
scanline -> current scanline inside the frame
frame    -> completed frame count

Initial timing model

341 PPU cycles per scanline
262 scanlines per frame

Suggested implementation example

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

Future compatibility

These tests intentionally check only counter behavior. They do not require exact VBlank/NMI side effects yet. Later steps may add side effects inside PPU.step(), but these counter invariants should remain true.

Out of scope

  • VBlank generation
  • NMI request
  • rendering
  • sprite 0 hit
  • sprite overflow
  • odd-frame cycle skip

Run this lesson

uv run pytest tests/chapter_04_ppu_timing_and_vblank/test_259_ppu_timing_counters.py -v