328. PPUCTRL 临时名称表位

将 PPUCTRL 的基础名称表选择复制到 temp_vram_addr。

328 / 356 · tests/chapter_13_scrolling/test_328_ppuctrl_temp_nametable_bits.py

需要更新的文件

emulator/ppu/ppu.py

参考文档

https://www.nesdev.org/wiki/PPU_scrolling#$2000_(PPUCTRL)_write
https://www.nesdev.org/wiki/PPU_registers

该步骤存在的原因

PPU 卷动地址由若干个 CPU 可见的寄存器写入组装而成。本项目已经处理了 $2005 PPUSCROLL 的大部分部分:

coarse X -> temp_vram_addr bits 0-4
coarse Y -> temp_vram_addr bits 5-9
fine Y   -> temp_vram_addr bits 12-14
fine X   -> separate fine_x field

缺失的部分是逻辑基础名称表选择

PPUCTRL bits 0-1 -> temp_vram_addr bits 10-11

没有这个连接,temp_vram_addr 的行为就好像卷动总是从逻辑名称表 0($2000)开始一样,即使游戏软件选择的是 $2400、$2800 或 $2C00。这样一来,未来的视口渲染器就会跨越错误的逻辑边界。

内部卷动布局

temp_vram_addr (t): yyy NN YYYYY XXXXX

XXXXX -> coarse X tile position
YYYYY -> coarse Y tile position
NN    -> logical nametable selection
yyy   -> fine Y pixel position

fine_x (x) is stored separately

PPUCTRL 映射

bits 0-1 = 00 -> logical nametable $2000
bits 0-1 = 01 -> logical nametable $2400
bits 0-1 = 10 -> logical nametable $2800
bits 0-1 = 11 -> logical nametable $2C00

建议的实现

# emulator/ppu/ppu.py

case 0x2000:
    self.ctrl = value

    # --- NEW LINE: COPY BASE NAMETABLE INTO temp_vram_addr: ...GH.. ........ <- value: ......GH
    self.temp_vram_addr = (self.temp_vram_addr & 0b1111_0011_1111_1111) | ((value & CTRL_BASE_NAMETABLE_MASK) << 10)
    # --- END NEW LINE ---

为什么要先清除第 10-11 位?游戏可能会从任意一个逻辑名称表切换到任意另一个逻辑名称表。仅使用 OR 运算只能设置新位,而无法清除旧位。先清除再插入的操作可以替换之前的选择,同时保留粗略 X、粗略 Y 和精细 Y。

为什么不使用旧的 scroll 字段?$2005 会接收两次写入,而兼容性 scroll 字段只存储最近的一个字节。temp_vram_addr 加上 fine_x 才能保留完整的硬件风格状态。

重要区别

本步骤只是记录逻辑名称表的选择。之后卡带镜像会将该逻辑选择映射到物理名称表 RAM 上。这两个操作本身都不会移动可见视口;视口提取/渲染将在之后的步骤中实现。

不在本步骤范围内

  • 推导视口像素 X/Y
  • 渲染相邻名称表
  • 在精确的硬件时钟点将 t 传输到 v
  • 修改 PpuBus 的镜像
  • 商业 ROM 测试夹具

运行本课

uv run pytest tests/chapter_13_scrolling/test_328_ppuctrl_temp_nametable_bits.py -v