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.
Renderizado CGB RGB888 con Paletas
Resumen
Implementación del pipeline completo de renderizado CGB RGB888 usando paletas nativas CGB (BGR555) sin depender de BGP. Añadidos helpers en MMU para leer paletas CGB sin efectos colaterales (read_bg_palette_data(), read_obj_palette_data()). Implementado framebuffer RGB888 de doble buffer en PPU (160×144×3 = 69120 bytes) para renderizado real en color. Función convert_framebuffer_to_rgb() convierte índices de color (0-3) a RGB888 usando paletas CGB con conversión BGR555→RGB888 según Pan Docs. Wrapper Cython get_framebuffer_rgb() expone framebuffer RGB con acceso zero-copy desde Python. Compilación exitosa. Tests demuestran que Tetris DX progresa correctamente (GameplayState=YES, TileData=56.6%, escrituras a VBK detectadas), mientras Zelda DX y Pokémon Red requieren boot ROM o inicialización mejorada (BGP=0x00, TileData=0%). Sistema preparado para renderizado dual-mode (DMG con índices+BGP, CGB con RGB+paletas nativas).
Concepto de Hardware (Pan Docs - CGB Palettes, Color Format)
Sistema de Paletas CGB
Game Boy Color introduce un sistema de paletas de 15 bits (BGR555) que permite 32768 colores simultáneos. A diferencia de DMG que usa paletas de 2 bits (4 tonos de gris) definidas en BGP (0xFF47), CGB tiene paletas dedicadas almacenadas en RAM interna.
Organización de Paletas
- Paletas BG (Background): 8 paletas × 4 colores × 2 bytes = 64 bytes
- Paletas OBJ (Sprites): 8 paletas × 4 colores × 2 bytes = 64 bytes
- Acceso: Via registros BCPS/BCPD (FF68/FF69) para BG, OCPS/OCPD (FF6A/FF6B) para OBJ
- Autoincremento: Bit 7 de BCPS/OCPS activa autoincremento del índice tras cada lectura/escritura
Formato de Color BGR555
Cada color CGB ocupa 2 bytes (Little Endian):
Byte 0 (low): GGGRRRRR
Byte 1 (high): XBBBBBGG
Donde:
- R: 5 bits de Red (0-31)
- G: 5 bits de Green (0-31, distribuido en 2 bytes)
- B: 5 bits de Blue (0-31)
- X: 1 bit sin usar (siempre 0)
Conversión BGR555 → RGB888
Para mostrar en pantallas modernas (RGB888, 8 bits por canal), convertimos:
uint16_t color_bgr555 = lo | (hi << 8);
uint8_t r5 = (color_bgr555 >> 0) & 0x1F;
uint8_t g5 = (color_bgr555 >> 5) & 0x1F;
uint8_t b5 = (color_bgr555 >> 10) & 0x1F;
// Escalar de 5 bits (0-31) a 8 bits (0-255)
uint8_t r8 = (r5 * 255) / 31;
uint8_t g8 = (g5 * 255) / 31;
uint8_t b8 = (b5 * 255) / 31;
Renderizado Dual-Mode (DMG vs CGB)
El emulador mantiene dos pipelines de renderizado:
- DMG Mode: Framebuffer de índices (0-3) → Python aplica paleta BGP (0xFF47)
- CGB Mode: Framebuffer de índices → C++ convierte a RGB usando paletas CGB → Python lee RGB directamente
Ventaja: Compatibilidad total con DMG (sin regresión) mientras se añade soporte CGB real.
Tile Attributes (VRAM Bank 1)
En CGB, cada tile en el tilemap tiene atributos asociados (VRAM Bank 1):
Bit 7: BG/Win Priority
Bit 6: Y-Flip
Bit 5: X-Flip
Bit 4: VRAM Bank (0 o 1)
Bit 3: CGB Palette (bit alto)
Bit 2-0: CGB Palette (bits bajos, 0-7)
Implementación Actual: Por simplicidad, esta versión usa paleta 0 para todos los tiles. La lectura de tile attributes desde VRAM Bank 1 se implementará en pasos futuros.
Implementación
3.1. Helpers de Acceso a Paletas CGB (MMU)
Archivo: src/core/cpp/MMU.hpp, MMU.cpp
Añadidos métodos inline para acceso directo a paletas sin efectos colaterales:
// MMU.hpp
inline uint8_t read_bg_palette_data(uint8_t index) const {
if (index < 0x40) {
return bg_palette_data_[index];
}
return 0xFF;
}
inline uint8_t read_obj_palette_data(uint8_t index) const {
if (index < 0x40) {
return obj_palette_data_[index];
}
return 0xFF;
}
Por qué es necesario: El acceso a paletas via BCPS/BCPD tiene autoincremento (bit 7 de BCPS). Para que la PPU lea paletas durante el renderizado sin afectar el estado de la CPU, necesitamos acceso directo a la RAM de paletas.
3.2. Framebuffer RGB888 (PPU)
Archivo: src/core/cpp/PPU.hpp, PPU.cpp
Añadido doble buffer RGB:
// PPU.hpp
std::vector<uint8_t> framebuffer_rgb_front_; // 160*144*3 = 69120 bytes
std::vector<uint8_t> framebuffer_rgb_back_; // 160*144*3 = 69120 bytes
uint8_t* get_framebuffer_rgb_ptr(); // Retorna puntero a front buffer
Doble Buffering: Mismo patrón que framebuffer de índices (front/back swap) para evitar condiciones de carrera entre C++ (escribe) y Python (lee).
3.3. Conversión BGR555 → RGB888
Archivo: src/core/cpp/PPU.cpp
void PPU::convert_framebuffer_to_rgb() {
if (mmu_ == nullptr) {
return;
}
// Leer paleta 0 de BG (simplificado: todos los tiles usan paleta 0)
uint16_t cgb_palette[4];
for (int i = 0; i < 4; i++) {
uint8_t lo = mmu_->read_bg_palette_data(i * 2);
uint8_t hi = mmu_->read_bg_palette_data(i * 2 + 1);
cgb_palette[i] = lo | (hi << 8);
}
// Convertir cada píxel del framebuffer de índices a RGB
for (size_t i = 0; i < FRAMEBUFFER_SIZE; i++) {
uint8_t color_index = framebuffer_front_[i];
if (color_index > 3) color_index = 0;
uint16_t bgr555 = cgb_palette[color_index];
// Extraer componentes BGR555
uint8_t r5 = (bgr555 >> 0) & 0x1F;
uint8_t g5 = (bgr555 >> 5) & 0x1F;
uint8_t b5 = (bgr555 >> 10) & 0x1F;
// Convertir a RGB888
uint8_t r8 = (r5 * 255) / 31;
uint8_t g8 = (g5 * 255) / 31;
uint8_t b8 = (b5 * 255) / 31;
// Escribir al framebuffer RGB
framebuffer_rgb_front_[i * 3 + 0] = r8; // Red
framebuffer_rgb_front_[i * 3 + 1] = g8; // Green
framebuffer_rgb_front_[i * 3 + 2] = b8; // Blue
}
}
Optimización Futura: Esta función se llamará automáticamente al final de cada frame cuando se detecte modo CGB. Por ahora, Python puede llamarla explícitamente antes de leer el framebuffer RGB.
3.4. Wrapper Cython
Archivos: src/core/cython/ppu.pxd, ppu.pyx
# ppu.pxd
cdef extern from "PPU.hpp":
cdef cppclass PPU:
uint8_t* get_framebuffer_rgb_ptr()
void convert_framebuffer_to_rgb()
# ppu.pyx
def get_framebuffer_rgb(self):
"""
Obtiene el framebuffer RGB888 como memoryview (Zero-Copy).
Tamaño: 160 * 144 * 3 = 69120 bytes (R, G, B por píxel).
"""
if self._ppu == NULL:
return None
cdef uint8_t* ptr = self._ppu.get_framebuffer_rgb_ptr()
if ptr == NULL:
return None
cdef unsigned char[:] view = <unsigned char[:144*160*3]>ptr
return view
Zero-Copy: El memoryview permite acceso directo a la memoria C++ sin copias intermedias.
Tests y Verificación
4.1. Compilación
Comando:
python3 setup.py build_ext --inplace
Resultado: ✅ Compilación exitosa. Archivo generado: viboy_core.cpython-312-x86_64-linux-gnu.so (2.7MB)
Validación: Warnings esperados (formato printf, variables no usadas) no afectan funcionalidad. Validación de módulo compilado C++ exitosa.
4.2. Tests Controlados (30s timeout)
Comando:
timeout 30s python3 main.py roms/tetris_dx.gbc > logs/step0405_tetris_dx_rgb.log 2>&1
timeout 30s python3 main.py roms/Oro.gbc > logs/step0405_zelda_dx_rgb.log 2>&1
timeout 30s python3 main.py roms/pkmn.gb > logs/step0405_pkmn_dmg.log 2>&1
Resultado: Tetris DX (CGB)
- ✅ GameplayState: YES (alcanzado en frame 720)
- ✅ BGP: 0xFC → 0xE4 (cambió en frame 711)
- ✅ TileData: 56.6% (3479/6144 bytes) en frame 840
- ✅ UniqueTiles: 185/256
- ✅ VBK Writes: Detectadas (PC:0x0590, VBK ← 0x00)
- ✅ HDMA5: 0xFF (inactivo, no usa HDMA)
- Conclusión: Tetris DX progresa correctamente con la inicialización CGB del Step 0404
Resultado: Zelda DX (Oro.gbc - CGB)
- ⚠️ GameplayState: NO (se mantiene en inicialización)
- ⚠️ BGP: 0x00 (no inicializado)
- ⚠️ TileData: 0/6144 (0.0%) - Sin tiles cargados
- ⚠️ TileMap: 100% (posiblemente limpiado/inicializado)
- ⚠️ IE/IF: 0x1F/0x07 (múltiples IRQs activas pero no servidas)
- ⚠️ VBK Writes: No detectadas
- Conclusión: Zelda DX requiere boot ROM o inicialización mejorada (polling en wait-loop esperando condición específica)
Resultado: Pokémon Red (DMG)
- ⚠️ GameplayState: NO
- ⚠️ BGP: 0x00 (no inicializado sin boot ROM)
- ⚠️ TileData: 0/6144 (0.0%)
- Conclusión: Patrón similar a Zelda DX. Juegos DMG también requieren boot ROM para inicialización completa
4.3. Análisis Comparativo (Tetris DX vs Zelda DX)
Diferencias clave:
| Aspecto | Tetris DX | Zelda DX |
|---|---|---|
| BGP Inicial | 0xFC (correcto) | 0x00 (incorrecto) |
| LCDC | 0x91 → 0x81 | 0xE3 (constante) |
| IE Handling | 0x00 → VBlank habilitado | 0x1F (todas activas) |
| Progresión | Lineal, alcanza gameplay | Bloqueado en inicialización |
Hipótesis: Tetris DX tiene inicialización más robusta que no depende de estado post-boot específico. Zelda DX espera condiciones exactas que solo boot ROM proporciona.
Próximos Pasos
- Renderer Python RGB: Actualizar
src/gpu/renderer.pypara usarget_framebuffer_rgb()cuandohardware_mode == CGB - Tile Attributes (VRAM Bank 1): Leer atributos de tiles para determinar paleta correcta por tile (actualmente usa paleta 0 para todo)
- Auto-conversión: Llamar
convert_framebuffer_to_rgb()automáticamente al final de cada frame en modo CGB - Boot ROM Legal: Documentar cómo obtener/usar boot ROM real (según guía Step 0403) para resolver inicialización de Zelda DX/Pokémon Red
- Instrumentación Dirigida: Monitores específicos para detectar qué registros/condiciones faltan en Zelda DX (basado en diferencias vs Tetris DX)
Comandos Git
git add src/core/cpp/MMU.hpp src/core/cpp/MMU.cpp \
src/core/cpp/PPU.hpp src/core/cpp/PPU.cpp \
src/core/cython/ppu.pxd src/core/cython/ppu.pyx \
logs/step0405_*.log \
docs/bitacora/entries/2026-01-01__0405__renderizado-cgb-rgb888-paletas.html \
docs/bitacora/index.html \
docs/informe_fase_2/
git commit -m "feat(ppu+cgb): renderizado RGB888 con paletas CGB BGR555 (Step 0405)
- Añadidos helpers en MMU: read_bg_palette_data(), read_obj_palette_data()
- Implementado framebuffer RGB888 doble buffer (69120 bytes) en PPU
- Función convert_framebuffer_to_rgb(): índices → RGB888 con paletas CGB
- Wrapper Cython get_framebuffer_rgb() con acceso zero-copy
- Conversión BGR555 → RGB888 según Pan Docs (escala 5-bit → 8-bit)
- Tests: Tetris DX ✅ progresa (GameplayState=YES, TileData=56.6%)
- Tests: Zelda DX/Pokémon Red ⚠️ requieren boot ROM (BGP=0x00, TileData=0%)
- Sistema preparado para renderizado dual-mode (DMG índices, CGB RGB)
Fuente: Pan Docs - CGB Palettes, Color Format"
git push