252. PPU 总线的调色板 RAM 地址映射
实现 PpuBus 的调色板 RAM 地址映射。
第 252 / 356 · tests/chapter_03_ppu_memory_and_graphics_data/test_252_ppu_bus_palette_ram_mapping.py
参考资料
https://www.nesdev.org/wiki/PPU_palettes
https://www.nesdev.org/wiki/PPU_memory_map需要更新的文件
emulator/bus/ppu_bus.py需要添加的常量
PALETTE_START = 0x3F00
PALETTE_END = 0x3FFF
PALETTE_SIZE = 0x20为什么需要这一步
PPU 地址范围 $3F00-$3FFF 是调色板内存。调色板内存不存储图块图形数据,而是存储供后续背景和精灵渲染使用的 NES 颜色索引。
硬件说明
在原版 NES 的 PPU 中,调色板 RAM 是 PPU 内部一块独立的 32 字节小内存区域。它既不是 CHR ROM,也不是名称表 VRAM。它在 PPU 地址范围 $3F00-$3FFF 中有自己的镜像规则。
教程简化
在这个阶段,我们还不需要把 Python 存储拆分成独立的 PaletteRAM 对象。只要 PpuBus 在读写之前先对调色板地址做归一化处理,我们仍然可以继续使用现有的大型 VRAM 后备数组作为物理存储位置。
思维模型
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本步骤的调色板镜像规则
$3F00-$3F1F is the 32-byte palette window
$3F20-$3FFF mirrors $3F00-$3F1F特殊的背景色镜像
$3F10 mirrors $3F00
$3F14 mirrors $3F04
$3F18 mirrors $3F08
$3F1C mirrors $3F0C建议的实现示例
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)重要的设计选择
调色板镜像逻辑应该放在 PpuBus 中,而不是 PPU 中。PPU 负责寄存器行为,PpuBus 负责 PPU 内存地址的路由。
不在本步骤范围内
- 实际的 RGB 颜色
- 渲染
- 背景/精灵调色板的选择
- 名称表属性解码
- 把调色板 RAM 拆分成独立的存储类
运行本课
uv run pytest tests/chapter_03_ppu_memory_and_graphics_data/test_252_ppu_bus_palette_ram_mapping.py -v