278. Selección de paleta mediante tabla de atributos

Decodificar la selección de paleta de fondo a partir de una tabla de atributos de nametable.

Lección 278 de 356 · tests/chapter_05_rendering_pipeline/test_278_attribute_table_palette_selection.py

Referencia

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

Archivo a crear

emulator/rendering/attribute_table.py

Por qué existe este paso

El renderizador de nametable actual usa una única paleta compartida de 4 colores para cada tile. Los fondos reales de la NES eligen entre cuatro subpaletas de fondo usando la tabla de atributos del nametable.

Este paso todavía no renderiza con atributos. Solo responde a una pequeña pregunta:

For tile coordinate (tile_x, tile_y), which palette ID does the attribute
table select?

¿Qué es una tabla de atributos?

Cada nametable tiene

960 bytes tile IDs
64 bytes attribute table

La tabla de atributos es una cuadrícula de bytes de 8x8. Cada byte de atributo cubre un área de tiles de 4x4, que son 32x32 píxeles.

Un byte de atributo se divide en cuatro cuadrantes de tiles de 2x2

+-----------------------+
| top-left | top-right  |
|  2x2     |   2x2      |
+----------+------------+
| bottom-l | bottom-r   |
|  2x2     |   2x2      |
+-----------------------+

Cada cuadrante almacena un ID de paleta de 2 bits

0b00 -> palette 0
0b01 -> palette 1
0b10 -> palette 2
0b11 -> palette 3

Distinción importante

La tabla de atributos no almacena colores RGB ni almacena índices de color de la NES. Selecciona qué subpaleta de fondo usar. Más adelante, la RAM de paletas y la paleta RGB de la NES convertirán esa selección en colores reales.

Diseño de bits dentro de un byte de atributo

bits 0-1 -> top-left quadrant
bits 2-3 -> top-right quadrant
bits 4-5 -> bottom-left quadrant
bits 6-7 -> bottom-right quadrant

Este paso implementa: desempaquetar/leer un cuadrante del byte.

Ejemplo de implementación legible

TABLE_SIZE = 64
BYTES_PER_ROW = 8


def get_attribute_palette_id(
    attribute_table: bytes,
    tile_x: int,
    tile_y: int,
) -> int:
    if len(attribute_table) != TABLE_SIZE:
        raise ValueError("Attribute table must be 64 bytes")

    attribute_x = tile_x // 4
    attribute_y = tile_y // 4
    attribute_index = attribute_y * BYTES_PER_ROW + attribute_x
    attribute_byte = attribute_table[attribute_index]

    quadrant_x = (tile_x % 4) // 2
    quadrant_y = (tile_y % 4) // 2

    is_top_left = quadrant_x == 0 and quadrant_y == 0
    is_top_right = quadrant_x == 1 and quadrant_y == 0
    is_bottom_left = quadrant_x == 0 and quadrant_y == 1

    if is_top_left:
        return attribute_byte & 0b11

    if is_top_right:
        return (attribute_byte >> 2) & 0b11

    if is_bottom_left:
        return (attribute_byte >> 4) & 0b11

    return (attribute_byte >> 6) & 0b11

¿Por qué tile_x // 4 y tile_y // 4? Porque cada byte de atributo cubre 4x4 tiles.

¿Por qué (tile_x % 4) // 2?

Dentro de esa área de 4x4

tile positions 0,1 belong to quadrant 0
tile positions 2,3 belong to quadrant 1

Así que

0 % 4 // 2 -> 0
1 % 4 // 2 -> 0
2 % 4 // 2 -> 1
3 % 4 // 2 -> 1

Fuera de alcance

  • renderizado de nametables con atributos
  • búsqueda en la RAM de paletas de la PPU
  • conversión a paleta RGB
  • desplazamiento (scrolling)
  • sprites
  • pantalla de pygame

Ejecutar esta lección

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