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.
Fix: Segmentation Fault en PPU - Signed Addressing
Resumen
Se corrigió un Segmentation Fault crítico que ocurría al ejecutar Tetris (y probablemente otros juegos) cuando la PPU intentaba renderizar el background. El problema tenía dos causas principales: (1) cálculo incorrecto de direcciones de tiles con signed addressing (usaba base 0x8800 en lugar de 0x9000) y (2) falta de validación de rangos VRAM que permitía accesos fuera de límites. Se implementaron validaciones exhaustivas y se corrigió la fórmula de cálculo según Pan Docs.
Concepto de Hardware
La Game Boy tiene dos modos de direccionamiento para los tiles en VRAM, controlados por el bit 4 del registro LCDC (0xFF40):
- Unsigned Addressing (LCDC bit 4 = 1): Los tile IDs van de 0-255, y los tiles están almacenados desde 0x8000. Fórmula:
tile_addr = 0x8000 + (tile_id * 16) - Signed Addressing (LCDC bit 4 = 0): Los tile IDs van de -128 a 127 (interpretados como int8_t), y el tile 0 está en 0x9000, no en 0x8800. Fórmula:
tile_addr = 0x9000 + (signed_tile_id * 16)
CRÍTICO: En signed addressing, aunque el bit 4 de LCDC indica que la base es 0x8800, el tile 0 (signed_tile_id = 0) está físicamente en 0x9000. Esto significa que:
- Tile -128 → Dirección 0x9000 + (-128 * 16) = 0x9000 - 0x800 = 0x8800
- Tile 0 → Dirección 0x9000 + (0 * 16) = 0x9000
- Tile 127 → Dirección 0x9000 + (127 * 16) = 0x9000 + 0x7F0 = 0x97F0
La VRAM ocupa el rango 0x8000-0x9FFF (8KB). Cualquier acceso fuera de este rango causa un Segmentation Fault en C++.
Fuente: Pan Docs - VRAM Tile Data, LCD Control Register (LCDC)
Implementación
Se corrigieron dos problemas en el método PPU::render_scanline():
1. Corrección del Cálculo de Direcciones con Signed Addressing
Problema: El código usaba tile_data_base (0x8800) para calcular direcciones con signed addressing, lo cual es incorrecto. El tile 0 debe estar en 0x9000.
Código anterior (incorrecto):
if (signed_addressing) {
int8_t signed_tile_id = static_cast<int8_t>(tile_id);
tile_addr = tile_data_base + (static_cast<int16_t>(signed_tile_id) * 16);
}
Código corregido:
if (signed_addressing) {
// Signed: tile_id como int8_t, tile 0 está en 0x9000
// NOTA: Cuando signed_addressing es true, tile_data_base es 0x8800,
// pero el tile 0 está en 0x9000, no en 0x8800.
// Fórmula: 0x9000 + (signed_tile_id * 16)
int8_t signed_tile_id = static_cast<int8_t>(tile_id);
tile_addr = 0x9000 + (static_cast<int16_t>(signed_tile_id) * 16);
}
2. Validación de Rangos VRAM
Se agregaron validaciones exhaustivas para prevenir accesos fuera de límites:
- Validación de dirección base del tile: Verifica que
tile_addresté en 0x8000-0x9FFF - Validación de dirección de línea del tile: Verifica que
tile_addr + tile_y_offset * 2ytile_addr + tile_y_offset * 2 + 1estén dentro de VRAM - Comportamiento seguro: Si alguna validación falla, se usa color 0 (transparente) en lugar de causar un crash
// CRÍTICO: Validar que la dirección del tile esté dentro de VRAM (0x8000-0x9FFF)
if (tile_addr < VRAM_START || tile_addr > VRAM_END) {
framebuffer_[line_start_index + x] = 0;
continue;
}
// Validar que la dirección de la línea del tile también esté dentro de VRAM
uint16_t tile_line_addr = tile_addr + tile_y_offset * 2;
if (tile_line_addr > VRAM_END || (tile_line_addr + 1) > VRAM_END) {
framebuffer_[line_start_index + x] = 0;
continue;
}
Componentes modificados
src/core/cpp/PPU.cpp: Métodorender_scanline()corregido con cálculo correcto de signed addressing y validaciones de rangos VRAM
Decisiones de diseño
- Comportamiento seguro en errores: En lugar de crashear, se usa color 0 (transparente) cuando hay un acceso inválido. Esto permite que el emulador continúe ejecutándose aunque haya datos corruptos en VRAM.
- Validación exhaustiva: Se valida tanto la dirección base del tile como la dirección de la línea específica que se va a leer. Esto previene accesos fuera de límites incluso con cálculos complejos.
Archivos Afectados
src/core/cpp/PPU.cpp- Corrección del cálculo de direcciones con signed addressing y agregado de validaciones de rangos VRAM enrender_scanline()
Tests y Verificación
Validación mediante ejecución real:
- ROM de test:
roms/tetris.gb(Tetris original de Game Boy) - Comportamiento anterior: Segmentation Fault inmediato al iniciar el juego
- Comportamiento esperado: El juego debe iniciar sin crashes y mostrar la pantalla correctamente
Comando de prueba:
python main.py roms/tetris.gb
Validación de módulo compilado C++: El fix requiere recompilación del módulo C++ con:
python setup.py build_ext --inplace
Nota: Este fix corrige un bug crítico que impedía la ejecución de juegos. La validación se realizará ejecutando Tetris y verificando que no ocurra Segmentation Fault.
Fuentes Consultadas
- Pan Docs: VRAM Tile Data, LCD Control Register (LCDC) - Sección sobre signed vs unsigned addressing de tiles
- Pan Docs: Memory Map - Rango de VRAM (0x8000-0x9FFF)
- Stack Trace del error: Identificó que el crash ocurría en
PPU::render_scanline()línea 753 deviboy.py(llamada aself._ppu.step())
Integridad Educativa
Lo que Entiendo Ahora
- Signed Addressing en Game Boy: Aunque el bit 4 de LCDC indica base 0x8800, el tile 0 está físicamente en 0x9000. Esto es un detalle crítico de la especificación hardware que puede causar bugs sutiles si no se implementa correctamente.
- Validación de Rangos en C++: En C++, los accesos fuera de límites de arrays/vectores causan Segmentation Faults. Es crítico validar todos los accesos a memoria, especialmente cuando se calculan direcciones dinámicamente.
- Comportamiento Defensivo: En lugar de crashear, es mejor usar un valor seguro (color 0 transparente) cuando hay datos inválidos. Esto permite que el emulador continúe ejecutándose y facilita el debugging.
Lo que Falta Confirmar
- Validación con múltiples ROMs: Verificar que el fix funciona correctamente con otros juegos además de Tetris, especialmente juegos que usen signed addressing extensivamente.
- Rendimiento: Las validaciones adicionales agregan overhead. Verificar que no impacten significativamente el rendimiento (aunque el impacto debería ser mínimo).
Hipótesis y Suposiciones
Suposición validada: El uso de color 0 (transparente) cuando hay un acceso inválido es el comportamiento correcto. Esto es consistente con cómo la Game Boy maneja datos corruptos o fuera de rango (simplemente no renderiza nada en ese píxel).
Próximos Pasos
- [ ] Recompilar módulo C++ y probar con Tetris para verificar que el Segmentation Fault está resuelto
- [ ] Probar con otras ROMs (Mario, Pokémon) para verificar que el fix es general
- [ ] Continuar con la implementación de renderizado de Window y Sprites en la PPU C++
- [ ] Optimizar el rendimiento si las validaciones causan overhead significativo