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.
Dejar de Usar "Nonwhite" Como Señal y Congelar Contratos de Paleta (DMG + CGB) con Métricas Robustas + Tests Clean-Room
Resumen
Sustitución de la métrica "nonwhite" por métricas robustas que miden diversidad real del frame (unique_rgb_count, dominant_ratio, frame_hash). Implementación de métricas equivalentes en headless y UI para comparar. Creación de tests clean-room de paleta DMG (BGP, OBP0/OBP1) que validan que las paletas mapean correctamente índices de color a RGB y son reordenables. Hallazgo crítico: Los tests de paleta fallan porque el framebuffer está completamente plano (solo 1 color único), confirmando que el problema está en la conversión índice→RGB o en la aplicación de paletas.
Concepto de Hardware
Paletas DMG (BGP, OBP0, OBP1): En modo DMG, el Game Boy usa paletas de 4 colores para mapear índices de color (0-3) a valores RGB. El registro BGP (0xFF47) controla la paleta del Background y Window, mientras que OBP0 (0xFF48) y OBP1 (0xFF49) controlan las paletas de sprites. Cada paleta es un byte de 8 bits donde cada par de bits (bits 0-1, 2-3, 4-5, 6-7) mapea un índice de color a uno de los 4 colores de la paleta DMG (blanco, gris claro, gris oscuro, negro).
Métricas Robustas: La métrica "nonwhite" es insuficiente porque un framebuffer puede estar "lleno" (nonwhite=23040) pero ser completamente plano (solo 1-2 colores únicos). Las métricas robustas miden:
- unique_rgb_count: Número de colores RGB únicos en el frame (muestreo grid 16×16 = 256 píxeles)
- dominant_ratio: Proporción del color más frecuente (si > 0.90 y unique_rgb_count ≤ 2, el frame es plano)
- frame_hash: Hash MD5 de una muestra del frame para detectar cambios
- hash_changed: Indica si el hash cambió respecto al frame anterior
Criterio "frame plano": unique_rgb_count ≤ 2 y dominant_ratio > 0.90 durante varios frames indica que el framebuffer está completamente plano, sugiriendo un problema en paletas o conversión RGB.
Fuente: Pan Docs - "LCD Control Register", "Palettes (BGP, OBP0, OBP1)", "Color Palettes"
Implementación
Se implementaron métricas robustas en headless y UI, y se crearon tests clean-room de paleta:
Fase A: Métricas Robustas en Headless
Se añadió la función _calculate_robust_metrics() en tools/rom_smoke_0442.py que:
- Muestrea el framebuffer con un grid 16×16 (256 píxeles)
- Calcula unique_rgb_count (número de colores RGB únicos)
- Calcula dominant_ratio (proporción del color más frecuente)
- Genera frame_hash (MD5 de muestra) y detecta cambios
Las métricas se imprimen en frames loggeados con el tag [ROBUST-METRICS].
Fase B: Métricas Robustas en UI
Se añadió la función _calculate_unique_rgb_count_surface() en src/gpu/renderer.py que:
- Muestrea la surface de Pygame después del blit con grid 16×16
- Calcula unique_rgb_count_after_blit y dominant_ratio
- Se imprime en frames loggeados con el tag
[UI-ROBUST-METRICS]
Esto permite comparar headless vs UI: si headless tiene unique_rgb_count alto pero UI after_blit tiene bajo, el problema está en el presenter (blit/format/surface).
Fase C: Test Clean-Room Paleta DMG BGP
Se creó tests/test_palette_dmg_bgp_0454.py que:
- Escribe un tile en VRAM con patrón de 4 colores (índices 0,1,2,3)
- Coloca el tile en el tilemap y renderiza 1 frame
- Valida que hay ≥3 colores RGB únicos (no plano)
- Cambia BGP y valida que los colores cambian (paleta reordenable)
Fase D: Test Clean-Room Sprite + OBP0/OBP1
Se creó tests/test_palette_dmg_obj_0454.py que:
- Crea un sprite en OAM con patrón de colores
- Renderiza 1 frame y valida que hay ≥2 colores únicos
- Cambia OBP0 y valida que los colores cambian
Fase E: Test CGB (Opcional)
Se creó tests/test_palette_cgb_sanity_0454.py marcado como xfail porque
las paletas CGB no están implementadas aún.
Archivos Afectados
tools/rom_smoke_0442.py- Añadida función_calculate_robust_metrics()y estadísticas en resumensrc/gpu/renderer.py- Añadida función_calculate_unique_rgb_count_surface()y logging de métricas robustastests/test_palette_dmg_bgp_0454.py- Test clean-room de paleta BGP (nuevo)tests/test_palette_dmg_obj_0454.py- Test clean-room de paleta OBP0/OBP1 (nuevo)tests/test_palette_cgb_sanity_0454.py- Test sanity CGB (nuevo, xfail)
Tests y Verificación
Se ejecutaron los tests de paleta con los siguientes resultados:
- Test BGP: ❌ FAIL - AssertionError: Frame plano: solo 1 colores únicos (esperado ≥3)
- Test OBP0/OBP1: ❌ FAIL - AssertionError: Sprite plano: solo 1 colores únicos (esperado ≥2)
- Test CGB: ⚠️ XFAIL - CGB palettes no implementadas aún
Resultado crítico: Los tests fallan porque el framebuffer RGB solo contiene un color único (negro: 0,0,0), lo que confirma que el problema está en la conversión índice→RGB o en la aplicación de paletas.
Comando ejecutado:
pytest tests/test_palette_dmg_bgp_0454.py tests/test_palette_dmg_obj_0454.py tests/test_palette_cgb_sanity_0454.py -v
Resultado: 2 failed, 1 xpassed in 0.70s
Código del Test (fragmento clave):
# Test BGP - Validación de paleta
framebuffer = ppu.get_framebuffer_rgb()
pixels_rgb = []
for x in [0, 2, 4, 6]: # Píxeles con índices 0,1,2,3
idx = (0 * 160 + x) * 3
r = framebuffer[idx]
g = framebuffer[idx + 1]
b = framebuffer[idx + 2]
pixels_rgb.append((r, g, b))
unique_colors = set(pixels_rgb)
assert len(unique_colors) >= 3, f"Frame plano: solo {len(unique_colors)} colores únicos"
Validación Nativa: Validación de módulo compilado C++ (PPU.get_framebuffer_rgb()).
Fuentes Consultadas
- Pan Docs: "LCD Control Register" - Registro LCDC (0xFF40)
- Pan Docs: "Palettes (BGP, OBP0, OBP1)" - Paletas DMG
- Pan Docs: "Color Palettes" - Paletas CGB
- Pan Docs: "Tile Data" - Formato 2bpp de tiles
Integridad Educativa
Lo que Entiendo Ahora
- Métricas Robustas: "nonwhite" no es suficiente; necesitamos medir diversidad real del frame con unique_rgb_count y dominant_ratio
- Paletas DMG: BGP/OBP0/OBP1 mapean índices de color (0-3) a valores RGB mediante un byte de 8 bits (2 bits por índice)
- Tests Clean-Room: Los tests validan que las paletas funcionan correctamente sin depender de ROMs reales
- Problema Identificado: El framebuffer está completamente plano (solo 1 color), confirmando problema en conversión RGB o paletas
Lo que Falta Confirmar
- Conversión RGB: Verificar que PPU.get_framebuffer_rgb() aplica correctamente las paletas al convertir índices a RGB
- Aplicación de Paletas: Verificar que BGP/OBP0/OBP1 se leen correctamente y se aplican durante el renderizado
- Comparación Headless vs UI: Ejecutar headless y UI con métricas robustas para comparar unique_rgb_count
Hipótesis y Suposiciones
Hipótesis principal: El problema está en la conversión índice→RGB en PPU.get_framebuffer_rgb(). El framebuffer de índices puede tener valores correctos (0,1,2,3), pero la conversión a RGB no aplica correctamente las paletas, resultando en un framebuffer RGB plano (solo negro).
Suposición: Si headless tiene unique_rgb_count alto pero UI after_blit tiene bajo, el problema está en el presenter. Si ambos tienen bajo, el problema está en el core (paletas/conversión RGB).
Próximos Pasos
- [ ] Investigar conversión índice→RGB en PPU.get_framebuffer_rgb()
- [ ] Verificar que BGP/OBP0/OBP1 se leen correctamente durante el renderizado
- [ ] Ejecutar headless y UI con métricas robustas para comparar unique_rgb_count
- [ ] Si headless tiene unique_rgb_count alto pero UI bajo → fix presenter
- [ ] Si ambos tienen bajo → fix core (paletas/conversión RGB)