249. Ppudata 读取缓冲区

实现 PPUDATA ($2007) 的缓冲读取行为。

249 / 356 · tests/chapter_03_ppu_memory_and_graphics_data/test_249_ppudata_read_buffer.py

参考资料

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

要更新的文件

emulator/ppu/ppu.py

要添加的状态

ppu_data_buffer: int = 0

为什么需要这一步

PPUDATA 不是普通的简单寄存器。它是一个通向 PPU 内存的 CPU 可见端口。对于大多数 PPU 内存读取,NES 会返回旧的内部读取缓冲区,然后再从当前的 PPU 内存地址重新加载该缓冲区。

正常的缓冲读取行为

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

这意味着第一次读取通常返回的是过时/陈旧的缓冲区数据,而第二次读取返回的是由第一次读取所加载的那个字节。

示例

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

兼容/调试字段

较早的教程模型曾把 data 当作一个简单的 PPUDATA 值。请把 self.data 保留为一个兼容/调试字段,其中包含最近一次

PPUDATA 读取所返回的值,但真实的行为应当使用

ppu_data_buffer
ppu_bus.read(vram_addr)

建议的实现伪代码

@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

关于未来的重要说明

在真实硬件上,从 $3F00-$3FFF 进行的调色板读取是一个例外。这类读取会立即返回调色板字节,而不是返回延迟的缓冲区值。本测试有意不实现该例外。

超出范围

  • 调色板读取例外
  • 调色板 RAM 精度
  • PPUDATA 写入,此前已测试过

运行本课

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