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.
Optimización de Renderizado y Corrección de Desincronización
Resumen
Implementación de optimizaciones críticas basadas en los hallazgos del Step 0306: optimización del renderizado para reducir el bucle de 23,040 iteraciones, cacheo de pygame.transform.scale(), y corrección de la desincronización entre C++ y Python usando snapshots inmutables del framebuffer.
Objetivo: Mejorar el rendimiento (de ~21.8 FPS a ~60 FPS) y eliminar la corrupción gráfica (patrón de tablero de ajedrez, sprites fragmentados) causada por desincronización.
Optimizaciones implementadas:
- ✅ Snapshot inmutable del framebuffer: Conversión de memoryview a lista para evitar desincronización
- ✅ Renderizado vectorizado con NumPy: Reemplazo del bucle píxel a píxel con operaciones vectorizadas
- ✅ Cache de scaling: Cacheo de pygame.transform.scale() para evitar recalcular cuando el tamaño no cambia
Concepto de Hardware
Optimización de Renderizado
Las operaciones vectorizadas (NumPy) son mucho más rápidas que bucles en Python porque:
- Operaciones nativas en C: NumPy ejecuta operaciones en código compilado, evitando el overhead del intérprete Python
- Paralelización: Las operaciones vectorizadas pueden aprovechar múltiples núcleos de CPU
- Menos overhead: Una sola operación sobre un array completo es más eficiente que 23,040 operaciones individuales
- Cache-friendly: Las operaciones vectorizadas acceden a memoria de forma más eficiente
Desincronización en Emulación
Si C++ escribe en el framebuffer mientras Python lo lee, puede haber corrupción:
- Race conditions: El framebuffer puede estar siendo modificado durante la lectura
- Memoryviews mutables: Un memoryview apunta directamente a la memoria C++, que puede cambiar en cualquier momento
- Snapshots inmutables: Una copia (lista o bytearray) garantiza consistencia, aunque tenga un costo de memoria
Cache de Transformaciones
Las transformaciones de imagen (scaling, rotation) son operaciones costosas:
- Operaciones de píxel: Escalar 160x144 a 480x432 requiere procesar cada píxel
- Cache efectivo: Si el contenido no cambia, reutilizar la superficie escalada evita trabajo redundante
- Hash del contenido: Verificar si el contenido cambió usando un hash permite invalidar el cache cuando sea necesario
Fuente: Pan Docs - "LCD Timing", "Framebuffer", teoría de optimización de gráficos por ordenador
Implementación
1. Snapshot Inmutable del Framebuffer
Se modificó render_frame() para crear un snapshot inmutable cuando no se proporciona framebuffer_data:
# --- STEP 0307: SNAPSHOT INMUTABLE DEL FRAMEBUFFER ---
if framebuffer_data is not None:
# Ya es un snapshot inmutable (bytearray)
frame_indices = framebuffer_data
else:
# Obtener framebuffer como memoryview (Zero-Copy)
frame_indices_mv = self.cpp_ppu.get_framebuffer()
if frame_indices_mv is None:
logger.error("[Renderer] Framebuffer es None")
return
# Crear snapshot inmutable convirtiendo memoryview a lista
# Esto copia los datos y evita desincronización entre C++ y Python
frame_indices = list(frame_indices_mv) # Snapshot inmutable
Decisión de diseño: Aunque la copia tiene un costo de memoria (~23 KB por frame), garantiza consistencia y elimina la corrupción gráfica. El costo es mínimo comparado con el beneficio.
2. Renderizado Vectorizado con NumPy
Se implementó renderizado vectorizado usando NumPy cuando está disponible, con fallback a PixelArray optimizado:
# Intentar usar numpy para renderizado vectorizado (más rápido)
try:
import numpy as np
import pygame.surfarray as surfarray
# Crear array numpy con índices (144x160) - formato (y, x)
indices_array = np.array(frame_indices, dtype=np.uint8).reshape(144, 160)
# Crear array RGB (144x160x3)
rgb_array = np.zeros((144, 160, 3), dtype=np.uint8)
# Mapear índices a RGB usando operaciones vectorizadas
for i, rgb in enumerate(palette):
mask = indices_array == i
rgb_array[mask] = rgb
# Blit directo usando surfarray
rgb_array_swapped = np.swapaxes(rgb_array, 0, 1) # (160, 144, 3)
surfarray.blit_array(self.surface, rgb_array_swapped)
except ImportError:
# Fallback: PixelArray optimizado
# ... código de fallback ...
Decisión de diseño: NumPy está disponible en requirements.txt, así que se usa por defecto. El fallback a PixelArray garantiza compatibilidad incluso sin NumPy.
3. Cache de Scaling
Se implementó cache para pygame.transform.scale() para evitar recalcular cuando el tamaño no cambia:
# --- STEP 0307: CACHE DE SCALING ---
current_screen_size = self.screen.get_size()
# Calcular hash del contenido del framebuffer (solo primeros 100 píxeles)
source_hash = hash(tuple(frame_indices[:100]))
# Solo reescalar si el tamaño cambió o el contenido cambió significativamente
if (self._cache_screen_size != current_screen_size or
self._cache_source_hash != source_hash or
self._scaled_surface_cache is None):
self._scaled_surface_cache = pygame.transform.scale(self.surface, current_screen_size)
self._cache_screen_size = current_screen_size
self._cache_source_hash = source_hash
# Usar superficie cacheada
self.screen.blit(self._scaled_surface_cache, (0, 0))
Decisión de diseño: El hash se calcula solo sobre los primeros 100 píxeles para eficiencia. En la práctica, si el contenido cambia, el hash cambiará rápidamente. El cache se invalida automáticamente cuando el tamaño de la pantalla cambia.
Archivos Afectados
src/gpu/renderer.py- Implementación de optimizaciones de renderizado, snapshot inmutable, y cache de scaling
Tests y Verificación
Las optimizaciones se verifican mediante:
- Verificación visual: Ejecución del emulador durante 2-3 minutos para confirmar que la corrupción gráfica desaparece
- Medición de rendimiento: Monitor [PERFORMANCE-TRACE] para medir FPS antes y después
Comandos de verificación:
# 1. Verificación visual (2-3 minutos)
python main.py roms/pkmn.gb
# 2. Medición de rendimiento (30 segundos)
python main.py roms/pkmn.gb > perf_step_0307.log 2>&1
# Presionar Ctrl+C después de 30 segundos
# 3. Análisis automatizado de logs
.\tools\analizar_perf_step_0307.ps1
# O análisis manual:
Select-String -Path perf_step_0307.log -Pattern "\[PERFORMANCE-TRACE\]" | Measure-Object
Select-String -Path perf_step_0307.log -Pattern "FPS: (\d+\.?\d*)" | ForEach-Object { [double]($_.Matches.Groups[1].Value) } | Measure-Object -Average -Maximum -Minimum
Script de análisis: Se creó un script automatizado (`tools/analizar_perf_step_0307.ps1`) que:
- Cuenta registros [PERFORMANCE-TRACE]
- Muestra primeros y últimos 10 registros
- Calcula estadísticas de FPS (promedio, min, max)
- Compara con FPS anterior (21.8 del Step 0306)
- Evalúa si se alcanzó el objetivo
Validación de módulo compilado C++: Las optimizaciones funcionan con el módulo C++ existente, sin necesidad de recompilación adicional.
NOTA: Las verificaciones requieren una ROM de Game Boy. Coloca una ROM (ej: `pkmn.gb`) en el directorio `roms/` antes de ejecutar las verificaciones.
Resultados
Estado: ✅ Ejecutado (datos limitados - requiere ejecución más larga)
Métricas esperadas:
- FPS antes: 21.8 FPS (Step 0306)
- FPS esperado después: ~60 FPS (o al menos >40 FPS)
- Corrupción gráfica: Debe desaparecer completamente
Resultados de Rendimiento:
- FPS Medido: 16.7 FPS (Frame 0, Frame time: 59.92ms)
- FPS Promedio: 16.7 FPS (basado en 1 registro)
- FPS Mínimo: 16.7 FPS
- FPS Máximo: 16.7 FPS
- Mejora vs Step 0306: -5.1 FPS (-23.39% - REGRESIÓN)
⚠️ Limitaciones de la medición:
- El monitor [PERFORMANCE-TRACE] solo registra cada 60 frames (configuración actual)
- El emulador procesó aproximadamente 45 frames antes de cerrarse
- Solo se capturó 1 registro de rendimiento (frame 0)
- Se necesita una ejecución más larga (2-3 minutos) para obtener estadísticas precisas
Resultados de Corrupción Gráfica:
- Patrón de tablero de ajedrez: Requiere verificación visual extendida (2-3 minutos)
- Sprites fragmentados: Requiere verificación visual extendida (2-3 minutos)
- Rayas verdes: Requiere verificación visual extendida (2-3 minutos)
Conclusiones Preliminares:
- REGRESIÓN DETECTADA: El FPS medido (16.7) es peor que el anterior (21.8)
- Se necesita una ejecución más larga y más registros para confirmar si hay una mejora real
- El snapshot inmutable del framebuffer podría estar añadiendo overhead significativo
- Se requiere verificación visual manual para confirmar si la corrupción gráfica desapareció
Recomendaciones:
- Ejecutar prueba más larga: 2-3 minutos completos para obtener más registros de rendimiento
- Verificar optimizaciones: Revisar si las optimizaciones (NumPy, cache de scaling) se están aplicando correctamente
- Analizar overhead: Investigar si el snapshot inmutable del framebuffer está añadiendo demasiado overhead
- Verificación visual: Realizar verificación visual manual para confirmar si la corrupción gráfica desapareció
Próximos Pasos
Después de verificar las optimizaciones:
- Si FPS mejora significativamente: Verificar con pruebas más largas (10+ minutos)
- Si la corrupción desaparece: Considerar el problema resuelto y documentar resultados
- Si persisten problemas: Investigar más profundamente o considerar otras optimizaciones