345. 记录可见扫描线滚动状态

在每条可见扫描线的点 1 记录有效视口状态。

345 / 356 · tests/chapter_13_scrolling/test_345_record_visible_scanline_scroll_state.py

要更新的文件

emulator/ppu/ppu.py

参考资料

https://www.nesdev.org/wiki/PPU_rendering#Cycles_1-256
https://www.nesdev.org/wiki/PPU_scrolling#Details

为什么需要这一步

现在 PPU 会按照水平和垂直渲染时序更新 v,但现有的高层渲染器是在整帧结束之后运行的。它需要一份简短的记录,记下代表每个可见屏幕行的有效地址。

一个不可变状态存储

vram_addr:
    the copied address represented by the prefetched pixels

fine_x:
    the separate 0-7 pixel offset inside the first tile

在点 1,真实的 v 超前两个图块列,因为点 321-336 已为当前扫描线预取了前两个图块。因此记录操作会执行:

copied visible v = real v rewound once, then rewound again

示例

current scanline = 20
real v coarse X  = 7

copied visible coarse X:
    7 -> 6 -> 5

stored destination:
    current_scanline_scroll_states[20]

回退改变的是水平图块位置,而不是扫描线索引。该状态属于当前扫描线 20,而不是扫描线 18。

记录条件

  • 背景或精灵渲染已启用
  • 扫描线可见:0-239
  • 当前点为 1

重要不变量

  • 缓冲区始终恰好有 240 个条目
  • 每个 PPU 拥有独立的缓冲区
  • 记录不会修改真实的 PPU.vram_addr
  • 细X单独存储
  • 后渲染、VBlank 和预渲染不会被记录

超出范围

  • 归档已完成的帧
  • 替换缺失的条目
  • 重置当前帧缓冲区
  • 帧缓冲或不透明度掩码合成

完整示例实现

# emulator/ppu/ppu.py

# --- NEW BLOCK: EFFECTIVE STATE FOR ONE VISIBLE SCANLINE ---
@dataclass(frozen=True)
class BackgroundScanlineState:
    vram_addr: int
    fine_x: int


@dataclass
class PPU:
    ...

    # --- NEW LINE: CURRENT FRAME'S VISIBLE SCANLINE STATES ---
    current_scanline_scroll_states: list[
        BackgroundScanlineState | None
    ] = field(default_factory=lambda: [None] * 240)

    ...

    # --- NEW BLOCK: RECORD THE CURRENT VISIBLE SCANLINE ---
    def _record_visible_scanline_scroll_state(self) -> None:
        rendering_enabled = self.mask & (
            MASK_SHOW_BACKGROUND | MASK_SHOW_SPRITES
        )
        if not rendering_enabled:
            return

        if not 0 <= self.scanline < 240:
            return

        if self.cycle != 1:
            return

        visible_vram_addr = decrement_horizontal_vram_addr(
            decrement_horizontal_vram_addr(self.vram_addr)
        )

        self.current_scanline_scroll_states[self.scanline] = (
            BackgroundScanlineState(
                vram_addr=visible_vram_addr,
                fine_x=self.fine_x,
            )
        )

    def step(self, cycles: int = 1) -> None:
        ...

        for _ in range(cycles):
            self.cycle += 1
            self._step_horizontal_rendering_address()
            self._step_vertical_rendering_address()

            # --- NEW LINE: RECORD THE CURRENT SCANLINE AT DOT 1 ---
            self._record_visible_scanline_scroll_state()

            ...

运行本课

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