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_mapFile to update
emulator/bus/ppu_bus.pyConstants to add
PALETTE_START = 0x3F00
PALETTE_END = 0x3FFF
PALETTE_SIZE = 0x20Why 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 bytePalette mirroring rules for this step
$3F00-$3F1F is the 32-byte palette window
$3F20-$3FFF mirrors $3F00-$3F1FSpecial backdrop mirrors
$3F10 mirrors $3F00
$3F14 mirrors $3F04
$3F18 mirrors $3F08
$3F1C mirrors $3F0CSuggested 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