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 pacingConsole 负责帧级别的模拟器协调。
建议的实现改动
# --- 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.shWindows 命令提示符
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