260. 基于时序生成 PPU 的 VBlank

基于 PPU 时序实现 VBlank 的生成。

260 / 356 · tests/chapter_04_ppu_timing_and_vblank/test_260_ppu_vblank_generation_from_timing.py

参考资料

https://www.nesdev.org/wiki/PPU_rendering#Line-by-line_timing
https://www.nesdev.org/wiki/PPU_registers#PPUSTATUS

需要更新的文件

emulator/ppu/ppu.py

需要添加的常量

PPU_VBLANK_START_SCANLINE = 241
PPU_PRE_RENDER_SCANLINE = 261

为什么需要这一步

许多 NES 程序在写入 PPU 内存之前会等待 VBlank。它们通常会轮询 PPUSTATUS($2002),直到第 7 位被置位。

本教程步骤的概念性行为

when the PPU enters scanline 241:
    set PPUSTATUS bit 7, VBLANK_STARTED

when the PPU enters scanline 261, the pre-render scanline:
    clear PPUSTATUS bit 7

这样可以在还没有实现完整渲染的情况下,给 ROM 提供一个基本的帧信号。

建议的实现示例

PPU_VBLANK_START_SCANLINE = 241
PPU_PRE_RENDER_SCANLINE = 261

def step(self, cycles: int = 1) -> None:
    for _ in range(cycles):
        self.cycle += 1

        if self.cycle >= PPU_CYCLES_PER_SCANLINE:
            self.cycle = 0
            self.scanline += 1

            if self.scanline >= PPU_SCANLINES_PER_FRAME:
                self.scanline = 0
                self.frame += 1

            # Simplified scanline-level timing event.
            if self.scanline == PPU_VBLANK_START_SCANLINE:
                self.status |= VBLANK_STARTED

            if self.scanline == PPU_PRE_RENDER_SCANLINE:
                self.status &= ~VBLANK_STARTED

重要的简化说明

这并不是逐点精确的 PPU 时序。在真实硬件上,状态变化发生在具体的 PPU 点(dot)/周期上,通常建模在相关扫描线的第 1 个点/周期附近。目前,本教程使用的是“进入扫描线时”的行为方式,因为目标是先讲清楚帧/VBlank 的概念,之后再追求完整的周期精度。

目前不属于本教程目标的内容

  • 逐点/逐周期精确的 VBlank 时序
  • 奇数帧的周期跳过
  • 在可见扫描线期间渲染像素
  • 0 号精灵命中(sprite 0 hit)
  • 精灵溢出
  • NMI 的产生/CPU 中断处理

运行本课

uv run pytest tests/chapter_04_ppu_timing_and_vblank/test_260_ppu_vblank_generation_from_timing.py -v