268. Console step advances ppu
Make Console.step() advance PPU time from CPU cycles.
Lesson 268 of 356 · tests/chapter_04_ppu_timing_and_vblank/test_268_console_step_advances_ppu.py
References
https://www.nesdev.org/wiki/CPU
https://www.nesdev.org/wiki/PPU_rendering#Line-by-line_timingFile to update
emulator/console.pyWhy this step exists
The emulator now has the pieces needed to connect CPU time to PPU time
CPU.step() returns base CPU cycles
PPU.step(cycles) advances PPU timing counters
Console.consume_nmi_if_requested() connects PPU NMI requests to CPU NMIConsole.step() is where those pieces become one machine-level step.
What is machine-level stepping? Machine-level stepping means advancing multiple emulated chips together according to their clock relationship.
Minimal NES timing rule
1 CPU cycle = 3 PPU cyclesMinimal example
CPU executes NOP
NOP takes 2 CPU cycles
PPU advances 2 * 3 = 6 PPU cyclesCommon misconception
Do not make CPU.step() call PPU.step(). The CPU should not own video timing. Console owns the coordination between chips.
Suggested implementation example
@dataclass
class Console:
cpu: CPU
ppu: PPU
def consume_nmi_if_requested(self) -> None:
if not self.ppu.nmi_requested:
return
self.ppu.nmi_requested = False
self.cpu.interrupt_nmi()
def step(self) -> int:
# This is base timing only. Later steps can add branch penalties,
# NMI latency, DMA stalls, and other timing details.
cpu_cycles = self.cpu.step()
self.ppu.step(cpu_cycles * 3)
self.consume_nmi_if_requested()
return cpu_cyclesImportant limitation
This is still simplified timing. CPU.step() currently returns base cycles only.
Later improvements can account for
branch taken extra cycles
branch page-cross extra cycles
indexed addressing page-cross extra cycles
NMI latency
OAM DMA stallsOut of scope
- dynamic CPU cycle penalties
- dot-accurate NMI latency
- OAM DMA
- rendering pixels
- pygame/frontend loop
- controller input
Run this lesson
uv run pytest tests/chapter_04_ppu_timing_and_vblank/test_268_console_step_advances_ppu.py -v