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

Dejar de Usar "Nonwhite" Como Señal y Congelar Contratos de Paleta (DMG + CGB) con Métricas Robustas + Tests Clean-Room

Fecha: 2026-01-03 Step ID: 0454 Estado: VERIFIED

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 resumen
  • src/gpu/renderer.py - Añadida función _calculate_unique_rgb_count_surface() y logging de métricas robustas
  • tests/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)