355. Cachear los píxeles del framebuffer de la nametable
Cachear los píxeles repetidos del framebuffer de la nametable según las entradas gráficas exactas.
Lección 355 de 356 · tests/chapter_14_optimizations/test_355_cache_nametable_framebuffer_pixels.py
Archivo a actualizar
emulator/rendering/nametable_renderer.pyPor qué existe este paso
El frontend solicita al emulador una imagen de fondo completa en cada fotograma mostrado. Producir una imagen de nametable lógica requiere decodificar los datos de patrón, seleccionar las paletas de tile a partir de la tabla de atributos y escribir 256x240 píxeles RGB. El desplazamiento puede cambiar qué porción de nametables adyacentes es visible sin cambiar los datos gráficos de origen dentro de ninguna de ellas, por lo que fotogramas consecutivos suelen solicitar exactamente la misma imagen de origen costosa.
A 60 FPS, un fotograma completo dispone solo de unos 16,67 ms para el avance de CPU/PPU, el renderizado de fondo y sprites, la subida del framebuffer y la presentación. Reconstruir 61.440 píxeles a partir de bytes sin cambios consume ese presupuesto limitado sin producir información nueva. Una caché acotada e indexada por contenido convierte los renderizados repetidos de origen en una búsqueda de píxeles inmutables más una copia de lista, dejando más tiempo de fotograma para el trabajo que realmente ha cambiado.
Nombres y responsabilidades
- No existing function is renamed.
- nametable_with_attributes_to_framebuffer() remains unchanged. It is still the
lower-level operation that performs the expensive render on a cache miss.
- nametable_with_palette_ram_to_framebuffer() keeps its public name and signature,
but its body changes into an ownership-preserving wrapper around cached pixels.
- _cached_nametable_with_palette_ram_pixels() is a new private helper containing
the old public function's palette-building and rendering work.Implementación completa
# functools.lru_cache is already imported for Test 354. Add this import if the
# previous step is being reproduced independently:
from functools import lru_cache
@lru_cache(maxsize=8)
def _cached_nametable_with_palette_ram_pixels(
nametable_bytes: bytes,
attribute_table: bytes,
pattern_table_bytes: bytes,
palette_ram: bytes,
) -> tuple[RGBColor, ...]:
background_palettes = build_background_palettes_from_palette_ram(
palette_ram
)
framebuffer = nametable_with_attributes_to_framebuffer(
nametable_bytes,
attribute_table,
pattern_table_bytes,
background_palettes,
)
# The cache must own immutable data so callers cannot poison later hits.
return tuple(framebuffer.pixels)
def nametable_with_palette_ram_to_framebuffer(
nametable_bytes: bytes,
attribute_table: bytes,
pattern_table_bytes: bytes,
palette_ram: bytes,
) -> Framebuffer:
cached_pixels = _cached_nametable_with_palette_ram_pixels(
nametable_bytes,
attribute_table,
pattern_table_bytes,
palette_ram,
)
# Preserve the historical mutable ownership contract.
return Framebuffer(
width=BACKGROUND_WIDTH,
height=BACKGROUND_HEIGHT,
pixels=list(cached_pixels),
)Dónde editar
Sustituye el cuerpo existente de nametable_with_palette_ram_to_framebuffer() por el envoltorio mostrado arriba, y añade el nuevo helper privado cacheado junto a él. No mantengas las antiguas sentencias de construcción de paleta/renderizado en el envoltorio público, porque eso realizaría el trabajo costoso antes de consultar la caché.
Límite de comprobación de tipos
nametable_with_attributes_to_framebuffer() devuelve un Framebuffer, así que devolver directamente esa llamada desde _cached_nametable_with_palette_ram_pixels() viola su tipo de retorno declarado tuple[RGBColor, ...]. Primero asigna el Framebuffer a la variable local mostrada arriba, y luego devuelve tuple(framebuffer.pixels). No cambies la anotación del helper cacheado a Framebuffer, porque eso cachearía estado público mutable.
Invariantes importantes
- las cuatro entradas de bytes inmutables exactas participan en la clave de caché
- un contenido de bytes igual reutiliza el trabajo incluso cuando los objetos tienen identidades distintas
- cambiar cualquier entrada gráfica provoca un fallo de caché
- los píxeles cacheados son inmutables
- los objetos Framebuffer públicos y las listas de píxeles tienen propiedad independiente
- la caché está acotada a ocho entradas
Consecuencia de rendimiento
Los aciertos de caché evitan la decodificación de patrones, la reconstrucción de paleta, el recorrido de tiles y la generación de píxeles RGB. Los fallos de caché conservan la ruta de renderizado original, de modo que cambiar cualquier entrada visual sigue produciendo píxeles frescos y correctos. El límite de tamaño acota el uso de memoria a la vez que conserva las imágenes de nametable reutilizadas recientemente.
Concepto erróneo habitual
No caches ni devuelvas una única instancia mutable de Framebuffer. El mecanismo de velocidad consiste en reutilizar contenido de píxeles inmutable, no en compartir la propiedad de un fotograma mutable.
Ejecutar esta lección
uv run pytest tests/chapter_14_optimizations/test_355_cache_nametable_framebuffer_pixels.py -v