250. Ppudata palette read exception

Implement the PPUDATA ($2007) palette read exception.

Lesson 250 of 356 · tests/chapter_03_ppu_memory_and_graphics_data/test_250_ppudata_palette_read_exception.py

Reference

https://www.nesdev.org/wiki/PPU_registers#PPUDATA

File to update

emulator/ppu/ppu.py

Constants to add

PALETTE_START_ADDR = 0x3F00
PALETTE_END_ADDR = 0x3FFF

Why this step exists

Normal PPUDATA reads are buffered

read $2007:
    return old ppu_data_buffer
    reload ppu_data_buffer from ppu_bus.read(vram_addr)
    increment vram_addr

Palette reads are the exception. When vram_addr points to palette memory:

$3F00-$3FFF

the PPU returns the palette byte immediately instead of returning the old buffer.

Important detail

The old buffer is still discarded. On normal NES PPUs, the buffer is reloaded from the shadowed nametable memory behind the palette address:

$3F00 -> reload buffer from $2F00
$3F10 -> reload buffer from $2F10

That means palette reads affect both

  • the value returned to the CPU
  • the internal ppu_data_buffer used by future normal reads

Suggested implementation pseudocode, matching the explicit comment style:

PALETTE_START_ADDR = 0x3F00
PALETTE_END_ADDR = 0x3FFF

case 0x2007:  # PPU DATA read
    # Palette data is returned immediately.
    if PALETTE_START_ADDR <= self.vram_addr <= PALETTE_END_ADDR:
        value = self.ppu_bus.read(self.vram_addr)

        # Read buffer is discarded and reloaded from shadowed memory:
        # vram_addr - 0x1000
        # Example: 0x3F00 -> 0x2F00
        self.ppu_data_buffer = self.ppu_bus.read(self.vram_addr - 0x1000)
    else:
        # Normal PPUDATA reads return the old buffer first.
        value = self.ppu_data_buffer

        # Then the buffer is reloaded from current PPU memory.
        self.ppu_data_buffer = self.ppu_bus.read(self.vram_addr)

    increment = 32 if self.ctrl & CTRL_VRAM_INCREMENT_BY_32 else 1

    # Keep vram_addr in the 14-bit PPU address range.
    self.vram_addr = (self.vram_addr + increment) & 0x3FFF

    # Preserve self.data for old test compatibility/debugging.
    self.data = value
    return self.data

Out of scope

  • palette RAM mirroring, such as $3F10 -> $3F00
  • accurate palette color values
  • rendering

Run this lesson

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