346. Completar el frame de scroll de scanlines

Publicar un frame de scanlines temporizado completo y reiniciar el búfer de registro actual.

Lección 346 de 356 · tests/chapter_13_scrolling/test_346_complete_scanline_scroll_frame.py

Archivo a actualizar

emulator/ppu/ppu.py

Referencia

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

Por qué existe este paso

La temporización de la PPU registra los estados de scanline visibles en una lista mutable del frame actual. El renderizador de alto nivel debe consumir datos estables de un frame que ya ha finalizado, no una lista que la PPU todavía está modificando.

Por ello, la PPU posee dos valores distintos

current_scanline_scroll_states:
    mutable list used while the active frame is being stepped

completed_scanline_scroll_states:
    immutable tuple published after the frame finishes

En el límite del frame

all 240 entries exist:
    publish a 240-state tuple

any entry is missing or the list length is not 240:
    publish an empty tuple

after either result:
    replace current state with a fresh [None] * 240 list

¿Por qué publicar una tupla vacía cuando los datos están incompletos? Una fila desconocida no debe heredar una dirección adivinada. La tupla vacía se convierte en una señal clara de que el renderizado posterior debe seguir usando la ruta de compatibilidad a nivel de frame existente para este frame.

Modelo intuitivo

current list     = notebook still being written
completed tuple  = sealed notebook safe for the renderer

Invariantes importantes

  • los datos completados contienen exactamente 240 estados o cero estados
  • los datos completados son inmutables
  • los contenedores actual y completado no son el mismo objeto
  • registrar el siguiente frame no puede alterar el frame completado
  • la publicación ocurre después de que el pre-render finaliza y antes de que los contadores entren en el frame 0

Concepto erróneo común

La finalización del frame no ocurre cuando el VBlank comienza en el scanline 241. El scanline de pre-render 261 todavía pertenece a la secuencia de temporización antes de que el emulador pase al siguiente frame.

Fuera de alcance

  • consumir los estados completados en el renderizado del framebuffer
  • la composición de la máscara de opacidad
  • los cambios de sprite-zero-hit

Implementación de ejemplo completa

# emulator/ppu/ppu.py

@dataclass
class PPU:
    ...
    current_scanline_scroll_states: list[BackgroundScanlineState | None] = field(
            default_factory= lambda: [None] * 240
    )

    # --- NEW LINE: LAST COMPLETE TIMED SCANLINE FRAME ---
    completed_scanline_scroll_states: tuple[
        BackgroundScanlineState, ...
    ] = ()

    ...

    # --- NEW BLOCK: PUBLISH AND RESET SCANLINE STATES ---
    def _complete_scanline_scroll_frame(self) -> None:
        current = self.current_scanline_scroll_states

        if (
            len(current) == 240
            and all(state is not None for state in current)
        ):
            self.completed_scanline_scroll_states = tuple(
                state
                for state in current
                if state is not None
            )
        else:
            self.completed_scanline_scroll_states = ()

        self.current_scanline_scroll_states = [None] * 240

    def step(self, cycles: int = 1) -> None:
        ...

        if self.cycle >= PPU_CYCLES_PER_SCANLINE:
            self.cycle = 0
            self.scanline += 1

            if self.scanline >= PPU_SCANLINES_PER_FRAME:
                # --- NEW LINE: PUBLISH BEFORE ENTERING THE NEXT FRAME ---
                self._complete_scanline_scroll_frame()
                self.scanline = 0
                self.frame += 1

        ...

Ejecutar esta lección

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