295. Manual main pygame background display

Add manual pygame background display to main_only_background.py.

Lesson 295 of 356 · tests/chapter_08_manual_main/test_295_manual_main_pygame_background_display.py

File to create/update on root folder

main_only_background.py

Why this step exists

core_validator.py proves that the emulator can boot a local ROM and step frames without pygame. main_only_background.py is the historical background-only visual manual runner: it should use pygame to display the background Framebuffer produced by the emulator after each frame.

Recommended workflow

Start by copying the working structure from core_validator.py, then add only the missing pygame/display pieces:

  • import pygame
  • import draw_framebuffer from tools.show_framebuffer
  • define SCALE
  • create an initial framebuffer for window dimensions
  • open a pygame window
  • process pygame.QUIT events
  • after each frame step, render the background framebuffer
  • draw the framebuffer and flip the display
  • call pygame.quit() in finally

Important boundary

pygame is allowed in main_only_background.py because it is a manual/frontend entry point. pygame must not be imported by emulator core modules.

Important legal/testing rule

The tutorial repository must not include commercial ROM files. Automated tests must not require MarioBros.nes or open a real pygame window.

Reference hash used during tutorial development [Mario Bros. (World).nes]:

MD5 5d7bcc400a2fb5fa27346da345d3bb62  MarioBros.nes
SHA1 314b6e46e814f955b52ac954f67dab849582fe77

This hash is only a manual reference. Tests must not require this file or this exact hash because users may have different legal dumps/revisions.

Suggested implementation example

from pathlib import Path

import pygame

from emulator.bus.cpu_bus import CpuBus
from emulator.cartridge.cartridge import Cartridge
from emulator.console import Console
from emulator.cpu.cpu import CPU
from tools.show_framebuffer import draw_framebuffer


ROM_PATH = Path("MarioBros.nes")
debug_mode = False
SCALE = 3


def main() -> None:
    if not ROM_PATH.exists():
        raise FileNotFoundError(
            "MarioBros.nes not found. Provide your own legal local copy. "
            "This file is intentionally not included in the tutorial repository."
        )

    cartridge = Cartridge.from_ines_bytes(ROM_PATH.read_bytes())

    cpu_bus = CpuBus(cartridge=cartridge)
    cpu = CPU(cpu_bus)
    console = Console(cpu=cpu, ppu=cpu_bus.ppu)

    cpu.reset()
    framebuffer = console.render_background_framebuffer()

    print(f"Loaded {ROM_PATH}")
    print(f"CPU reset PC = ${cpu.pc:04X}")
    print("Starting frame loop. Close the window or press Ctrl+C to stop.")

    pygame.init()
    try:
        window = pygame.display.set_mode(
            (framebuffer.width * SCALE, framebuffer.height * SCALE)
        )
        pygame.display.set_caption("NES Background")

        running = True
        while running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False

            executed = console.step_until_next_frame()

            framebuffer = console.render_background_framebuffer()
            draw_framebuffer(window, framebuffer, SCALE)
            pygame.display.flip()

            if debug_mode:
                print(
                    f"frame={console.ppu.frame} "
                    f"pc=${cpu.pc:04X} "
                    f"instructions={executed}"
                )
    except KeyboardInterrupt:
        print("

Stopped by user.")

    finally:
        pygame.quit()


if __name__ == "__main__":
    main()

Manual command

uv run python main_only_background.py

Expected manual behavior

main_only_background.py opens a pygame window and displays the current background framebuffer. The window may look incomplete because sprites are not implemented in this historical runner. Close the window or press Ctrl+C to stop.

Example visual expectation, roughly

+------------------------------+
|                              |
|          MARIO BROS.         |
|                              |
|        1 PLAYER GAME A       |
|        1 PLAYER GAME B       |
|        2 PLAYER GAME A       |
|        2 PLAYER GAME B       |
|                              |
|   background is shown        |
|   sprites are  missing       |
|                              |
+------------------------------+

After 30 seconds - 1 minute, you should also see a background/layout similar to the classic Mario Bros. 1983 stage. Sprites are still missing, but the background scene should make the emulator feel alive:

+------------------------------+
|  I-0000   TOP-0000  II-0000  |
|                              |
|  ====                  ====  |
|==                          ==|
|                              |
|        ──────────────        |
|─────                    ─────|
|                              |
|                              |
| ─────────── POW  ─────────── |
|====                      ====|
|------------------------------|
+------------------------------+

This is only an approximate ASCII sketch. The important manual signal is that the background/title/stage tiles appear and change over time. Missing moving characters/enemies are expected until sprite rendering is implemented.

Performance note

The manual pygame runner may feel slow right now. That is expected at this stage. The current draw_framebuffer helper is intentionally simple and draws many scaled rectangles from Python. Future optimization can replace it with a faster framebuffer upload path, but this step focuses on expected visual output and architecture boundaries, not speed.

Why this test does not call main()

main_only_background.py opens a real pygame window and runs a manual loop. Automated tests must stay finite and should inspect structure only.

Out of scope

  • fast framebuffer upload optimization
  • pygame keyboard/controller mapping
  • sprite rendering
  • verifying exact visual pixels from a commercial ROM
  • calling main() from pytest

Run this lesson

uv run pytest tests/chapter_08_manual_main/test_295_manual_main_pygame_background_display.py -v