280. Background palettes from palette ram

Build RGB background palettes from PPU palette RAM bytes.

Lesson 280 of 356 · tests/chapter_05_rendering_pipeline/test_280_background_palettes_from_palette_ram.py

Reference

https://www.nesdev.org/wiki/PPU_palettes#Palette_RAM

File to create

emulator/rendering/palette_ram.py

Why this step exists

The attribute-aware nametable renderer expects four resolved RGB background palettes:

background_palettes[palette_id][color_index]

But real NES games do not store RGB colors in the nametable or attribute table. They write NES color indexes into PPU palette RAM.

This helper converts

PPU palette RAM bytes -> RGB background palettes

What is PPU palette RAM?

PPU palette RAM is a small 32-byte area mapped at

$3F00-$3F1F

For this step we only use the first 16 bytes

$3F00-$3F0F = background palette area

Those bytes store NES color indexes $00-$3F, not RGB values.

Flow

palette RAM byte
    -> NES color index $00-$3F
    -> get_nes_rgb_color(index)
    -> RGB tuple

Backdrop / universal background color

For background rendering, color index 0 uses the shared backdrop color at $3F00.

So every returned background palette uses the same first RGB color

background_palettes[0][0] == backdrop
background_palettes[1][0] == backdrop
background_palettes[2][0] == backdrop
background_palettes[3][0] == backdrop

Background palette layout for $3F00-$3F0F:

$3F00 -> backdrop / universal background color

palette 0:
    entry 0 -> $3F00
    entry 1 -> $3F01
    entry 2 -> $3F02
    entry 3 -> $3F03

palette 1:
    entry 0 -> $3F00
    entry 1 -> $3F05
    entry 2 -> $3F06
    entry 3 -> $3F07

palette 2:
    entry 0 -> $3F00
    entry 1 -> $3F09
    entry 2 -> $3F0A
    entry 3 -> $3F0B

palette 3:
    entry 0 -> $3F00
    entry 1 -> $3F0D
    entry 2 -> $3F0E
    entry 3 -> $3F0F

Notice

$3F04, $3F08, and $3F0C are not used as independent background color-0
entries in this simplified helper. Color index 0 uses the shared backdrop.

Suggested implementation example

from emulator.rendering.framebuffer import RGBColor
from emulator.rendering.nes_palette import get_nes_rgb_color

PALETTE_RAM_SIZE = 16
TOTAL_PALETTES = 4
COLORS_PER_PALETTE = 4

BackgroundPalettes = list[list[RGBColor]]


def build_background_palettes_from_palette_ram(
    palette_ram: bytes,
) -> BackgroundPalettes:
    if len(palette_ram) != PALETTE_RAM_SIZE:
        raise ValueError(f"Background palette RAM must be {PALETTE_RAM_SIZE} bytes")

    backdrop_color = get_nes_rgb_color(palette_ram[0])
    background_palettes = []

    for palette_id in range(TOTAL_PALETTES):
        base = palette_id * COLORS_PER_PALETTE

        palette = [
            backdrop_color,
            get_nes_rgb_color(palette_ram[base + 1]),
            get_nes_rgb_color(palette_ram[base + 2]),
            get_nes_rgb_color(palette_ram[base + 3]),
        ]

        background_palettes.append(palette)

    return background_palettes

Out of scope

  • sprite palettes
  • sprite transparency behavior
  • palette RAM mirroring
  • reading from PPU bus directly
  • PPUMASK emphasis colors
  • pygame display

Run this lesson

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