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

CGB End-to-End Present Proof (Idx→RGB→Present)

Date:2026-01-08 StepID:0496 State: VERIFIED

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:

  1. 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.
  2. 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.
  3. 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 usingSDL_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 runpygame.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 dump
  • VIBOY_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 dimensions
  • present_bytes_len: Total buffer size in bytes

The data is obtained fromThreeBufferStatswhen available.

Affected Files

  • src/gpu/renderer.py- Headless mode, separate PRESENT dump
  • tools/rom_smoke_0442.py- PresentDetails in snapshot
  • docs/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