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

Bridge Inspection: Does data reach Python?

Date:2025-12-22 StepID:0213 State: VERIFIED

Summary

Even though the PPU in C++ reports correct operations and we force writing black pixels (Step 0212), the screen remains white. This suggests that the data is not correctly crossing the Cython bridge to Python, or is being misinterpreted by the Python renderer. We implemented probes in both C++ and Python to trace the framebuffer at each point in the pipeline.

Critical finding:The problem is NOT in the Cython bridge, but in thetime synchronization. Python was reading the framebufferafterthat C++ will clean it up for the next frame. The solution was to read the framebuffer just when the frame is completed (whenly_goes to 144, V-Blank start) and make a copy to preserve the data.

Hardware Concept: The Data Bridge

In a hybrid Python/C++ architecture, the framebuffer data flow follows this path:

  1. C++ (PPU.cpp):Write color indices (0-3) to an arrayuint8_t[23040].
  2. Cython (ppu.pyx):Exposes the array as amemoryviewfrom Python usingget_framebuffer_ptr().
  3. Python (viboy.py):Read thememoryviewand passes it to the renderer.
  4. Python (renderer.py):Convert color indices to RGB using the BGP palette and draw in Pygame.

The problem of the "perfect crime":We have evidence that:

  • C++ confesses: The probeVALID CHECK: PASS(Step 0211) confirms that the PPU's internal logic is working and the addresses are valid.
  • Visual evidence: The screen isWHITE.
  • The deduction: If C++ is writing3(black) in the framebuffer (as we confirmed with Step 0212), but Pygame draws0(white), thendata is being lost or corrupted in the bridge between C++ and Python.

The solution: Question the messenger.We're going to inspect the data right when it arrives in Python, before the renderer touches it. If Python says "I received a 3", then the problem is inrenderer.py(the palette or the drawing). If Python says "I received a 0", then the problem is inCython(we are reading the wrong memory or an empty copy).

Fountain:Hybrid system debugging methodology - "Data Probe" debugging in C++/Python interfaces.

Implementation

Diagnostic probes were implemented at multiple points in the pipeline to trace the framebuffer from C++ to Python. The probes revealed that the problem was one of time synchronization, not data integrity.

Probes in C++ (PPU.cpp)

Three C++ probes were added to check the framebuffer at different times:

  1. [C++ WRITE PROBE]: Right after writing to the framebuffer (line 0, pixel 0). Confirms that the value is written correctly.
  2. [C++ BEFORE CLEAR PROBE]: Just before clearing the framebuffer (whenly_ > 153). Verifies that the framebuffer contains the correct data before clearing itself.
  3. [C++ AFTER CLEAR PROBE]: Right after clearing the framebuffer. Confirm that cleaning is working correctly.

Modification insrc/viboy.py

Changed the main loop to read the framebuffer at the correct time (whenly_ == 144, V-Blank startup) and make a copy to preserve the data:

# --- Step 0213: Read framebuffer right after line 143 ---
# Read the framebuffer right after completing line 143
# (last visible line), before advancing to line 144
# and before it is cleared for the next frame.
if self._ppu is not None:
    current_ly = self._ppu.ly
    if current_ly == 144: # V-Blank start, frame complete
        # CRITICAL: Make a COPY of the framebuffer because the memoryview
        # is a view of memory. If the framebuffer is cleared afterwards,
        # the view will reflect the clean values. We need to preserve
        # the data of the entire frame.
        fb_view = self._ppu.framebuffer
        framebuffer_to_render = bytes(fb_view) # Copy the data
        
        # Diagnostic probe
        if not self._debug_frame_printed:
            p0 = framebuffer_to_render[0]
            p8 = framebuffer_to_render[8]
            mid = framebuffer_to_render[23040 // 2]
            print(f"\n--- [PYTHON DATA PROBE] ---")
            print(f"Full frame (LY=144), framebuffer read (COPY):")
            print(f"Pixel 0 (0,0): {p0} (Expected: 3)")
            print(f"Pixel 8 (8,0): {p8}")
            print(f"Pixel Center: {mid}")
            print(f"---------------------------\n")
            self._debug_frame_printed = True

Probes Results

The probes revealed the exact problem:

  • [C++ WRITE PROBE]: Written value: 3, Read value: 3 ✅
  • [C++ BEFORE CLEAR PROBE]: Pixel 0: 3, Pixel 8: 3, Pixel Center: 3 ✅
  • [C++ AFTER CLEAR PROBE]: Pixel 0: 0 ✅ (correct cleaning)
  • [PYTHON DATA PROBE](before fix): Pixel 0: 0 ❌ (read after cleaning)

Conclusion:The framebuffer was being cleared before Python read it. The solution was to read the framebuffer whenly_ == 144(V-Blank startup) and make a copy to preserve the data.

Probe Logic

The probe reads three strategic pixels from the framebuffer:

  • Pixel 0 (0.0):The first pixel on the screen. If Step 0212 is active, it should be3(Black).
  • Pixel 8 (8.0):The first pixel of the second tile. If the stripe pattern of Step 0212 is active, it should be0(White) or3(Black) depending on the pattern.
  • Pixel Center:A pixel in the center of the screen (index 11520) to verify that the data is present on the entire screen.

Analysis of the result:

  • If you seePixel 0:3:The data arrives well! The culprit issrc/gpu/renderer.py(probably the palette or the Pygame surface conversion).
  • If you seePixel 0:0:Houston, we have a problem in Cython! We are reading an empty buffer or disconnected from the real one.

Affected Files

  • src/core/cpp/PPU.cpp- Added three diagnostic probes to trace the framebuffer in C++ (write, before clean, after clean)
  • src/viboy.py- Modified the main loop to read the framebuffer whenly_ == 144(V-Blank startup) and make a copy to preserve data

Tests and Verification

Recompile required:This change requires recompiling the C++ module because we added probes inPPU.cpp.

Recompiling C++ module:

python setup.py build_ext --inplace
# Or using the PowerShell script:
.\rebuild_cpp.ps1

Running the emulator:

python main.py roms/tetris.gb

Observed result:The probes showed:

--- [C++ WRITE PROBE] ---
After writing to framebuffer[0]:
Written value: 3
Read value: 3
Framebuffer ptr: 00000170F46C9E00
Framebuffer size: 23040
---------------------------

--- [C++ BEFORE CLEAR PROBE] ---
Just BEFORE clearing framebuffer (ly_ > 153):
Pixel 0 (0.0): 3 (Expected: 3)
Pixel 8 (8.0): 3
Pixel Center (11520): 3
Framebuffer ptr: 00000170F46C9E00
---------------------------

--- [C++ AFTER CLEAR PROBE] ---
Right AFTER clearing framebuffer:
Pixel 0 (0.0): 0 (Expected: 0)
---------------------------

--- [PYTHON DATA PROBE] ---
Full frame (LY=144), framebuffer read (COPY):
Pixel 0 (0.0): 3 (Expected: 3) ✅
Pixel 8 (8.0): 3
Pixel Center: 3
---------------------------

Interpretation:

  • Identified problem:Python was reading the framebufferafterthat C++ will clean it up for the next frame. Hememoryviewis a memory view, so it reflects the current values ​​of the framebuffer, not the values ​​of the previous frame.
  • Implemented solution:Read the framebuffer whenly_ == 144(V-Blank start, full frame) and make a copy usingbytes(fb_view)to preserve data before it is wiped.
  • Result:Python probe now showsPixel 0:3, confirming that data is read correctly when captured at the right time.

Compiled C++ module validation:The C++ module was successfully recompiled with the diagnostic probes. The probes confirm that:

  • C++ correctly writes to the framebuffer (value 3).
  • The framebuffer keeps the correct data until before it is cleared.
  • Cleaning works correctly (value 0 after cleaning).
  • Python can read the right data when it is captured at the right time.

Sources consulted

  • Hybrid system debugging methodology: "Data Probe" debugging in C++/Python interfaces
  • Cython Documentation: Memory Views and Zero-Copy Arrays
  • Pan Docs: "PPU Framebuffer" - Pixel Buffer Structure

Educational Integrity

What I Understand Now

  • The data bridge:In a hybrid architecture, data must cross multiple layers (C++ → Cython → Python). Each layer can introduce errors or data loss.
  • Debugging by inspection:Sometimes the best way to find a problem is to inspect the data at each point in the pipeline. This probe allows us to see exactly what Python is receiving.
  • Zero-Copy vs. Copy:The use ofmemoryviewCython allows Zero-Copy access to C++ memory, but if the pointer is configured incorrectly, we may be reading incorrect memory.

What we confirm

  • The problem is NOT in Cython:The C++ → Python bridge works correctly. HememoryviewIt points to the correct memory and the data is transferred without problems.
  • The problem is time synchronization:Python was reading the framebuffer after C++ cleared it for the next frame. HememoryviewIt is a view of current memory, not a historical copy.
  • The solution works:When reading the framebuffer whenly_ == 144(V-Blank startup) and make a copy, Python can access the entire frame's data before it is cleaned.

Lessons Learned

  • Memoryview is a view, not a copy:Amemoryviewin Python/Cython it is a view of the current memory. If the memory changes after the view is created, the view will reflect the changes. To preserve data, we need to make an explicit copy.
  • Synchronization in hybrid architectures:In hybrid Python/C++ systems, it is crucial to understand the exact moment when data is read and written. A small time lag can cause incorrect data to be read.
  • Multiple probe debugging:Adding probes at multiple points in the pipeline (C++ before/after writing, before/after cleaning, Python on reading) allowed us to identify exactly where data was being lost.

Next Steps

  • [x] Identify the time synchronization problem
  • [x] Implement reading the framebuffer at the correct time (LY=144)
  • [x] Make a copy of the framebuffer to preserve data
  • [ ] Modify the renderer to use the copied framebuffer (if necessary)
  • [ ] Verify that the screen displays black pixels correctly
  • [ ] Restore normal rendering logic (without forcing black pixels)