NES TDD

274. RGB-палитра NES

Определение минимальной RGB-аппроксимации палитры NES.

Урок 274 из 356 · tests/chapter_05_rendering_pipeline/test_274_nes_palette_rgb.py

Файл эталонной палитры

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

Файл для создания

emulator/rendering/nes_palette.py

Почему этот шаг важен

Конвейер рендеринга теперь может преобразовывать сетки индексов цвета в данные Framebuffer, но вызывающему коду всё ещё нужно предоставлять RGB-палитру вручную.

Этот шаг определяет практичную RGB-аппроксимацию 64-цветной палитры NES

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

Важная аппаратная модель

NES не хранит RGB-цвета напрямую. PPU имеет палитровую RAM, содержащую индексы цветов NES, а аналоговая видеосхема PPU преобразует эти индексы в видеосигнал. В эмуляторе мы аппроксимируем этот вывод RGB-кортежами.

Приближённый к реальности поток

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

Поток в эмуляторе

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

Что такое 2C02G_U_wiki.pal? 2C02G — это ревизия PPU NES для NTSC. Указанный файл .pal — это RGB-аппроксимация палитры для поведения этого PPU/выхода. Это не буквальная RGB-таблица, хранящаяся внутри аппаратуры NES.

Почему в исходном файле больше 64 цветов? Некоторые файлы .pal включают варианты с акцентированием PPUMASK. Базовый диапазон индексов цвета NES — 64 цвета, но акцентирование может создать 8 вариантов отображения:

64 base colors * 8 emphasis states = 512 RGB entries

Для этого шага курса используйте только первые 64 записи RGB: обычную палитру без акцентирования. Поддержку акцентирования можно добавить позже, когда рендеринг будет использовать биты 5-7 PPUMASK.

Как воспроизвести эту таблицу из скачанного файла .pal:

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}),")

Пример возможной реализации

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]

Почему маскировать значением $3F?

Индексы цвета NES — это 6-битные значения

$00-$3F

Маскирование гарантирует, что индекс не выходит за пределы диапазона

index & 0x3F

Примеры

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

Вне рамок этого шага

  • акцентированные цвета PPUMASK
  • палитры PAL/Dendy
  • симуляция сигнала CRT/NTSC
  • поиск в палитровой RAM PPU по адресам $3F00-$3F1F
  • отображение через pygame

Запустить этот урок

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