268. Console.step() 推进 PPU

让 Console.step() 根据 CPU 周期数推进 PPU 的时间。

268 / 356 · tests/chapter_04_ppu_timing_and_vblank/test_268_console_step_advances_ppu.py

参考资料

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

需要更新的文件

emulator/console.py

此步骤存在的原因

模拟器现在已经具备了把 CPU 时间和 PPU 时间连接起来所需的组成部分

CPU.step() returns base CPU cycles
PPU.step(cycles) advances PPU timing counters
Console.consume_nmi_if_requested() connects PPU NMI requests to CPU NMI

Console.step() 正是把这些组成部分整合成一次机器级步进的地方。

什么是机器级步进?机器级步进是指按照多个模拟芯片之间的时钟关系,将它们一起向前推进。

最基本的 NES 时序规则

1 CPU cycle = 3 PPU cycles

最小示例

CPU executes NOP
NOP takes 2 CPU cycles
PPU advances 2 * 3 = 6 PPU cycles

常见误解

不要让 CPU.step() 去调用 PPU.step()。CPU 不应该拥有视频时序。芯片之间的协调应该由 Console 来负责。

建议的实现示例

@dataclass
class Console:
    cpu: CPU
    ppu: PPU

    def consume_nmi_if_requested(self) -> None:
        if not self.ppu.nmi_requested:
            return

        self.ppu.nmi_requested = False
        self.cpu.interrupt_nmi()

    def step(self) -> int:
        # This is base timing only. Later steps can add branch penalties,
        # NMI latency, DMA stalls, and other timing details.
        cpu_cycles = self.cpu.step()
        self.ppu.step(cpu_cycles * 3)
        self.consume_nmi_if_requested()
        return cpu_cycles

重要限制

这仍然是简化后的时序。目前 CPU.step() 只返回基本周期数。

以后的改进可以考虑

branch taken extra cycles
branch page-cross extra cycles
indexed addressing page-cross extra cycles
NMI latency
OAM DMA stalls

不在本步骤范围内

  • 动态的 CPU 周期惩罚
  • 逐点精确的 NMI 延迟
  • OAM DMA
  • 渲染像素
  • pygame/前端循环
  • 手柄输入

运行本课

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