287. Console step until next frame

Add Console.step_until_next_frame() for frame-level stepping.

Lesson 287 of 356 · tests/chapter_05_rendering_pipeline/test_287_console_step_until_next_frame.py

File to update

emulator/console.py

Why this step exists

Console already has a one-instruction stepping method

console.step()

That is the smallest machine-time operation in this emulator. It executes one CPU instruction, advances the PPU by CPU cycles * 3, then consumes any pending NMI.

Manual runners and future frontends usually need a larger operation

run emulation until one full PPU frame completes
then ask for a framebuffer explicitly

This step adds

console.step_until_next_frame(max_cpu_instructions: int | None = None) -> int

Example implementation

def step_until_next_frame(
    self,
    max_cpu_instructions: int | None = None,
) -> int:
    start_frame = self.ppu.frame
    executed = 0

    while self.ppu.frame == start_frame:
        if max_cpu_instructions is not None:
            if executed >= max_cpu_instructions:
                raise RuntimeError("Frame did not complete before instruction limit")

        self.step()
        executed += 1

    return executed

Difference between step() and step_until_next_frame():

step()
    executes exactly one CPU instruction
    advances PPU by that instruction's cycles * 3
    returns CPU cycles for that instruction

step_until_next_frame()
    calls step() repeatedly until ppu.frame changes
    returns how many CPU instructions were executed

Example usage

console.step_until_next_frame()
framebuffer = console.render_background_framebuffer()

Why max_cpu_instructions is optional: This parameter is not NES hardware behavior. It is an emulator debugging/testing guard.

With None, there is no artificial instruction limit. This is useful for real or manual execution:

console.step_until_next_frame()

With an integer, the helper raises if that many CPU instructions execute without a new frame. This is useful for tests and debugging because it prevents infinite loops if the CPU gets stuck, an opcode is missing, or a frame never completes:

console.step_until_next_frame(max_cpu_instructions=10)

Important separation

step_until_next_frame()
    advances emulation time

render_background_framebuffer()
    observes current PPU memory and returns Framebuffer data

Do not render automatically inside step_until_next_frame().

Out of scope

  • pygame display
  • sprites
  • OAMDMA
  • exact NMI latency
  • dynamic CPU cycle penalties
  • controller input

Run this lesson

uv run pytest tests/chapter_05_rendering_pipeline/test_287_console_step_until_next_frame.py -v