335. Ppu background viewport to opaque mask

Compose the horizontal opacity-mask viewport from current PPU scroll state.

Lesson 335 of 356 · tests/chapter_13_scrolling/test_335_ppu_background_viewport_to_opaque_mask.py

File to update

emulator/rendering/ppu_background_renderer.py

References

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

Why this looks similar to Step 334

The opacity-mask adapter intentionally copies the framebuffer adapter's addressing structure:

decode the same viewport X
select the same horizontal logical pair
process the same left and right bases
compose using the same viewport X

The important difference is that every data-producing operation must use the opacity-mask path:

ppu_background_to_opaque_mask()
compose_horizontal_opaque_mask_viewport()

It must not accidentally call

ppu_background_to_framebuffer()
compose_horizontal_framebuffer_viewport()

This deliberate copy keeps both paths easy to read. Their parity tests protect the small duplicated addressing mechanism from drifting.

The resulting mask will later be shared by sprite priority and sprite-zero-hit detection. This step only constructs it; it does not integrate either consumer.

Out of scope

  • framebuffer composition
  • Console integration
  • sprite-zero-hit integration
  • vertical pixel scrolling
  • pygame

Complete example implementation

# emulator/rendering/ppu_background_renderer.py

# --- NEW BLOCK: COPY FRAMEBUFFER ADDRESSING FOR THE OPACITY-MASK PATH ---
def ppu_background_viewport_to_opaque_mask(
    ppu: PPU,
) -> BackgroundOpaqueMask:
    viewport_x, _ = decode_background_viewport_position(
        temp_vram_addr=ppu.temp_vram_addr,
        fine_x=ppu.fine_x,
    )

    nametable_y = (ppu.temp_vram_addr >> 11) & 1
    left_base = BASE_NAMETABLE_ADDR + nametable_y * 0x0800
    right_base = left_base + 0x0400

    # Same addresses, but call the opacity-mask producer.
    left = ppu_background_to_opaque_mask(
        ppu,
        base_nametable_addr=left_base,
    )
    right = ppu_background_to_opaque_mask(
        ppu,
        base_nametable_addr=right_base,
    )

    # Use the opacity-mask compositor, not the framebuffer compositor.
    return compose_horizontal_opaque_mask_viewport(
        left=left,
        right=right,
        viewport_x=viewport_x,
    )

Run this lesson

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