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.
Debug: Rastreo Completo del Segmentation Fault en Referencia Circular PPU↔MMU
Resumen
Después de resolver el problema del puntero nulo en el constructor de PyPPU (Step 0142), el Segmentation Fault persistió pero ahora ocurre en un punto diferente: dentro de check_stat_interrupt() cuando se intenta leer el registro STAT (0xFF41) desde la MMU, que a su vez intenta llamar a ppu_->get_mode() para construir el valor dinámico de STAT. Este es un problema de referencia circular entre PPU y MMU.
Concepto de Hardware
En la Game Boy real, el registro STAT (0xFF41) tiene bits de solo lectura (0-2) que son actualizados dinámicamente por la PPU. Estos bits representan:
- Bits 0-1: Modo actual de la PPU (Mode 0: H-Blank, Mode 1: V-Blank, Mode 2: OAM Search, Mode 3: Pixel Transfer)
- Bit 2: LYC=LY Coincidence Flag (1 si LY == LYC, 0 si no)
Cuando la CPU o cualquier componente intenta leer STAT, debe obtener el valor actualizado de estos bits desde el estado interno de la PPU, no desde la memoria estática.
En nuestro emulador, esto crea una referencia circular:
- PPU tiene un puntero a MMU (
MMU* mmu_) para leer/escribir memoria - MMU tiene un puntero a PPU (
PPU* ppu_) para leer el estado dinámico de STAT
Cuando PPU llama a mmu_->read(IO_STAT), la MMU necesita llamar de vuelta a ppu_->get_mode() para construir el valor correcto. Si el puntero ppu_ en MMU apunta a memoria inválida o a un objeto que ya fue destruido, esto causa un Segmentation Fault.
Problema Identificado
El crash ocurre en la siguiente cadena de llamadas:
PPU::step()completarender_scanline()exitosamentePPU::step()llama acheck_stat_interrupt()check_stat_interrupt()llama ammu_->read(IO_STAT)(dirección0xFF41)MMU::read()detecta que es STAT y necesita llamar appu_->get_mode(),ppu_->get_ly(), yppu_->get_lyc()para construir el valor dinámico- CRASH al intentar llamar a
ppu_->get_mode()- el punteroppu_en MMU apunta a memoria inválida
Análisis del problema:
- El puntero
ppu_en MMU no esNULL(tiene un valor como00000000222F0040), pero apunta a memoria inválida o a un objeto que ya fue destruido - El problema es una referencia circular: PPU tiene un puntero a MMU (
mmu_), y MMU tiene un puntero a PPU (ppu_) - Cuando
PPUllama ammu_->read(), laMMUintenta llamar de vuelta appu_->get_mode(), pero el punteroppu_en MMU puede estar apuntando a un objeto que ya fue destruido o movido
Implementación de Debugging
Se agregaron logs extensivos en múltiples puntos del código para rastrear exactamente dónde ocurre el crash y qué valores tienen los punteros en cada momento.
Componentes modificados
- src/core/cpp/PPU.cpp: Logs en
step(),render_scanline(), ycheck_stat_interrupt() - src/core/cpp/MMU.cpp: Logs en
read()ysetPPU() - src/core/cython/ppu.pyx: Referencia a
_mmu_wrapperpara evitar destrucción prematura - src/core/cython/mmu.pyx: Logs en
set_ppu() - src/viboy.py: Logs en la llamada a
ppu.step()
Logs agregados
1. En PPU::step():
[PPU::step] Iniciando step() con X ciclos- Al inicio del método[PPU::step] render_scanline() retornó, continuando...- Después de render_scanline()[PPU::step] LY incrementado a X- Después de incrementar LY[PPU::step] LY o modo cambió, llamando a check_stat_interrupt()...- Antes de llamar a check_stat_interrupt()[PPU::step] step() completado, retornando a Python- Al final del método
2. En PPU::render_scanline():
[PPU::render_scanline] Iniciando renderizado de línea X- Al inicio[PPU::render_scanline] Bucle completado, retornando...- Al final
3. En PPU::check_stat_interrupt():
[PPU::check_stat_interrupt] Iniciando...- Al inicio[PPU::check_stat_interrupt] mmu_ puntero: 0x...- Valor del puntero mmu_[PPU::check_stat_interrupt] Llamando a mmu_->read(IO_STAT)...- Antes de leer STAT
4. En MMU::read():
[MMU::read] Iniciando, addr=0xFF41- Al inicio[MMU::read] Leyendo STAT (0xFF41)...- Detección de STAT[MMU::read] ppu_ puntero: 0x...- Valor del puntero ppu_[MMU::read] Llamando a ppu_->get_mode()...- Antes de llamar a get_mode()
5. En PyMMU::set_ppu() y MMU::setPPU():
[PyMMU::set_ppu] ptr_int obtenido: X (0x...)- Puntero obtenido de get_cpp_ptr_as_int()[PyMMU::set_ppu] c_ppu convertido: X (0x...)- Puntero convertido[MMU::setPPU] Llamado con puntero: 0x...- Puntero recibido en setPPU()[MMU::setPPU] ppu_ configurado a: 0x...- Puntero almacenado
Mejora en gestión de memoria
Se agregó una referencia al objeto PyMMU en PyPPU para evitar que el objeto MMU se destruya mientras PPU lo está usando:
cdef class PyPPU:
cdef ppu.PPU* _ppu
cdef object _mmu_wrapper # CRÍTICO: Mantener referencia al wrapper para evitar destrucción
def __cinit__(self, PyMMU mmu_wrapper):
# ...
self._mmu_wrapper = mmu_wrapper # Mantener referencia
Resultados del Debugging
Los logs muestran que:
- ✅
render_scanline()completa exitosamente - ✅
check_stat_interrupt()se llama correctamente - ✅
mmu_->read(IO_STAT)se llama correctamente - ✅ El puntero
ppu_en MMU no esNULL(tiene un valor) - ❌ El crash ocurre al intentar llamar a
ppu_->get_mode()
Esto indica que el puntero ppu_ en MMU apunta a memoria inválida o a un objeto que ya fue destruido, aunque no sea NULL.
Próximos Pasos
- Ejecutar el emulador con los nuevos logs para ver exactamente qué puntero se está configurando en
set_ppu() - Verificar si el puntero
ppu_en MMU se está configurando correctamente o si hay un problema en la conversión - Si el puntero se configura correctamente pero luego se invalida, investigar el ciclo de vida de los objetos
- Considerar usar
std::shared_ptrostd::weak_ptrpara manejar la referencia circular de forma segura
Archivos Modificados
src/core/cpp/PPU.cpp- Logs extensivos enstep(),render_scanline(), ycheck_stat_interrupt()src/core/cpp/MMU.cpp- Logs enread()ysetPPU()src/core/cython/ppu.pyx- Referencia a_mmu_wrapperpara evitar destrucción prematura, logs enstep()src/core/cython/mmu.pyx- Logs enset_ppu()src/viboy.py- Logs en la llamada appu.step()