This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Minimal and Verifiable Fix for IE (0xFFFF)
Summary
Implementation of microscopic instrumentation to diagnose the IE persistence/read bug (0xFFFF) identified in Step 0470. In Step 0470 we observed IEWrite>0 and IE read=0 sustained → IE persistence/read bug. Added instrumentation to MMU::write() and MMU::read() to track IE writes and reads, including PC, timestamp, and values. Test clean-room was created to verify immediate readback. ✅ Basic test passes confirming that write/read works in simple case.
Hardware Concept
The IE register (0xFFFF - Interrupt Enable) is a single, persistent register on the Game Boy. It has NO special cases, NO gating by DMG/CGB modes, and should NOT be automatically overwritten. When the game writes to 0xFFFF, the value MUST persist in memory_[0xFFFF] until the game writes a new value.
Correct Framing of the Problem: In Step 0470 we observed IEWrite>0 and IE read=0 sustained → bug in IE persistence/reading. This does NOT mean "games don't enable IE" (that was already passed in Step 0470). The problem is that writes DO occur (IEWrite>0) but IE is always read as 0x00, indicating that:
- (A) memory_[0xFFFF] is not written: MMU::write(0xFFFF, v) does not persist (does not save in memory_[0xFFFF])
- (B) MMU::read(0xFFFF) returns 0 by error: Wrong mapping / CGB branch / "uninitialized"
- (C) Something steps on IE=0 repeatedly: Some reset/boot/init writes memory_[0xFFFF] = 0x00 after each game write
Fundamental rule: 0xFFFF must be a single record (IE), persistent, without mode gating. There are no special cases that intercept or modify writes to 0xFFFF after they are written.
Fountain: Pan Docs - Interrupt Enable Register (IE), Memory Map
Implementation
Microscopic instrumentation was implemented to track IE writes and reads, allowing you to identify exactly where value is lost (write vs read).
Phase A - Microscopic Instrumentation (Gated)
Added static variables and instrumentation to MMU::write() and MMU::read() to track IE writes and reads:
- MMU.cpp: Additional static variables:
last_ie_write_pc(uint16_t): PC of last write to IElast_ie_write_timestamp(uint32_t): Timestamp of the last write (write counter)last_ie_read_value(uint8_t): Last value read from IEie_read_count(uint32_t): IE read counter
- MMU::write(): When addr == 0xFFFF, last_ie_write_pc, last_ie_write_timestamp are saved
- MMU::read(): When addr == 0xFFFF, last_ie_read_value is saved, ie_read_count is incremented, and if VIBOY_DEBUG_PPU=1 and there are writes but the read value is 0x00, [IE-DROP] is logged with write/read information
- MMU.hpp: Public getters get_last_ie_write_value(), get_last_ie_write_pc(), get_last_ie_read_value(), get_ie_read_count()
- Cython (.pxd/.pyx):Python wrappers for new getters
Phase B - Clean-Room Test "Immediate Readback"
was createdtests/test_ie_write_persists_0471.pyTo verify that writes to IE persist correctly:
- test_ie_write_readback_immediate_dmg: Case 1 (DMG path) - Write IE = 0x1F, read IE → should be 0x1F, write IE = 0x00, read IE → should be 0x00. Verify counters and values.
- test_ie_write_readback_immediate_cgb: Case 2 (CGB path) - Similar to Case 1 but forcing CGB mode. Verify that writes persist in CGB as well.
- test_ie_write_readback_multiple_cycles: Verifies that IE persists across multiple writes/reads (0x01, 0x03, 0x07, 0x0F, 0x1F, 0x00).
Phase C - Code Verification
It was verified thatmemory_[addr] = value;is executed after all special cases (line 2658 in MMU.cpp). The basic test passes, confirming that write/read works in the simple case. NO obvious bug was identified in the code; The problem could be in running real ROMs or in some component that overwrites IE after the game writes.
Phase D – rom_smoke update
It was updatedtools/rom_smoke_0442.pyto include the new metrics in snapshots:
- last_ie_write_value (IEWriteVal)
- last_ie_read_value (IEReadVal)
- ie_read_count (IEReadCount)
- last_ie_write_pc (IEWritePC)
Affected Files
src/core/cpp/MMU.cpp- Additional static variables and instrumentation in write()/read()src/core/cpp/MMU.hpp- Getters for IE microscopic instrumentationsrc/core/cpp/MMU.cpp- Implementation of getterssrc/core/cython/mmu.pxd- Cython declarations for new getterssrc/core/cython/mmu.pyx- Python wrappers for new getterstests/test_ie_write_persists_0471.py- Clean-room test for immediate readbacktools/rom_smoke_0442.py- Snapshot with new IE metrics
Tests and Verification
Compiled C++ module validation:
- Unit tests:
pytest tests/test_ie_write_persists_0471.py- 3 tests passing - Test 1: test_ie_write_readback_immediate_dmg - Check immediate readback in DMG mode
- Test 2: test_ie_write_readback_immediate_cgb - Check immediate readback in CGB mode
- Test 3: test_ie_write_readback_multiple_cycles - Check persistence across multiple cycles
Test Code:
def test_ie_write_readback_immediate_dmg(self):
"""Test Case 1 (DMG path): Immediate readback after write."""
# Case 1a: Write 0x1F and check immediate readback
self.mmu.write(0xFFFF, 0x1F)
ie_read_1 = self.mmu.read(0xFFFF)
assert ie_read_1 == 0x1F, \
f"IE write does not persist (Case 1a): wrote 0x1F, read 0x{ie_read_1:02X}"
# Check last value written
last_ie_write_value = self.mmu.get_last_ie_write_value()
assert last_ie_write_value == 0x1F, \
f"Last value written to IE incorrect: expected 0x1F, obtained 0x{last_ie_write_value:02X}"
# Case 1b: Write 0x00 and check immediate readback
self.mmu.write(0xFFFF, 0x00)
ie_read_2 = self.mmu.read(0xFFFF)
assert ie_read_2 == 0x00, \
f"IE write does not persist (Case 1b): wrote 0x00, read 0x{ie_read_2:02X}"
Result: All tests pass, confirming that write/read works correctly in simple case.
Sources consulted
- Bread Docs:Interrupts, Interrupt Enable Register (IE)
- Bread Docs:Memory Map
Educational Integrity
What I Understand Now
- IE (0xFFFF) is a persistent record: It has no special cases, it does not have mode gating. When written, the value must persist until the next write.
- Microscopic instrumentation: To diagnose persistence bugs, it is crucial to track both writes and reads, including PC, timestamp, and values.
- Clean-room test: Basic immediate readback tests are essential to verify that the basic implementation works before investigating problems in real ROMs.
What remains to be confirmed
- Running rom_smoke: Although the basic test passes, rom_smoke needs to be run with real ROMs to verify if the bug persists in real execution.
- Log analysis [IE-DROP]: If [IE-DROP] appears in rom_smoke logs, it indicates that there are writes but the value read is 0x00, which would help identify the exact problem.
Working Hypothesis
- The basic test passes, suggesting that the basic write/read code works correctly.
- The problem could be in some component that overwrites IE after game writes, or in some special case that is not activated in the basic tests.
- The added instrumentation will allow you to identify exactly where value is lost when running real ROMs.
Next Steps
- ✅ Microscopic instrumentation implemented
- ✅ Clean-room test created and passed
- ✅ rom_smoke updated with new metrics
- ⏳ Run rom_smoke with real ROMs (tetris_dx.gbc, mario.gbc) to check if the bug persists
- ⏳ Analyze logs to detect [IE-DROP] and identify where value is lost
- ⏳ Apply minimal fix based on rom_smoke findings (if necessary)