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.py

Por 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_FPS

Un fotograma objetivo es aproximadamente

16.64 milliseconds

Implementació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