⚠️ 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 Palette Reality Check (Cerrar el Blanco)

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

Resumen

Este step implementa un diagnóstico completo del problema de "pantalla blanca" en modo CGB (`tetris_dx.gbc`). Se implementaron herramientas de diagnóstico (CGB Detection, IO Watch para FF68-FF6B, dump de paletas CGB, Pixel Proof) que permitieron identificar que el problema estaba en `PPU::convert_framebuffer_to_rgb()`, que siempre usaba modo DMG incluso cuando el hardware era CGB. El fix aplicado permite que el código use correctamente las paletas CGB cuando está en modo CGB, resolviendo el problema del blanco.

Concepto de Hardware

Detección de Modo CGB: El Game Boy Color puede operar en dos modos: modo CGB nativo (usando paletas CGB) y modo compatibilidad DMG (usando paletas DMG incluso en hardware CGB). El modo se determina por el byte 0x0143 del header ROM (0x80 o 0xC0 = CGB) y por el bit 0 de LCDC (0xFF40): si LCDC bit 0 = 0 (LCD OFF), el hardware opera en modo compatibilidad DMG.

Paletas CGB: En modo CGB, las paletas se configuran escribiendo a BGPI/OBPI (Palette Index, 0xFF68/0xFF6A) y luego a BGPD/OBPD (Palette Data, 0xFF69/0xFF6B). Cada paleta tiene 4 colores, cada color es 2 bytes en formato BGR555 (bits 0-4 = Blue, 5-9 = Green, 10-14 = Red). Hay 8 paletas BG y 8 paletas OBJ, cada una con 4 colores = 64 bytes total por tipo.

Conversión BGR555→RGB888: Los colores CGB están en formato BGR555 (15 bits: 5 bits por componente). Para convertir a RGB888 (24 bits: 8 bits por componente), se extraen los componentes BGR555 y se escalan: `r8 = (r5 * 255) / 31`.

Referencia: Pan Docs - "CGB Palettes", "CGB Registers", "LCDC (0xFF40)", "BGR555 Format"

Implementación

Fase A: CGB Detection

A1-A2: Getters en MMU

Se implementó en MMU.hpp/MMU.cpp:

  • rom_header_cgb_flag_: Miembro que almacena el byte 0x0143 del header ROM
  • get_rom_header_cgb_flag(): Getter que retorna el flag CGB del header ROM
  • get_dmg_compat_mode(): Getter que retorna true si LCDC bit 0 = 0 (modo compatibilidad DMG dentro de CGB)

A3: Exposición Cython

Se expusieron los getters en mmu.pyx:

  • get_rom_header_cgb_flag(): Retorna el flag CGB del header ROM
  • get_dmg_compat_mode(): Retorna boolean indicando si está en modo compatibilidad DMG
  • get_hardware_mode(): Ya existía, retorna "CGB" o "DMG"

A4: Sección CGBDetection en Snapshot

Se añadió sección CGBDetection en rom_smoke_0442.py:

  • rom_header_cgb_flag: Byte 0x0143 del header ROM
  • machine_is_cgb: Flag interno del emulador (1 = CGB, 0 = DMG)
  • dmg_compat_mode: Modo compatibilidad DMG dentro de CGB

Fase B: IO Watch para FF68-FF6B

B1: Estructura IOWatchFF68FF6B

Se implementó en MMU.hpp:

  • Tracking de writes/reads a FF68 (BGPI/BCPS), FF69 (BGPD/BCPD), FF6A (OBPI/OCPS), FF6B (OBPD/OCPD)
  • Para cada registro: contadores de write/read, último PC, último valor

B2: Tracking en MMU

Se implementó tracking en MMU::write() y MMU::read():

  • Incrementa contadores y almacena último PC/valor para cada acceso a FF68-FF6B
  • Siempre activo (no gateado por variables de entorno)

B3: Exposición Cython y Snapshot

Se expuso el getter en mmu.pyx y se añadió sección IOWatchFF68FF6B en snapshot.

Fase C: Dump Compacto de RAM de Paletas

C1: Sección CGBPaletteRAM en Snapshot

Se añadió sección CGBPaletteRAM en rom_smoke_0442.py:

  • bg_palette_bytes_hex: Hex dump de 64 bytes de paleta BG
  • obj_palette_bytes_hex: Hex dump de 64 bytes de paleta OBJ
  • bg_palette_nonwhite_entries: Contador de entradas no blancas en paleta BG
  • obj_palette_nonwhite_entries: Contador de entradas no blancas en paleta OBJ

Fase D: Pixel Proof

D1: Sección PixelProof en Snapshot

Se añadió sección PixelProof en rom_smoke_0442.py:

  • Muestra hasta 5 píxeles no blancos del framebuffer RGB
  • Para cada píxel: coordenadas (x, y), índice de color (idx), paleta usada (BG/OBJ), color BGR555 raw, color RGB888 final

Fase E: Fix Mínimo

E1: Problema Identificado

En PPU::convert_framebuffer_to_rgb(), línea 5613:

bool is_dmg = true;  // ❌ Siempre DMG, incluso en CGB

Esto forzaba siempre el uso de paletas DMG (BGP/OBP0/OBP1), que en `tetris_dx.gbc` se pone en 0x00 (todo blanco), en lugar de usar las paletas CGB que tienen datos válidos.

E2: Fix Aplicado

Se implementó detección correcta de modo CGB:

HardwareMode hw_mode = mmu_->get_hardware_mode();
bool is_dmg = (hw_mode == HardwareMode::DMG);

// Si estamos en CGB pero en modo compatibilidad DMG (LCDC bit 0 = 0), usar BGP
if (!is_dmg && mmu_->get_dmg_compat_mode()) {
    is_dmg = true;  // Usar paletas DMG aunque sea hardware CGB
}

También se corrigió la conversión BGR555→RGB888 (extracción correcta de componentes B, G, R).

Fase F: Validación

F1: Ejecución y Resultados

Se ejecutó tetris_dx.gbc por 600 frames y se verificó que el fix funciona:

  • fb_nonzero=22910 ✅ (hay índices no cero)
  • PixelProof muestra píxeles con RGB no blancos: rgb(0,0,0) y rgb(197,197,197)
  • CGBDetection_MachineIsCGB=1 ✅ (emulador detecta CGB correctamente)
  • CGBPaletteRAM_BG_NonWhite=24 ✅ (paletas CGB tienen datos)

Archivos Afectados

  • src/core/cpp/MMU.hpp - Estructura IOWatchFF68FF6B, miembro rom_header_cgb_flag_, getters
  • src/core/cpp/MMU.cpp - Implementación de tracking FF68-FF6B, getters CGB detection
  • src/core/cpp/PPU.cpp - Fix en convert_framebuffer_to_rgb() para detectar modo CGB
  • src/core/cython/mmu.pxd - Declaraciones de estructuras y getters
  • src/core/cython/mmu.pyx - Exposición de getters a Python
  • tools/rom_smoke_0442.py - Secciones CGBDetection, IOWatchFF68FF6B, CGBPaletteRAM, PixelProof en snapshot

Tests y Verificación

Comando ejecutado:

PYTHONPATH=. python3 tools/rom_smoke_0442.py roms/tetris_dx.gbc --frames 600

Resultados (Frame 600):

  • CGBDetection_MachineIsCGB=1 ✅ (emulador detecta CGB)
  • CGBPaletteRAM_BG_NonWhite=24 ✅ (paletas CGB tienen datos)
  • fb_nonzero=22910 ✅ (framebuffer tiene índices no cero)
  • PixelProof_P0_(0,0)_idx3_palBG_15b0x0000_rgb(0,0,0)_P1_(1,0)_idx1_palBG_15b0x6318_rgb(197,197,197) ✅ (hay píxeles RGB no blancos)

Validación de módulo compilado C++:

# El código C++ se compiló correctamente con:
python3 setup.py build_ext --inplace

# Los getters Cython funcionan correctamente:
from viboy_core import PyMMU
mmu = PyMMU()
cgb_flag = mmu.get_rom_header_cgb_flag()  # ✅ Funciona
dmg_compat = mmu.get_dmg_compat_mode()   # ✅ Funciona
io_watch = mmu.get_io_watch_ff68_ff6b()  # ✅ Funciona

Conclusión

El problema del "blanco" en modo CGB estaba causado por que PPU::convert_framebuffer_to_rgb() siempre usaba modo DMG, forzando el uso de BGP (que el juego pone en 0x00 = blanco) en lugar de usar las paletas CGB que tienen datos válidos. El fix permite que el código detecte correctamente el modo CGB y use las paletas CGB cuando corresponde, resolviendo el problema.

Resultado Final: El framebuffer RGB ahora tiene colores no blancos (PixelProof muestra rgb(0,0,0) y rgb(197,197,197)), confirmando que el fix funciona correctamente.

Referencias

  • Plan: step_0495_-_cgb_palette_reality_check_(cerrar_el_blanco)_e693ca2d.plan.md
  • Reporte: docs/reports/reporte_step0495.md
  • Pan Docs: "CGB Palettes", "CGB Registers", "LCDC (0xFF40)", "BGR555 Format"
  • GBEDG: "CGB Palette System"