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.
UI/Pygame Presenter Triage + Performance (Mario cuelga / Pokémon blanco)
Resumen
Triage del UI/Pygame presenter para identificar y corregir problemas de rendimiento y presentación. Implementación de logging limitado para identificar qué path de renderizado se usa (cpp_rgb_view vs legacy_fallback), forzar uso de framebuffer_rgb en modo C++, garantizar pygame.event.pump() cada frame para evitar cuelgues, y eliminar copias innecesarias de buffers. Objetivo: confirmar con evidencia si la UI está presentando framebuffer_rgb del core C++ (correcto) o algún legacy renderer/fallback (lento/blanco).
Concepto de Hardware
Presentación de Framebuffer en UI: El emulador debe presentar el framebuffer generado por el PPU C++ en la ventana de Pygame. Existen dos paths principales:
- Path C++ RGB (correcto): Usa
get_framebuffer_rgb()que devuelve un memoryview RGB888 (69120 bytes = 160×144×3). Zero-copy mediantenp.frombuffer()que crea una vista, no una copia. - Path Legacy/Fallback (lento): Renderiza desde VRAM usando tiles Python, o usa framebuffer_data (índices de color DMG). Este path es más lento y puede causar problemas de rendimiento.
Event Pump en Pygame: pygame.event.pump() debe llamarse cada frame para que el sistema operativo no marque la aplicación como "no responde". Sin esto, el OS puede congelar la ventana.
Zero-Copy en NumPy: np.frombuffer() crea una vista (view) del buffer subyacente sin copiar datos. Esto es crítico para rendimiento: copiar 69120 bytes cada frame (60 FPS) sería ~4.1 MB/s innecesarios. Verificamos con flags['OWNDATA'] == False que no se creó una copia.
Fuente: Pygame documentation - "Event Pump", NumPy documentation - "Memory Views", Cython documentation - "Zero-Copy"
Implementación
Fase 1: Path Identification Logging
Objetivo: Añadir logging limitado para identificar qué path se usa en cada frame.
Implementación en renderer.py: Añadido bloque de logging al inicio de render_frame() que:
- Loggea los primeros 5 frames y luego cada 120 frames (para evitar saturación)
- Identifica el path:
cpp_rgb_view,cpp_framebuffer_data, olegacy_fallback - Mide métricas: buffer_len, buffer_shape, nonwhite_sample (estimación), frame_hash (primeros 1000 bytes), frame_time_ms, FPS
- Muestrea non-white pixels (cada 64º píxel) para detectar si el buffer está blanco
Salida de ejemplo:
[UI-PATH] Frame 0 | Path=cpp_rgb_view | Len=69120 | Shape=rgb_view | NonWhite=23040 | Hash=a1b2c3d4 | Time=2.45ms | FPS=408.2
Fase 2: Garantizar Event Pump Cada Frame
Implementación en viboy.py: Añadido pygame.event.pump() antes de _handle_pygame_events() en el bucle principal. Esto asegura que el OS no marque la aplicación como "no responde".
Fase 3: Forzar Path C++ RGB y Eliminar Legacy Fallback
Implementación en viboy.py: Modificado el bloque de renderizado para:
- Priorizar
get_framebuffer_rgb()si PPU C++ está disponible - No ejecutar fallback legacy si estamos en modo C++
- Loggear error claro si se intenta usar legacy path en modo C++
Lógica:
if self._ppu is not None and hasattr(self._ppu, 'get_framebuffer_rgb'):
rgb_view = self._ppu.get_framebuffer_rgb()
if rgb_view is not None:
self._renderer.render_frame(rgb_view=rgb_view)
# No fallback a legacy
self._ppu.confirm_framebuffer_read()
# Continuar con el siguiente frame
Fase 4: Verificación de Formato y Eliminación de Copias
Implementación en renderer.py: Añadidas verificaciones para:
np.frombuffer()crea vista (no copia):assert rgb_array.flags['OWNDATA'] == Falsereshape()también es vista:assert rgb_array.flags['OWNDATA'] == False- Shape correcto después de reshape:
assert rgb_array.shape == (144, 160, 3) - Shape correcto después de swapaxes:
assert rgb_array_swapped.shape == (160, 144, 3) - NO limpiar surface después del blit (evitar pantalla blanca)
Nota: np.ascontiguousarray() SÍ copia si el array no es contiguo, pero esto es necesario para swapaxes() en algunos casos. Verificamos que solo se copia si es estrictamente necesario.
Archivos Afectados
src/gpu/renderer.py- Añadido logging de path identification, verificaciones de formato y eliminación de copias innecesariassrc/viboy.py- Añadido pygame.event.pump() cada frame, forzar path C++ RGB, eliminar legacy fallback en modo C++
Tests y Verificación
Compilación:
python3 setup.py build_ext --inplace
BUILD_EXIT=0
Test Build:
python3 test_build.py
TEST_BUILD_EXIT=0
[EXITO] El pipeline de compilacion funciona correctamente
Suite de Tests:
pytest tests/ -v --tb=line
537 passed in 89.65s
Validación de Módulo Compilado C++: Todos los tests pasan, confirmando que las modificaciones no rompieron funcionalidad existente.
Logging de Path: El logging se activará en ejecución real de UI. Los primeros 5 frames y cada 120 frames mostrarán el path usado, permitiendo identificar si se usa cpp_rgb_view (correcto) o legacy_fallback (problema).
Fuentes Consultadas
- Pygame documentation: Event Pump
- NumPy documentation: numpy.frombuffer
- Cython documentation: Zero-Copy Memory Views
Integridad Educativa
Lo que Entiendo Ahora
- Path Identification: Es crítico saber qué path de renderizado se usa. El path C++ RGB es zero-copy y rápido, mientras que el legacy path puede ser lento y causar problemas de rendimiento.
- Event Pump:
pygame.event.pump()debe llamarse cada frame para evitar que el OS marque la aplicación como "no responde". Esto es especialmente importante en sistemas como macOS y Linux. - Zero-Copy en NumPy:
np.frombuffer()crea una vista del buffer subyacente sin copiar datos. Verificamos conflags['OWNDATA']que no se creó una copia. Esto es crítico para rendimiento a 60 FPS. - Eliminación de Legacy Fallback: En modo C++, no debemos usar el path legacy. Si llegamos al legacy path en modo C++, es un error que debemos loggear claramente.
Lo que Falta Confirmar
- Ejecución Real con ROMs: Necesitamos ejecutar Mario y Pokémon en UI y verificar los logs [UI-PATH] para confirmar qué path se usa realmente.
- Performance Real: Medir FPS real en UI con las modificaciones para confirmar que el cuelgue de Mario se resolvió.
- Pokémon Blanco: Verificar si el problema de pantalla blanca en Pokémon se resolvió con las verificaciones de formato y eliminación de copias.
Hipótesis y Suposiciones
Hipótesis Principal: Mario cuelga porque el UI está usando el path legacy (lento) en lugar del path C++ RGB. Con las modificaciones, forzamos el uso del path C++ RGB y eliminamos el legacy fallback, lo que debería resolver el problema de rendimiento.
Hipótesis Secundaria: Pokémon sale blanco porque hay una copia incorrecta del buffer o el surface se limpia después del blit. Con las verificaciones de formato y la eliminación de copias innecesarias, esto debería resolverse.
Próximos Pasos
- [ ] Ejecutar UI con Mario y capturar logs [UI-PATH] para confirmar path usado
- [ ] Ejecutar UI con Pokémon y verificar si pantalla blanca se resolvió
- [ ] Comparar métricas headless vs UI (nonwhite pixels, VRAM nonzero) para identificar discrepancias
- [ ] Si path es correcto pero sigue habiendo problemas, investigar más a fondo (timing, sincronización, etc.)