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

Diagnóstico de PC Stuck y Por Qué CGB Nunca Habilita IE/IME

Fecha: 2026-01-04 Step ID: 0470 Estado: VERIFIED

Resumen

Diagnóstico de por qué CGB nunca habilita IE/IME y por qué algunas ROMs tienen PC stuck. Se implementó instrumentación mínima gated para rastrear writes a IE/IF, ejecución de EI/DI, y watch de lecturas de IO. Se modificó rom_smoke_0442.py para snapshots con PC hotspots y IO reads top 3. Validación real con tetris_dx.gbc y mario.gbc reveló que ambos juegos escriben a IE múltiples veces pero IE permanece en 0x00, indicando que los writes se pierden o son sobrescritos. ✅ Causa dominante identificada: IE writes lost or overwritten.

Concepto de Hardware

En la Game Boy, el registro IE (0xFFFF - Interrupt Enable) controla qué interrupciones están habilitadas. Si IE bit0 = 0, aunque el PPU solicite VBlank interrupt (IF bit0 = 1), la CPU no la servirá. El registro IE es write-only desde la perspectiva del hardware (el juego puede escribir valores, pero algunos bits pueden ser read-only o tener comportamiento especial en CGB).

Registro IE (0xFFFF - Interrupt Enable): Indica qué interrupciones están habilitadas. Bit 0 = VBlank interrupt, Bit 1 = LCD STAT interrupt, Bit 2 = Timer interrupt, Bit 3 = Serial interrupt, Bit 4 = Joypad interrupt. En modo CGB, algunos bits pueden tener comportamiento diferente o ser read-only.

Instrucciones EI/DI:

  • EI (0xFB): Habilita IME (Interrupt Master Enable) con retraso de 1 instrucción. IME se activa DESPUÉS de ejecutar la siguiente instrucción. Esto permite que la instrucción siguiente a EI se ejecute sin interrupciones.
  • DI (0xF3): Desactiva IME inmediatamente. Se usa típicamente al inicio de rutinas críticas.

Framing Correcto del Problema: "IE bit0=0" en CGB no es una causa, es un síntoma de que el juego no está llegando al punto donde habilita interrupciones (o sus writes a IE se pierden). Si el juego intenta escribir IE pero IE permanece en 0x00, hay dos posibilidades:

  1. IE writes missing: El juego nunca intenta escribir IE (no hay writes a 0xFFFF)
  2. IE writes lost: El juego intenta escribir IE pero los writes se pierden o son sobrescritos

Fuente: Pan Docs - Interrupts, Interrupt Enable Register (IE), EI/DI Instructions

Implementación

Se implementó instrumentación mínima gated para diagnosticar por qué CGB nunca habilita IE/IME. La instrumentación incluye contadores de writes a IE/IF, ejecución de EI/DI, y watch de lecturas de IO.

Fase A - Instrumentación Mínima (Gated)

Se añadieron contadores estáticos a nivel de archivo para rastrear writes a IE/IF y ejecución de EI/DI:

  • MMU.cpp: Contadores `ie_write_count` y `if_write_count` (static uint32_t) que se incrementan cuando se escriben a 0xFFFF y 0xFF0F respectivamente. Log gated (solo si VIBOY_DEBUG_PPU=1) con límite de 20 logs.
  • MMU.cpp: Watch de lecturas de IO (JOYP, STAT, LY, IF, IE, KEY1, VBK, SVBK) usando std::map para contar lecturas por dirección.
  • CPU.cpp: Contadores `ei_count_global` y `di_count_global` (static uint32_t) que se incrementan cuando se ejecutan EI y DI.
  • MMU.hpp/CPU.hpp: Getters públicos `get_ie_write_count()`, `get_if_write_count()`, `get_last_ie_written()`, `get_last_if_written()`, `get_io_read_count()`, `get_ei_count()`, `get_di_count()`
  • Cython (.pxd/.pyx): Getters expuestos a Python para acceso desde herramientas de diagnóstico

Fase B - rom_smoke: Snapshots con "Hotspots"

Se modificó `rom_smoke_0442.py` para que en snapshots imprima PC hotspots y IO reads top 3:

  • Contadores de hotspots: `pc_samples` (Dict: PC -> count) que se actualiza cada 50 steps
  • Snapshots mejorados: Cada 60 frames (o frames 0, 60, 120, 180, 240) se imprimen:
    • Contadores IE/IF writes y EI/DI
    • IO reads top 3 (direcciones más leídas)
    • PC hotspots top 3 (PCs más frecuentes)
    • Métricas existentes (TilemapNZ, VRAMNZ, LCDC, STAT, LY)

Fase C - Test Opcional

Se creó `tests/test_ie_write_persists_0470.py` para verificar que writes a IE persisten correctamente:

  • test_ie_write_persists: Verifica que write a IE (0xFFFF=0x01) persiste (read 0xFFFF==0x01) y que el contador de writes se incrementa
  • test_ie_write_multiple_values: Verifica que múltiples writes a IE persisten correctamente (0x01, 0x03, 0x07, 0x0F)

Fase D - Decisión Automática

Se ejecutó `rom_smoke_0442.py` para tetris_dx.gbc y mario.gbc (240 frames cada uno) y se generó decisión automática basada en los datos recopilados:

  • tetris_dx.gbc: IEWrite=7, EI=2, DI=4, pero IE=0x00 → IE writes lost or overwritten + EI timing bug
  • mario.gbc: IEWrite=62, EI=0, DI=0, pero IE=0x00 → IE writes lost or overwritten + EI never executed

Causa Dominante Identificada: IE writes lost or overwritten. Ambos juegos escriben a IE múltiples veces pero IE permanece en 0x00, indicando que los writes se pierden o son sobrescritos inmediatamente.

Archivos Afectados

  • src/core/cpp/MMU.hpp - Getters para contadores IE/IF writes y IO reads
  • src/core/cpp/MMU.cpp - Contadores de writes a IE/IF y watch de lecturas de IO
  • src/core/cpp/CPU.hpp - Getters para contadores EI/DI
  • src/core/cpp/CPU.cpp - Contadores de ejecución de EI/DI
  • src/core/cython/mmu.pxd - Declaraciones Cython para getters MMU
  • src/core/cython/mmu.pyx - Wrappers Python para getters MMU
  • src/core/cython/cpu.pxd - Declaraciones Cython para getters CPU
  • src/core/cython/cpu.pyx - Wrappers Python para getters CPU
  • tools/rom_smoke_0442.py - Snapshots con PC hotspots y IO reads top 3
  • tests/test_ie_write_persists_0470.py - Test para verificar que writes a IE persisten

Tests y Verificación

Validación de la implementación:

  • Tests unitarios: pytest tests/test_ie_write_persists_0470.py - 2 tests pasando (0.25s)
    def test_ie_write_persists(self):
        """Test: Verificar que write a IE (0xFFFF) persiste."""
        self.mmu.write(0xFFFF, 0x01)
        ie_read = self.mmu.read(0xFFFF)
        assert ie_read == 0x01, \
            f"IE write no persiste: escribí 0x01, leí 0x{ie_read:02X}"
        
        ie_write_count = self.mmu.get_ie_write_count()
        assert ie_write_count > 0, \
            f"Contador de IE writes no se incrementó"
  • Validación Nativa: Validación de módulo compilado C++
  • ROMs de test: tetris_dx.gbc y mario.gbc (240 frames cada uno) con snapshots cada 60 frames
  • Decisión Automática: Análisis de datos recopilados para identificar causa dominante (IE writes lost or overwritten)

Decisión Automática

Basado en los datos recopilados de tetris_dx.gbc y mario.gbc (frames 120 y 180):

tetris_dx.gbc

  • Frame 120: IEWrite=1, EI=0, DI=1, IE=0x00, PC hotspots en 0x1304-0x1306
  • Frame 180: IEWrite=7, EI=2, DI=4, IE=0x00, PC hotspots en 0x1308, 0x1302, 0x1303
  • Causa: IE writes lost or overwritten + EI timing bug
  • Evidencia: IEWrite=7 pero IE=0x00, EI=2 pero IME=0, IOReads dominado por IF/IE (polling stuck)

mario.gbc

  • Frame 120: IEWrite=42, EI=0, DI=0, IE=0x00, PC hotspots en 0x12A0, 0x129D, 0x12A2
  • Frame 180: IEWrite=62, EI=0, DI=0, IE=0x00, PC hotspots en 0x12A0, 0x129D, 0x12A2
  • Causa: IE writes lost or overwritten + EI never executed
  • Evidencia: IEWrite=62 pero IE=0x00, EI=0, IOReads dominado por IF/IE (polling stuck)

Causa Dominante Global

IE writes lost or overwritten: Ambos juegos escriben a IE múltiples veces pero IE permanece en 0x00, indicando que los writes se pierden o son sobrescritos inmediatamente. El juego queda atrapado en loops de polling esperando que IE/IF cambien.

Hipótesis Principal: Algún componente del sistema (posiblemente relacionado con CGB o modo de hardware) está sobrescribiendo IE (0xFFFF) después de que el juego lo escribe, o los writes no persisten correctamente.

Fuentes Consultadas

  • Pan Docs: Interrupts, Interrupt Enable Register (IE), EI/DI Instructions
  • Pan Docs: CGB Registers (comportamiento especial de IE en modo CGB)

Integridad Educativa

Lo que Entiendo Ahora

  • IE writes tracking: Los contadores de writes a IE/IF permiten determinar si el juego intenta habilitar interrupciones o está atascado antes
  • EI/DI tracking: Los contadores de ejecución de EI/DI permiten determinar si el juego intenta habilitar IME
  • IO polling detection: El watch de lecturas de IO permite identificar qué registro está esperando el juego (LY/STAT, IF/IE, KEY1, etc.)
  • PC hotspots: Los hotspots de PC permiten identificar loops de polling esperando que algún registro cambie

Lo que Falta Confirmar

  • IE write persistence: Verificar que writes a IE persisten correctamente en modo CGB (posible bug en mapping de IE)
  • EI timing: Verificar timing de EI en tetris_dx.gbc (EI ejecutado pero IME sigue 0)
  • Componente que sobrescribe IE: Identificar qué componente sobrescribe IE después de que el juego lo escribe

Hipótesis y Suposiciones

Hipótesis Principal: Algún componente del sistema (posiblemente relacionado con CGB o modo de hardware) está sobrescribiendo IE (0xFFFF) después de que el juego lo escribe. Esto podría ser:

  • Bug en mapping de IE en modo CGB (IE se mapea a otra dirección o se lee desde otra fuente)
  • Componente que resetea IE periódicamente (posiblemente relacionado con boot ROM o inicialización)
  • Bug en persistencia de writes a IE (writes no se guardan correctamente en memoria)

Próximos Pasos

  • [ ] Step 0471: Añadir logging detallado de writes a IE para identificar qué componente sobrescribe IE
  • [ ] Step 0471: Verificar que writes a IE persisten correctamente en modo CGB
  • [ ] Step 0471: Verificar timing de EI en tetris_dx.gbc (EI ejecutado pero IME sigue 0)