337. 规划带时序的 VRAM 滚动
在修改 PPU 步进之前,理解带时序的 v/t/x 滚动方案。
第 337 / 356 · tests/chapter_13_scrolling/test_337_plan_timed_vram_scrolling.py
阅读后需确认的文件
emulator/ppu/ppu.py参考
https://www.nesdev.org/wiki/PPU_scrolling
https://www.nesdev.org/wiki/PPU_scrolling#During_rendering
https://www.nesdev.org/wiki/PPU_rendering#Line-by-line_timing此阅读步骤存在的原因
PPU.step() 是一个对时序高度敏感的子系统。一个小失误就可能产生一幅看起来几乎正确、却使用了错误扫描线、名称表或滚动位置的画面。在修改它之前,我们需要一个稳定的心智模型和一个清晰的迁移方案。
当前的问题
现有的帧级视口在整帧结束后解码 temp_vram_addr(t)加细 X。这对简单的合成滚动可行,但对在一帧内改变滚动位置的游戏则会失效。
《超级马里奥兄弟》大致采用以下形式:
visible rows 0-30:
fixed status-bar scroll
sprite-zero split:
CPU prepares another horizontal position
visible rows 31-239:
moving gameplay scroll一帧结束后采样的单个值无法同时描述这两个区域。
还存在第二个问题:t 并非专用于滚动的值。$2006(PPUADDR)在游戏加载名称表和调色板时也会写入 t。例如:
CPU writes PPUADDR $3F00
-> t becomes $3F00把这个最终值当作滚动来解码,可能会短暂地选中错误的视口。这就是为什么启动或关卡切换时,屏幕全黑却看起来在快速滚动的原因。
四个内部滚动值
v = current VRAM address used by rendering
t = temporary VRAM address prepared by CPU register writes
x = fine horizontal pixel offset
w = first/second-write toggle for $2005 and $2006直观模型
t is the next address configuration being prepared.
v is the address currently moving through rendering.
x is the 0-7 pixel offset inside the first tile.
w remembers which half of a two-write register comes next.常见误解
t is the current scroll position for the entire frame.正确模型
CPU writes assemble t and x.
PPU timing copies selected fields from t into v.
Rendering advances v while tiles and scanlines are processed.重要的带时序操作
background-fetch dots, every 8 dots:
increment horizontal v
dot 256:
increment vertical v
dot 257:
copy horizontal fields from t into v
pre-render dots 280-304:
copy vertical fields from t into v水平字段
coarse X
horizontal nametable bit垂直字段
coarse Y
fine Y
vertical nametable bit为什么复制是有选择性的
在第 257 点,只应刷新下一条扫描线的水平位置。复制整个 t 也会在错误的时机替换垂直状态。
为什么我们按扫描线记录
现有的名称表渲染器已经能生成正确的 RGB 源帧缓冲区。我们尚不需要用一个完整的逐点像素抓取流水线来替换它。相反,PPU 时序将为每条可见扫描线记录一次有效的 v + x 位置:
scanline 0 -> viewport X 0
scanline 1 -> viewport X 0
...
scanline 30 -> viewport X 0
scanline 31 -> viewport X 40
...在整帧结束后,高层渲染器在恰好一次地复制每个输出行时使用这些记录的位置。
预取细节
在第 1 点,v 实际上已经领先两个图块,因为第 321-336 点已经把前两个背景图块抓取到了硬件移位寄存器中。扫描线快照必须补偿这两次粗 X 递增,才能推导出可见视口。否则整个背景看起来会偏移 16 像素。
兼容性方案
- 保持现有的 v、t、x、w 寄存器行为和公开的渲染辅助函数不变
- 在把纯地址操作应用到 PPU.step() 内部之前,先添加这些操作
- 在迁移期间保留旧的帧级视口路径作为回退方案
- 只有在存在完整一帧的状态之后,才添加带时序的逐扫描线渲染
- 保持帧缓冲区和不透明度掩码的行选择方式不变
- 在每个编号步骤之后运行
uv run pytest
接下来的专项步骤
- 纯水平 v 递增与名称表环绕
- 纯水平 t 到 v 字段复制
- 水平抓取点递增与第 257 点复制
- 带细 Y 及第 29-31 行的纯垂直递增
- 纯垂直 t 到 v 字段复制
- 第 256 点的垂直递增与预渲染阶段的垂直复制
- 为每条可见扫描线记录有效的 v + 细 X 状态
- 按行限定的帧缓冲区组合
- 完全一致的按行限定不透明度掩码组合
- 用于 sprite-zero-hit 调度的、感知扫描线的掩码
- 手动验证《超级马里奥兄弟》的状态栏、移动、过场和帧率
精度边界
这种方案对滚动地址的时序和扫描线视口选择进行建模。它还不是一个完整的逐点背景抓取/移位寄存器渲染器。CPU 寄存器写入也仍以当前的指令步进粒度计时。
阅读后需要执行的操作
在 emulator/ppu/ppu.py 中,紧邻 vram_addr、temp_vram_addr、fine_x 和 second_write_toggle 字段处添加以下这段注释:
# pass_test_337此步骤中不要实现任何滚动更改。该注释是通过测试 337 所需的唯一生产文件改动。
运行本课
uv run pytest tests/chapter_13_scrolling/test_337_plan_timed_vram_scrolling.py -v