249. Ppudata read buffer

Implement PPUDATA ($2007) buffered read behavior.

Lesson 249 of 356 · tests/chapter_03_ppu_memory_and_graphics_data/test_249_ppudata_read_buffer.py

Reference

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

File to update

emulator/ppu/ppu.py

State to add

ppu_data_buffer: int = 0

Why this step exists

PPUDATA is not a normal simple register. It is a CPU-visible port into PPU memory. For most PPU memory reads, the NES returns the old internal read buffer, then reloads that buffer from the current PPU memory address.

Normal buffered read behavior

read $2007:
    value = ppu_data_buffer
    ppu_data_buffer = ppu_bus.read(vram_addr)
    vram_addr += increment
    return value

This means the first read usually returns stale/old buffer data, and the second read returns the byte that was loaded by the first read.

Example

ppu_bus[$2000] = $AA
ppu_bus[$2001] = $BB
ppu_data_buffer = $00
vram_addr = $2000

read $2007 -> returns $00, buffer becomes $AA, vram_addr becomes $2001
read $2007 -> returns $AA, buffer becomes $BB, vram_addr becomes $2002

Compatibility/debug field

The older tutorial model had data as a simple PPUDATA value. Keep self.data as a compatibility/debug field containing the value returned by the latest

PPUDATA read, but the real behavior should use

ppu_data_buffer
ppu_bus.read(vram_addr)

Suggested implementation pseudocode

@dataclass
class PPU:
    ...
    data: int = 0  # Compatibility/debug: last PPUDATA value
    ppu_data_buffer: int = 0

    def read_register(self, addr: int) -> int:
        match addr:
            ...
            case 0x2007:
                value = self.ppu_data_buffer
                self.ppu_data_buffer = self.ppu_bus.read(self.vram_addr)

                increment = 32 if self.ctrl & CTRL_VRAM_INCREMENT_BY_32 else 1
                self.vram_addr = (self.vram_addr + increment) & 0x3FFF

                # Preserve old compatibility/debug field.
                self.data = value
                return self.data

Important future note

Palette reads from $3F00-$3FFF are an exception on real hardware. They return the palette byte immediately instead of returning the delayed buffer value. That exception is intentionally not implemented in this test.

Out of scope

  • palette read exception
  • palette RAM accuracy
  • PPUDATA writes, already tested earlier

Run this lesson

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