323. Console 按帧调度精灵零命中

在 Console 推进一帧时自动调度精灵 0 命中。

323 / 356 · tests/chapter_11_sprite_zero_hit/test_323_console_schedules_sprite_zero_hit_per_frame.py

需要更新的文件

emulator/console.py

该步骤存在的原因

前面几步已经提供了所有必需的机制

ppu_sprite_zero_hit_position(ppu)
    -> extracts current PPU state and finds the overlap position

ppu.set_sprite_zero_hit_position(position)
    -> stores the future timing event

ppu.step(...)
    -> sets PPUSTATUS bit 6 when timing reaches that position

本步骤将它们连接到帧循环中。

架构决策

我们有意将精灵零命中的准备工作放在 Console.step_until_next_frame() 内部。像 main.py 这样的调用方应当只是请求 Console 推进完整的一个模拟帧,而无需知道哪些内部 PPU 时序事件必须提前准备好。

这使 main.py 专注于前端职责:

input
display
FPS reporting
frame pacing

Console 负责帧级别的模拟器协调。

建议的实现改动

# --- NEW LINE ---
from emulator.rendering.sprite_zero_hit import ppu_sprite_zero_hit_position
# --- END NEW LINE ---


def step_until_next_frame(
    self,
    max_cpu_instructions: int | None = None,
) -> int:
    # --- NEW BLOCK ---
    position = ppu_sprite_zero_hit_position(self.ppu)
    self.ppu.set_sprite_zero_hit_position(position)
    # --- END NEW BLOCK ---

    start_frame = self.ppu.frame
    executed = 0

    while self.ppu.frame == start_frame:
        ...

为什么要放在步进循环之前?CPU 可能会在模拟该帧的过程中轮询 PPUSTATUS。必须在 CPU/PPU 执行到达重叠像素之前调度好这次未来的命中。

手动兼容性检查点

完成本步骤后,学生可以临时将 main.py 中本地手动 ROM 路径改为自己合法拥有的副本:

main.py 中建议的实现改动:

ROM_PATH = Path("Super Mario Bros.nes")

然后用 PyPy 运行

Linux/macOS

sh launcher.sh

Windows 命令提示符

launcher.cmd

预期的手动改进效果

《超级马里奥兄弟》使用精灵 0 命中作为 PPU 时序信号。现在该命中已被检测、调度并通过 PPUSTATUS 暴露出来,标题/菜单界面应能进一步推进,马里奥应会出现,手柄输入也应变得可用。

已知的遗留限制

当马里奥水平前进时,场景可能仍然显示不正确,因为当前的背景渲染器尚未应用 PPU 卷动状态来选择和偏移可见的名称表区域。精灵 0 命中启用了游戏的时序路径,但并未实现水平背景卷动。

合法性/测试规则

Super Mario Bros.nes 仅用于用户自行提供的手动兼容性实验。不要提交该 ROM,也不要在自动化测试中要求使用它。这些测试使用伪造的 CPU/PPU 对象,仅检查协调行为。

不在本步骤范围内

  • 水平/垂直卷动
  • 精细 X 卷动
  • 相邻名称表的合成
  • 精确的 OAM Y+1 行为
  • PPUMASK 左边缘规则
  • x=255 精灵 0 命中例外
  • 商业 ROM 测试夹具

运行本课

uv run pytest tests/chapter_11_sprite_zero_hit/test_323_console_schedules_sprite_zero_hit_per_frame.py -v