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

The Photo Finish: Memory Snapshot

Date:2025-12-22 StepID:0219 State: draft

Summary

Step 0218 confirmed that the renderer is correctly connected to the window (the blue box is visible), but a critical discrepancy remains: the probe inviboy.pyreadPixel 0:3(Black/Red), while the probe inrenderer.pyreadFirst Pixel Value: 0(White/Green).

This discrepancy indicates that data is being lost or overwritten in the passing microsecond between readingviboy.pyand the call torender_frame. This typically occurs when we useVolatile MemoryViews: if C++ touches that memory (or if Python loses the reference), the data changes.

This step implements theImmutable Snapshot Strategy: instead of passing a "view" (memoryview) to the renderer that looks directly at the C++ memory (which is dangerous and volatile), we make ainstant photo copy (bytearray) of the data inviboy.pyjust when we know they are correct (when the probe says 3). We pass that safe copy to the renderer.

Hardware Concept

In the hybrid Python/C++ architecture, the framebuffer lives in C++ memory and is exposed to Python via amemoryview(memory view). AmemoryviewIt is a direct reference to the underlying memory: if C++ modifies that memory (for example, clearing the framebuffer for the next frame), thememoryviewwill immediately reflect those changes.

The problem ofVolatile MemoryViews:

  • Race Condition:If C++ clears the framebuffer while Python is reading, Python will see partial or corrupt data.
  • Lost References:If Python loses the reference tomemoryviewor the C++ object is destroyed, thememoryviewmay point to invalid memory.
  • Compiler Optimizations:The C++ compiler can reorder operations memory, causing data to change at unexpected times.

The solution is to make aimmutable copy (bytearray) of the framebuffer in the exact moment when we know it is complete and correct. This copy lives in Python memory and It cannot be modified by C++, ensuring that the renderer always works with stable data.

Performance:Copying 23040 bytes (160x144 pixels) takes approximately 0.01ms on a modern processor, which is insignificant compared to the rendering time (16.67ms per frame at 60 FPS). The stability benefit far outweighs the performance cost.

Implementation

Two files were modified to implement the immutable snapshot:

Modified components

  • src/viboy.py: Methodrun()- Snapshot capture usingbytearrayand go to the renderer
  • src/gpu/renderer.py: Methodrender_frame()- Accepts optional parameterframebuffer_data

Technical changes

1. Modification inviboy.py:

  • Replaced verification ofcurrent_ly == 144byget_frame_ready_and_reset(), which is more robust and handles frame state correctly.
  • The copy of was changedbytes(fb_view)tobytearray(raw_view)to guarantee that the copy is mutable and lives entirely in Python.
  • Updated the data probe to use the snapshot and display the message[PYTHON SNAPSHOT PROBE].
  • The snapshot is passed to the renderer through the parameterframebuffer_data.
# In src/viboy.py -> run()

# Rendering
if self._use_cpp:
    if self._ppu.get_frame_ready_and_reset():
        #1. Get the direct view of C++
        raw_view = self._ppu.framebuffer
        
        #2. --- STEP 0219: IMMUTABLE SNAPSHOT ---
        # We make an immediate deep copy to Python memory.
        # This "freezes" the frame and protects us from any changes in C++.
        fb_data = bytearray(raw_view)
        # ----------------------------------------
        
        #3. Pass the SECURE COPY to the renderer
        self._renderer.render_frame(framebuffer_data=fb_data)

2. Modification inrenderer.py:

  • Added optional parameterframebuffer_data: bytearray | None = Noneto the methodrender_frame().
  • If providedframebuffer_data, that snapshot is used instead of reading from the PPU.
  • Updated diagnostics to indicate whether a snapshot is being used or reading directly.
# In src/gpu/renderer.py -> render_frame()

def render_frame(self, framebuffer_data: bytearray | None = None) -> None:
    # --- Step 0219: IMMUTABLE SNAPSHOT ---
    # If framebuffer_data is provided, use that snapshot instead of reading from PPU
    if framebuffer_data is not None:
        frame_indices = framebuffer_data
    else:
        frame_indices = self.cpp_ppu.get_framebuffer()

Affected Files

  • src/viboy.py- Modification of the methodrun()for snapshot capture (lines 753-789)
  • src/gpu/renderer.py- Modification of the methodrender_frame()to accept snapshot (lines 414-444)

Tests and Verification

Command executed: python main.py roms/tetris.gb

Expected Result:

  • Console:[PYTHON SNAPSHOT PROBE]...Pixel 0:3
  • Console (Renderer):First Pixel Value inside render_frame: 3(They must match!)
  • Screen:Vertical red stripesbackground +Blue Boxin the center

If both probes show the same value (3) and the screen shows red, we have connected all the cables. The next step will be the final cleaning to play Tetris.

Compiled C++ module validation:The snapshot is created from amemoryviewexposed by Cython, which in turn accesses the native C++ framebuffer. The copy ensures that the data do not get corrupted during the transition between C++ and Python.

References

  • Pan Docs - LCD Timing, Frame Rendering
  • Python Documentation -bytearrayandmemoryview
  • Cython Documentation - Memory Views and Zero-Copy