Step 0411: Fix IRQ Pipeline + Post-Boot Registers Coherente

📋 Resumen Ejecutivo

Problema: Step 0410 reveló que Pokémon (pkmn.gb, Oro.gbc) quedan bloqueados con IF=0x00 esperando interrupciones que nunca llegan, impidiendo la carga de tiles.

Hipótesis: Posible mismatch entre post-boot CPU registers y modo HW detectado, o LCD/Timer apagados.

Solución: Instrumentación completa del pipeline de IRQ con contadores directos + alinear registros post-boot con modo HW (DMG vs CGB) detectado desde header ROM.

Resultado: ✅ IRQs generándose correctamente. Oro.gbc avanza significativamente (RETI, IE=0x1F). Pokémon Red/Tetris DX esperan Joypad input (nuevo Step requerido).

🔧 Concepto de Hardware

1. Pipeline de Interrupciones (IF, IE, IME)

Flujo de IRQ en Game Boy (Pan Docs - Interrupts):

  • IF (0xFF0F): Interrupt Flag - Bits activados cuando hardware solicita IRQ (VBlank, STAT, Timer, Serial, Joypad)
  • IE (0xFFFF): Interrupt Enable - Bits que habilitan qué IRQs pueden ser atendidas
  • IME: Interrupt Master Enable - Flag global que habilita/deshabilita todas las IRQs (activado con EI, desactivado con DI)
  • Condición para servir IRQ: IME && (IE & IF) != 0

Ejemplo de IRQ VBlank:

1. PPU entra en VBlank (LY=144) → request_interrupt(0) → IF bit 0 = 1
2. Si IE bit 0 = 1 (VBlank habilitado) Y IME = 1 → CPU salta a vector 0x0040
3. CPU ejecuta ISR VBlank
4. ISR limpia IF bit 0 (escribiendo 0 a IF) y ejecuta RETI

2. Post-Boot CPU Registers según Modo HW

Problema: El registro A post-boot identifica el hardware al juego:

  • DMG: A=0x01 → Juego sabe que está en Game Boy original
  • CGB: A=0x11 → Juego sabe que está en Game Boy Color

Juegos dual-mode (compatible DMG/CGB) toman caminos diferentes según A. Si A no coincide con el modo HW real, el juego puede quedar en estado inconsistente.

Post-Boot State completo (Pan Docs - Power Up Sequence):

DMG:  AF=0x01B0, BC=0x0013, DE=0x00D8, HL=0x014D, SP=0xFFFE, PC=0x0100
CGB:  AF=0x1180, BC=0x0000, DE=0xFF56, HL=0x000D, SP=0xFFFE, PC=0x0100

3. LCDC bit7 y Timer (TAC bit2)

  • LCDC bit7=0 (LCD OFF): PPU se detiene, LY queda en 0, NO hay VBlank IRQ
  • TAC bit2=0 (Timer OFF): Registro TIMA no incrementa, NO hay Timer IRQ

Si ambos están apagados, IF puede quedarse en 0 indefinidamente.

💻 Implementación

Tarea 1A: Contadores directos en MMU::request_interrupt

Añadidos contadores en MMU.hpp para rastrear todos los requests de IRQ (independientes de cambios en IF):

// MMU.hpp - Contadores de IRQ requests
mutable int irq_req_vblank_count_;   // Total requests VBlank (bit 0)
mutable int irq_req_stat_count_;     // Total requests STAT (bit 1)
mutable int irq_req_timer_count_;    // Total requests Timer (bit 2)
mutable int irq_req_serial_count_;   // Total requests Serial (bit 3)
mutable int irq_req_joypad_count_;   // Total requests Joypad (bit 4)

// MMU.cpp - request_interrupt()
void MMU::request_interrupt(uint8_t bit) {
    // Incrementar contador según el bit (SIEMPRE, sin importar IF)
    switch (bit) {
        case 0: irq_req_vblank_count_++; break;
        case 1: irq_req_stat_count_++; break;
        case 2: irq_req_timer_count_++; break;
        case 3: irq_req_serial_count_++; break;
        case 4: irq_req_joypad_count_++; break;
    }
    // ... resto del código
}

Tarea 1B: Resumen periódico de IRQ requests

Añadido log_irq_requests_summary() en MMU, llamado cada 120 frames desde PPU:

// PPU.cpp - Al entrar en VBlank
if (frame_counter_ % 120 == 0) {
    mmu_->log_irq_requests_summary(frame_counter_);
}

// MMU.cpp - log_irq_requests_summary()
[IRQ-SUMMARY] Requests generados (totales):
[IRQ-SUMMARY]   VBlank (bit 0): 2
[IRQ-SUMMARY]   STAT   (bit 1): 144
[IRQ-SUMMARY]   Timer  (bit 2): 3
[IRQ-SUMMARY] Estado actual:
[IRQ-SUMMARY]   IE (0xFFFF): 0x1F (VBlank STAT Timer Serial Joypad)
[IRQ-SUMMARY]   IF (0xFF0F): 0x01 (VBlank )
[IRQ-SUMMARY]   LCDC (0xFF40): 0xE3 (LCD ON)
[IRQ-SUMMARY]   TAC (0xFF07): 0x04 (Timer ON)
[IRQ-SUMMARY] Análisis:
[IRQ-SUMMARY]   ✓ HAY INTERRUPCIONES PENDIENTES

Tarea 2: Post-Boot Registers alineados con Modo HW

Añadido método apply_post_boot_state(bool is_cgb_mode) en CoreRegisters:

// Registers.cpp
void CoreRegisters::apply_post_boot_state(bool is_cgb_mode) {
    if (is_cgb_mode) {
        a = 0x11; b = 0x00; c = 0x00;
        d = 0xFF; e = 0x56;
        h = 0x00; l = 0x0D;
        f = 0x80; // Z=1, N=0, H=0, C=0
    } else {
        a = 0x01; b = 0x00; c = 0x13;
        d = 0x00; e = 0xD8;
        h = 0x01; l = 0x4D;
        f = 0xB0; // Z=1, N=0, H=1, C=1
    }
    sp = 0xFFFE;
    pc = 0x0100;
}

// viboy.py - Después de cargar ROM
hardware_mode_str = self._mmu.get_hardware_mode()
is_cgb_mode = (hardware_mode_str == "CGB")
self._regs.apply_post_boot_state(is_cgb_mode)

Tarea 3: Extender detector de wait-loop

Añadida captura de estado completo en CPU.cpp cuando se detecta wait-loop:

[WAITLOOP] Timer: TAC=0x00, DIV=0x42, TIMA=0x00
[WAITLOOP]   - Timer OFF
[WAITLOOP]   ⚠️ TIMER APAGADO: No habrá Timer IRQ
[WAITLOOP] CGB: KEY1=0x00, VBK=0x00, SVBK=0x00

🧪 Tests y Verificación

Comandos Ejecutados

python3 setup.py build_ext --inplace
timeout 60s python3 main.py roms/pkmn.gb > logs/step0411_pkmn_irq_fix.log 2>&1
timeout 60s python3 main.py roms/Oro.gbc > logs/step0411_oro_irq_fix.log 2>&1
timeout 30s python3 main.py roms/tetris_dx.gbc > logs/step0411_tetris_dx_baseline.log 2>&1

# Análisis seguro (sin saturar contexto)
grep -E "\[IRQ-SUMMARY\]|\[WAITLOOP\]|Post-Boot State" logs/step0411_*.log | head -n 160

Resultados: Oro.gbc (Pokémon Oro)

✅ GRAN AVANCE

  • ✅ VBlank IRQs: 2 (generándose correctamente)
  • ✅ STAT IRQs: 144 (LCD STAT interrupciones activas)
  • ✅ Timer IRQs: 3
  • ✅ IE=0x1F (todas las IRQs habilitadas)
  • ✅ RETI ejecutándose (ISRs funcionando)
  • ✅ LCDC cambia dinámicamente (0x91→0x00→0xE3)
  • ✅ Hardware Mode: CGB detectado, registros post-boot coherentes

Resultados: pkmn.gb (Pokémon Red)

⚠️ PROGRESO PARCIAL

  • ✅ VBlank IRQs: 1+ (generándose)
  • ✅ LCDC: Cambios dinámicos (0x91→0x80→0x81→0xE3)
  • ⚠️ IE=0x0D (VBlank, Timer, Joypad habilitadas, pero Timer OFF)
  • ⚠️ TAC=0x00 (Timer apagado → no hay Timer IRQ)
  • ⚠️ Esperando Joypad input (polling activo en 0x600A-0x600E)
  • ✅ Hardware Mode: DMG detectado, A=0x01

Resultados: tetris_dx.gbc (Tetris DX)

⚠️ ESPERANDO INPUT (sin regresión)

  • ✅ VBlank IRQs: 1+ (generándose)
  • ⚠️ IE=0x00 (interrupciones NO habilitadas al inicio)
  • ⚠️ Esperando Joypad input (polling activo)
  • ✅ Hardware Mode: CGB detectado, A=0x11

Fragmento de Test (Análisis IRQ)

// Test implícito: Verificación de contadores IRQ
// El test ejecuta el emulador y analiza logs

// Extracto de log (Oro.gbc):
[IRQ-SUMMARY] Step 0411 - Frame 0
[IRQ-SUMMARY] Requests generados (totales):
[IRQ-SUMMARY]   VBlank (bit 0): 2
[IRQ-SUMMARY]   STAT   (bit 1): 144
[IRQ-SUMMARY]   Timer  (bit 2): 3
[IRQ-SUMMARY] Estado actual:
[IRQ-SUMMARY]   IE (0xFFFF): 0x1F (VBlank STAT Timer Serial Joypad)
[IRQ-SUMMARY]   IF (0xFF0F): 0x01 (VBlank )
[IRQ-SUMMARY]   LCDC (0xFF40): 0xE3 (LCD ON)
[IRQ-SUMMARY]   TAC (0xFF07): 0x04 (Timer ON)
[IRQ-SUMMARY] Análisis:
[IRQ-SUMMARY]   ✓ HAY INTERRUPCIONES PENDIENTES

// Validación de módulo compilado C++ ✓

📁 Archivos Modificados

  • src/core/cpp/MMU.hpp - Añadidos contadores IRQ y declaración de log_irq_requests_summary()
  • src/core/cpp/MMU.cpp - Implementación de contadores en request_interrupt() y función de resumen
  • src/core/cpp/PPU.cpp - Llamada periódica a log_irq_requests_summary()
  • src/core/cpp/CPU.cpp - Extendido detector de wait-loop con Timer y CGB info
  • src/core/cpp/Registers.hpp - Declaración de apply_post_boot_state()
  • src/core/cpp/Registers.cpp - Implementación de apply_post_boot_state()
  • src/core/cython/registers.pyx - Binding Cython para apply_post_boot_state()
  • src/core/cython/registers.pxd - Declaración C++ para Cython
  • src/viboy.py - Llamada a apply_post_boot_state() después de cargar ROM

🔍 Hallazgos Clave

  1. Pipeline de IRQ funciona: Los contadores demuestran que request_interrupt() se llama correctamente (VBlank, STAT, Timer).
  2. Oro.gbc avanza significativamente: Ejecuta RETI, tiene IE=0x1F activo, y LCDC cambia dinámicamente (apagado/encendido). Esto indica que el juego progresa más allá del boot.
  3. Pokémon Red y Tetris DX esperan input: Ambos quedan en polling de Joypad (IE bajo, esperando botones). Necesitan simulación de input para avanzar.
  4. Post-Boot Registers ahora coherentes: A=0x01 (DMG) para pkmn.gb, A=0x11 (CGB) para Oro.gbc y tetris_dx.gbc.
  5. Diagnóstico mejorado: [IRQ-SUMMARY] proporciona análisis claro del estado de IRQs, facilitando debugging futuro.

🚀 Próximos Pasos

  1. Step 0412: Simulación de Joypad Input - Implementar auto-input simulado (ej: presionar START automáticamente tras N frames) para que juegos que esperan input puedan avanzar.
  2. Step 0413: Verificar carga de tiles post-input - Una vez que los juegos reciban input, verificar si cargan tiles correctamente.
  3. Step 0414: Investigar por qué Oro.gbc avanza pero pkmn.gb no - Analizar diferencias en inicialización entre ambos juegos Pokémon.

📚 Referencias