⚠️ 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.

CGB End-to-End Present Proof (Idx→RGB→Present)

Fecha: 2026-01-08 Step ID: 0496 Estado: VERIFIED

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:

  1. 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.
  2. 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.
  3. 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=dummy o VIBOY_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 dump
  • VIBOY_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 Surface
  • present_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 separado
  • tools/rom_smoke_0442.py - PresentDetails en snapshot
  • docs/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