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.
Step 0257: Hardware Palette Bypass (C++)
Resumen
Este Step modifica `src/core/cpp/PPU.cpp` para forzar valores estándar de paleta (`0xE4`) directamente en el motor de renderizado de C++, ignorando completamente los registros de paleta de la MMU (BGP, OBP0, OBP1). El objetivo es garantizar que los índices de color (0-3) generados desde la VRAM se preserven en el framebuffer, independientemente del estado de los registros de paleta en la MMU.
Si la pantalla sigue verde después del Step 0256 (paleta de debug en Python), significa que el framebuffer de C++ está lleno de ceros. Esto puede ocurrir si la PPU está aplicando una paleta con valor `0x00` que convierte todos los píxeles (incluso los negros) en índice `0` antes de escribirlos en el framebuffer. Al forzar `0xE4` (mapeo identidad: 3→3, 2→2, 1→1, 0→0), garantizamos que los datos visuales de la VRAM se preserven en el framebuffer.
Concepto de Hardware
En la Game Boy, la PPU (Pixel Processing Unit) genera índices de color (0-3) desde los datos de los tiles almacenados en VRAM. Estos índices pasan por los registros de paleta (BGP para fondo, OBP0/OBP1 para sprites) antes de escribirse en el framebuffer interno de la PPU.
Registro BGP (0xFF47): Paleta del Background. Cada par de bits (0-1, 2-3, 4-5, 6-7) mapea un índice de color crudo (0-3) a un índice final (0-3). El valor estándar es `0xE4` (11100100 en binario), que implementa un mapeo identidad:
- Bits 0-1 (00): Índice 0 → Color 0
- Bits 2-3 (01): Índice 1 → Color 1
- Bits 4-5 (10): Índice 2 → Color 2
- Bits 6-7 (11): Índice 3 → Color 3
Problema Crítico: Si BGP está en `0x00` (00000000), todos los índices se mapean al color 0:
- Índice 0 → Color 0 (blanco)
- Índice 1 → Color 0 (blanco)
- Índice 2 → Color 0 (blanco)
- Índice 3 → Color 0 (blanco)
Esto significa que incluso si la VRAM contiene datos válidos (tiles con píxeles negros, índice 3), la PPU los convierte a índice 0 antes de escribirlos en el framebuffer. Cuando Python lee el framebuffer, solo ve ceros, y la paleta de debug de Python (Step 0256) mapea el índice 0 a verde/blanco.
Solución de Bypass: Al forzar `BGP = 0xE4` directamente en el código C++ de la PPU, ignoramos cualquier valor erróneo que pueda estar en la MMU y garantizamos que los índices de color se preserven. Si después de este bypass vemos formas negras/grises en la pantalla, confirmamos que:
- La VRAM contiene datos válidos (tiles cargados correctamente).
- La PPU está leyendo y decodificando los tiles correctamente.
- El problema estaba en los registros de paleta (BGP/OBP) en la MMU.
Fuente: Pan Docs - Palette Registers (BGP, OBP0, OBP1), Background Palette Register
Implementación
Se modificaron dos funciones en `src/core/cpp/PPU.cpp` para forzar valores de paleta estándar (`0xE4`) en lugar de leerlos desde la MMU:
1. Modificación en `render_scanline()` (Background)
Se agregó código para forzar `BGP = 0xE4` y aplicar el mapeo de paleta antes de escribir en el framebuffer:
// --- Step 0257: HARDWARE PALETTE BYPASS ---
// Forzar BGP = 0xE4 (mapeo identidad: 3->3, 2->2, 1->1, 0->0)
// Esto garantiza que los índices de color se preserven en el framebuffer,
// independientemente del estado de los registros de paleta en la MMU.
// uint8_t bgp = mmu_->read(IO_BGP); // COMENTADO: Ignorar MMU
uint8_t bgp = 0xE4; // 11 10 01 00 (Mapeo identidad estándar)
// -------------------------------------------
// ... código de decodificación de tiles ...
// --- Step 0257: Aplicar paleta forzada (mapeo identidad) ---
// Aplicar BGP para mapear el índice de color crudo al índice final
// BGP = 0xE4 = 11 10 01 00
// color_index 0 -> (BGP >> 0) & 3 = 0
// color_index 1 -> (BGP >> 2) & 3 = 1
// color_index 2 -> (BGP >> 4) & 3 = 2
// color_index 3 -> (BGP >> 6) & 3 = 3
uint8_t final_color = (bgp >> (color_index * 2)) & 0x03;
framebuffer_[line_start_index + x] = final_color;
// -------------------------------------------------------------
2. Modificación en `render_sprites()` (Sprites)
Se agregó código para forzar `OBP0 = 0xE4` y `OBP1 = 0xE4` y aplicar el mapeo de paleta según el atributo del sprite:
// --- Step 0257: HARDWARE PALETTE BYPASS ---
// Forzar OBP0 = 0xE4 y OBP1 = 0xE4 (mapeo identidad)
// Esto garantiza que los índices de color de sprites se preserven en el framebuffer,
// independientemente del estado de los registros de paleta en la MMU.
// uint8_t obp0 = mmu_->read(IO_OBP0); // COMENTADO: Ignorar MMU
// uint8_t obp1 = mmu_->read(IO_OBP1); // COMENTADO: Ignorar MMU
uint8_t obp0 = 0xE4; // 11 10 01 00 (Mapeo identidad estándar)
uint8_t obp1 = 0xE4; // 11 10 01 00 (Mapeo identidad estándar)
// -------------------------------------------
// ... código de renderizado de sprites ...
// --- Step 0257: Aplicar paleta forzada (mapeo identidad) ---
// Aplicar OBP0 u OBP1 según el atributo del sprite
uint8_t palette = (palette_num == 0) ? obp0 : obp1;
// Aplicar paleta para mapear el índice de color crudo al índice final
// palette = 0xE4 = 11 10 01 00
// sprite_color_idx 0 -> (palette >> 0) & 3 = 0 (transparente, no se dibuja)
// sprite_color_idx 1 -> (palette >> 2) & 3 = 1
// sprite_color_idx 2 -> (palette >> 4) & 3 = 2
// sprite_color_idx 3 -> (palette >> 6) & 3 = 3
uint8_t final_sprite_color = (palette >> (sprite_color_idx * 2)) & 0x03;
framebuffer_line[final_x] = final_sprite_color;
// -------------------------------------------------------------
Decisiones de Diseño
- Bypass en C++: Se eligió forzar los valores de paleta directamente en C++ en lugar de solo en Python (Step 0256) para garantizar que el framebuffer de C++ contenga índices válidos (0-3) desde el principio. Esto elimina cualquier punto de fallo en la transferencia de datos desde C++ a Python.
- Valor 0xE4: Se eligió `0xE4` porque es el valor estándar que usan muchos juegos de Game Boy y implementa un mapeo identidad que preserva los índices originales. Esto permite ver los datos visuales reales de la VRAM sin distorsión.
- Aplicación de Paleta: Aunque el valor es un mapeo identidad, se aplica la lógica de paleta completa para mantener la consistencia con el hardware real. Esto facilita la depuración futura cuando se restaure la lectura normal de BGP/OBP.
- Comentarios Explicativos: Se agregaron comentarios detallados explicando el propósito del bypass y el mapeo de paleta para facilitar la comprensión y el mantenimiento futuro.
Archivos Afectados
src/core/cpp/PPU.cpp- Modificado `render_scanline()` y `render_sprites()` para forzar BGP = 0xE4 y OBP0/OBP1 = 0xE4 (Step 0257).
Tests y Verificación
Validación de Módulo Compilado C++:
- Recompilación: Ejecutar
.\rebuild_cpp.ps1para recompilar la extensión Cython con los cambios en C++. - Ejecución: Ejecutar
python main.py roms/pkmn.gb(o cualquier ROM con sprites). - Observación: Con la paleta de debug de Python (Step 0256) + el bypass de paleta de C++ (Step 0257), deberíamos ver:
- ✅ ÉXITO: Formas negras/grises moviéndose en la pantalla (logo de GAME FREAK, intro de Gengar vs Nidorino en Pokémon Red). Esto confirma que la VRAM contiene datos válidos y la PPU los está procesando correctamente.
- ❌ PROBLEMA: Si seguimos viendo todo verde/blanco, el problema está en la VRAM misma (tiles no cargados) o en la lectura de tiles desde VRAM.
Comando de Prueba:
.\rebuild_cpp.ps1
python main.py roms/pkmn.gb
Resultado Esperado: Pantalla con gráficos en blanco y negro (o gris/verde con la paleta de debug de Python), mostrando sprites y fondo correctamente renderizados.
Fuentes Consultadas
- Pan Docs: Palette Registers (BGP, OBP0, OBP1)
- Pan Docs: LCD Control Register (LCDC)
- Pan Docs: Tile Data
Integridad Educativa
Lo que Entiendo Ahora
- Pipeline de Renderizado: VRAM → PPU (decodificación de tiles) → Aplicación de Paleta (BGP/OBP) → Framebuffer → Python (aplicación de paleta final) → Pantalla. El bypass de paleta en C++ garantiza que los índices de color se preserven en el framebuffer, independientemente del estado de los registros de paleta en la MMU.
- Mapeo de Paleta: Los registros de paleta (BGP, OBP0, OBP1) mapean índices de color crudos (0-3) a índices finales (0-3). El valor `0xE4` implementa un mapeo identidad que preserva los índices originales, mientras que `0x00` convierte todos los índices a 0 (blanco).
- Diagnóstico por Capas: Al aplicar bypasses en diferentes capas (Python en Step 0256, C++ en Step 0257), podemos aislar el problema: si vemos gráficos después del bypass de C++, el problema está en los registros de paleta; si no vemos nada, el problema está en la VRAM o en la lectura de tiles.
Lo que Falta Confirmar
- Estado de VRAM: Verificar que la VRAM contiene datos válidos (tiles cargados) cuando se ejecuta el juego. Si la VRAM está vacía, el bypass de paleta no ayudará.
- Lectura de Tiles: Verificar que la PPU está leyendo correctamente los tiles desde VRAM. Si hay un error en la decodificación de tiles, el bypass de paleta no ayudará.
- Registros de Paleta en MMU: Una vez que confirmemos que la VRAM y la PPU funcionan correctamente, debemos investigar por qué los registros de paleta (BGP, OBP0, OBP1) están en `0x00` o por qué la MMU no los está sirviendo correctamente.
Hipótesis y Suposiciones
Hipótesis Principal: El framebuffer de C++ está lleno de ceros porque la PPU está aplicando una paleta con valor `0x00` que convierte todos los píxeles (incluso los negros, índice 3) en índice 0 antes de escribirlos en el framebuffer. Al forzar `0xE4` (mapeo identidad), los índices de color se preservan y deberíamos ver gráficos en la pantalla.
Suposición: Asumimos que la VRAM contiene datos válidos y que la PPU está leyendo y decodificando los tiles correctamente. Si después del bypass de paleta seguimos viendo todo verde/blanco, esta suposición es incorrecta y debemos investigar la VRAM y la lectura de tiles.
Próximos Pasos
- [ ] Ejecutar
.\rebuild_cpp.ps1para recompilar la extensión Cython con los cambios en C++. - [ ] Ejecutar
python main.py roms/pkmn.gby observar la pantalla. - [ ] Si vemos formas negras/grises:
- Confirmar que la VRAM y la PPU funcionan correctamente.
- Investigar por qué los registros de paleta (BGP, OBP0, OBP1) están en `0x00` o por qué la MMU no los está sirviendo correctamente.
- Corregir la lectura/escritura de los registros de paleta en la MMU.
- Restaurar la lógica normal de paletas (quitar el bypass) y validar que los colores se muestran correctamente.
- [ ] Si seguimos viendo todo verde/blanco:
- Verificar que el framebuffer de la PPU C++ contiene índices válidos (0-3) usando un debugger o logs.
- Verificar que la VRAM contiene datos válidos (tiles cargados) inspeccionando la memoria en tiempo de ejecución.
- Investigar por qué la PPU no está generando píxeles o por qué el framebuffer está vacío.