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
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 to
memoryviewor 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 renderersrc/gpu/renderer.py: Methodrender_frame()- Accepts optional parameterframebuffer_data
Technical changes
1. Modification inviboy.py:
- Replaced verification of
current_ly == 144byget_frame_ready_and_reset(), which is more robust and handles frame state correctly. - The copy of was changed
bytes(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 parameter
framebuffer_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 parameter
framebuffer_data: bytearray | None = Noneto the methodrender_frame(). - If provided
framebuffer_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