This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
CGB End-to-End Present Proof (Idx→RGB→Present)
Summary
This step implements an end-to-end diagnostic of the CGB rendering pipeline to identify exactly at which stage the "white screen" issue fails. Implemented support for headless mode in the renderer, separate PPM dump for FB_PRESENT, and PresentDetails in the snapshot. The results confirm that the PPU→RGB pipeline works correctly (IdxNonZero=22910, RgbNonWhite=22910), but FB_PRESENT_SRC is not captured in headless mode because rom_smoke does not use the renderer. Case A was identified: the problem is in the renderer/present, not the PPU or the palettes.
Hardware Concept
CGB Rendering Pipeline: The CGB mode rendering pipeline has three main stages:
- FB_INDEX: The PPU generates color indices (0-3) for each pixel based on tiles and palette attributes. These indexes are stored in the index framebuffer.
- FB_RGB: Indices are converted to RGB888 values using the CGB (BGPD/OBPD) palettes. Each index is mapped to a BGR555 color from the corresponding palette, which is then converted to RGB888.
- FB_PRESENT_SRC: The RGB buffer is delivered to the renderer (pygame Surface) that prepares it for presentation on the screen. This is the exact buffer that is passed to SDL/pygame before the flip.
End-to-End Diagnostics: To identify where the pipeline fails, we need evidence of all three stages in the same frame. If FB_INDEX has a signal but FB_RGB is white, the problem is in the conversion of indices to RGB (palettes). If FB_RGB has a signal but FB_PRESENT is white, the problem is in the renderer/present.
Headless mode: In headless mode (without a viewport), the renderer must be able to generate the same buffer that would be presented in UI mode, to allow diagnosis in CI and execution without display.
Reference:Pan Docs - "CGB Palettes", "PPU Rendering Pipeline", "Framebuffer Format"
Implementation
Phase 1: Headless Mode in Renderer
It was modifiedsrc/gpu/renderer.pyto support headless mode:
- Automatic detection: The renderer detects headless mode using
SDL_VIDEODRIVER=dummyeitherVIBOY_HEADLESS=1 - Temporary surface: If there is no screen available, a temporary Surface is created (
_headless_surface) to capture FB_PRESENT_SRC - Render without flip: In headless mode, does not run
pygame.display.flip(), but the temporary Surface is rendered the same as in normal mode
Phase 2: Separate PPM Dump for FB_PRESENT
Implemented separate dump using environment variables:
VIBOY_DUMP_PRESENT_FRAME: Frame in which to generate the dumpVIBOY_DUMP_PRESENT_PATH: PPM file path (supports####as frame placeholder)- Format: PPM P6 160x144 RGB888 (same format as FB_RGB)
Phase 3: PresentDetails in Snapshot
was addedPresentDetailsto the snapshot intools/rom_smoke_0442.py:
present_fmt: Surface format (0 = RGB888)present_pitch: Surface pitch (bytes per row)present_w,present_h: Surface dimensionspresent_bytes_len: Total buffer size in bytes
The data is obtained fromThreeBufferStatswhen available.
Affected Files
src/gpu/renderer.py- Headless mode, separate PRESENT dumptools/rom_smoke_0442.py- PresentDetails in snapshotdocs/reports/report_step0496.md- Complete step report
Tests and Verification
It was executedrom_smoke_0442.pywithtetris_dx.gbcfor 1200 frames:
export VIBOY_SIM_BOOT_LOGO=0
export VIBOY_DEBUG_PRESENT_TRACE=1
export VIBOY_DEBUG_CGB_PALETTE_WRITES=1
export VIBOY_DUMP_IDX_FRAME=600
export VIBOY_DUMP_IDX_PATH=/tmp/viboy_tetris_dx_idx_f####.ppm
export VIBOY_DUMP_RGB_FRAME=600
export VIBOY_DUMP_RGB_PATH=/tmp/viboy_tetris_dx_rgb_f####.ppm
export VIBOY_DUMP_PRESENT_FRAME=600
export VIBOY_DUMP_PRESENT_PATH=/tmp/viboy_tetris_dx_present_f####.ppm
python3 tools/rom_smoke_0442.py roms/tetris_dx.gbc --frames 1200
Results (Frame 600)
| Buffer | Metrics | Worth | State |
|---|---|---|---|
| FB_INDEX | IdxCRC32 | 0xBC5587A4 | ✅ Not white |
| IdxUnique | 4 | ✅ Multiple colors | |
| IdxNonZero | 22910 | ✅ Signal present | |
| FB_RGB | RGBCRC32 | 0xF87596C9 | ✅ Not white |
| RgbUnique | 4 | ✅ Multiple colors | |
| RgbNonWhite | 22910 | ✅ Signal present | |
| FB_PRESENT_SRC | PresentCRC32 | 0x00000000 | ❌ White |
| PresentNonWhite | 0 | ❌ No signal |
Generated PPM Dumps
/tmp/viboy_tetris_dx_idx_f600.ppm(68K) ✅/tmp/viboy_tetris_dx_rgb_f0600.ppm(68K) ✅/tmp/viboy_tetris_dx_rgb_f600.ppm(68K) ✅/tmp/viboy_tetris_dx_present_f600.ppm❌ (Not generated - renderer not used in rom_smoke)
Failure Classification
✅ CASE A Confirmed: The problem is in the renderer/present, not in the PPU or the palettes.
Evidence:
IdxNonZero=22910> 0 ✅ (PPU generates signal)RgbNonWhite=22910> 0 ✅ (Conversion to RGB works)PresentNonWhite=0❌ (Present buffer is white)
Sources consulted
- Pan Docs: "CGB Palettes", "PPU Rendering Pipeline", "Framebuffer Format"
- Step 0495: CGB Palette Reality Check (pre-implementation of CGB palettes)
- Step 0489: ThreeBufferStats (three buffer statistics structure)
Educational Integrity
What I Understand Now
- Rendering Pipeline: The pipeline has three clear stages (indexes, RGB, present). If one stage fails, the following ones also fail. ThreeBufferStats analysis allows you to identify exactly what stage the problem is at.
- Headless mode: The renderer can work without a viewport by creating a temporary Surface. This allows diagnosis in IC and execution without display.
- Synchronized Dumps: The PPM dumps of all three stages must be generated in the same frame to compare correctly.
What remains to be confirmed
- FB_PRESENT_SRC in UI: We need to run with UI (`main.py`) to capture actual FB_PRESENT_SRC and confirm if the issue persists when using the actual renderer.
- Root Cause of White Present: If PresentNonWhite is still 0 in UI, investigate Surface pitch, format (RGBA vs BGRA), order of operations, or buffer stale.
Hypotheses and Assumptions
Hypothesis: The problem is in the renderer/present because FB_INDEX and FB_RGB have signal, but FB_PRESENT is white. However, since rom_smoke does not use the renderer, we need to run with UI to confirm.
Next Steps
- [ ] Run with UI (`main.py`) with tetris_dx.gbc to capture actual FB_PRESENT_SRC
- [ ] Check if PresentNonWhite is still 0 running with UI
- [ ] If the problem persists, investigate Surface pitch, format (RGBA vs BGRA), order of operations, or buffer stale
- [ ] Implement minimal fix if root cause is identified