274. Nes palette rgb
Define a minimal NES RGB palette approximation.
Lesson 274 of 356 · tests/chapter_05_rendering_pipeline/test_274_nes_palette_rgb.py
Reference palette file
https://www.nesdev.org/wiki/File:2C02G_U_wiki.palFile to create
emulator/rendering/nes_palette.pyWhy this step exists
The rendering pipeline can now convert color-index grids into Framebuffer data, but callers still need to provide an RGB palette manually.
This step defines a practical 64-color NES RGB palette approximation
NES color index $00-$3F -> RGB tupleImportant hardware model
The NES does not store RGB colors directly. The PPU has palette RAM containing NES color indexes, and the PPU's analog video circuitry turns those indexes into a video signal. In an emulator, we approximate that output with RGB tuples.
Real-ish flow
pixel palette entry
-> PPU palette RAM value $00-$3F
-> PPU hardware color generator / analog video signal
-> TV colorEmulator flow
pixel palette entry
-> PPU palette RAM value $00-$3F
-> NES_PALETTE_RGB[index]
-> Framebuffer RGB pixelWhat is 2C02G_U_wiki.pal? 2C02G is an NTSC NES PPU revision. The referenced .pal file is an RGB palette approximation for that PPU/output behavior. It is not a literal RGB table stored inside NES hardware.
Why does the source file have more than 64 colors? Some .pal files include PPUMASK emphasis variants. The base NES color index range is 64 colors, but emphasis can create 8 display variants:
64 base colors * 8 emphasis states = 512 RGB entriesFor this tutorial step, use only the first 64 RGB entries: the normal no-emphasis palette. Emphasis support can be added later when rendering uses PPUMASK bits 5-7.
How to reproduce this table from the downloaded .pal file:
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}),")Suggested implementation example
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]Why mask with $3F?
NES color indexes are 6-bit values
$00-$3FMasking keeps lookup in range
index & 0x3FExamples
get_nes_rgb_color(0x40) == get_nes_rgb_color(0x00)
get_nes_rgb_color(0x41) == get_nes_rgb_color(0x01)Out of scope
- PPUMASK emphasis colors
- PAL/Dendy palettes
- CRT/NTSC signal simulation
- PPU palette RAM lookup at $3F00-$3F1F
- pygame display
Run this lesson
uv run pytest tests/chapter_05_rendering_pipeline/test_274_nes_palette_rgb.py -v