352. Select timed opaque mask path
Select timed opacity-mask composition only when one complete frame is available.
Lesson 352 of 356 · tests/chapter_13_scrolling/test_352_select_timed_opaque_mask_path.py
File to update
emulator/rendering/ppu_background_renderer.pyReferences
https://www.nesdev.org/wiki/PPU_rendering
https://www.nesdev.org/wiki/PPU_scrollingWhy this step exists
Step 351 built the mechanism for composing Boolean background-opacity rows from the same timed states as RGB framebuffer rows. The public mask adapter must now activate that mechanism without breaking startup or incomplete-frame behavior.
Selection boundary
exactly 240 completed states -> timed opacity-mask composition
any other tuple length -> existing t + fine-X snapshot fallbackThis exact gate matches the framebuffer adapter. A truthiness check is insufficient because a partial one-entry or 239-entry tuple is nonempty but cannot describe all visible rows.
Why keep the fallback? A new PPU starts with an empty completed tuple. The established snapshot path remains a deterministic compatibility mechanism until the first complete timed frame is published. It also contains the previously tested logical-pair and mirroring boundary.
Control flow
ppu_background_viewport_to_opaque_mask(ppu)
|
v
completed length == 240?
/ yes no
| |
v v
timed mask helper existing snapshot mask
| |
+------ return --+Important invariants
- exactly 240 entries select the timed helper
- the timed branch returns immediately
- old source-mask construction and full-frame composition do not also execute
- empty, partial, and oversized tuples select the old fallback
- fallback still reads ppu.temp_vram_addr and ppu.fine_x
- framebuffer path and sprite-zero-hit code remain unchanged
Common misconception
Do not catch errors from the timed helper and silently retry the fallback. The gate handles expected data availability. Once a complete frame selects the timed helper, an exception represents a violated invariant that should remain observable.
What changes in practice? Console already uses this viewport-aware mask for sprite/background composition. RGB and visual sprite priority can now use identical timed coordinates, correcting cases such as a mushroom incorrectly appearing in front of an opaque tile. The separate sprite-zero-hit helper still uses its older mask dependency and will be aligned in a later lesson.
Out of scope
- changing timed row composition
- changing the framebuffer gate
- changing sprite-zero-hit mask consumption
- full vertical source-row scrolling
Complete example implementation
# emulator/rendering/ppu_background_renderer.py
def ppu_background_viewport_to_opaque_mask(
ppu: PPU,
) -> BackgroundOpaqueMask:
# --- NEW BLOCK: USE TIMED DATA ONLY FOR A COMPLETE FRAME ---
if (
len(ppu.completed_scanline_scroll_states)
== NAMETABLE_PIXEL_HEIGHT
):
return _timed_scanlines_to_opaque_mask(ppu)
# Existing temp_vram_addr + fine_x fallback remains unchanged below.
viewport_x, _ = decode_background_viewport_position(
temp_vram_addr=ppu.temp_vram_addr,
fine_x=ppu.fine_x,
)
...Run this lesson
uv run pytest tests/chapter_13_scrolling/test_352_select_timed_opaque_mask_path.py -v