353. Sprite zero hit uses viewport mask

Use viewport-aware background opacity for sprite-zero-hit overlap detection.

Lesson 353 of 356 · tests/chapter_13_scrolling/test_353_sprite_zero_hit_uses_viewport_mask.py

File to update

emulator/rendering/sprite_zero_hit.py

References

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

Why this step exists

Sprite zero hit is a PPU status signal produced when an opaque pixel from OAM sprite entry 0 overlaps an opaque background pixel. Games can poll this signal to time a mid-frame register change, such as separating a fixed status area from scrolling gameplay.

The overlap helper indexes background opacity using screen coordinates

mask_index = screen_y * 256 + screen_x

That mask must therefore describe the background actually visible at those same coordinates. After timed scrolling, screen X may map to a different coarse-X column or logical nametable on each row. The old fixed-$2000 mask can inspect a different background pixel even though sprite 0 remains at the same screen position.

Example

sprite 0 screen X:       100
scanline viewport X:      40
visible background X:    140

fixed mask inspects X:   100
viewport mask inspects X: 140

If source X 140 is opaque and source X 100 is transparent, the fixed mask misses the overlap. The viewport-aware adapter selects timed composition for a complete frame and retains the previously tested snapshot fallback when timed data is unavailable.

Why use an import alias? Historical sprite-zero-hit tests monkeypatch the module-local name ppu_background_to_opaque_mask. Changing every call to a new local name would break that stable testing seam. Importing the viewport-aware adapter under the historical name changes production behavior while preserving old tests unchanged:

new viewport-aware function as old module-local name

Important invariants

  • sprite-zero-hit consumes the viewport-aware opacity adapter
  • the historical local dependency name remains monkeypatchable
  • the selected mask is passed unchanged to the pure overlap helper
  • sprite CHR pattern-table selection remains independent from background masking
  • rendering code does not set PPUSTATUS directly
  • Console and PPU timing continue scheduling and setting the hit

Common misconception

Sprite zero hit is not gameplay collision detection and does not mean that Mario hit an enemy or block. It is a rendering overlap involving only sprite entry 0 and an opaque background pattern pixel, commonly used by software as a timing signal.

Testing strategy

One test checks the production dependency identity so the old fixed producer cannot silently return. Another follows a complete timed frame through the real viewport adapter while replacing only its expensive row compositor, then verifies that the resulting mask reaches the existing pure overlap boundary unchanged.

Out of scope

  • changing the pure overlap search
  • changing sprite pattern-table selection
  • changing Console frame scheduling
  • exact left-edge, X=255, and OAM Y+1 hardware behavior
  • full vertical source-row scrolling

Complete example implementation

# emulator/rendering/sprite_zero_hit.py

# --- UPDATED BLOCK: VIEWPORT MASK WITH HISTORICAL LOCAL NAME ---
from emulator.rendering.ppu_background_renderer import (
    PATTERN_TABLE_0_ADDR,
    PATTERN_TABLE_1_ADDR,
    ppu_background_viewport_to_opaque_mask as ppu_background_to_opaque_mask,
)

...

Run this lesson

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