⚠️ Clean-Room / Educativo

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)

Fecha: 2026-01-02 Step ID: 0445 Estado: VERIFIED

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 mediante np.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, o legacy_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'] == False
  • reshape() 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 innecesarias
  • src/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

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 con flags['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.)