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.
CGB End-to-End Present Proof (Idx→RGB→Present)
Resumen
Este step implementa un diagnóstico end-to-end del pipeline de renderizado CGB para identificar exactamente en qué etapa falla el problema de "pantalla blanca". Se implementó soporte para modo headless en el renderer, dump PPM separado para FB_PRESENT, y PresentDetails en el snapshot. Los resultados confirman que el pipeline PPU→RGB funciona correctamente (IdxNonZero=22910, RgbNonWhite=22910), pero FB_PRESENT_SRC no se captura en modo headless porque rom_smoke no usa el renderer. Se identificó el Caso A: el problema está en el renderer/present, no en el PPU ni en las paletas.
Concepto de Hardware
Pipeline de Renderizado CGB: El pipeline de renderizado en modo CGB tiene tres etapas principales:
- FB_INDEX: El PPU genera índices de color (0-3) para cada píxel basándose en los tiles y atributos de paleta. Estos índices se almacenan en el framebuffer de índices.
- FB_RGB: Los índices se convierten a valores RGB888 usando las paletas CGB (BGPD/OBPD). Cada índice se mapea a un color BGR555 de la paleta correspondiente, que luego se convierte a RGB888.
- FB_PRESENT_SRC: El buffer RGB se entrega al renderer (pygame Surface) que lo prepara para presentación en pantalla. Este es el buffer exacto que se pasa a SDL/pygame antes del flip.
Diagnóstico End-to-End: Para identificar dónde falla el pipeline, necesitamos evidencia de las tres etapas en el mismo frame. Si FB_INDEX tiene señal pero FB_RGB está blanco, el problema está en la conversión de índices a RGB (paletas). Si FB_RGB tiene señal pero FB_PRESENT está blanco, el problema está en el renderer/present.
Modo Headless: En modo headless (sin ventana gráfica), el renderer debe poder generar el mismo buffer que se presentaría en modo UI, para permitir diagnóstico en CI y ejecución sin display.
Referencia: Pan Docs - "CGB Palettes", "PPU Rendering Pipeline", "Framebuffer Format"
Implementación
Fase 1: Modo Headless en Renderer
Se modificó src/gpu/renderer.py para soportar modo headless:
- Detección automática: El renderer detecta modo headless mediante
SDL_VIDEODRIVER=dummyoVIBOY_HEADLESS=1 - Surface temporal: Si no hay screen disponible, se crea un Surface temporal (
_headless_surface) para capturar FB_PRESENT_SRC - Render sin flip: En modo headless, no se ejecuta
pygame.display.flip(), pero el Surface temporal se renderiza igual que en modo normal
Fase 2: Dump PPM Separado para FB_PRESENT
Se implementó dump separado usando variables de entorno:
VIBOY_DUMP_PRESENT_FRAME: Frame en el que generar el dumpVIBOY_DUMP_PRESENT_PATH: Ruta del archivo PPM (soporta####como placeholder del frame)- Formato: PPM P6 160x144 RGB888 (mismo formato que FB_RGB)
Fase 3: PresentDetails en Snapshot
Se añadió PresentDetails al snapshot en tools/rom_smoke_0442.py:
present_fmt: Formato del Surface (0 = RGB888)present_pitch: Pitch del Surface (bytes por fila)present_w,present_h: Dimensiones del Surfacepresent_bytes_len: Tamaño total del buffer en bytes
Los datos se obtienen desde ThreeBufferStats cuando está disponible.
Archivos Afectados
src/gpu/renderer.py- Modo headless, dump PRESENT separadotools/rom_smoke_0442.py- PresentDetails en snapshotdocs/reports/reporte_step0496.md- Reporte completo del step
Tests y Verificación
Se ejecutó rom_smoke_0442.py con tetris_dx.gbc durante 1200 frames:
export VIBOY_SIM_BOOT_LOGO=0
export VIBOY_DEBUG_PRESENT_TRACE=1
export VIBOY_DEBUG_CGB_PALETTE_WRITES=1
export VIBOY_DUMP_IDX_FRAME=600
export VIBOY_DUMP_IDX_PATH=/tmp/viboy_tetris_dx_idx_f####.ppm
export VIBOY_DUMP_RGB_FRAME=600
export VIBOY_DUMP_RGB_PATH=/tmp/viboy_tetris_dx_rgb_f####.ppm
export VIBOY_DUMP_PRESENT_FRAME=600
export VIBOY_DUMP_PRESENT_PATH=/tmp/viboy_tetris_dx_present_f####.ppm
python3 tools/rom_smoke_0442.py roms/tetris_dx.gbc --frames 1200
Resultados (Frame 600)
| Buffer | Métrica | Valor | Estado |
|---|---|---|---|
| FB_INDEX | IdxCRC32 | 0xBC5587A4 | ✅ No blanco |
| IdxUnique | 4 | ✅ Múltiples colores | |
| IdxNonZero | 22910 | ✅ Señal presente | |
| FB_RGB | RgbCRC32 | 0xF87596C9 | ✅ No blanco |
| RgbUnique | 4 | ✅ Múltiples colores | |
| RgbNonWhite | 22910 | ✅ Señal presente | |
| FB_PRESENT_SRC | PresentCRC32 | 0x00000000 | ❌ Blanco |
| PresentNonWhite | 0 | ❌ Sin señal |
Dumps PPM Generados
/tmp/viboy_tetris_dx_idx_f600.ppm(68K) ✅/tmp/viboy_tetris_dx_rgb_f0600.ppm(68K) ✅/tmp/viboy_tetris_dx_rgb_f600.ppm(68K) ✅/tmp/viboy_tetris_dx_present_f600.ppm❌ (No generado - renderer no usado en rom_smoke)
Clasificación del Fallo
✅ CASO A Confirmado: El problema está en el renderer/present, no en el PPU ni en las paletas.
Evidencia:
IdxNonZero=22910> 0 ✅ (PPU genera señal)RgbNonWhite=22910> 0 ✅ (Conversión a RGB funciona)PresentNonWhite=0❌ (Present buffer está blanco)
Fuentes Consultadas
- Pan Docs: "CGB Palettes", "PPU Rendering Pipeline", "Framebuffer Format"
- Step 0495: CGB Palette Reality Check (implementación previa de paletas CGB)
- Step 0489: ThreeBufferStats (estructura de estadísticas de tres buffers)
Integridad Educativa
Lo que Entiendo Ahora
- Pipeline de Renderizado: El pipeline tiene tres etapas claras (índices, RGB, present). Si una etapa falla, las siguientes también fallan. El análisis de ThreeBufferStats permite identificar exactamente en qué etapa está el problema.
- Modo Headless: El renderer puede funcionar sin ventana gráfica creando un Surface temporal. Esto permite diagnóstico en CI y ejecución sin display.
- Dumps Sincronizados: Los dumps PPM de las tres etapas deben generarse en el mismo frame para comparar correctamente.
Lo que Falta Confirmar
- FB_PRESENT_SRC en UI: Necesitamos ejecutar con UI (`main.py`) para capturar FB_PRESENT_SRC real y confirmar si el problema persiste cuando se usa el renderer real.
- Causa Raíz del Present Blanco: Si PresentNonWhite sigue siendo 0 en UI, investigar pitch del Surface, formato (RGBA vs BGRA), orden de operaciones, o buffer stale.
Hipótesis y Suposiciones
Hipótesis: El problema está en el renderer/present porque FB_INDEX y FB_RGB tienen señal, pero FB_PRESENT está blanco. Sin embargo, como rom_smoke no usa el renderer, necesitamos ejecutar con UI para confirmar.
Próximos Pasos
- [ ] Ejecutar con UI (`main.py`) con tetris_dx.gbc para capturar FB_PRESENT_SRC real
- [ ] Verificar si PresentNonWhite sigue siendo 0 en ejecución con UI
- [ ] Si el problema persiste, investigar pitch del Surface, formato (RGBA vs BGRA), orden de operaciones, o buffer stale
- [ ] Implementar fix mínimo si se identifica la causa raíz