252. Ppu bus palette ram mapping

Implement PpuBus palette RAM address mapping.

Lesson 252 of 356 · tests/chapter_03_ppu_memory_and_graphics_data/test_252_ppu_bus_palette_ram_mapping.py

Reference

https://www.nesdev.org/wiki/PPU_palettes
https://www.nesdev.org/wiki/PPU_memory_map

File to update

emulator/bus/ppu_bus.py

Constants to add

PALETTE_START = 0x3F00
PALETTE_END = 0x3FFF
PALETTE_SIZE = 0x20

Why this step exists

The PPU address range $3F00-$3FFF is palette memory. Palette memory does not store tile graphics. It stores NES color indices used later by background and sprite rendering.

Hardware note

On the original NES PPU, palette RAM is a small, separate 32-byte memory area inside the PPU. It is not CHR ROM, and it is not nametable VRAM. It has its own mirroring behavior in the PPU address range $3F00-$3FFF.

Tutorial simplification

At this stage, we do not need to split the Python storage into a separate PaletteRAM object yet. We can keep using the existing large VRAM backing array as the physical storage location, as long as PpuBus normalizes palette addresses before reading or writing.

Mental model

PPU.write_register($2007, value)
    -> ppu_bus.write(vram_addr, value)
    -> PpuBus detects whether vram_addr is palette memory
    -> PpuBus normalizes palette mirrors
    -> backing storage receives the byte

Palette mirroring rules for this step

$3F00-$3F1F is the 32-byte palette window
$3F20-$3FFF mirrors $3F00-$3F1F

Special backdrop mirrors

$3F10 mirrors $3F00
$3F14 mirrors $3F04
$3F18 mirrors $3F08
$3F1C mirrors $3F0C

Suggested implementation example

PALETTE_START = 0x3F00
PALETTE_END = 0x3FFF
PALETTE_SIZE = 0x20

def normalize_palette_addr(self, addr: int) -> int:
    index = (addr - PALETTE_START) % PALETTE_SIZE

    if index in (0x10, 0x14, 0x18, 0x1C):
        index -= 0x10

    return PALETTE_START + index

def read(self, addr: int) -> int:
    addr = addr & PPU_ADDRESS_MASK

    if CHR_START <= addr <= CHR_END:
        ...

    if PALETTE_START <= addr <= PALETTE_END:
        return self.vram.read(self.normalize_palette_addr(addr))

    return self.vram.read(addr)

def write(self, addr: int, value: int) -> None:
    addr = addr & PPU_ADDRESS_MASK

    if CHR_START <= addr <= CHR_END:
        ...
        return

    if PALETTE_START <= addr <= PALETTE_END:
        self.vram.write(self.normalize_palette_addr(addr), value)
        return

    self.vram.write(addr, value)

Important design choice

Palette mirror logic belongs in PpuBus, not PPU. PPU handles register behavior; PpuBus handles PPU memory address routing.

Out of scope

  • actual RGB colors
  • rendering
  • background/sprite palette selection
  • nametable attribute decoding
  • separating palette RAM into its own storage class

Run this lesson

uv run pytest tests/chapter_03_ppu_memory_and_graphics_data/test_252_ppu_bus_palette_ram_mapping.py -v