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

Improved Update of STAT Bit 2 and write_byte_internal

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

Summary

Improved the implementation of STAT interrupts by adding a methodwrite_byte_internal()at MMU which allows internal components (such as the PPU) to update hardware registers without restrictions. Furthermore, it improved updating bit 2 of STAT (LYC=LY Coincidence Flag) in_check_stat_interrupt()to maintain memory consistency, ensuring that bit 2 is updated correctly when LY matches LYC, even if some code reads directly from memory without going throughread_byte().

Hardware Concept

The recordSTAT (0xFF41)has a hybrid structure: some bits are read-only (updated by hardware) and others are configurable by software. Specifically:

  • 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. It is set to 1 whenLY == LYC. read-only(hardware).
  • Bits 3-6: STAT interrupt enable flags (H-Blank, V-Blank, OAM Search, LYC=LY).Software configurable.
  • Bit 7: Not used (always 0).

On real hardware, when software writes to STAT, only the configurable bits (3-6) are saved. Bits 0-2 always They reflect the current state of the PPU and are automatically updated by the hardware. However, to maintain consistency in the emulator, it is useful for the PPU to update these bits in memory when they change, even though are technically calculated dynamically inget_stat().

Identified problem: Althoughget_stat()calculates bit 2 dynamically when read throughread_byte(), if some code directly accesses the internal memory (for example, to avoid recursion), bit 2 may not be updated. This can cause inconsistencies if the PPU reads STAT directly from memory in_check_stat_interrupt().

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

Implementation

Two main improvements were implemented:

1. write_byte_internal() method in MMU

Added a new methodwrite_byte_internal(addr, value)insrc/memory/mmu.pythat allows write directly to memory without going through restrictionswrite_byte(). This method is designed for internal use by system components (such as the PPU) that need to update hardware registers without restrictions.

Use:The PPU uses this method to update the STAT register (bits 0-2) without the restrictions ofwrite_byte()interfere. This is necessary becausewrite_byte()for STAT only save configurable bits (3-7) and clears bits 0-2, but the PPU needs to update these bits when they change.

2. Improved update of bit 2 in _check_stat_interrupt()

The method was improved_check_stat_interrupt()insrc/gpu/ppu.pyto update STAT bit 2 in memory when LY matches LYC (or when it does not match). Previously, bit 2 was only calculated dynamically inget_stat(), but now it is also updated in memory to maintain consistency.

Specific changes:

  • WhenLY == LYC: STAT is updated in memory with bit 2 active (0x04) and the current mode (bits 0-1).
  • WhenLY != LYC: STAT is updated in memory with bit 2 clear and the current mode (bits 0-1).
  • It is usedwrite_byte_internal()to update without restrictions, preserving the configurable bits (3-7).

Design decisions

  • Separation of responsibilities: write_byte_internal()is clearly marked as "for internal use only" and documented to avoid incorrect use from game code.
  • Memory consistency: Although technically bit 2 is dynamically calculated inget_stat(), Updating it in memory helps maintain consistency if some code reads directly from memory (for example, to avoid recursion).
  • Configurable Bit Preservation: When updating STAT, configurable bits (3-7) are preserved that the software may have written, and only the read-only bits (0-2) are updated.

Affected Files

  • src/memory/mmu.py- Added methodwrite_byte_internal()for unrestricted internal writes
  • src/gpu/ppu.py- Improved_check_stat_interrupt()to update STAT bit 2 in memory usingwrite_byte_internal()

Tests and Verification

Test Execution: python -m pytest tests/test_ppu_stat.py -v

  • Around:Windows, Python 3.13.5
  • Result:7 PASSED testsin 0.26s
  • What is valid:
    • STAT bit 2 is set correctly when LY == LYC
    • STAT interrupts are requested when LY == LYC and bit 6 is active
    • Rising edge detection works correctly (does not fire multiple times on the same line)
    • STAT interrupts for mode change (H-Blank, V-Blank, OAM Search) work correctly
    • Writing to LYC immediately checks if LY == LYC and requests interrupt if appropriate

Test code (essential fragment):

def test_stat_interrupt_lyc_coincidence(self) -> None:
    """Test: STAT interrupt is requested when LY == LYC and bit 6 is active."""
    mmu = MMU(None)
    ppu = PPU(mmu)
    mmu.set_ppu(ppu)
    
    # Turn on LCD
    mmu.write_byte(IO_LCDC, 0x80)
    
    #Set LYC = 20
    mmu.write_byte(IO_LYC, 20)
    
    # Enable LYC interrupt (STAT bit 6 = 1)
    mmu.write_byte(IO_STAT, 0x40) # Bit 6 active
    
    # Clear IF initially
    mmu.write_byte(IO_IF, 0x00)
    
    # Advance PPU until LY = 20
    ppu.step(20 * 456)
    assert ppu.get_ly() == 20
    
    # Verify that STAT interrupt was requested (IF bit 1)
    if_val = mmu.read_byte(IO_IF)
    assert (if_val & 0x02) != 0, "Bit 1 of IF must be active (STAT interrupt)"

Why this test demonstrates something about the hardware:This test verifies that when LY matches LYC and STAT bit 6 (LYC Int Enable) is active, IF bit 1 (LCD STAT interrupt) is activated. This is exactly hardware behavior: the PPU compares LY with LYC constantly, and when they match and the interrupt is enabled, the STAT interrupt is requested. The test also verifies that STAT bit 2 is updated correctly. (although this is verified indirectly because the interrupt is fired).

Sources consulted

Educational Integrity

What I Understand Now

  • Hybrid STAT log:The STAT register has read-only bits (0-2) that reflect the status PPU current and configurable bits (3-6) that the software can write. This hybrid structure requires Be careful when implementing: the software can only write the configurable bits, but the hardware updates the read-only bits automatically.
  • Memory consistency:Although technically read-only bits can be computed dynamically when read, keeping them up to date in memory helps avoid inconsistencies if some code directly accesses memory (e.g. to avoid recursion).
  • Internal methods:System components (PPU, Timer, etc.) sometimes need to update hardware registers bypassing the restrictions ofwrite_byte(). A methodwrite_byte_internal()allows this in a controlled manner, clearly marked as "for internal use only".

What remains to be confirmed

  • Exact bit 2 update timing:Is STAT bit 2 updated exactly when LY does it change to match LYC, or is there a small delay? For now, it updates immediately when verified in_check_stat_interrupt(), which seems to work correctly according to the tests.
  • Impact on performance:Does updating STAT in memory on every check have any impact in performance? For now, it looks negligible, but could be optimized if necessary.

Hypotheses and Assumptions

It is assumed that updating bit 2 of STAT in memory (in addition to dynamically calculating it inget_stat()) helps maintain consistency without having a negative impact on behavior. This seems to be correct according to the tests, but it is not fully documented in all the sources consulted. The implementation is conservative: it maintains consistency in memory while preserving dynamic computation inget_stat().

Next Steps

  • [ ] Verify behavior with real games (pkmn.gb, tetris_dx.gbc) to confirm that STAT interrupts are fired correctly
  • [ ] Analyze execution logs to identify if there are problems with STAT interrupt timing
  • [ ] Continue with other pending emulator features (APU, rendering improvements, etc.)