348. 扫描线视口 x

从一条被记录的扫描线状态解码水平视口X。

348 / 356 · tests/chapter_13_scrolling/test_348_scanline_viewport_x.py

要更新的文件

emulator/rendering/ppu_background_renderer.py

参考资料

https://www.nesdev.org/wiki/PPU_scrolling

为什么需要这一步

第 347 步选择了构成水平源对的两个逻辑命名表。后续的行合成器还需要知道这条扫描线在那个 512 像素宽的对内部开始的确切像素位置。

被记录的地址使用与 t 相同的打包滚动布局

yyy NN YYYYY XXXXX

水平视口X使用

XXXXX          coarse X tile column
low N bit      horizontal nametable selection, vram_addr bit 10
state.fine_x   pixel offset inside the first tile

像素换算

viewport_x = nametable_x * 256 + coarse_x * 8 + fine_x

示例

nametable X = 1
coarse X    = 5
fine X      = 3

viewport X = 1 * 256 + 5 * 8 + 3 = 299

为什么要复用 decode_background_viewport_position?现有的纯函数解码器已经拥有打包滚动字段的换算逻辑。t 和被记录的 v 具有相同的位布局,因此在这个渲染器里重复这些掩码和尺寸,会产生两个可能各自漂移的实现。

重要不变量

  • 水平命名表的第 10 位贡献 256 像素
  • 粗X每个图块贡献八个像素
  • 细X贡献最后的 0-7 像素偏移
  • 垂直命名表、粗Y和细Y不影响视口X
  • 结果保持在逻辑水平范围 0-511 之内
  • 被记录的地址不会再次被回退
  • 该辅助函数不执行任何渲染或内存访问

常见误解

被记录的地址不需要再一次两图块校正。第 345 步在构造 BackgroundScanlineState 之前,已经把 v 的一个副本回退了两次。在这里重复回退会错误地让每个渲染行向左移动十六个像素。

超出范围

  • 选择逻辑命名表地址
  • 合成帧缓冲行
  • 对目标像素应用水平回绕
  • 不透明度掩码合成
  • 在定时数据与旧的帧级备用路径之间做选择

完整示例实现

# emulator/rendering/ppu_background_renderer.py

# --- NEW BLOCK: DECODE ONE SCANLINE'S HORIZONTAL VIEWPORT ---
def _scanline_viewport_x(state: BackgroundScanlineState) -> int:
    viewport_x, _ = decode_background_viewport_position(
        temp_vram_addr=state.vram_addr,
        fine_x=state.fine_x,
    )

    return viewport_x

运行本课

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