This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Complete White Screen Research
Summary
Extensive diagnostic checks have been implemented at all stages of the rendering pipeline to investigate why screens are completely white. The logs confirm that the pipeline is working correctly: `render_scanline()` runs, writes data to the framebuffer (checkerboard), buffer swapping works, and Python reads the data correctly. The renderer also receives the data. However, a critical issue was identified: `render_scanline()` runs in Mode 2 (OAM Search) instead of Mode 0 (H-Blank), which can affect render timing.
Hardware Concept
C++ → Python Rendering Pipeline
The rendering pipeline works in several stages:
- C++ PPU (`render_scanline()`): Write color indices (0-3) to `framebuffer_back_` during H-Blank (Mode 0).
- C++ PPU (`swap_framebuffers()`): Swaps `framebuffer_back_` and `framebuffer_front_` when a full frame is completed (LY=144).
- C++ PPU (`get_frame_ready_and_reset()`): Marks the frame as ready and returns a pointer to `framebuffer_front_`.
- Python (`viboy.py`): Read the framebuffer from C++ and create an immutable snapshot.
- Python (`renderer.py`): Convert indices to RGB and draw on the screen.
Render Timing:According to Pan Docs, rendering a line should occur during H-Blank (Mode 0), after Mode 3 (Pixel Transfer) completes. If `render_scanline()` is run in a different mode, there may be synchronization problems.
Temporary Checkerboard:When VRAM is empty (`vram_is_empty_ == true`), a checkerboard pattern is activated (alternating indices 0 and 3) to display that rendering is working while the game loads tiles.
Implementation
Added diagnostic checks at 6 critical points in the pipeline:
Implemented Verifications
- Running `render_scanline()`: Logs at the beginning of the function confirming execution and mode/timing.
- Write to framebuffer: Check after writing all pixels of the line, counting non-white pixels and index distribution.
- Checkerboard activation: Logs when it is detected that the checkerboard should be activated and when it is written to the framebuffer.
- Framebuffer before Python reads it: Check in `get_frame_ready_and_reset()` before `swap_framebuffers()`, counting non-white pixels in `framebuffer_back_`.
- Framebuffer after swap: Check in `swap_framebuffers()` after swap, counting non-white pixels in `framebuffer_front_`.
- Framebuffer in Python: Check when Python reads the C++ framebuffer, parsing the first 100 pixels.
Key Findings
- ✅ `render_scanline()` is executed: Runs on every visible line (LY 0-143), but in Mode 2 (OAM Search) instead of Mode 0 (H-Blank).
- ✅ Data is written to the framebuffer: 80/160 non-white pixels per line (checkerboard), distribution 0=80, 3=80.
- ✅ Checkerboard activates: Triggers correctly when VRAM is empty and tiles are empty.
- ✅ Framebuffer has data before exchange: 11520/23040 non-white pixels (50% - full checkerboard).
- ✅ Framebuffer maintains data after exchange: 11520/23040 non-white pixels in `framebuffer_front_`.
- ✅ Python reads the data: 52/100 non-white pixels in the first 100 pixels.
- ✅ Renderer receives the data: 11520/23040 non-white pixels, correct checkerboard pattern [3,3,3,3,3,3,3,3,0,0,0,0,0,0,0,0,3,3,3,3].
Critical Issue Identified
`render_scanline()` runs in Mode 2 (OAM Search) instead of Mode 0 (H-Blank). According to Pan Docs, rendering should occur during H-Blank (Mode 0), after Mode 3 (Pixel Transfer) completes. Even if the data is written correctly, this can cause timing and synchronization problems.
Affected Files
src/core/cpp/PPU.cpp- Added checks in `render_scanline()`, `get_frame_ready_and_reset()`, and `swap_framebuffers()`src/viboy.py- Added check when Python reads the C++ framebufferlogs/test_*_step0372.log- Diagnostic logs of the 6 test ROMs
Tests and Verification
Short tests (30 seconds) were run with the 6 main ROMs to capture diagnostic logs:
- Tested ROMs:tetris.gb, mario.gbc, zelda-dx.gbc, Oro.gbc, pkmn.gb, pkmn-amarillo.gb
- Command executed:
timeout 30 python3 main.py roms/[ROM].gb[c] > logs/test_[ROM]_step0372.log 2>&1 - Log analysis:The logs were analyzed using `grep` commands with limits to identify where information is lost
Log Evidence
[PPU-RENDER-EXECUTION] Frame 1 | LY: 0 | render_scanline() executed | Count: 1
[PPU-RENDER-EXECUTION] Mode: 2 (0=H-Blank, 1=V-Blank, 2=OAM, 3=Pixel Transfer) | LY: 0 | Expected: H-Blank (0)
[PPU-FRAMEBUFFER-WRITE] Frame 1 | LY: 0 | Non-zero pixels written: 80/160 | Distribution: 0=80 1=0 2=0 3=80
[PPU-CHECKERBOARD-ACTIVATE] Frame 1 | LY: 0 | X: 0 | Checkerboard activated | Tile empty: YES | VRAM empty: YES | Count: 1
[PPU-FRAMEBUFFER-BEFORE-READ] Frame 1 | Total non-zero pixels: 11520/23040 | Distribution: 0=11520 1=0 2=0 3=11520
[PPU-FRAMEBUFFER-AFTER-SWAP] Frame 1 | Total non-zero pixels in front: 11520/23040 | Distribution: 0=11520 1=0 2=0 3=11520
[Viboy-Framebuffer-Read] Frame 1 | Non-zero pixels (first 100): 52/100 | Distribution: 0=48 1=0 2=0 3=52
[Renderer-Received] Frame 1 | First 20 indices: [3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3] | Non-zero pixels: 11520/23040 (50.00%)
Compiled C++ module validation:The module was successfully recompiled with the new checks. The logs confirm that all stages of the pipeline are working correctly.
Sources consulted
- Bread Docs:LCD Timing, Background, Window
- Bread Docs:PPU Modes (Mode 0, 1, 2, 3)
Educational Integrity
What I Understand Now
- Rendering pipeline:The C++ → Python pipeline works correctly. Data is written, exchanged, and read without loss of information.
- Render timing:`render_scanline()` must be run during H-Blank (Mode 0), not during OAM Search (Mode 2). This is critical for proper timing.
- Temporary Checkerboard:The checkerboard activates and works correctly when VRAM is empty, generating a visible pattern (50% non-white pixels).
What remains to be confirmed
- Timing problem:Why `render_scanline()` is executed in Mode 2 instead of Mode 0. This requires investigating where the function is called and at what point in the PPU cycle.
- RGB conversion:If the Python renderer is correctly converting indices (0, 3) to RGB. Indices 0 and 3 should be converted to black and white respectively based on the BGP palette.
- Drawn on screen:Whether Pygame is correctly drawing RGB pixels on the window surface.
Hypotheses and Assumptions
Main hypothesis:The problem with completely white screens is NOT in the data pipeline (which is working correctly), but possibly in:
- Incorrect timing of `render_scanline()` (Mode 2 vs Mode 0) which can cause synchronization problems.
- Converting indices to RGB in the Python renderer.
- The drawing of pixels on the surface of Pygame.
Next Steps
- [ ] Investigate why `render_scanline()` runs in Mode 2 instead of Mode 0 and fix the timing.
- [ ] Check the conversion of indices to RGB in the Python renderer (especially indices 0 and 3).
- [ ] Verify that Pygame is correctly drawing RGB pixels on the window surface.
- [ ] If the problem persists, consider completely disabling the temporary checkerboard and forcing normal rendering to isolate the problem.