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

Debug: Full Traceback of Segmentation Fault in PPU↔MMU Circular Reference

Date:2025-12-19 StepID:0143 State: 🔍 IN DEBUG

Summary

After solving the null pointer problem in the constructorPyPPU(Step 0142), theSegmentation Faultpersisted but now occurs at a different point: withincheck_stat_interrupt()when trying to read the STAT register (0xFF41) from the MMU, which in turn attempts to callppu_->get_mode()to construct the dynamic value of STAT. This is a problem ofcircular referencebetween PPU and MMU.

Hardware Concept

On the real Game Boy, the STAT register (0xFF41) has read-only bits (0-2) that are dynamically updated by the PPU. These bits represent:

  • Bits 0-1:Current PPU mode (Mode 0: H-Blank, Mode 1: V-Blank, Mode 2: OAM Search, Mode 3: Pixel Transfer)
  • Bit 2:LYC=LY Coincidence Flag (1 if LY == LYC, 0 otherwise)

When the CPU or any component tries to read STAT, it must obtain the updated value of these bits from the internal state of the PPU, not from static memory.

In our emulator, this creates acircular reference:

  • PPU has a pointer to MMU (MMU* mmu_) to read/write memory
  • MMU has a pointer to PPU (PPU* ppu_) to read the dynamic state from STAT

When PPU callsmmu_->read(IO_STAT), the MMU needs to call backppu_->get_mode()to construct the correct value. If the pointerppu_in MMU points to invalid memory or an object that has already been destroyed, this causes aSegmentation Fault.

Identified Problem

The crash occurs in the following call chain:

  1. PPU::step()completerender_scanline()successfully
  2. PPU::step()callcheck_stat_interrupt()
  3. check_stat_interrupt()callmmu_->read(IO_STAT)(address0xFF41)
  4. MMU::read()detects that it is STAT and needs to callppu_->get_mode(), ppu_->get_ly(), andppu_->get_lyc()to build dynamic value
  5. CRASHwhen trying to callppu_->get_mode()- the pointerppu_in MMU points to invalid memory

Problem analysis:

  • The pointerppu_in MMU it is notNULL(has a value like00000000222F0040), but points to invalid memory or an object that has already been destroyed
  • The problem is acircular reference: PPU has a pointer to MMU (mmu_), and MMU has a pointer to PPU (ppu_)
  • WhenPPUcallmmu_->read(), theMMUtry calling backppu_->get_mode(), but the pointerppu_in MMU it may be pointing to an object that was already destroyed or moved

Debugging Implementation

Extensive logging was added at multiple points in the code to track exactly where the crash occurs and what values ​​the pointers have at any given time.

Modified components

  • src/core/cpp/PPU.cpp: Logs instep(), render_scanline(), andcheck_stat_interrupt()
  • src/core/cpp/MMU.cpp: Logs inread()andsetPPU()
  • src/core/cython/ppu.pyx: Reference to_mmu_wrapperto avoid premature destruction
  • src/core/cython/mmu.pyx: Logs inset_ppu()
  • src/viboy.py: Logs in the call toppu.step()

Logs added

1. InPPU::step():

  • [PPU::step] Starting step() with X cycles- At the beginning of the method
  • [PPU::step] render_scanline() returned, continuing...- After render_scanline()
  • [PPU::step] LY incremented to X- After increasing LY
  • [PPU::step] LY or mode changed, calling check_stat_interrupt()...- Before calling check_stat_interrupt()
  • [PPU::step] step() completed, returning to Python- At the end of the method

2. InPPU::render_scanline():

  • [PPU::render_scanline] Starting X line rendering- At the beginning
  • [PPU::render_scanline] Loop completed, returning...- In the end

3. InPPU::check_stat_interrupt():

  • [PPU::check_stat_interrupt] Starting...- At the beginning
  • [PPU::check_stat_interrupt] mmu_ pointer: 0x...- mmu_ pointer value
  • [PPU::check_stat_interrupt] Calling mmu_->read(IO_STAT)...- Before reading STAT

4. InMMU::read():

  • [MMU::read] Starting, addr=0xFF41- At the beginning
  • [MMU::read] Reading STAT (0xFF41)...- STAT detection
  • [MMU::read] ppu_ pointer: 0x...- ppu_ pointer value
  • [MMU::read] Calling ppu_->get_mode()...- Before calling get_mode()

5. InPyMMU::set_ppu()andMMU::setPPU():

  • [PyMMU::set_ppu] ptr_int obtained: X (0x...)- Pointer obtained from get_cpp_ptr_as_int()
  • [PyMMU::set_ppu] converted c_ppu: X (0x...)- Converted pointer
  • [MMU::setPPU] Called with pointer: 0x...- Pointer received in setPPU()
  • [MMU::setPPU] ppu_ set to: 0x...- Stored pointer

Improved memory management

Added a reference to the objectPyMMUinPyPPUto prevent the MMU object from being destroyed while PPU is using it:

cdef class PyPPU:
    cdef ppu.PPU* _ppu
    cdef object _mmu_wrapper # CRITICAL: Keep reference to the wrapper to avoid destruction
    
    def __cinit__(self, PyMMU mmu_wrapper):
        #...
        self._mmu_wrapper = mmu_wrapper # Keep reference

Debugging Results

The logs show that:

  • render_scanline()successfully complete
  • check_stat_interrupt()is called correctly
  • mmu_->read(IO_STAT)is called correctly
  • ✅ The pointerppu_in MMU it is notNULL(has a value)
  • ❌ The crash occurs when trying to callppu_->get_mode()

This indicates that the pointerppu_in MMU points to invalid memory or an object that has already been destroyed, even if it is notNULL.

Next Steps

  1. Run the emulator with the new logs to see exactly what pointer is being set toset_ppu()
  2. Check if the pointerppu_in MMU is being configured correctly or if there is a problem in the conversion
  3. If the pointer is set correctly but then invalidated, investigate the life cycle of the objects
  4. Consider usingstd::shared_ptreitherstd::weak_ptrto handle circular reference safely

Modified Files

  • src/core/cpp/PPU.cpp- Extensive logs instep(), render_scanline(), andcheck_stat_interrupt()
  • src/core/cpp/MMU.cpp- Logs inread()andsetPPU()
  • src/core/cython/ppu.pyx- Reference to_mmu_wrapperTo avoid premature destruction, logs instep()
  • src/core/cython/mmu.pyx- Logs inset_ppu()
  • src/viboy.py- Logs in the call toppu.step()

References