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

Fix: Segmentation Fault en PPU - Signed Addressing

Fecha: 2025-12-19 Step ID: 0132 Estado: Verified

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_addr esté en 0x8000-0x9FFF
  • Validación de dirección de línea del tile: Verifica que tile_addr + tile_y_offset * 2 y tile_addr + tile_y_offset * 2 + 1 esté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étodo render_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 en render_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 de viboy.py (llamada a self._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