⚠️ 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 0456: Set Clean-Room Tests and Add Non-Flat Test

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

Summary

STOP:Before touching on the core, we formally validated that the paddle tests were well designed. Step 0454 created palette tests that failed with "flat framebuffer", but before assuming the problem was in the core, we verified that the tile pattern used in the tests actually generated the expected indexes.

Critical finding:The 0x00/0xFF pattern used in the original tests generates a constant index (=2), not the 4 indices (0/1/2/3) needed to validate pallets. This explained the false positives.

Action:We corrected the tests to use the 0x55/0x33 pattern which guarantees 0/1/2/3 indices, we added a "non-flat" anti-regression test, and created a 2bpp sanity test to formally validate the decoding.

Result:The fixed tests now fail because the core has a real bug (flat RGB framebuffer), confirming that the problem was NOT the design of the tests, but rather the index→RGB conversion in the core.

Hardware Concept

2bpp decoding (2 bits per pixel)

Fountain:Pan Docs - "Tile Data" / GBEDG

Game Boy tiles are stored in 2bpp format: each pixel requires 2 bits, and each row requires 8 pixels. requires 2 bytes (16 bits total). The low byte contains the least significant bit of each pixel, and the high byte contains the most significant bit.

Decoding Formula:

For each pixel i (0-7, MSB first):
    bit_low = (byte_low >> (7-i)) & 0x01
    bit_high = (byte_high >> (7-i)) & 0x01
    color_idx = bit_low | (bit_high<< 1)
    // Resultado: color_idx ∈ {0, 1, 2, 3}

Examples of Patterns:

  • 0x00/0x00:All bits = 0 → color_idx = 0 for all pixels
  • 0xFF/0xFF:All bits = 1 → color_idx = 3 for all pixels
  • 0x00/0xFF:byte_low=0, byte_high=0xFF → color_idx = 2 for all pixels (constant)
  • 0x55/0x33:Varied pattern → color_idx = [0, 1, 2, 3, 0, 1, 2, 3] (contains all 4 indices)

Implication for tests:To validate that a palette correctly maps all 4 indices, The tile used must generate the 4 indices (0/1/2/3). If the tile generates only 1 constant index, The test cannot distinguish whether the paddle works or not.

Implementation

Phase A: 2bpp Sanity Test

We createtest_tile_2bpp_decode_sanity_0456.pyto formally validate what patterns generate what indices. This test is pure (does not use the core) and mathematically demonstrates the behavior of the 2bpp decode.

Health Test Results:

Pattern 0x00/0xFF generates indices: [2, 2, 2, 2, 2, 2, 2, 2]
Unique indexes: {2}
⚠️ WARNING: Pattern 0x00/0xFF generates constant index = 2
   This pattern is NOT suitable for paddle tests requiring 0/1/2/3

Pattern 0x55/0x33 generates indices: [0, 1, 2, 3, 0, 1, 2, 3]
Unique indexes: {0, 1, 2, 3}
✅ Pattern 0x55/0x33 generates the 4 indices (suitable for palette tests)

Phase B: Correction of Palette Tests

We modifytest_palette_dmg_bgp_0454.pyandtest_palette_dmg_obj_0454.pyto use the pattern 0x55/0x33 instead of 0x00/0xFF. We also adjusted the asserts to be more robust (not exact RGB values, just distinction and change).

Key Changes:

# BEFORE (wrong pattern):
tile_data = [
    0x00, 0xFF, # Row 0: indices 0,1,2,3 (INCORRECT: generates constant 2)
    ...
]

# AFTER (correct pattern):
tile_data = [
    0x55, 0x33, # Row 0: indices 0,1,2,3,0,1,2,3 (CORRECT: generates 0/1/2/3)
    ...
]

Phase C: "Non-Flat" Anti-Regression Test

We createtest_framebuffer_not_flat_0456.pywhich validates that the RGB framebuffer has at least 3 unique colors. This test prevents us from "by chance" passing through an entire color again in the future.

Design Decisions

  • DO NOT touch the core:The plan explicitly specifies that we should NOT modify the core, just the tests. The objective is to validate that the tests are well designed before assuming bugs in the core.
  • Reproducible evidence:The 2bpp sanity test is pure (does not depend on the core) and demonstrates mathematically which patterns generate which indices.
  • Robust Asserts:Palette tests do not verify exact RGB values, just that there are multiple different colors and changing the palette rearranges the colors.

Affected Files

  • tests/test_tile_2bpp_decode_sanity_0456.py(created) - 2bpp decoding sanity test
  • tests/test_palette_dmg_bgp_0454.py(modified) - Fixed tile pattern to 0x55/0x33
  • tests/test_palette_dmg_obj_0454.py(modified) - Fixed tile pattern to 0x55/0x33
  • tests/test_framebuffer_not_flat_0456.py(created) - "Non-flat" anti-regression test

Tests and Verification

Test Sanity 2bpp

Command: pytest -v tests/test_tile_2bpp_decode_sanity_0456.py -s

Result:✅ 3/3 tests pass

Evidence:

def decode_2bpp_line(byte_low: int, byte_high: int) -> list[int]:
    """Decodes a 2bpp tile line to color indices (0..3)."""
    color_indexes = []
    for i in range(8):
        bit_pos = 7 - i # Bit position (MSB first)
        bit_low = (byte_low >> bit_pos) & 0x01
        bit_high = (byte_high >> bit_pos) & 0x01
        color_idx = bit_low | (bit_high<< 1)
        color_indices.append(color_idx)
    return color_indices

Corrected Palette Tests

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

Result:❌ 0/2 tests pass (expected: the core has a real bug)

Evidence of failure:

FAILED tests/test_palette_dmg_bgp_0454.py::test_dmg_bgp_palette_mapping
AssertionError: Flat frame: only 2 unique colors (expected ≥3 with pattern 0x55/0x33)
assert 2 >= 3
 + where 2 = len({(85, 85, 85), (255, 255, 255)})

FAILED tests/test_palette_dmg_obj_0454.py::test_dmg_obj_palette_mapping
AssertionError: Flat sprite: only 1 unique colors (expected ≥2 with pattern 0x55/0x33)
assert 1 >= 2
 + where 1 = len({(0, 0, 0)})

Interpretation:The tests now fail because the core has a real bug (flat RGB framebuffer). The sanity test confirms that the 0x55/0x33 pattern generates indices 0/1/2/3 correctly, so the problem It is NOT the design of the tests, but rather the index→RGB conversion in the core.

"Non-Flat" Anti-Regression Test

Command: pytest -v tests/test_framebuffer_not_flat_0456.py

Result:❌ 0/1 tests pass (expected: the core has a real bug)

Compiled C++ module validation:The tests callppu.get_framebuffer_rgb()which returns the converted RGB framebuffer from the index framebuffer. The test validates that there are at least 3 unique colors in the RGB framebuffer.

Sources consulted

  • Pan Docs: "Tile Data" - 2bpp tile format
  • GBEDG: "Tile Format" - Tiles decoding

Educational Integrity

What I Understand Now

  • 2bpp decoding:The 2bpp format requires 2 bytes per row of 8 pixels. The low byte contains the least significant bit and the high byte contains the most significant bit. The formulacolor_idx = bit_low | (bit_high<< 1)generates indices 0-3.
  • Tile Patterns:Not all patterns generate all 4 indices. The 0x00/0xFF pattern generates constant index (=2), while 0x55/0x33 generates the 4 indices (0/1/2/3).
  • Test Design:To validate palettes, the tile used must generate the 4 indexes. If the tile generates only 1 constant index, the test cannot distinguish if the palette works.
  • Guardrails:Before touching the core, we formally validate that the tests are correct designed. The 2bpp sanity test is pure (does not use the core) and mathematically demonstrates the behavior.

What remains to be confirmed

  • Bug in the core:The fixed tests fail because the RGB framebuffer is flat. The next step will be to investigate the index→RGB conversion in the core (PPU or renderer).
  • RGB conversion:We need to verify that the functionconvert_framebuffer_to_rgb()correctly applies BGP/OBP0/OBP1 palettes to color indices.

Hypotheses and Assumptions

Confirmed hypothesis:The 0x00/0xFF pattern used in the original tests generated constant index, causing false positives. The fixed tests now detect a real bug in the core.

Next Steps

  • [ ] Investigate the index→RGB conversion in the core (PPU or renderer)
  • [ ] Verify thatconvert_framebuffer_to_rgb()correctly applies BGP/OBP0/OBP1 palettes
  • [ ] Fix flat RGB framebuffer bug in core
  • [ ] Re-run the corrected palette tests to validate the fix