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.pyWhy 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 314b6e46e814f955b52ac954f67dab849582fe77This 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.pyExpected 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