343. 垂直 vram 时序

在 PPU 的时序点上应用垂直的 v 递增和垂直的 t 到 v 复制。

343 / 356 · tests/chapter_13_scrolling/test_343_vertical_vram_timing.py

要更新的文件

emulator/ppu/ppu.py

参考资料

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

为什么需要这一步

第 341 步和第 342 步实现了纯函数式的垂直地址机制。本步骤将它们接入 PPU 时序。

当渲染启用时

visible and pre-render scanlines, dot 256:
    increment fine/coarse Y in v

pre-render scanline, every dot 280-304:
    copy fine Y, vertical nametable, and coarse Y from t into v

简化时序

visible scanline:
    ... 255 256 257 ...
            |   |
            |   +-- horizontal reload
            +------ vertical increment

pre-render scanline 261:
    ... 256 257 ... 280----------------304 ...
        |   |       |
        |   |       +-- vertical reload active at every dot
        |   +---------- horizontal reload
        +-------------- vertical increment

为什么在点 280-304 的每个点都重复垂直重载?如果 CPU 写入在该区间内改变了 t,后面的点就可以复制到较新的垂直状态。把该区间当作点 280 处的一次操作,会丢失这种行为。

当 PPUMASK 的背景位或精灵位任一置位时,渲染即处于启用状态。后渲染扫描线和 VBlank 扫描线不会执行这些自动更新。

点 256 处的重要顺序

水平获取递增先运行,垂直递增后运行。它们各自负责不同的字段,因此两处更改都必须体现在最终的 v 中。

超出范围

  • 逐扫描线的有效视口记录
  • 两图块预取补偿
  • 帧缓冲行合成
  • 不透明度掩码行合成

完整示例实现

# emulator/ppu/ppu.py

class PPU:
    ...

    # --- NEW BLOCK: APPLY VERTICAL ADDRESS TIMING ---
    def _step_vertical_rendering_address(self) -> None:
        rendering_enabled = self.mask & (
            MASK_SHOW_BACKGROUND | MASK_SHOW_SPRITES
        )
        if not rendering_enabled:
            return

        rendering_scanline = (
            0 <= self.scanline < 240
            or self.scanline == PPU_PRE_RENDER_SCANLINE
        )
        if not rendering_scanline:
            return

        if self.cycle == 256:
            self.vram_addr = increment_vertical_vram_addr(
                self.vram_addr
            )

        vertical_reload = (
            self.scanline == PPU_PRE_RENDER_SCANLINE
            and 280 <= self.cycle <= 304
        )
        if vertical_reload:
            self.vram_addr = copy_vertical_scroll_bits(
                self.vram_addr,
                self.temp_vram_addr,
            )

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

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

            # --- NEW LINE: APPLY VERTICAL TIMING AT THE CURRENT DOT ---
            self._step_vertical_rendering_address()

            ...

运行本课

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