321. Ppu sets scheduled sprite zero hit
Let PPU timing set sprite 0 hit at a previously detected screen position.
Lesson 321 of 356 · tests/chapter_11_sprite_zero_hit/test_321_ppu_sets_scheduled_sprite_zero_hit.py
File to update
emulator/ppu/ppu.pyWhy this step exists
Step 320 can find the first overlapping sprite 0/background pixel and return its screen position:
(screen_x, screen_y)The PPU must not set PPUSTATUS bit 6 immediately when that position is discovered. It should store the position and set the flag only when emulated PPU timing reaches the corresponding visible pixel.
Simplified coordinate mapping
screen y -> PPU scanline y
screen x -> PPU cycle x + 1Visible framebuffer coordinates begin at x=0, while this project's simplified PPU timing treats visible output as beginning at cycle 1:
screen x=0 -> PPU cycle 1
screen x=1 -> PPU cycle 2
screen x=40 -> PPU cycle 41Suggested implementation changes
# --- NEW LINE ---
SpriteZeroHitPosition = tuple[int, int]
# --- END NEW LINE ---
@dataclass
class PPU:
...
scanline: int = 0
frame: int = 0
nmi_requested: bool = False
# --- NEW BLOCK ---
sprite_zero_hit_position: SpriteZeroHitPosition | None = None
def set_sprite_zero_hit_position(
self,
position: SpriteZeroHitPosition | None,
) -> None:
self.sprite_zero_hit_position = position
# --- END NEW BLOCK ---
def step(self, cycles: int = 1) -> None:
...
for _ in range(cycles):
self.cycle += 1
# --- NEW BLOCK ---
if self.sprite_zero_hit_position is not None:
hit_x, hit_y = self.sprite_zero_hit_position
if self.scanline == hit_y and self.cycle == hit_x + 1:
self.status |= SPRITE_ZERO_HIT
self.sprite_zero_hit_position = None
# --- END NEW BLOCK ---
...Why consume the position? The position describes one future timing event. After the event fires, setting it to None prevents the same scheduled event from firing again in a later frame. The PPUSTATUS flag itself remains set until Step 319's pre-render clear.
Important distinction
set_sprite_zero_hit_position((x, y))
stores a future position
PPU.step()
sets SPRITE_ZERO_HIT when timing reaches that positionImportant boundary
PPU receives only a neutral tuple[int, int] | None. It must not import rendering modules or know how CHR/background overlap was detected.
Out of scope
- Console wiring
- calling find_sprite_zero_hit_position()
- selecting sprite/background pattern tables
- PPUMASK rendering-enable rules
- x=255 hardware exception
- OAM Y+1 correction
- Super Mario Bros. validation
Run this lesson
uv run pytest tests/chapter_11_sprite_zero_hit/test_321_ppu_sets_scheduled_sprite_zero_hit.py -v