318. Ritmo de fotogramas en main
Limita el bucle del frontend manual a la velocidad NTSC de NES.
Lección 318 de 356 · tests/chapter_10_performance/test_318_main_frame_pacing.py
Archivo a actualizar
main.pyPor qué existe este paso
Tras la carga del framebuffer con pygame más rápida y los lanzadores opcionales de PyPy, el emulador manual puede funcionar más rápido que la NES real. Durante el desarrollo del tutorial, la ruta sin límite alcanzó aproximadamente 90-120 FPS.
Ejecutar más rápido es una evidencia de rendimiento útil, pero hace que la jugabilidad, la animación y la sincronización de entrada sean demasiado rápidas. El frontend debería esperar cuando un fotograma termina antes de tiempo.
Término clave: ritmo de fotogramas El ritmo de fotogramas significa retrasar la presentación cuando un fotograma emulado termina más rápido que la duración objetivo en tiempo real.
Temporización NTSC objetivo
NES_NTSC_FPS = 60.0988
TARGET_FRAME_SECONDS = 1.0 / NES_NTSC_FPSUn fotograma objetivo es aproximadamente
16.64 millisecondsImplementación de ejemplo
# --- 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 ---Invariantes importantes
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¿Por qué no dormir en Console/CPU/PPU? El núcleo del emulador posee las transiciones de estado emulado deterministas. El ritmo en tiempo real es una política del frontend, así que solo main.py debería dormir.
Concepto erróneo común
El ritmo de fotogramas no corrige la precisión de ciclos de la CPU ni hace que una emulación lenta sea más rápida. Solo evita que una emulación suficientemente rápida funcione más rápido que el hardware objetivo.
Referencia manual actual
Continúa usando el MarioBros.nes local proporcionado por el usuario para este paso de rendimiento. La validación con Super Mario Bros. se pospone hasta que se implemente sprite 0 hit.
¿Por qué tests de forma del código fuente? Los tests automatizados no deben llamar a main(), dormir de verdad, abrir pygame ni requerir una ROM comercial. Estos tests verifican la estructura del ritmo sin ejecutar el bucle manual.
Compatibilidad futura
El paso 356 sustituye la forma de código fuente de espera relativa de esta lección por un ritmo de plazo absoluto. Cuando se detecte esa sustitución completa, solo se omiten las seis aserciones de forma de código fuente obsoletas indicadas a continuación. Las aserciones duraderas del límite frontend/núcleo y de un fotograma emulado por bucle permanecen activas, mientras que el Test 356 valida el nuevo contrato. No añadas código muerto de compatibilidad con wait_time o time.sleep a main.py.
Fuera de alcance
- precisión de ciclos de rama/cruce de página de la CPU
- sprite 0 hit
- validación con Super Mario Bros.
- optimización del renderizado con pygame (completada en el paso anterior)
- llamar a main() desde pytest
Ejecutar esta lección
uv run pytest tests/chapter_10_performance/test_318_main_frame_pacing.py -v