350. Seleccionar la ruta de framebuffer temporizado

Seleccionar la composición de framebuffer temporizado solo cuando hay un frame completo disponible.

Lección 350 de 356 · tests/chapter_13_scrolling/test_350_select_timed_framebuffer_path.py

Archivo a actualizar

emulator/rendering/ppu_background_renderer.py

Referencias

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

Por qué existe este paso

El compositor temporizado privado requiere un BackgroundScanlineState para cada fila visible. Esa evidencia no está disponible durante el arranque y puede faltar tras un frame incompleto, así que el adaptador público necesita un límite de compatibilidad explícito:

exactly 240 completed states -> use timed row composition
any other tuple length       -> use the existing t + fine-X snapshot path

¿Por qué exigir una longitud exacta en lugar de simplemente comprobar si es verdadero? Una tupla vacía es falsa, pero una tupla parcial de una entrada o de 239 entradas es verdadera. Los datos de temporización parciales todavía dejan filas desconocidas y no deben entrar en un compositor que requiere los 240 estados. La invariante estructural, no si la tupla no está vacía, decide qué mecanismo es seguro.

¿Por qué conservar la ruta antigua? Una PPU nueva no tiene ningún frame completado. Los llamadores históricos y el renderizado de arranque ya tienen un comportamiento determinista basado en temp_vram_addr más fine_x. Mantener ese cuerpo sin cambios proporciona un respaldo seguro hasta que se publique el primer frame temporizado completo.

Flujo de control

ppu_background_viewport_to_framebuffer(ppu)
                     |
                     v
         completed length == 240?
                /                               yes              no
               |                |
               v                v
      timed row helper    existing snapshot path
               |                |
               +------ return --+

Invariantes importantes

  • exactamente 240 entradas seleccionan el auxiliar temporizado
  • la composición temporizada retorna de inmediato
  • el renderizador de origen antiguo y el compositor de frame completo no se ejecutan también
  • las tuplas vacías, parciales o con exceso de entradas seleccionan el respaldo establecido
  • el respaldo sigue decodificando ppu.temp_vram_addr y ppu.fine_x
  • la selección de la máscara de opacidad permanece sin cambios en esta lección

Concepto erróneo común

No captures el ValueError del auxiliar temporizado ni reintentes el respaldo de forma silenciosa. La puerta pública posee la disponibilidad esperada; un error después de la puerta de frame completo indica una invariante rota que debe seguir siendo visible durante la depuración.

Fuera de alcance

  • la composición temporizada de la máscara de opacidad
  • la corrección de prioridad sprite/fondo
  • la integración de la máscara de sprite-zero-hit
  • el desplazamiento vertical completo de la fila de origen

Implementación de ejemplo completa

# emulator/rendering/ppu_background_renderer.py

def ppu_background_viewport_to_framebuffer(ppu: PPU) -> Framebuffer:
    # --- NEW BLOCK: USE TIMED DATA ONLY WHEN THE FRAME IS COMPLETE ---
    if (
        len(ppu.completed_scanline_scroll_states)
        == NAMETABLE_PIXEL_HEIGHT
    ):
        return _timed_scanlines_to_framebuffer(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,
    )
    ...

Punto de control manual tras esta lección

Con tu propia ROM legal de Super Mario Bros., ahora puedes jugar lo suficiente para intentar atrapar un champiñón mientras observas el fondo horizontal temporizado. En este hito exacto, el champiñón puede aparecer delante de un tile de fondo sólido que debería ocluirlo visualmente. Ese síntoma es esperado: las filas del framebuffer RGB ahora usan estados de scanline temporizados, pero la máscara de opacidad de fondo usada para la prioridad de sprites todavía usa una única instantánea antigua a nivel de frame. La siguiente lección compondrá las filas de la máscara de opacidad a partir de los mismos estados temporizados para que las decisiones de color y de prioridad usen coordenadas de pantalla idénticas.

Ejecutar esta lección

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