This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
V-Blank Polling: Independent IF of IME
Summary
It was verified and documented that the recordIF (Interrupt Flag, 0xFF0F)is updatedalwayswhen V-Blank occurs, regardless of the status ofIME (Interrupt Master Enable). This allows games to do manual IF polling to detect V-Blank without using interrupts automatic. Specific tests were created to validate this critical behavior and the documentation in the code to make this aspect of the hardware clear.
Hardware Concept
On the Game Boy, there are two mechanisms to detect hardware events (V-Blank, Timer, etc.):
-
Automatic Interruptions: Yeah
IME=True,I.E.has the bit active, andI.F.has the bit active, the CPU automatically jumps to the interrupt vector. -
Manual Polling: The game manually reads the log
I.F.and check if some bit is active. If it detects V-Blank (bit 0), it runs its own update routine.
CRITICAL: The recordI.F.ispure hardware. When an event occurs
(V-Blank, Timer, etc.), hardwarealwaysactivates the corresponding bit inI.F.,
regardlessof:
- The state of
IME(Interrupt Master Enable) - The state of
I.E.(Interrupt Enable, 0xFFFF)
This means that even if a game runsD.I.(Disable Interrupts,IME=False)
and never executesEI(Enable Interrupts), hardware keeps updatingI.F.when
V-Blank occurs. The game can readI.F.manually and detect V-Blank to update graphics.
Fountain: Pan Docs - Interrupts, V-Blank Interrupt Flag
Implementation
It was verified that the current implementation insrc/gpu/ppu.pyalready updatedI.F.correctly when V-Blank occurs, without depending onIME. However, it improved
documentation to make this critical behavior explicit.
Components created/modified
-
src/gpu/ppu.py: Added explicit documentation in the methodstep()explaining thatI.F.is always updated, regardless ofIME. -
tests/test_ppu_vblank_polling.py: New file with 3 tests that validate:test_vblank_sets_if_with_ime_false: Check thatI.F.is activated withIME=Falsetest_vblank_if_persists_until_cleared: Check thatI.F.persists until manual cleaningtest_vblank_if_independent_of_ie: Check thatI.F.is updated regardless ofI.E.
Design decisions
The current implementation was already correct: the PPU updatesI.F.directly callingmmu.write_byte(0xFF0F, if_val | 0x01)without checking the status ofIME.
This is correct becauseI.F.is a hardware registry that is automatically updated
when the event occurs, not when the CPU processes the interrupt.
The MMU allows free writing on0xFF0F(there are no special restrictions), which
It is correct because the hardware can updateI.F.whenever.
Affected Files
src/gpu/ppu.py- Improvement of documentation in methodstep()to explain thatI.F.is updated regardless ofIMEtests/test_ppu_vblank_polling.py- New file with tests to validate V-Blank polling
Tests and Verification
The new tests were executed to validate the behavior of V-Blank polling:
Command executed
pytest tests/test_ppu_vblank_polling.py -v
Around
- YOU:Windows 10
- Python: 3.13.5
Result
============================= test session starts =============================
platform win32 -- Python 3.13.5, pytest-9.0.2, pluggy-1.6.0
collecting ... collected 3 items
tests/test_ppu_vblank_polling.py::TestPPUVBlankPolling::test_vblank_sets_if_with_ime_false PASSED [ 33%]
tests/test_ppu_vblank_polling.py::TestPPUVBlankPolling::test_vblank_if_persists_until_cleared PASSED [ 66%]
tests/test_ppu_vblank_polling.py::TestPPUVBlankPolling::test_vblank_if_independent_of_ie PASSED [100%]
======================== 3 passed, 2 warnings in 0.31s ========================
How valid
-
test_vblank_sets_if_with_ime_false: Validates that the record
I.F.is updated when V-Blank occurs (LY=144), even whenIME=False. This shows that the hardware updateI.F.regardless of the state ofIME, allowing manual polling. -
test_vblank_if_persists_until_cleared: Validates that
I.F.remains active until the game manually clears it, and it is reactivated on the next V-Blank. This shows polling behavior: the game can readI.F., detect V-Blank, do your job, and clear the bit manually. -
test_vblank_if_independent_of_ie: Validates that
I.F.it even updates whenI.E.(Interrupt Enable) has bit 0 disabled. This shows thatI.F.It is pure hardware and does not depend on the configuration ofI.E..
Test code
Essential fragment of the main test:
def test_vblank_sets_if_with_ime_false(self) -> None:
"""Test: IF is updated when V-Blank occurs, even with IME=False."""
mmu = MMU(None)
cpu = CPU(mmu)
ppu = PPU(mmu)
mmu.set_ppu(ppu)
# Set IME=False (automatic interrupts disabled)
cpu.ime = False
# Clear IF initially
mmu.write_byte(IO_IF, 0x00)
# Advance PPU to V-Blank (line 144)
total_cycles = 144 * 456
ppu.step(total_cycles)
# CRITICAL: IF must have bit 0 set, even with IME=False
if_val = mmu.read_byte(IO_IF)
assert(if_val & 0x01) == 0x01
Why this test demonstrates something about the hardware: This test verifies that the recordI.F.behaves like pure hardware: it updates automatically when the event occurs (V-Blank), without depending
of the software configuration (IME). This is exactly how the real hardware works
Game Boy, allowing games to do manual polling even when automatic interrupts are on
disabled.
Sources consulted
- Bread Docs:Interrupts- Explanation of IF, IE, IME and polling
- Bread Docs:LCD Timing- V-Blank and LY registration
Educational Integrity
What I Understand Now
-
IF is pure hardware: The record
IF (0xFF0F)updates automatically by the hardware when an event occurs (V-Blank, Timer, etc.), regardless of the status ofIMEeitherI.E.. This is different from other systems where interrupt flags may depend on software configuration. -
Polling vs Automatic Interrupts: Games can use two strategies to detect
V-Blank: (1) Automatic interrupts (requires
IME=True,I.E.with active bit, andI.F.with active bit), or (2) Manual Polling (readI.F.periodically and check bits). The second strategy works even withIME=False. -
Separation of responsibilities:
I.F.indicates "what events have occurred",I.E.indicates "what events I want to process automatically", andIMEindicates "yes I want" "process interrupts automatically." Hardware always updatesI.F., but only processes automatically ifIME=Trueand the corresponding bit is active inI.E..
What remains to be confirmed
-
Behavior on real hardware: Although documentation and tests confirm that
I.F.is updated regardless ofIME, it would be useful to verify this with a specific ROM test that pollI.F.withIME=Falseand verify that it detects V-Blank correctly. -
Exact timing: The current test verifies that
I.F.is activated whenLY=144, but it does not verify the exact timing within the line cycle. On real hardware,I.F.is activated at the beginning of line 144, but it is not clear whether it occurs at the beginning or the end of the line cycle.
Hypotheses and Assumptions
Verified assumption: The current implementation assumes thatI.F.is updated whenL.Y.reaches 144, regardless ofIME. The tests confirm that this assumption
is correct and matches the documented behavior of the hardware.
Outstanding assumption: We assume that the activation timing ofI.F.(at the beginning of
line 144) is correct, but this is not completely verified with test ROMs. If a game polls
very precise timing, there could be minor discrepancies.
Next Steps
- [ ] Verify that the games that poll
I.F.withIME=Falsecan detect V-Blank correctly - [ ] If the game still does not progress, investigate whether there are other aspects of polling that are not implemented correctly
- [ ] Consider adding optional logging to track when the game reads/writes to
I.F.to debug polling - [ ] Check if the game expects any specific status
LCDCor other records before polling