340. 水平 VRAM 时序

在 PPU 时序点应用水平 v 递增和水平 t 到 v 复制。

340 / 356 · tests/chapter_13_scrolling/test_340_horizontal_vram_timing.py

待更新文件

emulator/ppu/ppu.py

参考

https://www.nesdev.org/wiki/PPU_scrolling#During_rendering
https://www.nesdev.org/wiki/PPU_rendering#Line-by-line_timing

此步骤存在的原因

步骤 338 和 339 实现了纯粹的水平地址机制。此步骤将它们与 PPU 时间连接起来。

在渲染已启用的可见扫描线和预渲染扫描线上

dots 8, 16, 24, ... 256:
    increment horizontal v after each fetched tile

dot 257:
    copy coarse X and horizontal nametable from t into v

dots 328 and 336:
    increment horizontal v while prefetching the first tiles for the next scanline

只要 PPUMASK 中的背景渲染或精灵渲染有一项被启用,渲染就处于启用状态。地址时序在后渲染和 VBlank 扫描线期间处于非活动状态。

简化时间线

1---------256 257 --------320 321------336 ----340
tile fetches  reload X          prefetch tiles
  ^ every 8                     ^ 328 and 336

重要的顺序

PPU.step() 首先推进 self.cycle。得到的值被当作当前点,然后应用水平地址时序。因此,一个从第 256 点开始的 PPU,在一次 step 之后会在第 257 点执行水平重载。

常见误解

水平递增并不局限于可见像素。第 328 点和第 336 点为下一条扫描线准备前两个背景图块,同时也会递增 v。

范围之外

  • 第 256 点的垂直递增
  • 预渲染期间的垂直 t 到 v 复制
  • 扫描线视口记录
  • 帧缓冲区和不透明度掩码的更改

完整示例实现

# emulator/ppu/ppu.py

class PPU:
    ...

    # --- NEW BLOCK: APPLY HORIZONTAL ADDRESS TIMING ---
    def _step_horizontal_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

        visible_fetch_increment = (
            1 <= self.cycle <= 256
            and self.cycle % 8 == 0
        )
        prefetch_increment = (
            321 <= self.cycle <= 336
            and self.cycle % 8 == 0
        )

        if visible_fetch_increment or prefetch_increment:
            self.vram_addr = increment_horizontal_vram_addr(
                self.vram_addr
            )

        if self.cycle == 257:
            self.vram_addr = copy_horizontal_scroll_bits(
                self.vram_addr,
                self.temp_vram_addr,
            )

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

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

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

            ...

运行本课

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