333. Ppu logical nametable to opaque mask

Build an opacity mask from one selected logical PPU nametable.

Lesson 333 of 356 · tests/chapter_13_scrolling/test_333_ppu_logical_nametable_to_opaque_mask.py

File to update

emulator/rendering/ppu_background_renderer.py

Reference

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

Why this step exists

The existing helper builds the mask used by sprite priority and sprite-zero-hit detection, but originally reads only logical nametable $2000. Horizontal scrolling also needs the mask belonging to the adjacent logical nametable.

Compatibility

Omitting base_nametable_addr still selects $2000, so existing Console and sprite-zero hit callers remain valid until viewport integration is performed deliberately.

Opacity inputs

  • 960 tile IDs from the selected logical nametable
  • the background pattern table selected by PPUCTRL bit 4

Opacity does not use attribute bytes, palette RAM, or final RGB colors.

Out of scope

  • composing the left and right masks
  • framebuffer rendering
  • Console integration
  • sprite-zero-hit viewport integration
  • vertical scrolling

Complete example implementation

# emulator/rendering/ppu_background_renderer.py

def ppu_background_to_opaque_mask(
    ppu: PPU,
    # --- NEW LINE: OPTIONAL LOGICAL NAMETABLE SELECTION ---
    base_nametable_addr: int = BASE_NAMETABLE_ADDR,
) -> BackgroundOpaqueMask:
    # --- 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)
    )

    ...
    # Everything remains the same below this point.

Run this lesson

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