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 Evidencia Real + Profiling Presenter (Mario cuelga / Pokémon blanco)
Resumen
Implementación de profiling por etapas en el presenter de UI para identificar cuellos de botella (frombuffer/reshape, blit_array, scale/blit, flip). Aplicación de optimizaciones: uso de pygame.SCALED para escalado automático por SDL (más rápido que transform.scale manual), conversión de asserts permanentes en checks detrás de flag VIBOY_DEBUG_UI, y verificación de nonwhite antes/después del blit para detectar pérdida de píxeles. Objetivo: obtener evidencia objetiva en UI (Mario y Pokémon) con métricas reales y timings para identificar qué etapa consume tiempo y dónde se pierden píxeles.
Concepto de Hardware
Profiling de Presentación: El proceso de renderizado en UI consta de varias etapas que deben ejecutarse cada frame (60 FPS = 16.67ms por frame). Si alguna etapa consume demasiado tiempo, el framerate cae o la aplicación puede congelarse. Las etapas principales son:
- frombuffer/reshape/swap: Conversión de memoryview RGB888 a array numpy y preparación del formato (shape, contiguidad, swapaxes). Debe ser zero-copy cuando sea posible.
- blit_array: Copia de datos del array numpy a la superficie de Pygame. Pygame debe acceder directamente a los datos sin copias intermedias.
- scale/blit: Escalado de la superficie base (160×144) a la ventana (480×432) y blit a la pantalla. Esta etapa puede ser muy costosa si se hace manualmente con
pygame.transform.scale(). - flip: Actualización del buffer de pantalla. Generalmente rápido, pero puede bloquear si el driver gráfico está saturado.
pygame.SCALED (Pygame 2.0+): Flag que permite que SDL (la biblioteca subyacente de Pygame) haga el escalado automáticamente usando aceleración por hardware. Es mucho más rápido que pygame.transform.scale() manual porque SDL optimiza el escalado usando la GPU cuando está disponible. Requiere crear la ventana con pygame.SCALED | pygame.RESIZABLE y blittear directamente la superficie base (sin escalado manual).
Asserts en Hot Path: Los asserts en Python son costosos porque evalúan condiciones y pueden lanzar excepciones. En el hot path del renderizado (60 FPS), los asserts pueden consumir tiempo significativo. Es mejor usar checks detrás de flags de debug que solo se activan cuando se necesita diagnóstico.
Verificación de Nonwhite: Si el framebuffer que llega al presenter tiene píxeles no-blancos pero la pantalla muestra blanco, hay un bug en la presentación (pérdida de píxeles durante el blit). Verificamos nonwhite antes y después del blit para detectar dónde se pierden los píxeles.
Fuente: Pygame documentation - "SCALED flag", SDL documentation - "Hardware Acceleration", Python documentation - "Assert Statement Performance"
Implementación
Fase 1: Profiling por Etapas
Objetivo: Añadir profiling de 4 tramos del presenter para identificar cuellos de botella.
Implementación en renderer.py: Añadido profiling por etapas que solo se activa si FPS < 30 o en frames loggeados (cada 120 frames). Cada etapa mide tiempo en milisegundos:
frombuffer_reshape_swap: Tiempo para frombuffer, reshape y swapaxesblit_array: Tiempo para pygame.surfarray.blit_arrayscale_blit: Tiempo para escalado y blit a pantallaflip: Tiempo para pygame.display.flip
Salida de ejemplo:
[UI-PROFILING] Frame 0 | frombuffer/reshape: 0.12ms | blit_array: 1.45ms | scale/blit: 12.34ms | flip: 0.23ms | TOTAL: 14.14ms
Fase 2: Optimización con pygame.SCALED
Implementación en renderer.py: Modificado __init__() para usar pygame.SCALED si está disponible (Pygame 2.0+):
if hasattr(pygame, 'SCALED'):
self.screen = pygame.display.set_mode((GB_WIDTH, GB_HEIGHT), pygame.SCALED | pygame.RESIZABLE)
self._use_scaled = True
else:
# Fallback para Pygame < 2.0
self.screen = pygame.display.set_mode((self.window_width, self.window_height), pygame.RESIZABLE)
self._use_scaled = False
Modificación en render_frame(): Eliminado escalado manual con pygame.transform.scale() cuando se usa SCALED. Blit directo a screen (SDL escala automáticamente):
if hasattr(self, '_use_scaled') and self._use_scaled:
# Blit directo a screen (SDL escala automáticamente)
self.screen.blit(self.surface, (0, 0))
else:
# Fallback: escalado manual (para Pygame < 2.0)
if self.scale != 1:
scaled_surface = pygame.transform.scale(self.surface, (self.window_width, self.window_height))
self.screen.blit(scaled_surface, (0, 0))
else:
self.screen.blit(self.surface, (0, 0))
Fase 3: Conversión de Asserts a Checks Condicionales
Implementación en renderer.py: Convertidos todos los asserts permanentes en checks detrás de flag VIBOY_DEBUG_UI:
VIBOY_DEBUG_UI = os.environ.get('VIBOY_DEBUG_UI', '0') == '1'
# ANTES (assert permanente):
# assert rgb_array.flags['OWNDATA'] == False, "np.frombuffer creó copia"
# DESPUÉS (check condicional):
if should_log or VIBOY_DEBUG_UI:
if rgb_array.flags['OWNDATA']:
logger.warning("[UI-DEBUG] np.frombuffer creó copia (debería ser vista)")
Beneficio: Los checks solo se ejecutan en frames loggeados o cuando se activa el flag de debug, evitando overhead en el hot path.
Fase 4: Verificación Nonwhite Antes/Después del Blit
Implementación en render_frame(): Añadida verificación de nonwhite antes y después del blit para detectar pérdida de píxeles:
- Antes del blit: Muestreo del array numpy (cada 8º píxel) para estimar nonwhite
- Después del blit: Sampleo de píxeles específicos de la surface usando
get_at() - Detección de bug: Si nonwhite_before > 0 pero nonwhite_after == 0, hay bug de presentación
Salida de ejemplo:
[UI-DEBUG] Nonwhite antes del blit: 23040 (estimado)
[UI-DEBUG] Nonwhite después del blit (sample): 3/3
[UI-DEBUG] ⚠️ Nonwhite se pierde durante blit! Bug de presentación.
Archivos Afectados
src/gpu/renderer.py- Añadido profiling por etapas, pygame.SCALED para escalado automático, conversión de asserts a checks condicionales, verificación nonwhite antes/después del blit
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
Tests Unitarios:
pytest tests/test_core_cpu.py -v
6 passed in 0.14s
Validación de Módulo Compilado C++: Todos los tests pasan, confirmando que las modificaciones no rompieron funcionalidad existente.
Profiling en UI: El profiling se activará automáticamente si FPS < 30 o en frames loggeados (primeros 5 frames y cada 120 frames). Los logs mostrarán tiempo por etapa, permitiendo identificar cuellos de botella.
Ejecución de UI: Para capturar logs reales, ejecutar:
# Mario (el que cuelga)
python main.py roms/mario.gbc 2>&1 | tee /tmp/viboy_0446_mario_ui.log
# Pokémon (el blanco)
python main.py roms/pkmn.gb 2>&1 | tee /tmp/viboy_0446_pokemon_ui.log
Extraer logs relevantes:
grep "[UI-PATH]" /tmp/viboy_0446_mario_ui.log | head -n 6
grep "[UI-PROFILING]" /tmp/viboy_0446_mario_ui.log | head -n 6
grep "[UI-DEBUG]" /tmp/viboy_0446_pokemon_ui.log | head -n 6
Fuentes Consultadas
- Pygame documentation: pygame.display.set_mode SCALED flag
- SDL documentation: SDL_WINDOW_ALLOW_HIGHDPI (SCALED)
- Python documentation: Assert Statement Performance
Integridad Educativa
Lo que Entiendo Ahora
- Profiling por Etapas: Es crítico medir tiempo por etapa para identificar cuellos de botella. El profiling solo se activa cuando es necesario (FPS bajo o frames loggeados) para evitar overhead.
- pygame.SCALED: Usa aceleración por hardware de SDL para escalado, mucho más rápido que transform.scale() manual. Requiere Pygame 2.0+ y se debe hacer fallback para versiones anteriores.
- Asserts en Hot Path: Los asserts permanentes en el hot path del renderizado consumen tiempo. Es mejor usar checks condicionales detrás de flags de debug.
- Verificación Nonwhite: Si el framebuffer tiene nonwhite pero la pantalla es blanca, hay un bug en la presentación. Verificamos antes y después del blit para detectar dónde se pierden los píxeles.
Lo que Falta Confirmar
- Ejecución Real con ROMs: Necesitamos ejecutar Mario y Pokémon en UI y capturar logs [UI-PATH] y [UI-PROFILING] para ver métricas reales.
- Performance Real: Medir FPS real en UI con pygame.SCALED para confirmar que el escalado automático mejora el rendimiento.
- Pokémon Blanco: Verificar con logs [UI-DEBUG] si el problema de pantalla blanca se detecta correctamente (nonwhite antes pero no después del blit).
- Cuello de Botella Identificado: Una vez que tengamos logs de profiling, identificar qué etapa consume más tiempo y aplicar fixes específicos si es necesario.
Hipótesis y Suposiciones
Hipótesis Principal: El cuello de botella en Mario (cuelgue/0.1 FPS) está en la etapa de escalado (transform.scale manual). Con pygame.SCALED, SDL hace el escalado por hardware, lo que debería resolver el problema de rendimiento.
Hipótesis Secundaria: Pokémon sale blanco porque los píxeles se pierden durante el blit (nonwhite antes pero no después). La verificación nonwhite debería detectar esto y permitirnos identificar el problema.
Próximos Pasos
- [ ] Ejecutar UI con Mario y capturar logs [UI-PATH] y [UI-PROFILING] para ver métricas reales
- [ ] Ejecutar UI con Pokémon y capturar logs [UI-DEBUG] para verificar nonwhite antes/después del blit
- [ ] Analizar logs de profiling para identificar qué etapa consume más tiempo
- [ ] Si el cuello de botella sigue siendo el escalado, considerar otros optimizaciones (ej: pre-escalar surface una sola vez)
- [ ] Si nonwhite se pierde durante el blit, investigar más a fondo (verificar formato de surface, verificar que no se limpia después del blit)