321. El PPU establece el sprite zero hit programado

Permitir que el timing del PPU establezca el sprite 0 hit en una posición de pantalla detectada previamente.

Lección 321 de 356 · tests/chapter_11_sprite_zero_hit/test_321_ppu_sets_scheduled_sprite_zero_hit.py

Archivo a actualizar

emulator/ppu/ppu.py

Por qué existe este paso

El paso 320 puede encontrar el primer píxel solapado de sprite 0/fondo y devolver su posición en pantalla:

(screen_x, screen_y)

El PPU no debe establecer el bit 6 de PPUSTATUS inmediatamente cuando se descubre esa posición. Debe almacenar la posición y establecer el flag solo cuando el timing del PPU emulado alcance el píxel visible correspondiente.

Mapeo de coordenadas simplificado

screen y -> PPU scanline y
screen x -> PPU cycle x + 1

Las coordenadas del framebuffer visible comienzan en x=0, mientras que el timing del PPU simplificado de este proyecto considera que la salida visible comienza en el ciclo 1:

screen x=0 -> PPU cycle 1
screen x=1 -> PPU cycle 2
screen x=40 -> PPU cycle 41

Cambios de implementación sugeridos

# --- 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 ---

            ...

¿Por qué consumir la posición? La posición describe un único evento de timing futuro. Una vez disparado el evento, establecerla en None evita que el mismo evento programado se dispare de nuevo en un frame posterior. El flag de PPUSTATUS en sí permanece establecido hasta el borrado de pre-render del paso 319.

Distinción importante

set_sprite_zero_hit_position((x, y))
    stores a future position

PPU.step()
    sets SPRITE_ZERO_HIT when timing reaches that position

Límite importante

El PPU recibe solo un tuple[int, int] | None neutro. No debe importar módulos de renderizado ni saber cómo se detectó el solapamiento entre CHR y el fondo.

Fuera del alcance

  • Conexión de Console
  • llamar a find_sprite_zero_hit_position()
  • selección de las tablas de patrones de sprite/fondo
  • reglas de activación de renderizado de PPUMASK
  • excepción de hardware en x=255
  • corrección de Y+1 de OAM
  • validación con Super Mario Bros.

Ejecutar esta lección

uv run pytest tests/chapter_11_sprite_zero_hit/test_321_ppu_sets_scheduled_sprite_zero_hit.py -v