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.pyReferencia
https://www.nesdev.org/wiki/PPU_renderingPor 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 finishesEn 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 rendererInvariantes 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