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

Close Ambiguity PPU vs Palettes vs Presentation

Date:2026-01-06 StepID:0489 State: VERIFIED

Summary

Step 0489 implements instrumentation at three critical points in the rendering pipeline to close the ambiguity about where the blank screen issue occurs: FB_INDEX (index buffer generated by PPU), FB_RGB (RGB buffer after palette mapping), and FB_PRESENT_SRC (exact buffer passed to SDL/window). Additionally, instrumentation is added for tracking writes to CGB palettes and DMG tile data reading counters. The results reveal that the PPU is not reading tile data (TileBytesTotal=0), which explains why the framebuffer is completely blank.

Hardware Concept

The Game Boy rendering pipeline has multiple stages where a blank screen issue can occur:

  1. FB_INDEX: The PPU generates a 160x144 pixel buffer with color indices (0-3) based on VRAM tile data
  2. FB_RGB: Indices are mapped to RGB colors using palettes (DMG: BGP, CGB: CGB palettes)
  3. FB_PRESENT_SRC: The RGB buffer is transferred to the SDL texture for screen presentation

To diagnose where the problem occurs, we need to capture statistics (CRC32, non-white pixel counts) at each of these three points. If FB_INDEX is blank but FB_RGB is not, the problem is with the palette mapping. If FB_RGB has data but FB_PRESENT is blank, the problem is with the blit/presentation. If all three are blank, the problem is in the PPU (tile fetch/decode).

Reference: Pan Docs - LCD Control Register (FF40 - LCDC), Background Palette (FF47 - BGP), CGB Palettes (FF68-FF6B), Tile Data (0x8000-0x97FF).

Implementation

5 phases of instrumentation are implemented to close the ambiguity:

Phase A: ThreeBufferStats

Structure is addedThreeBufferStatstoPPU.hppwith statistics for all three buffers:

  • FB_INDEX: idx_crc32, idx_unique, idx_nonzero
  • FB_RGB: rgb_crc32, rgb_unique_colors_approx, rgb_nonwhite_count
  • FB_PRESENT_SRC: present_crc32, present_nonwhite_count, present_fmt, present_pitch, present_w, present_h

The functioncompute_three_buffer_stats()is executed when LY=144 andframe_ready_is true. For FB_RGB, both CGB mode (uses RGB buffer directly) and DMG mode (converts indices to RGB using BGP) are handled. FB_PRESENT_SRC is captured from Python inrenderer.pyjust beforepygame.display.flip().

Gate: VIBOY_DEBUG_PRESENT_TRACE=1

Phase B: Dump PPM RGB

It is implemented_dump_rgb_framebuffer_to_ppm()inrom_smoke_0442.pythat:

  • Read the index framebuffer from the PPU
  • Convert indices to RGB using BGP (DMG) or CGB palettes
  • Write a PPM file (Netpbm P6) with the RGB framebuffer

Gate: VIBOY_DUMP_RGB_FRAMEandVIBOY_DUMP_RGB_PATH

Phase C: CGB Palette Write Stats

Structure is addedCGBPaletteWriteStatstoMMU.hppto track writes to CGB palettes:

  • bgpd_write_count, last_bgpd_write_pc, last_bgpd_value, last_bgpi
  • obpd_write_count, last_obpd_write_pc, last_obpd_value, last_obpi

Tracking is carried out inMMU::write()when registers 0xFF68-0xFF6B are written.

Gate: VIBOY_DEBUG_CGB_PALETTE_WRITES=1

Phase D: DMG Tile Fetch Stats

Structure is addedDMGTileFetchStatstoPPU.hppto track tile data readings:

  • tile_bytes_read_total_count: Total bytes of tile data read
  • tile_bytes_read_nonzero_count: Non-zero bytes read

Tracking is carried out inPPU::decode_tile_line()when bytes are read from VRAM to decode tiles.

Gate: VIBOY_DEBUG_DMG_TILE_FETCH=1

Phase E: Execution and Reporting

It runsrom_smoke_0442.pywith all active flags and ROMtetris.gb(DMG). Complete report is generated indocs/reports/reporte_step0489.md.

Affected Files

  • src/core/cpp/PPU.hpp- StructuresThreeBufferStatsandDMGTileFetchStats
  • src/core/cpp/PPU.cpp- Methodscompute_three_buffer_stats()and tracking indecode_tile_line()
  • src/core/cython/ppu.pxd / ppu.pyx- Exposure of structures and methods to Python
  • src/core/cpp/MMU.hpp- StructureCGBPaletteWriteStats
  • src/core/cpp/MMU.cpp- Tracking of writes to CGB palettes
  • src/core/cython/mmu.pxd / mmu.pyx- Exhibition ofCGBPaletteWriteStatsto Python
  • src/gpu/renderer.py- CaptureFB_PRESENT_SRCfrom Pygame Surface
  • tools/rom_smoke_0442.py- Integration of all statistics and function_dump_rgb_framebuffer_to_ppm()
  • docs/reports/reporte_step0489.md- Complete report with analysis of results

Tests and Verification

It was executedrom_smoke_0442.pywith ROMtetris.gb(DMG) and flags:

VIBOY_DEBUG_PRESENT_TRACE=1
VIBOY_DEBUG_CGB_PALETTE_WRITES=1
VIBOY_DEBUG_DMG_TILE_FETCH=1
VIBOY_DUMP_RGB_FRAME=5
VIBOY_DUMP_RGB_PATH=docs/reports/dumps/tetris_frame_####.ppm

Snapshot results:

  • Frame 0: ThreeBufferStats=IdxCRC32=0x00000000 IdxUnique=1 IdxNonZero=0 | RgbCRC32=0x00000000 RgbUnique=1 RgbNonWhite=2304 | PresentCRC32=0x00000000 PresentNonWhite=0
  • Frame 1: Similar to Frame 0
  • Frame 2: RgbCRC32=0x4F0D7000butRgbNonWhite=0(inconsistency detected)
  • CGBPaletteWriteStats: No writes (expected for DMG)
  • DMGTileFetchStats: TileBytesTotal=0 TileBytesNonZero=0 ⚠️ CRITICAL

Dump PPM generated: docs/reports/dumps/tetris_frame_0005.ppm(68KB)

Compilation: ✅ No errors,test_build.pypasses correctly

Sources consulted

  • Pan Docs: LCD Control Register (FF40 - LCDC)
  • Pan Docs: Background Palette (FF47 - BGP)
  • Pan Docs: CGB Palettes (FF68-FF6B)
  • Pan Docs: Tile Data (0x8000-0x97FF)

Educational Integrity

What I Understand Now

  • Rendering Pipeline: The rendering process has multiple stages (fetch → decode → palette → present) and each one can be the source of a blank screen issue.
  • Three-buffer instrumentation: Capturing statistics on FB_INDEX, FB_RGB and FB_PRESENT allows you to identify exactly where the problem occurs.
  • Tile fetch tracking: If the PPU is not reading tile data, the framebuffer will be blank regardless of palettes or layout.

What remains to be confirmed

  • Why decode_tile_line() is not executed: The tracking shows TileBytesTotal=0, indicating thatdecode_tile_line()not being called or VRAM reads are stuck.
  • VRAM locks during mode 3: I need to check if there are locks onMMU.cppthat block VRAM reads during Pixel Transfer mode.
  • Inconsistency in RGB conversion: Frame 2 shows RgbCRC32≠0 but RgbNonWhite=0, suggesting a bug in sampling or conversion.

Hypotheses and Assumptions

Main hypothesis: The PPU is not reading tile data because VRAM is empty (confirmed: 0/6144 non-zero bytes) or because there are locks blocking reads during mode 3. This explains why the framebuffer is completely blank.

Next Steps

  • [ ] Investigate whydecode_tile_line()not running or VRAM reads are stuck
  • [ ] Check VRAM locks inMMU.cppduring mode 3 (Pixel Transfer)
  • [ ] Fix inconsistency in RGB conversion (sampling every 10 pixels may be causing the problem)
  • [ ] Check timing: PPU might try to render before VRAM is ready
  • [ ] Run with CGB ROM (tetris_dx.gbc) to compare behavior