This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Close Framebuffer=0 with Short and Conclusive Test
Summary
Definitive diagnosis of the framebuffer problem at 0 through a pre/post reset experiment. Method addedis_frame_ready()which only verifies without resetting, the test was modified to read framebuffer BEFORE and AFTERget_frame_ready_and_reset(), and 4 key pieces of evidence were collected. Result: ✅ Cause identified -get_framebuffer_indices()reads the clean front buffer BEFORE the swap. The back framebuffer has the correct data (nz_post=17280), but the front is clean until the swap is done. Solution: Add auto present toget_framebuffer_indices_ptr()(similar toget_framebuffer_ptr()).
Hardware Concept
Observed problem: After the fix of 0466 (read_vram with absolute addr), the framebuffer is still all at 0. The plan identified 3 suspects:
- Semantics of
get_frame_ready_and_reset()/buffer swap: The reset/swap leaves the wrong or cleaned buffer reading - The "ready" frame is not being real:
run_one_frame()waitget_frame_ready_and_reset()but it never arrives - BG does run but it is writing 0: PPU decodes wrong bitplanes or reads wrong bytes
Double Buffering in PPU: The PPU uses double buffering to avoid race conditions:
framebuffer_front_: Buffer that Python reads (public, stable, not modified during rendering)framebuffer_back_: Buffer where C++ writes (private, modified during rendering)
When a frame is completed (LY=144),get_frame_ready_and_reset()callswap_framebuffers()which swaps the buffers and cleans the back (which is now the old front).
Automatic Present: get_framebuffer_ptr()does "automatic present" - if there is a swap pending (framebuffer_swap_pending_), does the swap before returning the pointer. This ensures that Python always sees the latest content. However,get_framebuffer_indices_ptr()It does NOT do automatic present, so it can return the clean front if called before the swap.
Reference: Pan Docs - PPU Modes, Frame Timing. Step 0364 - Double Buffering. Step 0428 - Automatic Present.
Implementation
The diagnosis was implemented in four phases according to the plan:
Phase A: Methodis_frame_ready()(Only Verify, No Reset)
Method addedis_frame_ready()in C++ and Cython that only checks the status offrame_ready_without resetting it:
// In PPU.hpp:
bool is_frame_ready() const; // Only verifies, does not reset
// In PPU.cpp:
bool PPU::is_frame_ready() const {
return frame_ready_;
}
// In ppu.pyx:
def is_frame_ready(self):
"""Check if there is a ready frame without resetting."""
if self._ppu == NULL:
return False
return self._ppu.is_frame_ready()
This allows the framebuffer to be read BEFORE callingget_frame_ready_and_reset().
Phase B: Written BG Pixel Counter (Already Existed)
The accountantbg_pixels_written_count_already existed withVIBOY_DEBUG_PPUand is exposed to Python usingget_bg_render_stats(). It was used in the test to verify that BG is indeed writing pixels.
Phase C: Last Bytes Read Debug Variables (Already Existed)
The variableslast_tile_bytes_read_andlast_tile_addr_read_they already existed withVIBOY_DEBUG_PPUand are exposed to Python usingget_last_tile_bytes_read_info(). They were used in the test to verify which bytes the PPU is reading.
Phase D: Pre/Post Reset Experiment
The test was modifiedtest_tilemap_base_select_9800()for:
- Step until
is_frame_ready()returntrue(without reset) - Read framebuffer BEFORE reset (
buf_pre) - call
get_frame_ready_and_reset()(makes swap) - Read framebuffer AFTER reset (
buf_post) - Collect 4 evidence:
nz_pre,nz_post,bg_pixels_written,last_tile_bytes
# Step until frame is ready (without reset)
while not self.ppu.is_frame_ready() and cycles_accumulated< max_cycles:
cycles = self.cpu.step()
cycles_accumulated += cycles
self.timer.step(cycles)
self.ppu.step(cycles)
# Leer ANTES de reset
buf_pre = self.ppu.get_framebuffer_indices()
nz_pre = sum(1 for i in range(160 * 144) if (buf_pre[i] & 0x03) != 0)
# Ahora sí resetear
ready = self.ppu.get_frame_ready_and_reset()
# Leer DESPUÉS de reset
buf_post = self.ppu.get_framebuffer_indices()
nz_post = sum(1 for i in range(160 * 144) if (buf_post[i] & 0x03) != 0)
Affected Files
src/core/cpp/PPU.hpp- Added methodis_frame_ready() constsrc/core/cpp/PPU.cpp- Implementedis_frame_ready()and fixed compilation error (tile_map_offsetundeclared)src/core/cython/ppu.pxd- Added declarationis_frame_ready() constsrc/core/cython/ppu.pyx- Added Python wrapperis_frame_ready()tests/test_bg_tilemap_base_and_scroll_0464.py- Modifiedtest_tilemap_base_select_9800()for pre/post reset experiment
Tests and Verification
Command executed:
VIBOY_DEBUG_PPU=1 pytest -v tests/test_bg_tilemap_base_and_scroll_0464.py::TestBGTilemapBaseAndScroll::test_tilemap_base_select_9800 -s
Result: ✅ Test passes
Evidence Collected:
- nz_pre=0, nz_post=17280: The framebuffer is all at 0 BEFORE the reset, but has data AFTER the reset. This confirms that the problem is that
get_framebuffer_indices()reads the clean front buffer BEFORE the swap. - row0_pre=[0, 0, 0, 0, 0, 0, 0, 0]: Confirmation that the prebuffer is empty.
- row0_post=[0, 1, 2, 3, 0, 1, 2, 3]: The post buffer has the correct pattern (P0).
- bg_pixels_written=23040:BG itself is writing pixels (all 23040 pixels).
- last_tile_bytes=[85, 51], addr=0x8000, valid=True: The bytes read are correct (0x55=85, 0x33=51).
Diagnosis:The problem is thatget_framebuffer_indices_ptr()DOES NOT make present automatic likeget_framebuffer_ptr(). When called beforeget_frame_ready_and_reset(), returns the clean front. After the swap, the front has the correct data.
Identified Solution: Add auto present toget_framebuffer_indices_ptr()(similar toget_framebuffer_ptr()). However, the method isconst, so you need to make it non-const or create a version that does present.
C++ Compiled Module Validation:✅ Compilation successful. Methodis_frame_ready()correctly exposed to Python.
Sources consulted
- Pan Docs: PPU Modes, Frame Timing, V-Blank
- Step 0364: Double Buffering in PPU
- Step 0428: Automatic Present in
get_framebuffer_ptr() - Step 0457: Debug API for tests -
get_framebuffer_indices_ptr()
Educational Integrity
What I Understand Now
- Double Buffering: The PPU uses double buffering to avoid race conditions. The back buffer is written during rendering, and is swapped with the front buffer when a frame is completed.
- Automatic Present: Methods that return pointers to the framebuffer must do "auto present" - if a swap is pending, do the swap before returning the pointer. This ensures that Python always sees the latest content.
- Pre/Post Reset Experiment: To diagnose timing/buffering problems, it is useful to read the framebuffer BEFORE and AFTER critical operations (such as swap/reset) to identify where data is lost.
What remains to be confirmed
- Fix
get_framebuffer_indices_ptr(): Add automatic present. The method isconst, so you need to make it non-const or create a version that does present. - Impact on other tests: Check if other tests are affected by the change of
get_framebuffer_indices_ptr()to non-const.
Hypotheses and Assumptions
Confirmed hypothesis: The problem is NOT that BG is not writing (bg_pixels_written=23040), nor that the bytes read are incorrect (last_tile_bytes=[85, 51]). The problem is thatget_framebuffer_indices()reads the clean front buffer BEFORE the swap.
Next Steps
- [ ] Add auto present to
get_framebuffer_indices_ptr()(make it non-const or create version that does present) - [ ] Verify that all tests pass after the fix
- [ ] Document the change in the code (comments explaining why it is automatically present)