354. Optimización de caché de la máscara opaca de fondo
Comenzar el Capítulo 14 cacheando la construcción repetida de la máscara de opacidad de fondo.
Lección 354 de 356 · tests/chapter_14_optimizations/test_354_cache_optimization_background_opaque_mask.py
Archivo a actualizar
emulator/rendering/nametable_renderer.pyPor qué existe esta pequeña optimización
El renderizador de viewport y la preparación de sprite-zero-hit pueden solicitar una máscara de opacidad para los mismos bytes de tabla de patrones y de nametable. Construir esa máscara repetidamente decodifica 256 tiles CHR y visita cada píxel de un fondo de 256x240.
Una caché LRU almacena un resultado calculado recientemente y descarta la entrada usada menos recientemente cuando se alcanza su límite de tamaño. Aquí, los bytes de entrada inmutables exactos son la clave de caché:
same pattern bytes + same nametable bytes -> cache hit
changed pattern bytes or nametable bytes -> cache missLa implementación sencilla es un pequeño cambio de límite en lugar de una reescritura del renderizado:
from functools import lru_cache
# This is our old build_background_opaque_mask implementation under a private
# name. It now returns a tuple so callers cannot mutate the cached value. A
# public wrapper below will preserve the historical mutable list API.
@lru_cache(maxsize=8)
def _cached_background_opaque_mask(
pattern_table: bytes,
nametable: bytes,
) -> tuple[bool, ...]:
if len(nametable) != NAMETABLE_SIZE:
raise ValueError("Nametable must be 960 bytes")
decoded_tiles = decode_pattern_table(pattern_table)
opaque_mask: BackgroundOpaqueMask = [False] * (
BACKGROUND_WIDTH * BACKGROUND_HEIGHT
)
for tile_y in range(NAMETABLE_ROWS):
for tile_x in range(NAMETABLE_TILES_PER_ROW):
nametable_index = (
tile_y * NAMETABLE_TILES_PER_ROW + tile_x
)
tile_index = nametable[nametable_index]
tile = decoded_tiles[tile_index]
for pixel_y in range(CHR_TILE_HEIGHT):
for pixel_x in range(CHR_TILE_WIDTH):
color_index = tile[pixel_y][pixel_x]
screen_x = tile_x * CHR_TILE_WIDTH + pixel_x
screen_y = tile_y * CHR_TILE_HEIGHT + pixel_y
mask_index = (
screen_y * BACKGROUND_WIDTH + screen_x
)
opaque_mask[mask_index] = color_index != 0
return tuple(opaque_mask)
def build_background_opaque_mask(
pattern_table: bytes,
nametable: bytes,
) -> BackgroundOpaqueMask:
return list(_cached_background_opaque_mask(pattern_table, nametable))¿Por qué cachear una tupla pero devolver una lista? functools.lru_cache devuelve exactamente el objeto almacenado; no hace una copia. Cachear directamente la lista mutable pública permitiría que una llamada modificara el resultado observado por llamadas futuras. La tupla privada hace que el estado cacheado sea inmutable, mientras que la copia de la lista pública conserva la API histórica de BackgroundOpaqueMask y una propiedad independiente.
Esta prueba comprueba un contrato completo
- el mismo contenido realiza la decodificación costosa una sola vez
- los resultados públicos son iguales pero son objetos de lista distintos
- mutar un resultado no puede contaminar un acierto de caché posterior
- un contenido de nametable cambiado provoca un fallo de caché
- la caché está acotada a ocho entradas
Concepto erróneo habitual
La optimización no se basa en la identidad del objeto ni en el número de fotograma. Dos objetos bytes distintos con el mismo contenido se comparan como la misma clave de caché, mientras que unos datos gráficos cambiados forman de forma natural una clave distinta sin necesidad de una señal explícita de invalidación por parte de la PPU.
Ejecutar esta lección
uv run pytest tests/chapter_14_optimizations/test_354_cache_optimization_background_opaque_mask.py -v