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

Fix: Access violation crash due to Infinite Recursion in STAT

Date:2025-12-19 StepID:0128 State: Verified

Summary

This step fixes a critical bugstack overflowcaused by an infinite recursion betweenMMU::read(0xFF41)andPPU::get_stat(). The problem occurred when the CPU tried to read the STAT register (0xFF41): the MMU calledPPU::get_stat(), which in turn was trying to read STAT from the MMU, creating an infinite loop that consumed all the stack memory in milliseconds and caused a crashaccess violation.

The solution implements an architectural redesign: theMMU is the owner of memoryand construct the value of STAT directly, querying the PPU only for its state (mode, LY, LYC) without creating circular dependencies.

Hardware Concept

The recordSTAT (LCD Status, 0xFF41)on the Game Boy it is a hybrid register with read-only bits and writable bits:

  • Bits 0-1 (read only):Current PPU mode (0=H-Blank, 1=V-Blank, 2=OAM Search, 3=Pixel Transfer). Dynamically updated by the PPU.
  • Bit 2 (read only):LYC=LY Coincidence Flag. Triggered when LY == LYC.
  • Bits 3-6 (read/write):Software-configurable interruption flags.
  • Bit 7 (read only):Always 1 according to Pan Docs.

The architectural problem:In the initial implementation, the PPU had a methodget_stat()which was trying to construct the full value of STAT reading the writable bits from the MMU and combining them with its internal state. However, when the MMU read STAT, it calledPPU::get_stat(), which in turn calledMMU::read(0xFF41), creating an infinite recursion.

The correct solution:The MMU is the owner of the address space and should be responsible for constructing the STAT value when it is read. The PPU should only provide its current state (mode, LY, LYC) using read-only methods, without attempting to read memory.

Fountain:Pan Docs - LCD Status Register (STAT), section on read-only and write-only bits.

Implementation

Method removedPPU::get_stat()and was redesignedMMU::read(0xFF41)to build STAT directly:

Modified components

  • PPU.hpp / PPU.cpp:Removed methodget_stat(). The PPU now only exposes read-only methods:get_mode(), get_ly(), get_lyc().
  • MMU.cpp:ModifiedMMU::read(0xFF41)to build STAT by combining:
    • Writable bits (3-7) frommemory_[0xFF41]
    • Current mode sinceppu_->get_mode()
    • LYC=LY Coincidence from comparisonppu_->get_ly()andppu_->get_lyc()
    • Bit 7 always at 1
  • ppu.pxd:Deleted declaration ofget_stat()of the Cython wrapper.

Design decisions

Architecture of responsibilities:The MMU is solely responsible for constructing register values ​​that combine read-only and write-only bits. Peripheral components (PPU, APU, etc.) only provide their internal state using read-only methods, without attempting to read memory.

Avoid circular dependencies:This pattern avoids circular dependencies between MMU and peripheral components. The MMU can query the status of components, but components never read memory through the MMU during register read operations.

Performance:The construction of STAT inMMU::read()is O(1) and does not introduce significant overhead, since it is only executed when the STAT register is read (infrequent operation compared to the main emulation loop).

Affected Files

  • src/core/cpp/PPU.hpp- Removed methodget_stat()
  • src/core/cpp/PPU.cpp- Removed implementation ofget_stat()
  • src/core/cpp/MMU.cpp- Redesignedread(0xFF41)to build STAT directly
  • src/core/cython/ppu.pxd- Removed declarationget_stat()
  • tests/test_core_ppu_modes.py- Tests already usedmmu.read(0xFF41)correctly

Tests and Verification

The existing tests already validate the correct reading of STAT from the MMU:

  • Test: test_ppu_stat_register()- Verify that STAT includes the current mode in bits 0-1
  • Test: test_ppu_stat_lyc_coincidence()- Verify that bit 2 (LYC=LY) is updated correctly

Verification command:

pytest tests/test_core_ppu_modes.py -v

Expected result:All tests pass without crashesaccess violation.

Native validation:The tests validate the compiled C++ module through the Cython wrapper, confirming that the infinite recursion has been removed and that STAT is read correctly.

Key Test Code

def test_ppu_stat_register(self):
    """Verify that the STAT register is read correctly with PPU modes."""
    mmu = PyMMU()
    ppu = PyPPU(mmu)
    mmu.set_ppu(ppu) # Connect PPU to MMU
    
    mmu.write(0xFF40, 0x91) # LCD ON
    mmu.write(0xFF41, 0x78) # Write configurable bits
    
    # Read STAT - must include the current mode in bits 0-1
    stat = mmu.read(0xFF41) # ← This line no longer causes infinite recursion
    
    mode_from_stat = stat & 0x03
    assert mode_from_stat == ppu.mode

Sources consulted

Educational Integrity

What I Understand Now

  • Circular dependencies in emulation:When a peripheral component attempts to read memory through the MMU during a register read operation, it can create circular dependencies if the MMU needs to query that same component to construct the register value.
  • Architecture of responsibilities:The MMU should be solely responsible for constructing hybrid register values ​​(with read-only and write-only bits). Peripheral components should only provide their internal state using read-only methods.
  • Stack overflow in C++:An infinite recursion consumes all the stack memory quickly, causing a crashaccess violationon Windows orsegmentation faulton Linux.

What remains to be confirmed

  • STAT build performance:Verify that the STAT build inMMU::read()It does not introduce significant overhead into the main emulation loop.
  • Other hybrid records:Identify if there are other registers on the Game Boy with read-only and write-only bits that require a similar pattern.

Hypotheses and Assumptions

Validated assumption:The construction of STAT inMMU::read()It is fast enough not to affect performance, since reading STAT is an infrequent operation compared to the main emulation loop (each CPU instruction).

Next Steps

  • [ ] Recompile the C++ module and verify that the tests pass without crashes
  • [ ] Run the emulator with a test ROM to verify that the white screen is resolved
  • [ ] Implement Native CPU: Jumps and Flow Control (Step 0129)
  • [ ] Verify that there are no other hybrid records that require the same pattern