287. Console 步进到下一帧

添加 Console.step_until_next_frame() 用于按帧步进。

287 / 356 · tests/chapter_05_rendering_pipeline/test_287_console_step_until_next_frame.py

待更新文件

emulator/console.py

为什么需要这一步

Console 已经有一个单指令步进方法

console.step()

那是这个模拟器中最小的机器时间操作单元。它执行一条 CPU 指令,让 PPU 前进 CPU 周期数 * 3,然后处理任何待处理的 NMI。

手动运行器和未来的前端通常需要一个更大粒度的操作

run emulation until one full PPU frame completes
then ask for a framebuffer explicitly

这一步添加了

console.step_until_next_frame(max_cpu_instructions: int | None = None) -> int

实现示例

def step_until_next_frame(
    self,
    max_cpu_instructions: int | None = None,
) -> int:
    start_frame = self.ppu.frame
    executed = 0

    while self.ppu.frame == start_frame:
        if max_cpu_instructions is not None:
            if executed >= max_cpu_instructions:
                raise RuntimeError("Frame did not complete before instruction limit")

        self.step()
        executed += 1

    return executed

step() 和 step_until_next_frame() 的区别:

step()
    executes exactly one CPU instruction
    advances PPU by that instruction's cycles * 3
    returns CPU cycles for that instruction

step_until_next_frame()
    calls step() repeatedly until ppu.frame changes
    returns how many CPU instructions were executed

用法示例

console.step_until_next_frame()
framebuffer = console.render_background_framebuffer()

为什么 max_cpu_instructions 是可选的:这个参数不是 NES 硬件行为,而是模拟器调试/测试用的保护措施。

传入 None 时,没有人为的指令数限制,这在真实执行或手动执行时很有用:

console.step_until_next_frame()

传入整数时,如果执行了这么多条 CPU 指令仍未产生新的一帧,辅助函数就会抛出异常。这在测试和调试中很有用,因为它可以防止在 CPU 卡住、缺少某个操作码或某一帧永远无法完成时陷入死循环:

console.step_until_next_frame(max_cpu_instructions=10)

重要的职责分离

step_until_next_frame()
    advances emulation time

render_background_framebuffer()
    observes current PPU memory and returns Framebuffer data

不要在 step_until_next_frame() 内部自动进行渲染。

范围之外

  • pygame 显示
  • 精灵
  • OAMDMA
  • 精确的 NMI 延迟
  • 动态的 CPU 周期惩罚
  • 手柄输入

运行本课

uv run pytest tests/chapter_05_rendering_pipeline/test_287_console_step_until_next_frame.py -v