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
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:
PPU::step()completerender_scanline()successfullyPPU::step()callcheck_stat_interrupt()check_stat_interrupt()callmmu_->read(IO_STAT)(address0xFF41)MMU::read()detects that it is STAT and needs to callppu_->get_mode(),ppu_->get_ly(), andppu_->get_lyc()to build dynamic value- CRASHwhen trying to call
ppu_->get_mode()- the pointerppu_in MMU points to invalid memory
Problem analysis:
- The pointer
ppu_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_) - When
PPUcallmmu_->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 in
step(),render_scanline(), andcheck_stat_interrupt() - src/core/cpp/MMU.cpp: Logs in
read()andsetPPU() - src/core/cython/ppu.pyx: Reference to
_mmu_wrapperto avoid premature destruction - src/core/cython/mmu.pyx: Logs in
set_ppu() - src/viboy.py: Logs in the call to
ppu.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 pointer
ppu_in MMU it is notNULL(has a value) - ❌ The crash occurs when trying to call
ppu_->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
- Run the emulator with the new logs to see exactly what pointer is being set to
set_ppu() - Check if the pointer
ppu_in MMU is being configured correctly or if there is a problem in the conversion - If the pointer is set correctly but then invalidated, investigate the life cycle of the objects
- Consider using
std::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()