This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Fix: Interrupt Timing and EI Delay
Summary
Critical fix to CPU interrupt timing to resolve crashes in games (Tetris, Pokémon, Tetris DX) and non-responsive controls issues. It was implementeddelay of 1 EI instruction(Enable Interrupts) and corrected the order checking for interruptions in the instruction cycle. Interruptions are now check correctly before each instruction, and EI activates IME after the next instruction, like in real hardware.
Hardware Concept
On real Game Boy hardware, interrupt handling has two behaviors critical that must be emulated correctly:
1. Interrupt Check Timing
Interrupts are checkedbeforeafter executing each instruction, not after. This means that the correct flow is:
- Check if there are any pending interrupts (IE & IF) and if IME is active
- If interrupt is pending and IME active, jump to vector immediately (without executing the instruction)
- If there is no interrupt or IME is disabled, execute the normal instruction
2. Delay of 1 EI Instruction (Enable Interrupts)
The instructionEI(opcode 0xFB) has a special behavior:not active
IME immediately, but instead schedules its activation for after the next instruction.
This allows the instruction that followsEIrun without interruptions, and then
IME is activated automatically.
This behavior is critical for many games that use patterns such as:
EI ; Program IME activation after the following instruction
RETI ; This instruction is executed with IME still False
; [Here IME is activated automatically]
; [In the next step(), interrupts with active DT are checked]
3. HALT and Interrupt Wakeup
When the CPU is in stateHALTand there are pending interrupts (in IE and IF),
CPU should wake up even if IME is disabled. If IME is disabled, the CPU
wakes up but does not jump to the interrupt vector, allowing the code to check
manually interrupts using IF polling.
Fountain:Pan Docs - CPU Instruction Set (EI timing), Interrupts, HALT behavior
Implementation
The method was modifiedstep()CPU and opcodeEIto implement
the correct timing of interruptions:
1. ime_scheduled attribute
Attribute addedself.ime_scheduled: bool = Falsein the CPU constructor
to track whenEIyou have scheduled IME activation.
2. Modification of _op_ei()
The method_op_ei()now schedule IME activation instead of activating it immediately:
def _op_ei(self) -> int:
# DO NOT activate IME immediately, schedule it for after the next instruction
self.ime_scheduled = True
return 1
3. Modification of step()
The methodstep()now follow this flow:
- Activate IME if programmed: Yeah
ime_scheduledis active, activate IME and clear the flag. This occurs at the beginning of the step, after the instruction previous one that executedEI. - Check HALT: If the CPU is in HALT, consume 1 cycle and check interrupts. If an interrupt is processed, return. If not processed but woken up (IME disabled), continue executing the instruction normally.
- Check interruptions: If interruption is pending and IME active, skip to the vector immediately (without executing the instruction).
- Execute instruction: If there is no interrupt, execute the normal instruction.
Modified components
src/cpu/core.py:- Added attribute
ime_scheduledin__init__ - Modified
_op_ei()to program IME activation - Modified
step()to activate IME at first and check interrupts before executing instruction - Corrected handling of HALT so that it continues executing instruction if it woke up without processing interrupt
- Added attribute
Affected Files
src/cpu/core.py- Modification of the instruction cycle and EI opcode
Tests and Verification
All interruption tests were run to verify that the fix did not break functionality existing and that the behavior is now correct:
Execution of Unit Tests
Command executed:
python -m pytest tests/test_cpu_interrupts.py -v
Around:
- OS: Windows 10
- Python: 3.13.5
Result:
============================= test session starts =============================
platform win32 -- Python 3.13.5, pytest-9.0.2, pluggy-1.6.0
collected 7 items
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_vblank_interrupt PASSED [ 14%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_interrupt_priority PASSED [ 28%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_halt_wakeup PASSED [ 42%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_no_interrupt_if_ime_disabled PASSED [ 57%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_timer_interrupt_vector PASSED [ 71%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_all_interrupt_vectors PASSED [ 85%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_reti_reactivates_ime PASSED [100%]
============================== 7 passed in 0.05s ==============================
What is valid:
- test_vblank_interrupt: Verifies that V-Blank interrupts are processed correctly when IME is active, jumping to vector 0x0040, disabling IME, clearing IF and consuming 5 M-Cycles. This test demonstrates that the timing of interrupt checking is correct (they are checked before executing the instruction).
- test_interrupt_priority: Verifies that when multiple interrupts are pending, the one with the highest priority is processed first (V-Blank over Timer).
- test_halt_wakeup: Verify that HALT wakes up with pending interrupts even if IME is disabled, and after waking up it executes the instruction normally (does not jump to vector if IME is disabled). This test demonstrates that the management of HALT is correct and that the CPU continues executing after waking up.
- test_no_interrupt_if_ime_disabled: Verifies that interruptions are not processed if IME is disabled.
- test_timer_interrupt_vector: Verifies that the Timer interrupt jumps to correct vector (0x0050).
- test_all_interrupt_vectors: Verifies that all interrupt vectors They are correct.
- test_reti_reactivates_ime: Verifies that RETI reactivates IME correctly.
Relevant Tests Code
The testtest_halt_wakeupis especially relevant because it validates behavior
HALT fix:
def test_halt_wakeup(self) -> None:
"""
Test: HALT wakes up with pending interrupts, even if IME is False.
If the CPU is in HALT and there are interrupts pending (in IE and IF),
CPU should wake up (halted = False), but NOT jump to vector
if IME is disabled. After waking up, the CPU continues to execute
normally (advances PC with the next instruction).
"""
mmu = MMU(None)
cpu = CPU(mmu)
cpu.registers.set_pc(0x3000)
cpu.halted = True # CPU in HALT
cpu.ime = False # IME disabled
# Enable V-Blank in IE and activate flag in IF
mmu.write_byte(IO_IE, 0x01)
mmu.write_byte(IO_IF, 0x01)
# Put a NOP at 0x3000
mmu.write_byte(0x3000, 0x00) # NOP
# Execute step
cycles = cpu.step()
# Should wake up but NOT jump to interrupt (IME disabled)
assert cpu.halted is False, "CPU must wake up from HALT"
# After waking up, execute the normal instruction (NOP)
assert cpu.registers.get_pc() == 0x3001, "PC must advance (executed NOP)"
assert cycles == 1, "Must consume 1 cycle (NOP), not interrupt cycles"
This test demonstrates that the corrected behavior allows HALT to wake up and continue executing the instruction when IME is disabled, which is critical for many games that use manual polling of interrupts after HALT.
Testing with ROMs (Pending)
State: draft
The fix is implemented and the unit tests pass, but it has not yet been tested with real ROMs (Tetris, Pokémon, Tetris DX) to verify that it resolves the reported crashes. It is recommended Test with these ROMs to validate the behavior in real conditions.
Legal notes:The ROMs mentioned (Tetris, Pokémon, Tetris DX) are provided by the user for local testing and are not distributed in the repository.
Sources consulted
- Pan Docs - CPU Instruction Set (EI timing behavior)
- Pan Docs – Interrupts (Interrupt Check Timing)
- Pan Docs - HALT behavior
Educational Integrity
What I Understand Now
- Interrupt Timing:Interrupts are checked before execute each instruction, not after. This is critical for correct behavior of the hardware.
- EI delay:The EI instruction does not activate IME immediately, but rather schedules its activation for after the next instruction. This allows the instruction following EI is executed without interruptions.
- HALT and wake up:When the CPU is in HALT and interrupts are pending, CPU should wake up even if IME is disabled. If IME is disabled, the CPU wakes up but does not jump to the vector, allowing manual polling.
What remains to be confirmed
- Validation with real ROMs:Although unit tests pass, they are missing verify that the fix resolves the crashes reported in Tetris, Pokémon and Tetris DX. It is recommended to test with these ROMs to validate the behavior in real conditions.
- EI behavior with pending interrupts:If EI is executed and There are pending interrupts, are they processed immediately after the next instruction or is there any other special behavior? Current tests do not cover this specific case.
Hypotheses and Assumptions
The current implementation assumes that the hardware behavior is exactly as described. described in Pan Docs: interrupts are checked before each instruction, and EI activates IME after the following instruction. If the crashes persist after this fix, There could be other timing or hardware behavior issues that are not documented. or that require more precise implementation.
Next Steps
- [ ] Test with real ROMs (Tetris, Pokémon, Tetris DX) to verify that the fix resolves the crashes
- [ ] If crashes persist, investigate other timing issues (e.g. timing of V-Blank, Timer, Joypad)
- [ ] Add additional tests for EI edge cases with pending outages
- [ ] Document any additional behavior discovered during ROM testing