This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
PPU Phase F: Implementation of STAT Interrupts
Summary
The emulator was in adeadlockpersistent because the CPU in stateHALTHe never woke up. Although the HALT architecture implemented in Step 0173 was correct, the problem was that the PPU did not generate theSTAT Interruptsthat the game was waiting to continue. This Step documents the final verification and correction of the STAT interrupt system in the C++ PPU, ensuring that the V-Blank interrupt uses therequest_interrupt()to maintain consistency, and confirms that access toimein the Cython wrapper it is already correctly implemented.
Hardware Concept: The STAT Interrupt Dialog
The recordSTAT(0xFF41) not only reports the current mode of the PPU, but also allows the game to request notifications when certain events occur via interrupts. This interrupt system is critical for synchronization between the game and the graphics hardware.
STAT Interrupt Enable Bits
- STAT Bit 6:Enable interrupt if
LY == LYC(line match). - STAT Bit 5:Enable interruption when entering Mode 2 (OAM Scan).
- STAT Bit 4:Enable interruption when entering Mode 1 (V-Blank).
- STAT Bit 3:Enable interruption when entering Mode 0 (H-Blank).
Rising Edge Detection
A critical detail of the actual hardware is therising edge detection: The interrupt is only requested at the instant the condition becomes true (when it passes fromfalsetotrue), not continuously while it remains active. This prevents multiple interrupts from being generated while the PPU mode remains constant (for example, during the entire H-Blank period).
When the PPU detects that one of these conditions is met AND the corresponding bit inSTATis activated, you must request an interruptsetting bit 1 of the register to 1I.F.(0xFF0F). This is the mechanism that allows the CPU to wake up fromHALTwhen the game is waiting for a specific PPU event to occur.
Fountain:Pan Docs - LCD Status Register (STAT), STAT Interrupt, Rising Edge Detection
Implementation
The implementation of STAT interrupts was already present in the code (from previous Steps), but an important fix was made for consistency: changing the V-Blank interrupt request to use therequest_interrupt()instead of writing directly to the registryI.F..
A. V-Blank Interruption Correction
Insrc/core/cpp/PPU.cpp, whenL.Y.reaches 144 (V-Blank start), it was writing directly to the logI.F.. To maintain consistency with the rest of the code and follow good practices, it was changed to use the methodrequest_interrupt(0)from the MMU:
// Before (writing directly):
if (ly_ == VBLANK_START) {
uint8_t if_val = mmu_->read(IO_IF);
if_val |= 0x01; // Set bit 0 (V-Blank interrupt)
mmu_->write(IO_IF, if_val);
frame_ready_ = true;
}
// After (using request_interrupt):
if (ly_ == VBLANK_START) {
mmu_->request_interrupt(0); // Bit 0 = V-Blank Interrupt
frame_ready_ = true;
}
B. IME Setter Verification
It was verified that access toimein the Cython wrapper it was already correctly implemented:
- The method
set_ime(bool value)exists inCPU.hppandCPU.cpp. - It is displayed in
cpu.pyxas property with getter and setter using decorator@property. - The tests confirm that it works correctly (
test_cpu_ime_setterhappens).
C. Existing STAT Interrupt System
The methodcheck_stat_interrupt()inPPU.cppIt was already implemented correctly:
- Detects active interrupt conditions (LYC=LY, Mode 0, Mode 1, Mode 2).
- Implements rising edge detection using
stat_interrupt_line_to track the previous status. - Request interruptions using
mmu_->request_interrupt(1)when it detects a rising edge. - It is called correctly at the appropriate times during the scanline cycle.
Affected Files
src/core/cpp/PPU.cpp- Fixed V-Blank interrupt request to userequest_interrupt().
Note:The other components (STAT interrupts, IME setter) were already implemented correctly in previous Steps.
Tests and Verification
All STAT interrupt tests and the IME setter pass correctly:
STAT Interrupt Tests
def test_stat_hblank_interrupt():
"""Verifies that a STAT interrupt is requested when entering Mode 0 (H-Blank)."""
mmu = PyMMU()
ppu = PyPPU(mmu)
mmu.set_ppu(ppu)
mmu.write(0xFF40, 0x80) # LCD ON
# Enable H-Blank interrupt in STAT (bit 3)
mmu.write(0xFF41, 0x08)
mmu.write(0xFF0F, 0x00) # Clear IF
# Advance PPU to H-Blank
ppu.step(252)
assert ppu.mode == 0
assert (mmu.read(0xFF0F) & 0x02) != 0 # Bit 1 (STAT) set
Command executed: pytest tests/test_core_ppu_interrupts.py -v
Result:6 tests passed correctly:
test_stat_hblank_interrupt- Check interruption in H-Blanktest_stat_vblank_interrupt- Check interruption in V-Blanktest_stat_oam_search_interrupt- Check interruption in OAM Searchtest_stat_lyc_coincidence_interrupt- Check LYC=LY interruptiontest_stat_interrupt_rising_edge- Verifies rising edge detectiontest_cpu_ime_setter- Check the IME setter
Native Validation
All tests validate the compiled C++ module and confirm that:
- STAT interrupts are generated correctly in the appropriate modes.
- Rising edge detection works (no multiple interrupts generated).
- The IME setter allows you to configure the interrupt status from the tests.
- The V-Blank interrupt is successfully requested when LY reaches 144.
Sources consulted
- Bread Docs:LCD Status Register (STAT)
- Bread Docs:STAT Interrupt, Rising Edge Detection
- Bread Docs:Interrupt Flag (IF) Register
- Bread Docs:HALT behavior and Interrupts
Educational Integrity
What I Understand Now
- The alarm clock never rang:The CPU was entering HALT correctly, but the PPU was not generating the STAT interrupts that the game expected. Without interrupts, there was nothing to wake up the CPU, creating a deadlock.
- Rising edge detection:STAT interrupts are only fired when the condition goes from false to true, not while it remains active. This is critical to avoid multiple interruptions over long periods (like all H-Blank).
- Code consistency:Wear
request_interrupt()instead of writing directly to IF makes the code more maintainable and consistent, since all components use the same method to request interrupts. - The complete wiring:The connection between the PPU state machine and the interrupt system is complete. The PPU detects mode and condition changes and requests interruptions when appropriate.
What remains to be confirmed
- Running with real ROM:Run the emulator with a real ROM (ex:
tetris.gb) to verify that the deadlock has been broken and the game can progress beyond initialization. - Precise timing:Validate that the timing of STAT interrupts exactly matches the behavior of real hardware.
Hypotheses and Assumptions
We assume that with STAT interrupts working correctly, the CPU should be able to wake up from HALT when the game expects them, breaking the deadlock it held.L.Y.stuck at 0. This should allow the game to progress past initialization and eventually display the Nintendo logo.
Next Steps
- [ ] Recompile the C++ module with the corrections made
- [ ] Run the emulator with a real ROM (ex:
tetris.gb) and verify that the deadlock has been broken - [ ] Validate that
L.Y.advances correctly beyond 0 - [ ] Verify that the game can complete initialization and display the Nintendo logo
- [ ] Add additional tests if problems are identified during execution with real ROMs