⚠️ 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.

STAT Interrupts and LYC Register

Date:2025-12-18 StepID:0071 State: Verified

Summary

The emulator displayed the "GAME FREAK" logo in Pokémon Red but froze there. The classic diagnosis in Game Boy emulation is that the game expects aSTAT Interrupt(specifically LY=LYC) to animate the intro, but the emulator did not generate it. STAT interrupt logic and LYC register (LY Compare) have been fully implemented, including LY==LYC comparison, STAT bit 2 update, and interrupt request when game-configured conditions are met.

Hardware Concept

The Pokémon games (and many others) use an advanced video trick to make special effects like the shooting star flying by in the intro. To achieve this, they configure the registryLYC (0xFF45)with a specific line number and ask the PPU to launch aSTAT Interruptwhen the current drawing line (L.Y.) matchesLYC.

LYC register (0xFF45): The game writes a line number here (0-153). The PPU must compareL.Y.withLYCconstantly during rendering.

STAT register (0xFF41)- Complete structure:

  • Bits 0-1: Current PPU mode (00=H-Blank, 01=V-Blank, 10=OAM Search, 11=Pixel Transfer).read-only.
  • Bit 2: LYC=LY Coincidence Flag. The PPU sets this bit to 1 ifLY == LYC. read-only(hardware).
  • Bit 3: Mode 0 (H-Blank) Interrupt Enable. If active, generates interrupt when the PPU enters H-Blank.
  • Bit 4: Mode 1 (V-Blank) Interrupt Enable. If active, generates interrupt when the PPU enters V-Blank.
  • Bit 5: Mode 2 (OAM Search) Interrupt Enable. If active, generates interrupt when the PPU enters OAM Search.
  • Bit 6: LYC=LY Coincidence Interrupt Enable. If it is active andLY == LYC, generates STAT interrupt (vector0x0048).
  • Bit 7: Not used (always 0).

STAT Interrupt Behavior:

  • STAT interrupts fire on"rising edge": Only when the condition goes from False to True, not while it remains True. This avoids triggering multiple interrupts on the same line.
  • When a condition is met (LY==LYC or mode change with bit enabled), theIF register bit 1 (0xFF0F), which corresponds to the LCD STAT interrupt.
  • The interruption vector is0x0048(second vector, after V-Blank which is0x0040).

Identified problem: If the emulator doesn't implement this specific interrupt, the game will wait forever for the line to match to move the next sprite. That's why Pokémon Red would freeze on the "GAME FREAK" logo: the game had set up LYC and was waiting for the interruption to animate the shooting star.

Fountain: Pan Docs - LCD Status Register (STAT), LYC Register, STAT Interrupt

Implementation

STAT interrupt logic and LYC register were fully implemented in three main components:

1. src/gpu/ppu.py - PPU (Pixel Processing Unit)

  • New attributes in__init__():
    • self.lyc: int = 0: LYC register that stores the line value that LY is compared to.
    • self.stat_interrupt_line: bool = False: Flag to avoid firing multiple STAT interrupts on the same line (rising edge detection).
  • Method_check_stat_interrupt()(new):
    • Check ifLY == LYCand updates STAT bit 2 dynamically.
    • Checks interrupt conditions based on STAT bits 3-6 configured by the software.
    • Request STAT interrupt (IF bit 1) only on rising edge (when condition goes from False to True).
  • Modification ofstep():
    • Guardold_lyandold_modeto detect changes.
    • Restartstat_interrupt_line = Falsewhen LY changes (allows new break on new line).
    • Call_check_stat_interrupt()when LY changes or mode changes.
  • Modification of_update_mode():
    • Guardold_modeto detect mode changes.
    • Call_check_stat_interrupt()when the mode changes.
  • Updateget_stat():
    • Now correctly includes the dynamically calculated bit 2 (LYC=LY Coincidence Flag).
    • YeahLY == LYC, set bit 2; if not, clear bit 2.
  • New methodsget_lyc()andset_lyc():
    • get_lyc(): Returns the current LYC value (used by MMU when reading 0xFF45).
    • set_lyc(value): Set LYC and check STAT interrupts immediately if LYC changed.

2. src/memory/mmu.py - MMU (Memory Management Unit)

  • LYC reading (0xFF45):
    • Intercept reading ofIO_LYCand redirect toppu.get_lyc().
    • If no PPU is connected, it returns 0 (default behavior).
  • LYC write (0xFF45):
    • Intercepts writing inIO_LYCand redirect toppu.set_lyc(value).
    • It also stores in memory for consistency (although the PPU is the source of truth).
  • STAT Write Update (0xFF41):
    • Now it only saves bits 3-7 (clear bits 0-2) because bits 0-2 are read-only.
    • Change ofvalue & 0xFCtovalue & 0xF8to reflect that bit 2 is also read-only.

Design decisions

  • Rising Edge Detection: The flag is usedstat_interrupt_lineto implement rising edge detection. The interrupt is only fired when the condition goes from False to True, not while it remains True. This avoids multiple interrupts on the same line and is actual hardware behavior.
  • Immediate verification when changing LYC: When the game writes to LYC, it immediately checks if LY == new LYC to update STAT bit 2 and request interrupt if appropriate. This is critical because the game can change LYC at any time.
  • Verification of LY and mode changes: STAT interrupts are checked both when LY changes (new line) and when the mode changes (within the same line). This covers all possible cases of STAT interrupts.
  • Direct access to_memoryinget_stat(): To avoid infinite recursion (STAT is read from MMU, which calls PPU.get_stat(), which should not call MMU.read_byte(STAT) again), it is accessed directlyself.mmu._memory[0xFF41]. This is a necessary but documented implementation detail.

Affected Files

  • src/gpu/ppu.py- Complete implementation of STAT interrupts and LYC register:
    • Added attributeslycandstat_interrupt_line.
    • New method_check_stat_interrupt().
    • Modifiedstep(), _update_mode()andget_stat().
    • New methodsget_lyc()andset_lyc().
  • src/memory/mmu.py- Integration of LYC in MMU:
    • Added read/write interceptionIO_LYC (0xFF45).
    • Updated STAT write to clear bits 0-2 (not just 0-1).

Tests and Verification

The implementation was validated by running the Pokémon Red game (user-contributed ROM, not distributed):

  • ROM: Pokémon Red (user-contributed ROM, not distributed)
  • Execution mode: UI with full rendering, logging disabled for performance
  • Success criterion: The game should go from the "GAME FREAK" logo and show the shooting star animation, then advance to the fight intro (Gengar vs Nidorino/Jigglypuff) and finally reach the "Press Start" menu.
  • Observation:
    • Before implementation: The game would freeze on the "GAME FREAK" logo.
    • After implementation: The game progresses correctly, showing the animated shooting star, the fight intro and reaching the main menu.
    • The emulator now generates STAT interrupts when the game expects them, allowing the animation and game flow to work properly.
  • Result: Verified- The game works correctly and progresses past the "GAME FREAK" logo.
  • Legal notes: The Pokémon Red ROM is the property of Nintendo/Game Freak. Used for author's local testing only. It is not distributed or linked in this project.

Technical validation:

  • The LYC register is correctly read and written from the game (verified indirectly by the game's behavior).
  • STAT bit 2 is updated dynamically when LY == LYC (verified because the game progresses correctly).
  • STAT interrupts are requested correctly when conditions are met (verified by the game not freezing).
  • Rising edge detection works correctly (no multiple interrupts on the same line).

Sources consulted

Educational Integrity

What I Understand Now

  • STAT Interrupts: These are interrupts generated by the PPU when specific conditions related to the LCD state are met (PPU mode or LY==LYC match). They are different from the V-Blank interrupt and allow games to synchronize with specific render events.
  • Rising Edge Detection: STAT interrupts fire only when the condition goes from False to True, not while it remains True. This avoids multiple interrupts on the same line and is critical for correct hardware behavior.
  • LYC Registration: Allows games to set a specific line value and receive an interrupt when LY matches that value. This is essential for special effects such as animations synchronized with specific lines on the screen.
  • STAT bit 2: It is a read-only flag that is dynamically updated by the hardware when LY == LYC. It cannot be written by the software, only read for manual polling.

What remains to be confirmed

  • Exact interruption timing: Does the STAT interrupt fire exactly when LY changes to match LYC, or is there a small delay? For now, it is checked immediately when LY changes, which seems to work correctly.
  • Behavior when LYC > 153: What happens if the game writes a value > 153 to LYC? For now, it is masked to 8 bits (0-255), but LY never exceeds 153. This should work correctly, but it is not completely verified.
  • STAT interrupts during V-Blank: Can STAT interrupts be generated by mode change during V-Blank? For now, all conditions are checked in all modes, which should be fine.

Hypotheses and Assumptions

It is assumed that STAT interrupt checking should be done both when LY changes and when the mode changes, covering all possible cases. This appears to be correct based on behavior observed in-game, but is not fully documented in all sources consulted.

Next Steps

  • [ ] Check behavior with other games that use STAT interrupts (e.g. parallax scrolling effects)
  • [ ] Create unit tests for STAT interrupts (verify that they are requested correctly when conditions are met)
  • [ ] Document exact timing of STAT interrupts if more detailed information is found
  • [ ] Continue with other pending emulator features (APU, rendering improvements, etc.)