⚠️ 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 Palette Reality Check (Close White)

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

Summary

This step implements a complete diagnosis of the "white screen" problem in CGB mode (`tetris_dx.gbc`). Diagnostic tools were implemented (CGB Detection, IO Watch for FF68-FF6B, CGB palette dump, Pixel Proof) that allowed us to identify that the problem was in `PPU::convert_framebuffer_to_rgb()`, which always used DMG mode even when the hardware was CGB. The applied fix allows the code to correctly use CGB palettes when in CGB mode, resolving the white issue.

Hardware Concept

CGB Mode Detection: The Game Boy Color can operate in two modes: native CGB mode (using CGB palettes) and DMG compatibility mode (using DMG palettes even on CGB hardware). The mode is determined by byte 0x0143 of the ROM header (0x80 or 0xC0 = CGB) and by LCDC bit 0 (0xFF40): if LCDC bit 0 = 0 (LCD OFF), the hardware operates in DMG compatibility mode.

CGB pallets: In CGB mode, palettes are configured by writing to BGPI/OBPI (Palette Index, 0xFF68/0xFF6A) and then to BGPD/OBPD (Palette Data, 0xFF69/0xFF6B). Each palette has 4 colors, each color is 2 bytes in BGR555 format (bits 0-4 = Blue, 5-9 = Green, 10-14 = Red). There are 8 BG palettes and 8 OBJ palettes, each with 4 colors = 64 bytes total per type.

BGR555→RGB888 conversion: CGB colors are in BGR555 format (15 bits: 5 bits per component). To convert to RGB888 (24 bits: 8 bits per component), the BGR555 components are extracted and scaled: `r8 = (r5 * 255) / 31`.

Reference:Pan Docs - "CGB Palettes", "CGB Registers", "LCDC (0xFF40)", "BGR555 Format"

Implementation

Phase A: CGB Detection

A1-A2: Getters in MMU

It was implemented inMMU.hpp/MMU.cpp:

  • rom_header_cgb_flag_: Member that stores byte 0x0143 of the ROM header
  • get_rom_header_cgb_flag(): Getter that returns the CGB flag of the ROM header
  • get_dmg_compat_mode(): Getter that returnstrueif LCDC bit 0 = 0 (DMG compatibility mode within CGB)

A3: Cython Exposure

The getters were exposed inmmu.pyx:

  • get_rom_header_cgb_flag(): Returns the CGB flag of the ROM header
  • get_dmg_compat_mode(): Returns boolean indicating whether it is in DMG compatibility mode
  • get_hardware_mode(): Already existed, returns "CGB" or "DMG"

A4: CGBDetection Section in Snapshot

Section addedCGBDetectioninrom_smoke_0442.py:

  • rom_header_cgb_flag: Byte 0x0143 of the ROM header
  • machine_is_cgb: Emulator internal flag (1 = CGB, 0 = DMG)
  • dmg_compat_mode: DMG compatibility mode within CGB

Phase B: IO Watch for FF68-FF6B

B1: IOWatchFF68FF6B structure

It was implemented inMMU.hpp:

  • Tracking of writes/reads to FF68 (BGPI/BCPS), FF69 (BGPD/BCPD), FF6A (OBPI/OCPS), FF6B (OBPD/OCPD)
  • For each register: write/read counters, last PC, last value

B2: Tracking in MMU

Tracking was implemented inMMU::write()andMMU::read():

  • Increments counters and stores last PC/value for each access to FF68-FF6B
  • Always active (not crawled by environment variables)

B3: Cython and Snapshot Exposure

The getter was exposed inmmu.pyxand section was addedIOWatchFF68FF6Bin snapshot.

Phase C: Compact Pallet RAM Dump

C1: CGBPaletteRAM Section in Snapshot

Section addedCGBPaletteRAMinrom_smoke_0442.py:

  • bg_palette_bytes_hex: Hex dump 64 bytes from BG palette
  • obj_palette_bytes_hex: Hex dump 64 bytes from OBJ palette
  • bg_palette_nonwhite_entries: Counter of non-white entries in BG palette
  • obj_palette_nonwhite_entries: Counter of non-white entries in OBJ palette

Phase D: Pixel Proof

D1: PixelProof Section in Snapshot

Section addedPixelProofinrom_smoke_0442.py:

  • Display up to 5 non-white pixels from the RGB framebuffer
  • For each pixel: coordinates (x, y), color index (idx), palette used (BG/OBJ), BGR555 raw color, final RGB888 color

Phase E: Minimum Fix

E1: Problem Identified

InPPU::convert_framebuffer_to_rgb(), line 5613:

bool is_dmg = true;  // ❌ Always DMG, even in CGB

This always forced the use of DMG palettes (BGP/OBP0/OBP1), which in `tetris_dx.gbc` is set to 0x00 (all white), instead of using the CGB palettes that have valid data.

E2: Fix Applied

Implemented correct CGB mode detection:

HardwareMode hw_mode = mmu_->get_hardware_mode();
bool is_dmg = (hw_mode == HardwareMode::DMG);

// If we are in CGB but in DMG compatibility mode (LCDC bit 0 = 0), use BGP
if (!is_dmg && mmu_->get_dmg_compat_mode()) {
    is_dmg = true;  // Use DMG palettes even if it's CGB hardware
}

Also fixed BGR555→RGB888 conversion (correct extraction of B, G, R components).

Phase F: Validation

F1: Execution and Results

It was executedtetris_dx.gbcfor 600 frames and it was verified that the fix works:

  • fb_nonzero=22910✅ (there are non-zero indices)
  • PixelProofshows pixels with RGB not white:rgb(0,0,0)andrgb(197,197,197)
  • CGBDetection_MachineIsCGB=1✅ (emulator detects CGB correctly)
  • CGBPaletteRAM_BG_NonWhite=24✅ (CGB palettes have data)

Affected Files

  • src/core/cpp/MMU.hpp- IOWatchFF68FF6B structure, member rom_header_cgb_flag_, getters
  • src/core/cpp/MMU.cpp- Implementation of FF68-FF6B ​​tracking, getters CGB detection
  • src/core/cpp/PPU.cpp- Fix convert_framebuffer_to_rgb() to detect CGB mode
  • src/core/cython/mmu.pxd- Structure and getter declarations
  • src/core/cython/mmu.pyx- Exposure of getters to Python
  • tools/rom_smoke_0442.py- CGBDetection, IOWatchFF68FF6B, CGBPaletteRAM, PixelProof sections in snapshot

Tests and Verification

Command executed:

PYTHONPATH=. python3 tools/rom_smoke_0442.py roms/tetris_dx.gbc --frames 600

Results (Frame 600):

  • CGBDetection_MachineIsCGB=1✅ (emulator detects CGB)
  • CGBPaletteRAM_BG_NonWhite=24✅ (CGB palettes have data)
  • fb_nonzero=22910✅ (framebuffer has non-zero indices)
  • PixelProof_P0_(0,0)_idx3_palBG_15b0x0000_rgb(0,0,0)_P1_(1,0)_idx1_palBG_15b0x6318_rgb(197,197,197)✅ (there are non-white RGB pixels)

Compiled C++ module validation:

# The C++ code compiled successfully with:
python3 setup.py build_ext --inplace

# Cython getters work correctly:
from viboy_core import PyMMU
mmu = PyMMU()
cgb_flag = mmu.get_rom_header_cgb_flag() # ✅ Works
dmg_compat = mmu.get_dmg_compat_mode() # ✅ Works
io_watch = mmu.get_io_watch_ff68_ff6b() # ✅ It works

Conclusion

The "white" problem in CGB mode was caused byPPU::convert_framebuffer_to_rgb()always used DMG mode, forcing the use of BGP (which the game sets to 0x00 = blank) instead of using the CGB palettes that have valid data. The fix allows the code to correctly detect CGB mode and use CGB palettes when appropriate, resolving the issue.

Bottom line: The RGB framebuffer now has non-white colors (PixelProofshows rgb(0,0,0) and rgb(197,197,197)), confirming that the fix works correctly.

References

  • Plan:step_0495_-_cgb_palette_reality_check_(close_the_white)_e693ca2d.plan.md
  • Report:docs/reports/report_step0495.md
  • Pan Docs: "CGB Palettes", "CGB Registers", "LCDC (0xFF40)", "BGR555 Format"
  • GBEDG: "CGB Palette System"