318. Выравнивание кадров в main
Ограничиваем цикл ручного интерфейса скоростью NES NTSC.
Урок 318 из 356 · tests/chapter_10_performance/test_318_main_frame_pacing.py
Файл для обновления
main.pyЗачем нужен этот шаг
После более быстрой загрузки framebuffer в 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Почему не делать sleep в Console/CPU/PPU? Ядро эмулятора отвечает за детерминированные переходы эмулируемого состояния. Выравнивание по реальному времени — это политика интерфейса, поэтому спать должен только main.py.
Распространённое заблуждение
Выравнивание кадров не исправляет точность по тактам CPU и не делает медленную эмуляцию быстрее. Оно лишь не позволяет достаточно быстрой эмуляции работать быстрее целевого оборудования.
Текущий ручной эталон
Продолжайте использовать локальный, предоставленный пользователем MarioBros.nes для этого шага по производительности. Проверка на Super Mario Bros. откладывается до реализации sprite 0 hit.
Почему тесты формы исходного кода? Автоматические тесты не должны вызывать main(), реально засыпать, открывать pygame или требовать коммерческий ROM. Эти тесты проверяют структуру выравнивания без запуска ручного цикла.
Совместимость в будущем
Шаг 356 заменяет форму исходного кода с относительным sleep из этого урока на выравнивание по абсолютному дедлайну. Когда обнаруживается эта полная замена, пропускаются только шесть устаревших утверждений формы исходного кода ниже. Устойчивая граница интерфейс/ядро и утверждения об одном эмулируемом кадре за проход цикла остаются активными, пока тест 356 проверяет новый контракт. Не добавляйте в main.py мёртвый код совместимости с wait_time или time.sleep.
Вне рамок
- точность по тактам CPU при переходе через страницу/ветвлении
- sprite 0 hit
- проверка на Super Mario Bros.
- оптимизация рендеринга pygame (завершена на предыдущем шаге)
- вызов main() из pytest
Запустить этот урок
uv run pytest tests/chapter_10_performance/test_318_main_frame_pacing.py -v