332. Ppu logical nametable to framebuffer

Render one selected logical PPU nametable as a framebuffer.

Lesson 332 of 356 · tests/chapter_13_scrolling/test_332_ppu_logical_nametable_to_framebuffer.py

File to update

emulator/rendering/ppu_background_renderer.py

Reference

https://www.nesdev.org/wiki/PPU_nametables

Why this step exists

The original helper always rendered logical nametable $2000. Horizontal scrolling also needs the adjacent logical nametable, so the helper now accepts one of:

$2000, $2400, $2800, $2C00

Memory layout relative to the selected base

base + $000-$3BF: 960 visible tile IDs
base + $3C0-$3FF: 64 attribute bytes

Compatibility

Omitting base_nametable_addr must still render $2000. Existing callers therefore do not need to change.

Mirroring boundary

Rendering requests logical addresses through PpuBus. PpuBus remains responsible for mapping those addresses to horizontally or vertically mirrored physical VRAM.

Out of scope

  • selecting and composing two adjacent framebuffers
  • opacity-mask address selection
  • Console integration
  • vertical viewport composition
  • pygame

Example implementation

# emulator/rendering/ppu_background_renderer.py

# --- NEW LINES: ACCEPTED LOGICAL NAMETABLE BASE ADDRESSES ---
LOGICAL_NAMETABLE_BASE_ADDRS = (
    0x2000,
    0x2400,
    0x2800,
    0x2C00,
)


def ppu_background_to_framebuffer(
    ppu: PPU,
    # --- NEW LINE: OPTIONAL LOGICAL NAMETABLE SELECTION ---
    base_nametable_addr: int = BASE_NAMETABLE_ADDR,
) -> Framebuffer:
    # --- NEW BLOCK: REJECT NON-NAMETABLE BASE ADDRESSES ---
    if base_nametable_addr not in LOGICAL_NAMETABLE_BASE_ADDRS:
        raise ValueError(
            "Logical nametable base address must be $2000, $2400, $2800, $2C00"
        )

    nametable_bytes = bytes(
        # --- UPDATED LINE: READ FROM THE SELECTED NAMETABLE ---
        ppu.ppu_bus.read(base_nametable_addr + offset)
        for offset in range(NAMETABLE_SIZE)
    )

    # --- NEW LINE: DERIVE THE SELECTED ATTRIBUTE-TABLE BASE ---
    attribute_table_base = base_nametable_addr + NAMETABLE_SIZE
    attribute_table = bytes(
        # --- UPDATED LINE: READ THE SELECTED ATTRIBUTE TABLE ---
        ppu.ppu_bus.read(attribute_table_base + offset)
        for offset in range(ATTR_TABLE_SIZE)
    )

   ... 
   # Everything remains the same below this point
   

Run this lesson

uv run pytest tests/chapter_13_scrolling/test_332_ppu_logical_nametable_to_framebuffer.py -v