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

📋 Executive Summary

Problem: Step 0410 revealed that Pokémon (pkmn.gb, Oro.gbc) are blocked withIF=0x00waiting for interruptions that never arrive, preventing the loading of tiles.

Hypothesis: Possible mismatch between post-boot CPU registers and HW mode detected, or LCD/Timer turned off.

Solution: Complete instrumentation of the IRQ pipeline with direct counters + align post-boot registers with HW mode (DMG vs CGB) detected from header ROM.

Result: ✅ IRQs being generated correctly. Oro.gbc advances significantly (RETI, IE=0x1F). Pokémon Red/Tetris DX expect Joypad input (new Step required).

🔧 Hardware Concept

1. Interrupt Pipeline (IF, IE, IME)

IRQ flow on Game Boy(Pan Docs - Interrupts):

  • IF (0xFF0F): Interrupt Flag - Bits set when hardware requests IRQ (VBlank, STAT, Timer, Serial, Joypad)
  • IE (0xFFFF): Interrupt Enable - Bits that enable which IRQs can be served
  • IME: Interrupt Master Enable - Global flag that enables/disables all IRQs (enabled with EI, deactivated with DI)
  • Condition to serve IRQ: IME && (IE & IF) != 0

VBlank IRQ Example:

1. PPU enters VBlank (LY=144) → request_interrupt(0) → IF bit 0 = 1
2. If IE bit 0 = 1 (VBlank enabled) AND IME = 1 → CPU jumps to vector 0x0040
3. CPU runs ISR VBlank
4. ISR clears IF bit 0 (writing 0 to IF) and executes RETI

2. Post-Boot CPU Registers according to HW Mode

Problem: The recordTOpost-boot identifies the hardware to the game:

  • DMG: A=0x01→ Game knows it's on the original Game Boy
  • CGB: A=0x11→ Game knows it's on Game Boy Color

Dual-mode games (DMG/CGB compatible) take different paths depending onTO. YeahTOdoes not match the real HW mode, the game may be left in an inconsistent state.

Complete Post-Boot State(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 and Timer (TAC bit2)

  • LCDC bit7=0 (LCD OFF): PPU stops, LY remains at 0,NO VBlank IRQ
  • TAC bit2=0 (Timer OFF): TIMA register does not increment,NO IRQ Timer

If both are off,I.F.can stay at 0 indefinitely.

💻 Implementation

Task 1A: Direct counters inMMU::request_interrupt

Added counters inMMU.hppto trackallIRQ requests (independent of changes in IF):

// MMU.hpp - Counters for 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) {
    // Increment counter according to the bit (ALWAYS, regardless of 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;
    }
    // ... rest of the code
}

Task 1B: Periodic summary of IRQ requests

Addedlog_irq_requests_summary()in MMU, called every 120 frames from PPU:

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

// MMU.cpp - log_irq_requests_summary()
[IRQ-SUMMARY] Requests generated (total):
[IRQ-SUMMARY] VBlank (bit 0): 2
[IRQ-SUMMARY] STAT (bit 1): 144
[IRQ-SUMMARY] Timer (bit 2): 3
[IRQ-SUMMARY] Current status:
[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] Analysis:
[IRQ-SUMMARY] ✓ THERE ARE PENDING INTERRUPTIONS

Task 2: Post-Boot Registers aligned with HW Mode

Added methodapply_post_boot_state(bool is_cgb_mode)inCoreRegisters:

// 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 - After loading 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)

Task 3: Extend wait-loop listener

Added full state capture inCPU.cppwhen wait-loop is detected:

[WAITLOOP] Timer: TAC=0x00, DIV=0x42, TIMA=0x00
[WAITLOOP] - Timer OFF
[WAITLOOP] ⚠️ TIMER OFF: There will be no IRQ Timer
[WAITLOOP] CGB: KEY1=0x00, VBK=0x00, SVBK=0x00

🧪 Tests and Verification

Commands Executed

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

# Safe parsing (without cluttering context)
grep -E "\[IRQ-SUMMARY\]|\[WAITLOOP\]|Post-Boot State" logs/step0411_*.log | head -n 160

Results: Oro.gbc (Pokémon Gold)

✅ BIG ADVANCE

  • ✅ VBlank IRQs: 2 (generating correctly)
  • ✅ STAT IRQs: 144 (LCD STAT interrupts active)
  • ✅ Timer IRQs: 3
  • ✅ IE=0x1F (all IRQs enabled)
  • ✅ RETI running (ISRs working)
  • ✅ LCDC changes dynamically (0x91→0x00→0xE3)
  • ✅ Hardware Mode: CGB detected, consistent post-boot logs

Results: pkmn.gb (Pokémon Red)

⚠️ PARTIAL PROGRESS

  • ✅ VBlank IRQs: 1+ (generating)
  • ✅ LCDC: Dynamic changes (0x91→0x80→0x81→0xE3)
  • ⚠️ IE=0x0D (VBlank, Timer, Joypad enabled, but Timer OFF)
  • ⚠️ TAC=0x00 (Timer off → no IRQ Timer)
  • ⚠️ Waiting for Joypad input (polling active at 0x600A-0x600E)
  • ✅ Hardware Mode: DMG detected,A=0x01

Results: tetris_dx.gbc (Tetris DX)

⚠️ WAITING FOR INPUT (no regression)

  • ✅ VBlank IRQs: 1+ (generating)
  • ⚠️ IE=0x00 (interrupts NOT enabled at startup)
  • ⚠️ Waiting for Joypad input (active polling)
  • ✅ Hardware Mode: CGB detected,A=0x11

Test Fragment (IRQ Analysis)

// Implicit test: Verification of IRQ counters
// The test runs the emulator and analyzes logs

// Log extract (Oro.gbc):
[IRQ-SUMMARY] Step 0411 - Frame 0
[IRQ-SUMMARY] Requests generated (total):
[IRQ-SUMMARY] VBlank (bit 0): 2
[IRQ-SUMMARY] STAT (bit 1): 144
[IRQ-SUMMARY] Timer (bit 2): 3
[IRQ-SUMMARY] Current status:
[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] Analysis:
[IRQ-SUMMARY] ✓ THERE ARE PENDING INTERRUPTIONS

// Compiled C++ module validation ✓

📁 Modified Files

  • src/core/cpp/MMU.hpp- Added IRQ counters and declarationlog_irq_requests_summary()
  • src/core/cpp/MMU.cpp- Implementation of counters inrequest_interrupt()and summary function
  • src/core/cpp/PPU.cpp- Periodic call tolog_irq_requests_summary()
  • src/core/cpp/CPU.cpp- Extended wait-loop detector with Timer and CGB info
  • src/core/cpp/Registers.hpp- Declaration ofapply_post_boot_state()
  • src/core/cpp/Registers.cpp- Implementation ofapply_post_boot_state()
  • src/core/cython/registers.pyx- Binding Cython forapply_post_boot_state()
  • src/core/cython/registers.pxd- C++ declaration for Cython
  • src/viboy.py- Call toapply_post_boot_state()after loading ROM

🔍 Key Findings

  1. IRQ pipeline works: The counters show thatrequest_interrupt()is called correctly (VBlank, STAT, Timer).
  2. Oro.gbc advances significantly: Run RETI, have IE=0x1F active, and LCDC changes dynamically (off/on). This indicates that the game progresses beyond boot.
  3. Pokémon Red and Tetris DX await input: Both remain in Joypad polling (IE low, waiting for buttons). They need input simulation to move forward.
  4. Post-Boot Registers now consistent: A=0x01(DMG) for pkmn.gb,A=0x11(CGB) for Oro.gbc and tetris_dx.gbc.
  5. Improved diagnostics: [IRQ-SUMMARY]provides clear analysis of the status of IRQs, facilitating future debugging.

🚀 Next Steps

  1. Step 0412: Joypad Input Simulation- Implement simulated auto-input (e.g. automatically pressing START after N frames) so that games that expect input can advance.
  2. Step 0413: Verify post-input tile loading- Once the games receive input, check if they load tiles correctly.
  3. Step 0414: Investigate why Oro.gbc advances but pkmn.gb does not- Analyze differences in initialization between both Pokémon games.

📚 References