Este proyecto es educativo y Open Source. No se copia código de otros emuladores. Implementación basada únicamente en documentación técnica y tests permitidas.
Verificación y Corrección del Limitador de FPS
Resumen
Verificación y corrección del limitador de FPS que ya existía en el código. A pesar de que clock.tick(60) estaba implementado, el reporte de FPS en la barra de título mostraba 300+ FPS, lo que sugería que el limitador funcionaba pero el reporte no reflejaba el FPS limitado correctamente. Se corrigieron los cálculos de FPS en la barra de título y el monitor de rendimiento para que reflejen el FPS limitado real (~60 FPS) en lugar del FPS de renderizado sin limitar.
Concepto de Hardware
El Game Boy original ejecuta a 59.7 FPS (aproximadamente 60 FPS), lo que significa que cada frame debe durar aproximadamente 16.67ms (1000ms / 60 FPS). Para mantener la sincronización correcta con el hardware real, el emulador debe:
- Ejecutar la emulación a la velocidad correcta: 70,224 ciclos T por frame (4.194304 MHz / 59.7 FPS)
- Limitar el renderizado a 60 FPS: Usar un limitador de FPS (como
pygame.Clock.tick(60)) para sincronizar el renderizado con el tiempo real - Reportar el FPS limitado correctamente: El reporte de FPS debe reflejar el FPS limitado, no el FPS de renderizado sin limitar
El problema encontrado fue que, aunque el limitador estaba activo (el emulador se ejecutaba a 60 FPS reales), el reporte mostraba el FPS de renderizado sin limitar (300+ FPS). Esto ocurría porque:
- El
clock.get_fps()puede no funcionar correctamente si se llama antes de que el tick tenga efecto - El monitor de rendimiento medía solo el tiempo de renderizado, no el tiempo entre frames (que incluye la espera del
clock.tick())
La solución fue calcular el FPS desde el tiempo real entre frames consecutivos (que incluye el tiempo de espera del limitador), en lugar de usar solo el tiempo de renderizado.
Implementación
Se implementaron correcciones en dos áreas principales: el reporte de FPS en la barra de título y el monitor de rendimiento.
1. Corrección del Reporte de FPS en la Barra de Título
Problema detectado: El reporte usaba clock.get_fps() que podía retornar el FPS sin limitar en lugar del FPS limitado.
Solución: Calcular el FPS desde el tick_time retornado por clock.tick(), que es más preciso:
# Step 0309: Corregir cálculo de FPS para reflejar el FPS limitado
if self._clock is not None:
tick_time_ms = self._clock.tick(TARGET_FPS)
# Log temporal para verificación (cada segundo)
if self.frame_count % 60 == 0:
print(f"[FPS-LIMITER] Frame {self.frame_count} | Tick time: {tick_time_ms:.2f}ms | Target: {TARGET_FPS} FPS")
# Título con FPS (cada 60 frames para no frenar)
if self.frame_count % 60 == 0 and self._clock is not None:
import pygame
import time
# Opción A: Usar get_fps() que debería retornar FPS limitado
fps_from_clock = self._clock.get_fps()
# Opción B: Calcular desde tick_time (más preciso)
if tick_time_ms is not None and tick_time_ms > 0:
fps_calculated = 1000.0 / tick_time_ms
# Usar el cálculo basado en tick_time para mayor precisión
fps = fps_calculated
else:
# Fallback a get_fps() si tick_time no está disponible
fps = fps_from_clock if fps_from_clock > 0 else TARGET_FPS
pygame.display.set_caption(f"Viboy Color v0.0.2 - FPS: {fps:.1f}")
2. Verificación de Sincronización
Se añadió una verificación de sincronización que se ejecuta cada minuto (3600 frames) para detectar drift entre frames reales y frames esperados:
# Step 0309: Verificación de sincronización (cada minuto)
if not hasattr(self, '_start_time'):
import time
self._start_time = time.time()
if self.frame_count % 3600 == 0 and self.frame_count > 0: # Cada minuto (60 * 60 frames)
import time
elapsed_real = time.time() - self._start_time
expected_frames = elapsed_real * TARGET_FPS
actual_frames = self.frame_count
drift = actual_frames - expected_frames
print(f"[SYNC-CHECK] Real: {elapsed_real:.1f}s | Expected: {expected_frames:.0f} frames | Actual: {actual_frames} | Drift: {drift:.0f}")
3. Corrección del Monitor de Rendimiento
Problema detectado: El monitor medía solo el tiempo de renderizado, no el tiempo entre frames (que incluye la espera del clock.tick()).
Solución: Calcular el tiempo entre frames consecutivos, que incluye el tiempo de espera del limitador:
# Step 0309: Calcular tiempo entre frames consecutivos (incluye clock.tick())
# Guardar tiempo del frame anterior para calcular tiempo entre frames
self._last_frame_end_time = None
# En el monitor de rendimiento:
if self._performance_trace_enabled and frame_start is not None:
frame_end = time.time()
frame_time = (frame_end - frame_start) * 1000 # Tiempo de renderizado en ms
# Calcular tiempo entre frames consecutivos (incluye clock.tick())
time_between_frames = None
fps_limited = None
if self._last_frame_end_time is not None:
time_between_frames = (frame_end - self._last_frame_end_time) * 1000 # ms
fps_limited = 1000.0 / time_between_frames if time_between_frames > 0 else 0
self._last_frame_end_time = frame_end
if self._performance_trace_count % 10 == 0:
fps_render = 1000.0 / frame_time if frame_time > 0 else 0
if fps_limited is not None:
print(f"[PERFORMANCE-TRACE] Frame {self._performance_trace_count} | "
f"Frame time (render): {frame_time:.2f}ms | FPS (render): {fps_render:.1f} | "
f"Time between frames: {time_between_frames:.2f}ms | FPS (limited): {fps_limited:.1f} | "
f"...")
Beneficio: Ahora el monitor reporta tanto el FPS de renderizado (que puede ser alto) como el FPS limitado (que debe ser ~60 FPS), permitiendo identificar problemas de sincronización.
Decisiones de diseño
- Uso de
tick_timepara cálculo de FPS: Más preciso queget_fps()porque refleja directamente el tiempo que esperó el limitador - Medición de tiempo entre frames: Permite distinguir entre FPS de renderizado (sin limitar) y FPS limitado (real)
- Verificación de sincronización periódica: Permite detectar drift a largo plazo que podría indicar problemas de sincronización
Archivos Afectados
src/viboy.py- Corrección del reporte de FPS en barra de título, logs de verificación del limitador, y verificación de sincronizaciónsrc/gpu/renderer.py- Corrección del monitor de rendimiento para calcular tiempo entre frames y reportar FPS limitado
Tests y Verificación
La verificación requiere ejecutar el emulador y observar:
- Barra de título: Debe mostrar FPS ≈ 60.0 (no 300+)
- Logs [FPS-LIMITER]: Debe mostrar tick time ≈ 16.67ms cada segundo
- Logs [PERFORMANCE-TRACE]: Debe mostrar FPS (limited) ≈ 60.0
- Logs [SYNC-CHECK]: Debe mostrar drift cercano a 0 después de 1 minuto
Comando para verificación:
python main.py roms/pkmn.gb > perf_step_0309.log 2>&1
Análisis esperado:
- FPS promedio (limited) debe ser ≈ 60 FPS (no 300+)
- FPS mínimo debe ser > 55 FPS
- FPS máximo debe ser < 65 FPS
- Frame time (limited) debe ser ≈ 16.67ms (1000ms / 60 FPS)
Validación de módulo compilado C++: No se requieren cambios en C++, solo correcciones en Python/Cython.
Fuentes Consultadas
- Pan Docs: LCD Timing - https://gbdev.io/pandocs/LCDC.html
- Pygame Clock Documentation: https://www.pygame.org/docs/ref/time.html#pygame.time.Clock
Integridad Educativa
Lo que Entiendo Ahora
- Limitador de FPS:
clock.tick(60)limita el renderizado a 60 FPS añadiendo una espera si el frame se renderizó demasiado rápido. El tiempo de espera se retorna comotick_time. - FPS de renderizado vs FPS limitado: El FPS de renderizado puede ser muy alto (300+ FPS) si el renderizado es rápido, pero el FPS limitado debe ser ~60 FPS para sincronización correcta.
- Tiempo entre frames: Para medir el FPS limitado correctamente, hay que medir el tiempo entre el final de un frame y el final del siguiente, no solo el tiempo de renderizado.
Lo que Falta Confirmar
- Verificación práctica: Ejecutar el emulador y verificar que el FPS reportado sea ~60 FPS en lugar de 300+ FPS
- Sincronización a largo plazo: Verificar con [SYNC-CHECK] que no hay drift significativo después de varios minutos
Hipótesis y Suposiciones
Asumo que clock.tick(60) funciona correctamente y que el problema era solo el reporte. Si después de estas correcciones el FPS reportado sigue siendo incorrecto, podría haber un problema con el limitador mismo o con la sincronización entre el bucle de emulación y el renderizado.
Próximos Pasos
- [ ] Ejecutar verificación práctica para confirmar que el FPS reportado es ~60 FPS
- [ ] Analizar logs [SYNC-CHECK] para verificar que no hay drift significativo
- [ ] Si el FPS sigue siendo incorrecto, investigar si hay un problema con el limitador mismo
- [ ] Considerar desactivar logs temporales [FPS-LIMITER] después de verificación exitosa