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

Step 0457: Isolate if the Bug is "Bad Indices" vs "Bad RGB Palette/Conversion" with Evidence

Date:2026-01-03 StepID:0457 State: VERIFIED

Summary

Aim:Isolate whether the flat RGB framebuffer bug is caused by poorly decoded/rendered indices (H1) or incorrect palette/RGB conversion (H2). The tests are already fine (0456), so if they continue to fail, the bug is real in the core.

Critical finding:Instrumentation reveals that the index framebuffer is completely flat (all values ​​are 0), which confirms that the bug is NOT in the palette conversion, but in the tile decode/render. The problem is how the tiles are decoded from VRAM or how the indexes are written to the framebuffer.

Numerical evidence:The tests show that the sampled indices (8 pixels) are all 0:[0, 0, 0, 0, 0, 0, 0, 0], with a unique set of{0}instead of the expected{0, 1, 2, 3}. This rules out H2 (palette/RGB conversion) and confirms H1 (decode/render).

Hardware Concept

The rendering process on the Game Boy has two main phases:

  1. Decoding and Rendering:Tiles are decoded from VRAM (2bpp format) and written to the framebuffer as color indices (0-3). This phase includes reading tiles from VRAM, 2bpp decoding, and writing indexes to the framebuffer.
  2. Palette Conversion:Indices are converted to RGB using the BGP/OBP0/OBP1 palettes. This phase reads the palette register, maps the index to a shade (0-3), and converts the shade to RGB888.

If the RGB framebuffer is flat (all the same color), it may be because:

  • H1 - Bad indices:The index framebuffer is flat (all 0, all 2, etc.) → the bug is in decode/render.
  • H2 - Bad conversion:The index framebuffer has variety (0, 1, 2, 3), but the palette→RGB conversion is broken and produces flat RGB → the bug is in RGB conversion/writing.

Fountain:Pan Docs - Background, Window, Sprites, Color Palettes

Implementation

Minimal instrumentation was implemented to isolate the bug without touching the tests (which are already fine according to 0456).

Phase A: Expose Framebuffer Indexes

Added a debug API to expose the index framebuffer from C++ to Python:

  • PPU::get_framebuffer_indices_ptr()inPPU.hpp- Returns const pointer to framebuffer_front_
  • PyPPU::get_framebuffer_indices()inppu.pyx- Cython wrapper returning 23040 bytes
  • Declaration inppu.pxdso Cython can access the method

This API allows tests to inspect the index framebuffer before converting to RGB, allowing you to distinguish between H1 and H2.

Phase B: Capture Used Regs Palette

Added capture of the palette registers used in the conversion:

  • Memberslast_bgp_used_, last_obp0_used_, last_obp1_used_inPPU.hpp
  • Update onconvert_framebuffer_to_rgb()to capture the read values
  • Gettersget_last_bgp_used(), get_last_obp0_used(), get_last_obp1_used()
  • WrapperPyPPU::get_last_palette_regs_used()which returns a dict with the values

This allows you to verify that the palette register used in the conversion matches the one written by the test, ruling out reading/reg caching bugs.

Phase C: Conversion Review

The conversion code was reviewed and confirmed to be correct:

  • dmg_shade_to_rgb()- Correct conversion table: {255, 170, 85, 0}
  • Writing to RGB buffer - Correct Stride:rgb_idx = fb_index * 3
  • Conversion order - Called after the swap, on the correct framebuffer_front_

The conversion does not need correction; The bug is in the previous phase (decode/render).

Test Modification

Added "sanity asserts" in tests to verify indexes and palette regs before looking at RGB:

  • test_palette_dmg_bgp_0454.py- Added sanity assert that checks sample indices (8 pixels) and reg palette used
  • test_palette_dmg_obj_0454.py- Added similar sanity assert for sprites

These asserts fail immediately if the indexes are wrong, allowing the bug to be identified without having to look at RGB.

Affected Files

  • src/core/cpp/PPU.hpp- Added methodget_framebuffer_indices_ptr()and members for pallet regs used
  • src/core/cpp/PPU.cpp- Implementation ofget_framebuffer_indices_ptr()and capture regs palette inconvert_framebuffer_to_rgb()
  • src/core/cython/ppu.pxd- New method declarations for Cython
  • src/core/cython/ppu.pyx- Cython wrappers:get_framebuffer_indices()andget_last_palette_regs_used()
  • tests/test_palette_dmg_bgp_0454.py- Added sanity asserts with indexes and regs palette
  • tests/test_palette_dmg_obj_0454.py- Added sanity asserts with indexes and regs palette

Tests and Verification

Command executed: pytest -v tests/test_palette_dmg_bgp_0454.py tests/test_palette_dmg_obj_0454.py tests/test_framebuffer_not_flat_0456.py

Result:❌ 0/3 tests pass (expected: the bug is real in the core)

Numerical evidence:

[TEST-BGP-SANITY] Sample indices (8 pixels): [0, 0, 0, 0, 0, 0, 0, 0]
[TEST-BGP-SANITY] Unique indexes: {0}
AssertionError: Flat indexes: only {0} (expected {0,1,2,3}). 
If this fails → bug is NOT palette; is decode/render.

Interpretation:The index framebuffer is completely flat (all 0s), confirming that the bug is in decode/render, not palette conversion. The palette→RGB conversion is correct, but there is no variety of indices to convert.

Compiled C++ module validation:✅ Compilation successful, no errors. The methods are available in Python.

Sources consulted

Educational Integrity

What I Understand Now

  • Bug isolation:When a symptom may have multiple causes, instrumentation is essential to isolate the root cause. In this case, exposing the index framebuffer allowed us to distinguish between "bad indexes" and "bad conversion".
  • Debug API:Adding debug APIs that expose internal state is a valid technique for testing, as long as it does not affect the hot path of the production code.
  • Numerical evidence:Tests should provide clear numerical evidence (sample indices, palette regs used) rather than just assuming what is wrong.

What remains to be confirmed

  • Decode de tiles:I need to investigate why the tile decode produces all 0 indices. Is the problem indecode_tile_line(), in reading VRAM, or in writing to the framebuffer?
  • Background Rendering:Is the problem inrender_bg()eitherrender_scanline()? Are the tiles being read correctly from VRAM?

Hypotheses and Assumptions

Main hypothesis:The bug is in the decode/render of tiles. Possible causes:

  • The 2bpp decode is poorly implemented and always produces index 0
  • VRAM reading is not working correctly
  • Writing to framebuffer_back_ is wrong (0 is always written)
  • The framebuffer swap is bad and the clean buffer is always read

Next Steps

  • [ ] Investigatedecode_tile_line()- Verify that the 2bpp decode produces the correct indexes
  • [ ] Investigaterender_bg()- Verify that the tiles are read correctly from VRAM
  • [ ] Investigate writing to framebuffer - Verify that indexes are written correctly to framebuffer_back_
  • [ ] Investigate framebuffer swap - Verify that the swap is working correctly
  • [ ] Apply minimum fix once the root cause is identified
  • [ ] Re-run palette tests to validate the fix