⚠️ 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: Crash de access violation por Recursión Infinita en STAT

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

Resumen

Este paso corrige un bug crítico de stack overflow causado por una recursión infinita entre MMU::read(0xFF41) y PPU::get_stat(). El problema ocurría cuando la CPU intentaba leer el registro STAT (0xFF41): la MMU llamaba a PPU::get_stat(), que a su vez intentaba leer STAT desde la MMU, creando un bucle infinito que consumía toda la memoria de la pila en milisegundos y causaba un crash access violation.

La solución implementa un rediseño arquitectónico: la MMU es la dueña de la memoria y construye el valor de STAT directamente, consultando a la PPU solo por su estado (modo, LY, LYC) sin crear dependencias circulares.

Concepto de Hardware

El registro STAT (LCD Status, 0xFF41) en la Game Boy es un registro híbrido con bits de solo lectura y bits escribibles:

  • Bits 0-1 (solo lectura): Modo PPU actual (0=H-Blank, 1=V-Blank, 2=OAM Search, 3=Pixel Transfer). Actualizados dinámicamente por la PPU.
  • Bit 2 (solo lectura): LYC=LY Coincidence Flag. Se activa cuando LY == LYC.
  • Bits 3-6 (lectura/escritura): Flags de interrupción configurables por software.
  • Bit 7 (solo lectura): Siempre 1 según Pan Docs.

El problema arquitectónico: En la implementación inicial, la PPU tenía un método get_stat() que intentaba construir el valor completo de STAT leyendo los bits escribibles desde la MMU y combinándolos con su estado interno. Sin embargo, cuando la MMU leía STAT, llamaba a PPU::get_stat(), que a su vez llamaba a MMU::read(0xFF41), creando una recursión infinita.

La solución correcta: La MMU es la dueña del espacio de direcciones y debe ser responsable de construir el valor de STAT cuando se lee. La PPU solo debe proporcionar su estado actual (modo, LY, LYC) mediante métodos de solo lectura, sin intentar leer memoria.

Fuente: Pan Docs - LCD Status Register (STAT), sección sobre bits de solo lectura y escritura.

Implementación

Se eliminó el método PPU::get_stat() y se rediseñó MMU::read(0xFF41) para construir STAT directamente:

Componentes modificados

  • PPU.hpp / PPU.cpp: Eliminado método get_stat(). La PPU ahora solo expone métodos de solo lectura: get_mode(), get_ly(), get_lyc().
  • MMU.cpp: Modificado MMU::read(0xFF41) para construir STAT combinando:
    • Bits escribibles (3-7) desde memory_[0xFF41]
    • Modo actual desde ppu_->get_mode()
    • LYC=LY Coincidence desde comparación de ppu_->get_ly() y ppu_->get_lyc()
    • Bit 7 siempre en 1
  • ppu.pxd: Eliminada declaración de get_stat() del wrapper Cython.

Decisiones de diseño

Arquitectura de responsabilidades: La MMU es la única responsable de construir valores de registros que combinan bits de solo lectura y escritura. Los componentes periféricos (PPU, APU, etc.) solo proporcionan su estado interno mediante métodos de solo lectura, sin intentar leer memoria.

Evitar dependencias circulares: Este patrón evita dependencias circulares entre MMU y componentes periféricos. La MMU puede consultar el estado de los componentes, pero los componentes nunca leen memoria a través de la MMU durante operaciones de lectura de registros.

Rendimiento: La construcción de STAT en MMU::read() es O(1) y no introduce overhead significativo, ya que solo se ejecuta cuando se lee el registro STAT (operación poco frecuente comparada con el bucle principal de emulación).

Archivos Afectados

  • src/core/cpp/PPU.hpp - Eliminado método get_stat()
  • src/core/cpp/PPU.cpp - Eliminada implementación de get_stat()
  • src/core/cpp/MMU.cpp - Rediseñado read(0xFF41) para construir STAT directamente
  • src/core/cython/ppu.pxd - Eliminada declaración de get_stat()
  • tests/test_core_ppu_modes.py - Tests ya usan mmu.read(0xFF41) correctamente

Tests y Verificación

Los tests existentes ya validan la lectura correcta de STAT desde la MMU:

  • Test: test_ppu_stat_register() - Verifica que STAT incluye el modo actual en bits 0-1
  • Test: test_ppu_stat_lyc_coincidence() - Verifica que el bit 2 (LYC=LY) se actualiza correctamente

Comando de verificación:

pytest tests/test_core_ppu_modes.py -v

Resultado esperado: Todos los tests pasan sin crashes de access violation.

Validación nativa: Los tests validan el módulo compilado C++ a través del wrapper Cython, confirmando que la recursión infinita ha sido eliminada y que STAT se lee correctamente.

Código del Test Clave

def test_ppu_stat_register(self):
    """Verifica que el registro STAT se lee correctamente con los modos PPU."""
    mmu = PyMMU()
    ppu = PyPPU(mmu)
    mmu.set_ppu(ppu)  # Conectar PPU a MMU
    
    mmu.write(0xFF40, 0x91)  # LCD ON
    mmu.write(0xFF41, 0x78)  # Escribir bits configurables
    
    # Leer STAT - debe incluir el modo actual en bits 0-1
    stat = mmu.read(0xFF41)  # ← Esta línea ya no causa recursión infinita
    
    mode_from_stat = stat & 0x03
    assert mode_from_stat == ppu.mode

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • Dependencias circulares en emulación: Cuando un componente periférico intenta leer memoria a través de la MMU durante una operación de lectura de registro, puede crear dependencias circulares si la MMU necesita consultar ese mismo componente para construir el valor del registro.
  • Arquitectura de responsabilidades: La MMU debe ser la única responsable de construir valores de registros híbridos (con bits de solo lectura y escritura). Los componentes periféricos solo deben proporcionar su estado interno mediante métodos de solo lectura.
  • Stack overflow en C++: Una recursión infinita consume toda la memoria de la pila rápidamente, causando un crash access violation en Windows o segmentation fault en Linux.

Lo que Falta Confirmar

  • Rendimiento de construcción de STAT: Verificar que la construcción de STAT en MMU::read() no introduce overhead significativo en el bucle principal de emulación.
  • Otros registros híbridos: Identificar si hay otros registros en la Game Boy con bits de solo lectura y escritura que requieran un patrón similar.

Hipótesis y Suposiciones

Suposición validada: La construcción de STAT en MMU::read() es lo suficientemente rápida para no afectar el rendimiento, ya que la lectura de STAT es una operación poco frecuente comparada con el bucle principal de emulación (cada instrucción de CPU).

Próximos Pasos

  • [ ] Recompilar el módulo C++ y verificar que los tests pasan sin crashes
  • [ ] Ejecutar el emulador con una ROM de test para verificar que la pantalla blanca se resuelve
  • [ ] Implementar CPU Nativa: Saltos y Control de Flujo (Step 0129)
  • [ ] Verificar que no hay otros registros híbridos que requieran el mismo patrón