349. Scanlines temporizados al framebuffer
Componer un framebuffer a partir de 240 estados de scanline temporizados completados.
Lección 349 de 356 · tests/chapter_13_scrolling/test_349_timed_scanlines_to_framebuffer.py
Archivo a actualizar
emulator/rendering/ppu_background_renderer.pyReferencias
https://www.nesdev.org/wiki/PPU_rendering
https://www.nesdev.org/wiki/PPU_scrollingPor qué existe este paso
El frame completado puede contener una posición de viewport horizontal distinta para cada fila visible. Una única instantánea de t al final del frame no puede representar a la vez un área de estado fija y un área de juego en movimiento, así que el renderizador de alto nivel debe componer cada fila a partir de su BackgroundScanlineState correspondiente.
Para cada fila de destino
state = completed_scanline_scroll_states[screen_y]
left_base, right_base = logical pair selected by state
viewport_x = horizontal pixel position decoded from statePara cada píxel de destino
logical_x = (viewport_x + screen_x) % 512
logical X 0-255: read the left source framebuffer
logical X 256-511: read the right source framebuffer after subtracting 256La fila de origen sigue siendo screen_y en este hito horizontal. La selección completa de la fila de origen vertical es trabajo futuro independiente.
¿Por qué almacenar en caché los pares de origen? Las filas pueden tener distintos valores de X de viewport mientras leen las mismas dos nametables. Volver a renderizar ambas nametables de origen completas en cada fila realizaría hasta 480 renderizados de origen. En su lugar, un diccionario local almacena cada par lógico una sola vez para esta composición:
$2000 key -> rendered ($2000, $2400) pair
$2800 key -> rendered ($2800, $2C00) pairLa caché es intencionadamente local. Los datos de nametable, de patrones, de atributos o de paleta pueden cambiar antes de un frame posterior, así que una caché entre frames requeriría reglas de invalidación explícitas y propensas a errores.
Invariantes importantes
- la entrada contiene exactamente 240 estados completados
- las dimensiones de salida son exactamente 256x240
- el índice de estado, la fila de destino y la fila de origen son el mismo screen_y
- cada fila de destino recibe exactamente 256 píxeles
- la selección horizontal se desborda dentro del par lógico de 512 píxeles
- cada par de origen lógico se renderiza como máximo una vez por composición
- los cambios de X de viewport no invalidan ni duplican un par en caché
- el mirroring del cartucho sigue siendo comportamiento de PpuBus dentro del renderizado de origen
Concepto erróneo común
No invoques el compositor de viewport horizontal de frame completo existente una vez por fila. Eso construiría 240 framebuffers temporales de 256x240 para conservar solo una fila de cada uno. Este auxiliar escribe cada fila de destino directamente en un único framebuffer de resultado.
Estrategia de pruebas
Estas pruebas sustituyen el renderizado de nametables por framebuffers de origen sintéticos cuyas tuplas RGB codifican la base lógica, la X de origen y la Y de origen. Esto aísla la mecánica de composición de filas de la decodificación de patrones, la selección de paleta, la memoria de la PPU y el mirroring.
Fuera de alcance
- cambiar el adaptador de viewport público
- el comportamiento de respaldo cuando no hay un frame temporizado disponible
- la composición de la máscara de opacidad
- el desplazamiento vertical completo de la fila de origen
- las cachés de renderizado persistentes
Implementación de ejemplo completa
# emulator/rendering/ppu_background_renderer.py
# --- UPDATED LINES: IMPORT FRAMEBUFFER DIMENSIONS ---
from emulator.rendering.background_viewport import (
NAMETABLE_PIXEL_HEIGHT,
NAMETABLE_PIXEL_WIDTH,
...
)
# --- NEW BLOCK: COMPOSE COMPLETED TIMED SCANLINES ---
def _timed_scanlines_to_framebuffer(ppu: PPU) -> Framebuffer:
states = ppu.completed_scanline_scroll_states
if len(states) != NAMETABLE_PIXEL_HEIGHT:
raise ValueError(
"Timed framebuffer requires exactly 240 scanline states"
)
result = Framebuffer(
width=NAMETABLE_PIXEL_WIDTH,
height=NAMETABLE_PIXEL_HEIGHT,
)
pair_cache: dict[int, tuple[Framebuffer, Framebuffer]] = {}
logical_width = NAMETABLE_PIXEL_WIDTH * 2
for screen_y, state in enumerate(states):
left_base, right_base = _scanline_horizontal_pair(state)
if left_base not in pair_cache:
pair_cache[left_base] = (
ppu_background_to_framebuffer(
ppu,
base_nametable_addr=left_base,
),
ppu_background_to_framebuffer(
ppu,
base_nametable_addr=right_base,
),
)
left, right = pair_cache[left_base]
viewport_x = _scanline_viewport_x(state)
destination_row = screen_y * NAMETABLE_PIXEL_WIDTH
for screen_x in range(NAMETABLE_PIXEL_WIDTH):
logical_x = (viewport_x + screen_x) % logical_width
if logical_x < NAMETABLE_PIXEL_WIDTH:
source = left
source_x = logical_x
else:
source = right
source_x = logical_x - NAMETABLE_PIXEL_WIDTH
destination_index = destination_row + screen_x
source_index = destination_row + source_x
result.pixels[destination_index] = source.pixels[source_index]
return resultEjecutar esta lección
uv run pytest tests/chapter_13_scrolling/test_349_timed_scanlines_to_framebuffer.py -v