318. 主循环帧节奏控制
将手动前端循环限制在 NES NTSC 速度。
第 318 / 356 · tests/chapter_10_performance/test_318_main_frame_pacing.py
需要更新的文件
main.py为什么需要这一步
在采用了更快的 pygame 帧缓冲上传方式以及可选的 PyPy 启动脚本之后,手动运行的模拟器可能会比真实 NES 运行得更快。在教程开发过程中,未加限制的路径大约达到了 90-120 FPS。
运行得更快是有用的性能证据,但这会让游戏玩法、动画和输入时序都变得过快。当一帧提前完成时,前端应当等待。
关键术语:帧节奏 帧节奏是指当模拟出的一帧比目标的实际时钟时长更快完成时,延迟其呈现的做法。
目标 NTSC 时序
NES_NTSC_FPS = 60.0988
TARGET_FRAME_SECONDS = 1.0 / NES_NTSC_FPS一个目标帧大约是
16.64 milliseconds示例实现
# --- NEW BLOCK: NES FRAME-TIME TARGET ---
NES_NTSC_FPS = 60.0988
TARGET_FRAME_SECONDS = 1.0 / NES_NTSC_FPS
# --- END NEW BLOCK ---
...
while running:
# --- NEW LINE: START THIS FRAME'S TIMER ---
frame_start_time = time.perf_counter()
# --- END NEW LINE ---
for event in pygame.event.get():
...
executed = console.step_until_next_frame()
framebuffer = console.render_framebuffer()
draw_framebuffer(window, framebuffer, SCALE)
pygame.display.flip()
# --- NEW BLOCK: WAIT FOR THE UNUSED FRAME BUDGET ---
# Sleep only for the unused part of this frame's time budget.
frame_end_time = time.perf_counter()
frame_elapsed_time = frame_end_time - frame_start_time
wait_time = TARGET_FRAME_SECONDS - frame_elapsed_time
if wait_time > 0:
time.sleep(wait_time)
# --- END NEW BLOCK ---
# --- UPDATED BLOCK: MEASURE FPS AFTER PACING ---
# Measure FPS after sleeping so the report shows paced gameplay speed.
frames_since_last_report += 1
now = time.perf_counter()
elapsed = now - last_fps_report_time
if elapsed >= FPS_REPORT_INTERVAL_SECONDS:
fps = frames_since_last_report / elapsed
print(f"fps={fps:.1f}")
frames_since_last_report = 0
last_fps_report_time = now
# --- END UPDATED BLOCK ---重要的不变量
Fast frame:
frame_elapsed_time < TARGET_FRAME_SECONDS
wait_time > 0
frontend sleeps for the remaining budget
Slow frame:
frame_elapsed_time >= TARGET_FRAME_SECONDS
wait_time <= 0
frontend does not sleep为什么不在 Console/CPU/PPU 中休眠?模拟器核心负责确定性的模拟状态转换。基于实际时钟的节奏控制属于前端策略,因此只有 main.py 应当休眠。
常见的误解
帧节奏并不能修正 CPU 周期精度,也不会让缓慢的模拟变得更快。它只是防止运行速度足够快的模拟超过目标硬件的速度。
当前手动参考
在这一性能步骤中,继续使用本地由用户自行提供的 MarioBros.nes。对《超级马里奥兄弟》的验证要延后到实现精灵 0 命中之后再进行。
为什么要做源码结构测试?自动化测试不得调用 main()、进行真实的休眠、打开 pygame,也不得要求使用商业 ROM。这些测试只验证节奏控制的结构,而不实际运行手动循环。
未来的兼容性
步骤 356 会用基于绝对截止时间的节奏控制,替换本课中基于相对休眠的源码结构。当检测到这种完全替换时,下面这六条过时的源码结构断言会被跳过。而那些持久有效的前端/核心边界以及“每次循环只模拟一帧”的断言仍然保持生效,由测试 356 来验证新的约定。不要在 main.py 中添加已废弃的 wait_time 或 time.sleep 兼容代码。
本步骤范围之外
- CPU 分支/跨页周期精度
- 精灵 0 命中
- 《超级马里奥兄弟》验证
- pygame 渲染优化(已在上一步完成)
- 从 pytest 中调用 main()
运行本课
uv run pytest tests/chapter_10_performance/test_318_main_frame_pacing.py -v