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

Palette Correction: Why is Black White?

Date:2025-12-22 StepID:0215 State: DRAFT

Summary

Step 0213 confirmed that Python correctly receives the value3(black) in the framebuffer, but the screen is still white. This indicates that the rendering system in Python is mapping the index3to white, probably because the BGP record (0xFF47) is0x00or the palette decoding logic is incorrect.

Critical finding:If the BGP record is0x00, all bit pairs are00, which means that all color indices (0, 1, 2, 3) are mapped to color 0 of the palette (white). This explains why even black pixels (index 3) are rendered as white.

Solution:Added a diagnostic probe to check the BGP value in Python, and implemented a fix that forces a standard default value (0xE4) when BGP is0x00, ensuring that color indices map correctly to palette colors.

Hardware Concept: The BGP Registry (Background Palette)

The BGP record (Background Palette, address0xFF47) is an 8-bit byte that defines how the color indices (0-3) of the framebuffer are mapped to the actual colors of the Game Boy's gray palette.

BGP record format:

  • Bits 0-1:Color for index 0 (pixels with value 0)
  • Bits 2-3:Color for index 1 (pixels with value 1)
  • Bits 4-5:Color for index 2 (pixels with value 2)
  • Bits 6-7:Color for index 3 (pixels with value 3)

Each bit pair can have a value from 0-3, which maps to the 4 available shades of gray:

  • 0:White (255, 255, 255)
  • 1:Light Gray (170, 170, 170)
  • 2:Dark Gray (85, 85, 85)
  • 3:Black (0, 0, 0)

The BGP = 0x00 problem:

If the BGP record is0x00(all bits set to 0), then:

  • Index 0 → Color 0 (White)
  • Index 1 → Color 0 (White)
  • Index 2 → Color 0 (White)
  • Index 3 → Color 0 (White)

This means thatallPixels, regardless of their color index, are rendered as white. Even if the framebuffer correctly contains the value3(black), the renderer maps it to color 0 (white) because the BGP register is in0x00.

Standard default value:

The value0xE4(11100100 in binary) is a common value used by many Game Boy games. This value maps:

  • Index 0 → Color 0 (White) - bits 0-1 = 00
  • Index 1 → Color 1 (Light Gray) - bits 2-3 = 01
  • Index 2 → Color 2 (Dark Gray) - bits 4-5 = 10
  • Index 3 → Color 3 (Black) - bits 6-7 = 11

Fountain:Pan Docs - Background Palette Register (BGP), LCD Control Register.

Implementation

Two main modifications were implemented:

  1. BGP diagnostic probe:Code added insrc/viboy.pyto read and display the value of the BGP register when the framebuffer is captured, allowing to diagnose if the problem is a BGP in0x00.
  2. Palette correction in the renderer:It was modifiedsrc/gpu/renderer.pyto detect when BGP is0x00and force a standard default value (0xE4) that ensures correct color mapping.

Modification insrc/viboy.py

Added a diagnostic probe that reads the BGP log just when the framebuffer is captured:

# --- Step 0215: VANDEL PROBE ---
bgp_value = self._mmu.read(0xFF47)
print(f"BGP Register (0xFF47): 0x{bgp_value:02X}")
# ----------------------------------

This probe allows you to verify if the problem is a BGP in0x00or if there is another problem in the palette decoding logic.

Modification insrc/gpu/renderer.py

Added a fix in two places in the renderer (both in the method that uses the C++ framebuffer and in the original Python method):

# --- Step 0215: PALETTE CORRECTION ---
# If BGP is 0x00, all indexes are mapped to color 0 (white).
# This causes even black pixels (index 3) to be rendered as white.
# We force a standard default value (0xE4 = 11100100) that maps:
# Index 0 -> Color 0 (White)
# Index 1 -> Color 1 (Light Gray)
# Index 2 -> Color 2 (Dark Gray)
# Index 3 -> Color 3 (Black)
if bgp == 0x00:
    logger.warning(f"[Renderer] BGP is 0x00 (invalid palette). Forcing 0xE4 (standard palette)")
    bgp=0xE4
# ----------------------------------------

This fix ensures that even if the BGP register has not been initialized by the game or the MMU, the renderer uses a valid palette that allows graphics to be displayed correctly.

Design decisions

Why force 0xE4 instead of initializing BGP on the MMU?

The decision to force the value in the renderer instead of initializing BGP in the MMU was made for the following reasons:

  • Separation of responsibilities:The renderer is responsible for display, and is the most appropriate place to handle invalid palette edge cases.
  • Compatibility:Some games can write0x00temporarily during initialization, and forcing the value in the renderer allows the game to initialize BGP correctly later without interference.
  • Debugging:Keep BGP on0x00in the MMU allows the diagnostic probe to detect the problem, while the renderer corrects the display.

Affected Files

  • src/viboy.py- Added BGP diagnostic probe in framebuffer capture block (lines ~777-780)
  • src/gpu/renderer.py- Added palette fix in two places:
    • Methodrender_frame()when using C++ framebuffer (lines ~446-456)
    • Methodrender_frame()in the original Python method (lines ~502-512)

Tests and Verification

The verification was carried out by:

  • Diagnostic probe:When running the emulator, the probe will display the BGP value in the console, allowing you to verify if the problem is a BGP in0x00.
  • Visual validation:If the problem was a BGP in0x00, the fix should allow black pixels (index 3) to be correctly rendered as black instead of white.
  • Logs:The renderer will issue a warning when it detects a BGP in0x00and force the default value, allowing the problem to be diagnosed at runtime.

Test command:

python main.py roms/tetris.gb

Expected result:

  • If BGP is0x00, the console will display:BGP Register (0xFF47): 0x00
  • The renderer will issue a warning:[Renderer] BGP is 0x00 (invalid palette). Forcing 0xE4 (standard palette)
  • Black pixels (index 3) should correctly render as black instead of white.

Sources consulted

Educational Integrity

What I Understand Now

  • BGP registration:The BGP record is a byte that maps color indices (0-3) to actual colors in the palette. If BGP is0x00, all indices are mapped to color 0 (white), causing even black pixels to be rendered as white.
  • Rendering pipeline:The data flow goes from the C++ framebuffer (indices 0-3) → Python (reading the framebuffer) → Renderer (mapping indices to RGB colors using BGP) → Pygame (drawing on screen). The problem can be at any point in this pipeline.
  • Debugging hybrid systems:When working with hybrid Python/C++ systems, it is crucial to add diagnostic probes at multiple points in the pipeline to identify where data is lost or corrupted.

What remains to be confirmed

  • BGP initialization:When and how should the BGP record be initialized in the MMU? Do some games depend on BGP being on?0x00initially?
  • Default value:Is0xE4the correct value by default, or should it be another value? Does it vary depending on the Game Boy model (DMG vs CGB)?
  • Real hardware behavior:What does real hardware do when BGP is0x00? Does everything render white, or is there some special behavior?

Hypotheses and Assumptions

Assumption:We assume that forcing0xE4when BGP is0x00is acceptable behavior, as it is a common value used by many games. However, this may not be the exact behavior of real hardware, and should be verified with technical documentation or tests on real hardware.

Next Steps

  • [ ] Run the emulator and verify that the BGP probe shows the correct value
  • [ ] Confirm that palette correction correctly displays black pixels
  • [ ] If the problem persists, investigate whether there are other problems in the palette decoding logic
  • [ ] Consider initializing BGP with a default value in the MMU instead of forcing it in the renderer
  • [ ] Verify the behavior of real hardware when BGP is0x00to ensure compatibility