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
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 when
LY == 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:
- When
LY == LYC: STAT is updated in memory with bit 2 active (0x04) and the current mode (bits 0-1). - When
LY != LYC: STAT is updated in memory with bit 2 clear and the current mode (bits 0-1). - It is used
write_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 in
get_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 writessrc/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
- Bread Docs:LCD Status Register (STAT)
- Bread Docs:LYC Register (LY Compare)
- Bread Docs:LCD STAT Interrupt
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 of
write_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.)