276. Nametable a framebuffer

Renderizar un fondo de nametable simplificado en datos de framebuffer.

Lección 276 de 356 · tests/chapter_05_rendering_pipeline/test_276_nametable_to_framebuffer.py

Referencia

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

Archivo a crear

emulator/rendering/nametable_renderer.py

Por qué existe este paso

Las tablas de patrones contienen gráficos de tiles, pero no describen la disposición del fondo. Los nametables proporcionan esa disposición.

¿Qué es un nametable? Un nametable es memoria de la PPU que almacena qué tile de fondo aparece en cada celda de tile de la pantalla.

Ejemplo mínimo

nametable[0] = 5

Esto significa

top-left 8x8 background cell uses pattern table tile #5

Distinción importante

pattern table = tile graphics dictionary
nametable     = tile layout/map
palette       = colors used by tile pixels

Modelo de renderizado simplificado para este paso

nametable tile ID
    -> decoded_tiles[tile_id]
    -> tile pixel color index 0-3
    -> same shared 4-color palette[color_index]
    -> framebuffer pixel

Memoria real de nametable de la NES

960 bytes tile IDs
64 bytes attribute table

Este paso usa intencionadamente solo los 960 bytes de tiles visibles y una única paleta compartida de 4 colores para todos los tiles. La selección de paleta mediante tabla de atributos es un paso posterior.

Simplificación importante

Este primer renderizador de nametable es un punto de partida básico. Cada tile usa la misma paleta de 4 colores. Tests posteriores añaden un renderizador consciente de atributos donde diferentes regiones de la pantalla pueden elegir distintas paletas de fondo.

Tamaño de pantalla a partir de la geometría del nametable

32 tiles across * 8 pixels = 256 pixels
30 tiles down   * 8 pixels = 240 pixels

Ejemplo de implementación sugerida

from emulator.ppu.chr_decoder import (
    CHR_TILE_HEIGHT,
    CHR_TILE_WIDTH,
    decode_pattern_table,
)
from emulator.rendering.framebuffer import Framebuffer, RGBColor

NAMETABLE_ROWS = 30
NAMETABLE_TILES_PER_ROW = 32
NAMETABLE_SIZE = NAMETABLE_TILES_PER_ROW * NAMETABLE_ROWS

BACKGROUND_WIDTH = NAMETABLE_TILES_PER_ROW * CHR_TILE_WIDTH
BACKGROUND_HEIGHT = NAMETABLE_ROWS * CHR_TILE_HEIGHT


def nametable_to_framebuffer(
    nametable_bytes: bytes,
    pattern_table_bytes: bytes,
    palette: list[RGBColor],
) -> Framebuffer:
    if len(nametable_bytes) != NAMETABLE_SIZE:
        raise ValueError("Nametable visible tile area must be 960 bytes")

    decoded_tiles = decode_pattern_table(pattern_table_bytes)

    framebuffer = Framebuffer(width=BACKGROUND_WIDTH, height=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_id = nametable_bytes[nametable_index]
            tile = decoded_tiles[tile_id]

            for row in range(CHR_TILE_HEIGHT):
                for col in range(CHR_TILE_WIDTH):
                    # Basic starting point: every tile uses the same
                    # 4-color palette. A later renderer will use the
                    # attribute table to select different palettes.
                    color_index = tile[row][col]
                    rgb = palette[color_index]

                    pixel_x = tile_x * CHR_TILE_WIDTH + col
                    pixel_y = tile_y * CHR_TILE_HEIGHT + row
                    framebuffer.set_pixel(pixel_x, pixel_y, rgb)

    return framebuffer

Regla de arquitectura

No dupliques aquí la lógica de decodificación CHR. Reutiliza decode_pattern_table().

Fuera de alcance

  • decodificación de la tabla de atributos
  • selección de paleta por tile
  • desplazamiento (scrolling)
  • espejado de nametable
  • lectura directa desde el bus de la PPU
  • sprites
  • OAMDMA
  • pantalla de pygame

Ejecutar esta lección

uv run pytest tests/chapter_05_rendering_pipeline/test_276_nametable_to_framebuffer.py -v