⚠️ 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 0396: Fix BGP Consistent and Rendering Respecting Game Palette

Date:2025-12-31 |StepID:0396 |State: VERIFIED

Executive Summary

Step 0395 identified two critical issues:Inconsistent BGP(render_bg() forced 0xE4 while other functions read from MMU) andFrame 676 white(completely white framebuffer even though VRAM had 14.2% TileData). This step fixes the inconsistency by removing the BGP=0xE4 hardcode and having render_bg() read BGP from the MMU, respecting the palette that the game configures.

Key results:

  • Tetris DX:BGP changes from 0xE4 → 0x00 in Frame 577 (intentional for fade out), then 0x00 → 0xE4 in Frame 675.
  • Zelda DX:BGP=0x00 from Frame 1 (intentional white screen during loading).
  • Frame 676 (Tetris):BGP=0xE4, vram_has_tiles=0, empty tilemap (0x00), explaining white framebuffer.
  • Frame 676 (Zelda):BGP=0x00, vram_is_empty_=1, tilemap with 0x7F (empty tile), explaining white framebuffer.

Hardware Concept

BGP Registration (Background Palette - 0xFF47)

According toBread Docs, the BGP register (0xFF47) controls how the Background color indices (0-3) are mapped to the final display colors on Game Boy Classic (DMG). Each pair of bits in BGP represents the final color for an index:

BGP = 0xE4 = 11 10 01 00 (binary)
  Index 3 → Color 3 (black)
  Index 2 → Color 2 (dark gray)
  Index 1 → Color 1 (light gray)
  Index 0 → Color 0 (white)

Common values:

  • 0xE4: Identity mapping (standard, used by most games)
  • 0xFC: Post-BIOS (initial value after bootrom)
  • 0x00: Everything maps to white (used for fade out or transitions)

Identified problem:The original code inrender_bg()(line 2208) forcedBGP=0xE4hardcoded, ignoring the value the game wrote to the MMU. This caused inconsistency with other functions (lines 3803, 3915, 4459) that did read BGP from MMU.

Solution:Read BGP from MMU consistently in all rendering functions, respecting the value that the game configures.

Identified Problem

1. Inconsistent BGP

Original code:

// render_bg() line 2208 (BEFORE)
uint8_t bgp = 0xE4;  // Hardcoded

// Other functions (lines 3803, 3915, 4459)
uint8_t bgp = mmu_->read(IO_BGP);  // Read from MMU

Consequence:If the game wroteBGP=0x00to the MMU (for fade out),render_bg()ignored the change and continued using0xE4, while other functions used0x00, causing visual inconsistency.

2. Frame 676 White (Tetris DX)

Step 0395 detected that Frame 676 had a completely white framebuffer (0=23040) even though VRAM had 14.2% TileData. The Step 0396 diagnosis revealed:

  • BGP:0xE4 (correct)
  • vram_has_tiles:0 (VRAM still considered empty by the detection system)
  • Tilemap:First 10 tiles = 0x00 (empty tilemap)
  • Tiledata:First 16 bytes = 0x00 (empty tile 0)

Conclusion:The white framebuffer on Frame 676 is correct because the tilemap points to empty tiles (0x00), not because BGP is wrong.

3. Frame 676 White (Zelda DX)

Step 0396 Diagnosis:

  • BGP:0x00 (everything maps to white - game intention)
  • vram_is_empty_:1 (VRAM still empty)
  • Tilemap:First 10 tiles = 0x7F (empty tile in signed addressing mode)
  • Tiledata:First 16 bytes = 0x00 (empty tile 0)

Conclusion:The white framebuffer on Frame 676 (Zelda) is intentional - the game usesBGP=0x00for white screen during charging.

Implementation

1. BGP Consistent Read from MMU

Archive: src/core/cpp/PPU.cpp(line 2203-2229)

// --- Step 0396: CONSISTENT BGP FROM MMU ---
// Read BGP from MMU to respect the palette that the game configures.
// Previously we forced 0xE4, but this caused inconsistency when
// the game wrote other values (e.g. 0x00 for fade out).
static uint8_t last_bgp = 0xFF;
uint8_t bgp = mmu_->read(IO_BGP);

// Limited log of BGP changes (only in LY=0, max 10 changes)
if (bgp != last_bgp && ly_ == 0) {
    static int bgp_change_log_count = 0;
    if (bgp_change_log_count< 10) {
        bgp_change_log_count++;
        printf("[PPU-BGP-CHANGE] Frame %llu | BGP: 0x%02X ->0x%02X\n", 
               frame_counter_ + 1, last_bgp, bgp);
    }
    last_bgp = bgp;
}

// Limited warning if BGP=0x00 (everything maps to white - may be intentional)
if (bgp == 0x00 && ly_ == 0) {
    static int bgp_zero_warning_count = 0;
    if (bgp_zero_warning_count< 5) {
        bgp_zero_warning_count++;
        printf("[PPU-BGP-WARNING] Frame %llu | BGP=0x00 (todo mapea a blanco) - "
               "¿Intencional del juego?\n", frame_counter_ + 1);
    }
}

2. Frame 676 Specific Diagnostic

Archive: src/core/cpp/PPU.cpp(line 2231-2257)

// --- Step 0396: Specific Frame 676 Diagnostics ---
// Frame 676 showed white framebuffer even though VRAM had 14.2% TileData
if (frame_counter_ + 1 == 676 && ly_ == 0) {
    printf("[FRAME676-DIAG] === FRAME DIAGNOSIS 676 ===\n");
    printf("[FRAME676-DIAG] Current BGP: 0x%02X\n", bgp);
    printf("[FRAME676-DIAG] vram_is_empty_: %d\n", vram_is_empty_ ? 1 : 0);
    printf("[FRAME676-DIAG] vram_has_tiles: %d\n", vram_has_tiles ? 1 : 0);
    printf("[FRAME676-DIAG] LCDC: 0x%02X (BG Enable: %d)\n", 
           lcdc, (lcdc & 0x01) ? 1 : 0);
    printf("[FRAME676-DIAG] Tilemap base: 0x%04X\n", tile_map_base);
    printf("[FRAME676-DIAG] Base tiledata: 0x%04X\n", tile_data_base);
    
    // Check first 10 tile IDs of the tilemap
    printf("[FRAME676-DIAG] First 10 tile IDs: ");
    for (int i = 0; i< 10; i++) {
        uint8_t tile_id = mmu_->read(tile_map_base + i);
        printf("0x%02X ", tile_id);
    }
    printf("\n");
    
    // Check first 16 bytes of the first tile
    printf("[FRAME676-DIAG] First 16 bytes of tile 0: ");
    for (int i = 0; i< 16; i++) {
        uint8_t tile_byte = mmu_->read(tile_data_base + i);
        printf("0x%02X ", tile_byte);
    }
    printf("\n[FRAME676-DIAG] === END DIAGNOSIS ===\n");
}

Tests and Verification

Commands Executed

cd /media/fabini/8CD1-4C30/ViboyColor
python3 setup.py build_ext --inplace
timeout 30s python3 main.py roms/tetris_dx.gbc > logs/step0396_tetris_dx.log 2>&1
timeout 30s python3 main.py roms/Oro.gbc > logs/step0396_zelda_dx.log 2>&1

Results: BGP Changes

Tetris DX:

[PPU-BGP-CHANGE] Frame 1 | BGP: 0xFF -> 0xE4
[PPU-BGP-CHANGE] Frame 577 | BGP: 0xE4 -> 0x00
[PPU-BGP-WARNING] Frame 577 | BGP=0x00 (all maps to white) - Game intent?
[PPU-BGP-WARNING] Frame 578 | BGP=0x00 (all maps to white) - Game intent?
[PPU-BGP-WARNING] Frame 579 | BGP=0x00 (all maps to white) - Game intent?
[PPU-BGP-WARNING] Frame 580 | BGP=0x00 (all maps to white) - Game intent?
[PPU-BGP-WARNING] Frame 581 | BGP=0x00 (all maps to white) - Game intent?
[PPU-BGP-CHANGE] Frame 675 | BGP: 0x00 -> 0xE4
[PPU-BGP-CHANGE] Frame 732 | BGP: 0xE4 -> 0x00

Zelda DX:

[PPU-BGP-CHANGE] Frame 1 | BGP: 0xFF -> 0x00
[PPU-BGP-WARNING] Frame 1 | BGP=0x00 (all maps to white) - Game intent?
[PPU-BGP-WARNING] Frame 2 | BGP=0x00 (all maps to white) - Game intent?
[PPU-BGP-WARNING] Frame 3 | BGP=0x00 (all maps to white) - Game intent?
[PPU-BGP-WARNING] Frame 4 | BGP=0x00 (all maps to white) - Game intent?

Results: Frame 676 Diagnostic

Tetris DX:

[FRAME676-DIAG] === FRAME DIAGNOSIS 676 ===
[FRAME676-DIAG] BGP current: 0xE4
[FRAME676-DIAG] vram_is_empty_: 0
[FRAME676-DIAG] vram_has_tiles: 0
[FRAME676-DIAG] LCDC: 0x91 (BG Enable: 1)
[FRAME676-DIAG] Base tilemap: 0x9800
[FRAME676-DIAG] Base tiledata: 0x8000
[FRAME676-DIAG] First 10 tile IDs: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 
[FRAME676-DIAG] First 16 bytes of tile 0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 
[FRAME676-DIAG] === END DIAGNOSIS ===

Zelda DX:

[FRAME676-DIAG] === FRAME DIAGNOSIS 676 ===
[FRAME676-DIAG] BGP current: 0x00
[FRAME676-DIAG] vram_is_empty_: 1
[FRAME676-DIAG] vram_has_tiles: 0
[FRAME676-DIAG] LCDC: 0xE3 (BG Enable: 1)
[FRAME676-DIAG] Base tilemap: 0x9800
[FRAME676-DIAG] Base tiledata: 0x9000
[FRAME676-DIAG] First 10 tile IDs: 0x7F 0x7F 0x7F 0x7F 0x7F 0x7F 0x7F 0x7F 0x7F 0x7F 
[FRAME676-DIAG] First 16 bytes of tile 0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 
[FRAME676-DIAG] === END DIAGNOSIS ===

Validation

  • Compilation:Successful with no linter errors
  • Consistent BGP:render_bg() reads BGP from MMU correctly
  • BGP changes:Detected and logged (Tetris: 0xE4→0x00→0xE4, Zelda: 0x00 constant)
  • Frame 676 (Tetris):White framebuffer explained by empty tilemap (0x00), not by BGP
  • Frame 676 (Zelda):White framebuffer explained by intentional BGP=0x00
  • Native Validation:C++ module compiled and executed successfully

Key Findings

1. Dynamic BGP in Real Games

Games change BGP dynamically for visual effects:

  • Tetris DX:Use BGP=0x00 for ~98 frames (577-675) to fade out between screens
  • Zelda DX:Use BGP=0x00 at startup for white screen during loading

Implication:The hardcode of BGP=0xE4 was incorrect and caused visual inconsistency.

2. Frame 676 Was Not a BGP Bug

The white framebuffer on Frame 676 (Tetris) was not caused by inconsistent BGP, but by:

  • Empty tilemap:First 10 tile IDs = 0x00
  • Tile 0 empty:First 16 bytes = 0x00
  • vram_has_tiles=0:Detection system still did not detect loaded tiles

Conclusion:The rendering is correct. The problem is in the VRAM loading timing or in the detection of loaded tiles.

3. BGP=0x00 is Legitimate

According to Pan Docs, BGP=0x00 is a valid value that maps all color indices to white. Some games use it intentionally to:

  • Fade out (Tetris DX)
  • White screen during charging (Zelda DX)
  • Transitions between scenes

Implication:The emulator must respect BGP=0x00 without forcing a minimum.

Summary Table: BGP Changes Detected

Game Frame BGP Previous BGP New Interpretation
Tetris DX 1 0xFF 0xE4 Post-BIOS Initialization
Tetris DX 577 0xE4 0x00 Fade out (transition)
Tetris DX 675 0x00 0xE4 End of fade out
Tetris DX 732 0xE4 0x00 New fade out
Zelda DX 1 0xFF 0x00 White screen during charging

Next Steps

With BGP now consistent, the next steps should focus on:

  1. Improve loaded VRAM detection:vram_has_tiles=0 on Frame 676 although VRAM has 14.2% TileData
  2. Check VRAM loading timing:Why does the tilemap point to empty tiles in Frame 676?
  3. Implement verification of loaded tiles:Detect when the game loads non-empty tiles
  4. Optimize rendering:Now that BGP is correct, check for other palette issues

Modified Files

  • src/core/cpp/PPU.cpp- Consistent BGP read from MMU, Frame 676 diagnostics
  • logs/step0396_tetris_dx.log- Tetris DX execution log (30s)
  • logs/step0396_zelda_dx.log- Zelda DX execution log (30s)
  • build_log_step0396.txt- Compilation log

References