⚠️ Clean-Room / Educational

This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.

Operation "Interrupt Awakening" - Interrupt Activation Debugging

Date:2025-12-25 StepID:0280 State: draft

Summary

This Step implements theOperation Interrupt Awakeningto investigate why Pokémon Red is stuck in an infinite loop waiting for the flag0xD732change. Analysis of Step 0279 confirmed that the problemIt is NOT a Reset Loop, but a"induced coma": The game is stuck in the polling loop (PC: 0x614D-0x6153) waiting for a V-Blank ISR to modify the flag, but interrupts are disabled (IE=0x00). Although it was detected aEIinPC:0x60A6, interrupts do not appear to be active during polling. This Step adds ultra-precise instrumentation to track the status ofI.E.andIMEwhen it runsEI, and monitors the polling loop to detect if anyone is writing toI.E.during the wait.

Hardware Concept

The Delay of an EI Instruction Cycle

The instructionEI(Enable Interrupts, opcode0xFB) has special behavior on real Game Boy hardware:the Interrupt Master Enable (IME) is activated AFTER the following instruction is executed, not immediately. This one-cycle delay is critical because it allows the instruction followingEIrun without interruptions, which is necessary for atomic configurations or to avoid race conditions.

Interrupt activation flow:

  1. EI Execution: The opcode0xFBit runs, butIMEIt does not activate immediately. Instead, an internal flag (ime_scheduled_) which indicates thatIMEshould be activated after the next instruction.
  2. Execution of the following instruction: The instruction that followsEIis executed withIME=false, ensuring that it is not interrupted.
  3. IME Activation: At the beginning of the next instruction cycle, before the fetch, it is checked ifime_scheduled_istrue. If it is, it is activatedIMEand the flag is cleaned.
  4. Interrupt processing: OnceIMEis active, the system can process pending interrupts ifIE & IF != 0.

Fountain:Pan Docs - "EI Instruction": "Interrupts are enabled after the instruction following EI."

Interrupt Logs

The Game Boy has two critical registers for interrupt handling:

  • IE (0xFFFF) - Interrupt Enable: Interrupt sources enable register. Each bit enables a specific source:
    • Bit 0: V-Blank
    • Bit 1: LCD STAT
    • Bit 2: Timer
    • Bit 3: Serial
    • Bit 4: Joypad
  • IF (0xFF0F) - Interrupt Flag: Pending interrupt flag register. Each bit indicates whether an interrupt is pending.
  • IME (Interrupt Master Enable): Internal CPU flag that controls whether interrupts can be processed. It can only be activated byEI(with delay) or deactivate viaD.I.(immediate).

Condition to process an interrupt: IME == true && (IE & IF) != 0

Fountain:Pan Docs - "Interrupts": "An interrupt is processed if IME is set and the corresponding bit in both IE and IF is set."

Implementation

This Step adds two critical instrumentations to debug the interrupt triggering issue:

1. Ultra-Precise EI and IME Tracking

The case was modified0xFB(EI) inCPU.cppto capture the exact status ofI.E.andIMEwhen trying to enable interrupts. This allows us to identify if the problem is thatI.E.is in0x00when it runsEI, or ifIMEdoes not activate correctly.

case 0xFB://EI (Enable Interrupts)
{
    // --- Step 0280: Ultra-Precise EI and IME Tracking ---
    uint8_t ie_val = mmu_->read(0xFFFF);
    printf("[CPU-EI] EI instruction on PC:0x%04X | Current IE:0x%02X | Previous IME:%d | Programmed IME:%d\n",
           original_pc, ie_val, ime_ ? 1 : 0, ime_scheduled_ ? 1 : 0);
    ime_scheduled_ = true;
    cycles_ += 1;
    return 1;
}

Information captured:

  • Original PC: Address where it was executedEI
  • Current IE: IE register value at run timeEI
  • Previous IME: IME status before runningEI
  • programmed IME: If there was already aEIslope (should be 0 normally)

2. Polling Sniper with IE Status

Added a polling loop monitor (PC: 0x614D-0x6153) that captures the status ofI.E., I.F., IMEand the flag0xD732during the wait. This allows us to see if someone is writing inI.E.while waiting, or ifI.E.magically changes to zero.

// --- Step 0280: Polling Sniper with IE Status ---
if (original_pc >= 0x614D && original_pc<= 0x6153) {
    static int polling_watch_count = 0;
    if (polling_watch_count < 20) {
        printf("[POLLING-WATCH] PC:%04X | IE:0x%02X | IF:0x%02X | IME:%d | D732:0x%02X\n",
               original_pc, mmu_->read(0xFFFF), mmu_->read(0xFF0F), 
               ime_? 1 : 0, mmu_->read(0xD732));
        polling_watch_count++;
    }
}

Information captured:

  • PC: Current address within the polling loop
  • I.E.: IE register value during wait
  • I.F.: IF register value (pending interrupts)
  • IME: Status of Interrupt Master Enable
  • D732: Value of the flag that the game is waiting for to change

3. Verification of handle_interrupts Logic

It was verified that the functionhandle_interrupts()does not modify the registryI.E.(only reads it). The function reads onlyI.E.to calculate pending interruptions (pending = IE & IF), but never writes inI.E.. This confirms that the problem is not in the interrupt processing, but in the activation ofI.E.eitherIME.

Modified components

  • src/core/cpp/CPU.cpp:
    • Modifiedcase 0xFB(EI) to add detailed IE and IME tracing
    • Added polling sniper at the end ofstep()to monitor the wait loop

Design decisions

  • Limit of 20 logs in polling: The number of polling sniper logs is limited to 20 to avoid saturating the log, but it is enough to see the loop pattern.
  • Real-time IE capture: It readsI.E.directly from the MMU in each log to capture changes in real time, a cached value is not used.
  • Using original_pc: Usedoriginal_pc(PC before fetch) to ensure that we capture the correct address, even if the PC moves forward during the execution of the instruction.

Affected Files

  • src/core/cpp/CPU.cpp- Modified case 0xFB (EI) and added polling sniper

Tests and Verification

Verification will be carried out through log analysis during the execution of Pokémon Red:

  • Logs [CPU-EI]: They should appear when the game runsEI. YeahCurrent IEis0x00at the time ofEI, we will confirm that the problem is that the game is enabling the master (IME) but you have not enabled any individual fonts (I.E.).
  • Logs [POLLING-WATCH]: They should appear when the game enters the polling loop. YeahI.E.magically changes to zero during the wait, we will know that something is writing toI.E.incorrectly.
  • C++ Compiled Module Validation: The code compiles without errors and the module is generated correctly.

Test command:

python setup.py build_ext --inplace
python main.py roms/pkmn.gb > debug_step_0280.log 2>&1

Expected analysis:

  • Look for[CPU-EI]in the log to see the status of IE when EI is executed
  • Look for[POLLING-WATCH]to see IE status during wait loop
  • Check if IE changes between the time of the EI and the polling loop

Sources consulted

Educational Integrity

What I Understand Now

  • Delay of an EI cycle:The instructionEInot activeIMEimmediately, but after executing the next instruction. This is actual hardware behavior, not a limitation of our implementation.
  • Difference between IE and IME: I.E.(0xFFFF) enables specific sources of interrupts, whileIMEis an internal flag that controls whether interrupts can be processed. Both must be active for an interrupt to be processed.
  • The real problem: The game is stuck in a waiting loop becauseIE=0x00, which prevents interrupts from being processed, even ifIMEis active and there are interruptions pending onI.F..

What remains to be confirmed

  • Why does IE stay at 0x00?: We need to check if the game never writes toI.E.after theEI, or if something is writing0x00inI.E.incorrectly.
  • Is the EI executed correctly?: We need to verify if theEIinPC:0x60A6really programIMEcorrectly, and ifIMEis activated after the next instruction.
  • Are there writes to IE during polling?: The polling sniper will tell us if someone is writing inI.E.during the wait, which could explain whyI.E.stays in0x00.

Hypotheses and Assumptions

Main hypothesis:The game runsEIinPC:0x60A6, butI.E.is in0x00then. The game expects something else (probably an ISR or initialization code) to enable interrupt sources inI.E., but that never happens because interrupts are disabled.

Assumption:The game should write toI.E.(For example,LD A, 0x0D; LDH (0xFF), Ato enable V-Blank and Timer) before or after theEI, but for some reason it doesn't or it runs incorrectly.

Next Steps

  • [ ] Run the emulator with the new instrumentation and analyze the logs[CPU-EI]and[POLLING-WATCH]
  • [ ] Check ifI.E.is in0x00when it runsEIinPC:0x60A6
  • [ ] Check ifI.E.changes during polling loop
  • [ ] YeahI.E.is in0x00, look in the game code where it should be enabled
  • [ ] YeahI.E.change to0x00during polling, identify what code you are writing inI.E.
  • [ ] Implement correction based on findings