347. 扫描线水平对

为一条被记录的扫描线选择水平逻辑命名表对。

347 / 356 · tests/chapter_13_scrolling/test_347_scanline_horizontal_pair.py

要更新的文件

emulator/rendering/ppu_background_renderer.py

参考资料

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

为什么需要这一步

已完成的定时帧为每条可见扫描线存储一个有效的 vram_addr 和细X值。在后续渲染器能够复制该扫描线的像素之前,它必须知道是哪两个水平相邻的逻辑命名表构成了源对。

vram_addr 内部的命名表选择字段为:

bit 10: horizontal nametable position inside a 512-pixel-wide pair
bit 11: vertical nametable row that selects the pair

因此配对选择只使用第 11 位

bit 11 clear: ($2000, $2400)
bit 11 set:   ($2800, $2C00)

直观模型

upper logical row:  $2000 | $2400
lower logical row:  $2800 | $2C00

该辅助函数选择一个完整的行。后续步骤将使用第 10 位、粗X和细X来选择该行内的水平像素位置。

重要不变量

  • 只有 vram_addr 的第 11 位影响所选的对
  • 第 10 位不改变该对
  • 粗X、粗Y、细Y和细X不改变该对
  • 返回值是逻辑 PPU 命名表地址
  • 卡带镜像仍属于 PpuBus 的行为
  • 该辅助函数是纯函数,不执行任何渲染或内存访问

常见误解

第 10 位并不在上、下两个水平对之间做选择。它标识该对内部左侧或右侧的水平命名表,并将在下一课中成为视口X计算的一部分。

超出范围

  • 解码水平视口X
  • 读取命名表或图案表字节
  • 应用卡带镜像
  • 合成帧缓冲行
  • 在已完成的定时数据与旧的备用路径之间做选择

完整示例实现

# emulator/rendering/ppu_background_renderer.py

# --- UPDATED LINES: IMPORT THE RECORDED SCANLINE VALUE ---
from emulator.ppu.ppu import (
    BackgroundScanlineState,
    CTRL_BACKGROUND_PATTERN_TABLE,
    PPU,
)

...

# --- NEW BLOCK: SELECT ONE SCANLINE'S LOGICAL HORIZONTAL PAIR ---
def _scanline_horizontal_pair(
    state: BackgroundScanlineState,
) -> tuple[int, int]:
    nametable_y = (state.vram_addr >> 11) & 1
    left_base = BASE_NAMETABLE_ADDR + nametable_y * 0x0800

    return left_base, left_base + 0x0400

运行本课

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