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)
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 headerget_rom_header_cgb_flag(): Getter that returns the CGB flag of the ROM headerget_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 headerget_dmg_compat_mode(): Returns boolean indicating whether it is in DMG compatibility modeget_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 headermachine_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 paletteobj_palette_bytes_hex: Hex dump 64 bytes from OBJ palettebg_palette_nonwhite_entries: Counter of non-white entries in BG paletteobj_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_, getterssrc/core/cpp/MMU.cpp- Implementation of FF68-FF6B tracking, getters CGB detectionsrc/core/cpp/PPU.cpp- Fix convert_framebuffer_to_rgb() to detect CGB modesrc/core/cython/mmu.pxd- Structure and getter declarationssrc/core/cython/mmu.pyx- Exposure of getters to Pythontools/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"