This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Step 0256: Debug Palette (High Contrast)
Summary
This Step implements a high contrast debug palette in the Python renderer (`src/gpu/renderer.py`) that completely ignores the hardware palette registers (BGP, OBP0, OBP1) and directly maps the color indices (0-3) of the PPU framebuffer to fixed high contrast colors. The goal is to reveal any pixels that the PPU is generating, even if the palette registers are at `0x00` (all white) or if the MMU is not correctly serving the palette values to the frontend.
If after this modification we see black/gray shapes moving on the screen (like the GAME FREAK logo or the Gengar vs Nidorino intro in Pokémon Red), we will know that the C++ core is working correctly and the problem is only in the reading/writing of the palette registers (`FF47-FF49`) in the MMU.
Hardware Concept
On the Game Boy, palette registers control how color indices (0-3) generated by the PPU are translated into on-screen RGB colors. The PPU framebuffer contains color indices (0, 1, 2, 3), not RGB colors directly. These indices must pass through a palette to become visible colors.
Palette Records:
- BGP (0xFF47): Background palette. Each pair of bits (0-1, 2-3, 4-5, 6-7) maps a color index (0-3) to a shade of gray (0-3). If BGP is `0x00`, all indices are mapped to color 0 (white), causing even black pixels (index 3) to be rendered as white.
- OBP0 (0xFF48): Sprites palette (channel 0). Similar to BGP, but color 0 is always transparent on sprites.
- OBP1 (0xFF49): Sprite Palette (channel 1). Similar to OBP0.
Critical Problem:If the palette registers are at `0x00` or if the MMU is not correctly serving these values, all pixels will be rendered as white, even if the PPU is correctly generating the color indices. This makes it impossible to distinguish between a rendering problem (PPU does not generate pixels) and a palette problem (PPU generates pixels but they are rendered as white).
Debug Solution:By forcing a fixed high-contrast palette that directly maps indices 0-3 to visible colors (White, Light Gray, Dark Gray, Black), we can "see" any pixels the PPU is generating, regardless of the state of the palette registers. If we see black/gray shapes, we know the PPU is working; If we continue to see everything white, the problem is in the PPU itself.
Fountain:Pan Docs - Palette Registers (BGP, OBP0, OBP1)
Implementation
Changed the `render_frame()` method in `src/gpu/renderer.py` to force a high-contrast debug palette in two places:
1. Rendering with PPU C++ (lines 444-515)
Replaced BGP read and decode logic with a direct mapping from indexes to colors:
# --- Step 0256: DEBUG PALETTE FORCE (HIGH CONTRAST) ---
# We ignore hardware BGP/OBP to see raw PPU indexes.
# This will confirm if the PPU is drawing sprites/background.
# Fixed high contrast palette: 0=White, 1=Light Grey, 2=Dark Grey, 3=Black
debug_palette_map = {
0: (224, 248, 208), #00: White/Greenish (Color 0)
1: (136, 192, 112), #01: Light Gray (Color 1)
2: (52, 104, 86), #10: Dark Gray (Color 2)
3: (8, 24, 32) #11: Black (Color 3)
}
# Direct mapping: framebuffer index -> RGB color
palette = [
debug_palette_map[0],
debug_palette_map[1],
debug_palette_map[2],
debug_palette_map[3]
]
# ----------------------------------------
2. Rendering with Python method (lines 525-832)
The same debug palette was applied to the Python method that computes tiles from VRAM, replacing BGP decoding.
3. Sprite Rendering (lines 873-1027)
Changed the `render_sprites()` method to use the same debug palette, ignoring OBP0 and OBP1:
# --- Step 0256: DEBUG PALETTE FORCE (HIGH CONTRAST) ---
# We ignore hardware OBP0/OBP1 to see raw sprite indices.
debug_palette_map = {
0: (224, 248, 208), # 00: White/Greenish (Color 0 - transparent in sprites)
1: (136, 192, 112), #01: Light Gray (Color 1)
2: (52, 104, 86), #10: Dark Gray (Color 2)
3: (8, 24, 32) #11: Black (Color 3)
}
palette0 = [debug_palette_map[0], debug_palette_map[1],
debug_palette_map[2], debug_palette_map[3]]
palette1 = [debug_palette_map[0], debug_palette_map[1],
debug_palette_map[2], debug_palette_map[3]]
# ----------------------------------------
Design decisions
- High Contrast Palette:Colors with sufficient contrast were chosen so that any pixel with index > 0 is clearly visible, even on light backgrounds.
- Direct Mapping:Any BGP/OBP decoding is avoided to eliminate potential points of failure. If the framebuffer has index 3, it is rendered as black directly.
- Visual Consistency:The same palette is used for background and sprites to facilitate visual comparison.
- No Recompiling Required:This modification is purely in Python, so it does not require recompiling C++. This allows for quick iteration during debugging.
Affected Files
src/gpu/renderer.py- Modified `render_frame()` and `render_sprites()` to force high contrast debug palette (Step 0256).
Tests and Verification
Manual Validation:
- Execute:
python main.py roms/pkmn.gb(or any ROM with sprites). - Observe the screen:
- If we see black/gray shapes moving (GAME FREAK logo, Gengar vs Nidorino intro):✅ SUCCESS- The PPU works correctly, the problem is in the paddle registers.
- If we continue seeing everything white/green:❌ PROBLEM- The PPU is not generating pixels or the framebuffer is not being read correctly.
- Interpretation of Results:
- If we see shapes: The C++ core is perfect. The problem is in the reading/writing of BGP/OBP0/OBP1 in the MMU or in the paddle mapping.
- If we do not see ways: The problem is in the PPU (it does not generate pixels) or in the transfer of the framebuffer from C++ to Python.
Note:This Step does not require unit tests because it is a temporary debug modification. Once the problem is identified, normal paddle logic will be restored.
Sources consulted
- Bread Docs:Palette Registers (BGP, OBP0, OBP1)
- Bread Docs:LCD PPU - Background and Sprite Rendering
Educational Integrity
What I Understand Now
- Palettes as Translation:Palette registers act as a "translation table" that converts color indices (0-3) into visible RGB colors. If this translation fails (BGP=0x00), all pixels are rendered as white, even if the PPU correctly generates the indices.
- Removal Debugging:By forcing a fixed palette, we remove the "palette" variable from the problem. If after this we see shapes, we know that the problem is in the palette; If we don't see shapes, the problem is in the PPU itself.
- Framebuffer as Indexes:The PPU framebuffer contains color indices (0-3), not RGB colors. These indices must pass through a palette to become visible colors.
What remains to be confirmed
- Visual Result:Will we see black/gray forms in Pokémon Red after this modification? This will confirm whether the problem is with the paddle or the PPU.
- Registration Status:If we see shapes, we will need to check why BGP/OBP0/OBP1 are at 0x00 or why the MMU is not serving them correctly.
Hypotheses and Assumptions
Main Hypothesis:The game (Pokémon Red) has the palette registers set to `0x00` (all white) or the MMU is not correctly saving/serving these values. If we force a high contrast palette and see shapes, we will confirm this hypothesis.
Assumption:The C++ PPU framebuffer contains valid indices (0-3) and is being transferred correctly to Python. If this assumption is wrong, we won't see shapes even with the forced palette.
Next Steps
- [ ] Execute
python main.py roms/pkmn.gband observe the screen. - [ ] If we see black/gray shapes:
- Check why BGP/OBP0/OBP1 are at 0x00 or why the MMU is not serving them correctly.
- Correct reading/writing of palette registers in the MMU.
- Restore normal palette logic and validate that colors are displayed correctly.
- [ ] If we don't see shapes:
- Verify that the C++ PPU framebuffer contains valid indexes (0-3).
- Verify that the framebuffer is being transferred correctly from C++ to Python.
- Investigate why the PPU is not generating pixels or why the framebuffer is empty.