274. Paleta RGB de la NES

Definir una aproximación mínima de la paleta RGB de la NES.

Lección 274 de 356 · tests/chapter_05_rendering_pipeline/test_274_nes_palette_rgb.py

Archivo de paleta de referencia

https://www.nesdev.org/wiki/File:2C02G_U_wiki.pal

Archivo a crear

emulator/rendering/nes_palette.py

Por qué existe este paso

La canalización de renderizado ya puede convertir cuadrículas de índices de color en datos de Framebuffer, pero los llamadores todavía tienen que proporcionar manualmente una paleta RGB.

Este paso define una aproximación práctica de paleta RGB de 64 colores de la NES

NES color index $00-$3F -> RGB tuple

Modelo de hardware importante

La NES no almacena colores RGB directamente. La PPU tiene una RAM de paletas que contiene índices de color de la NES, y el circuito de vídeo analógico de la PPU convierte esos índices en una señal de vídeo. En un emulador, aproximamos esa salida con tuplas RGB.

Flujo casi real

pixel palette entry
    -> PPU palette RAM value $00-$3F
    -> PPU hardware color generator / analog video signal
    -> TV color

Flujo del emulador

pixel palette entry
    -> PPU palette RAM value $00-$3F
    -> NES_PALETTE_RGB[index]
    -> Framebuffer RGB pixel

¿Qué es 2C02G_U_wiki.pal? 2C02G es una revisión NTSC de la PPU de la NES. El archivo .pal referenciado es una aproximación de paleta RGB para ese comportamiento de PPU/salida. No es una tabla RGB literal almacenada dentro del hardware de la NES.

¿Por qué el archivo fuente tiene más de 64 colores? Algunos archivos .pal incluyen variantes de énfasis de PPUMASK. El rango base de índices de color de la NES es de 64 colores, pero el énfasis puede crear 8 variantes de visualización:

64 base colors * 8 emphasis states = 512 RGB entries

Para este paso del tutorial, usa solo las primeras 64 entradas RGB: la paleta normal sin énfasis. El soporte de énfasis se puede añadir más adelante cuando el renderizado use los bits 5-7 de PPUMASK.

Cómo reproducir esta tabla a partir del archivo .pal descargado:

file = "2C02G_U_wiki.pal"
data = open(file, "rb").read()

for i in range(0, 64 * 3, 3):
    rgb = data[i:i + 3]
    if len(rgb) == 3:
        r, g, b = rgb[0], rgb[1], rgb[2]
        print(f"({r},{g},{b}),")

Ejemplo de implementación sugerida

from emulator.rendering.framebuffer import RGBColor

NES_PALETTE_SIZE = 64

NES_PALETTE_RGB: list[RGBColor] = [
    (87,87,87),     (0,12,142),     (8,0,166),      (52,0,150),      (85,0,97),      (99,0,21),      (90,0,0),       (60,14,0),
    (17,40,0),      (0,59,0),       (0,66,0),       (0,58,5),        (0,38,82),      (0,0,0),        (0,0,0),        (0,0,0),

    (165,165,165),  (0,65,217),     (47,30,255),    (103,4,242),     (148,0,180),    (170,0,87),     (163,24,0),     (128,57,0),
    (75,91,0),      (19,118,0),     (0,129,0),      (0,121,35),      (0,98,136),     (0,0,0),        (0,0,0),        (0,0,0),

    (255,255,255),  (74,159,255),   (121,126,255),  (175,99,255),    (221,85,255),   (247,87,194),   (247,106,99),   (220,136,16),
    (174,169,0),    (120,196,0),    (74,210,17),    (47,207,100),    (47,189,196),   (65,65,65),     (0,0,0),        (0,0,0),

    (255,255,255),  (185,221,255),  (202,209,255),  (222,198,255),   (240,192,255),  (252,192,238),  (253,198,202),  (245,208,170),
    (228,221,149),  (208,232,146),  (189,238,162),  (178,238,192),   (176,232,227),  (179,179,179),  (0,0,0),        (0,0,0),
]

def get_nes_rgb_color(index: int) -> RGBColor:
    return NES_PALETTE_RGB[index & 0x3F]

¿Por qué aplicar máscara con $3F?

Los índices de color de la NES son valores de 6 bits

$00-$3F

El enmascaramiento mantiene la búsqueda dentro del rango

index & 0x3F

Ejemplos

get_nes_rgb_color(0x40) == get_nes_rgb_color(0x00)
get_nes_rgb_color(0x41) == get_nes_rgb_color(0x01)

Fuera de alcance

  • colores de énfasis de PPUMASK
  • paletas PAL/Dendy
  • simulación de señal CRT/NTSC
  • búsqueda en la RAM de paletas de la PPU en $3F00-$3F1F
  • pantalla de pygame

Ejecutar esta lección

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