← Return to Index

Step 0407: Diagnostic + Fix: MBC/Banking and TileData Load (pkmn.gb + Oro.gbc)

Aim

After the success of the CGB RGB render pipeline in Step 0406, the actual gameplay lockpkmn.gb(DMG) andGold.gbc(CGB) is thatthey don't load TileData(0x8000-0x97FF) →gameplay_state=NO.

This Step performs alimited diagnosisof the systemCartridge/MBC/Bankingin core C++ (MMU.cpp) for:

  • Instrument MBC Writes (0x0000-0x7FFF)
  • Instrument reads of flashed ROM (0x4000-0x7FFF)
  • Correlate with TileData load to VRAM
  • Apply immediate fixes if clear bugs are detected
  • Validate with both target ROMs + baseline (Tetris DX)

Hardware Concept

Memory Bank Controllers (MBC)

Fountain:Pan Docs - "Memory Bank Controllers (MBC1/MBC3/MBC5)"

Game Boy cartridges use banking controllers (MBC) to access larger ROMs than the 32KB address space:

MBC Control Ranges

  • 0x0000-0x1FFF:RAM Enable (write 0x0A to enable)
  • 0x2000-0x3FFF:ROM Bank Number (low bits)
  • 0x4000-0x5FFF:ROM Bank Number (high bits) or RAM Bank Select
  • 0x6000-0x7FFF:Banking Mode Select (MBC1) or Latch Clock (MBC3)

MBC1 - Banking Mode

  • Mode 0 (ROM Banking):
    • 0x0000-0x3FFF: Bank 0 fixed
    • 0x4000-0x7FFF: Banks 1-127 (high bits + low bits)
  • Mode 1 (RAM Banking):
    • 0x0000-0x3FFF: Bank selected by high bits
    • 0x4000-0x7FFF: Bank selected by low bits

MBC3 - ROM + RTC

MBC3 supports up to 128 ROM banks and 4 RAM banks, plus aReal-Time Clock (RTC)optional. The RTC is mapped to external RAM space (0xA000-0xBFFF) using special registers (0x08-0x0C).

Bank Normalization

The selected bank mustnormalizeusing module (bank % rom_bank_count_). Furthermore, bank 0should never be mapped to 0x4000-0x7FFF(forced to 1).

Implementation

Task 1: Bounded Instrumentation of MBC/Banking

Archive: src/core/cpp/MMU.cpp

1A. MBC Writes Monitor (0x0000-0x7FFF)

Added in functionMMU::write()before the MBC switch-case:

// --- Step 0407: MBC writes full monitor (0x0000-0x7FFF) ---
if (addr< 0x8000) {
    static int mbc_write_count = 0;
    const int MBC_WRITE_LIMIT = 200;  // Límite para evitar saturación

    if (mbc_write_count < MBC_WRITE_LIMIT) {
        const char* range_name = nullptr;
        if (addr < 0x2000) {
            range_name = "RAM-ENABLE";
        } else if (addr < 0x4000) {
            range_name = "BANK-LOW";
        } else if (addr < 0x6000) {
            range_name = "BANK-HIGH/RAM";
        } else {
            range_name = "MODE/LATCH";
        }

        printf("[MBC-WRITE-0407] %s | addr:0x%04X val:0x%02X | PC:0x%04X | "
               "MBC:%d | bank0:%d bankN:%d | mode:%d\n",
               range_name, addr, value, debug_current_pc,
               static_cast<int>(mbc_type_), bank0_rom_, bankN_rom_, mbc1_mode_);
        mbc_write_count++;
    }
}

1B. Banked ROM Read Monitor (0x4000-0x7FFF)

Reactivated in functionMMU::read()with improved logic:

// --- Step 0407: Bounded monitor of flashed ROM reads ---
static int bank_read_count = 0;
static uint16_t last_logged_bank = 0xFFFF;
const int BANK_READ_LIMIT = 150;

if (bank_read_count< BANK_READ_LIMIT) {
    // Loguear: (1) primeras 30 lecturas, (2) cuando bank cambia
    bool should_log = (bank_read_count < 30) || 
                     (bankN_rom_ != last_logged_bank && bank_read_count < 100);
    
    if (should_log) {
        printf("[BANK-READ-0407] addr:0x%04X bank:%d offset:0x%04X ->val:0x%02X | PC:0x%04X\n",
               addr, bankN_rom_, (uint16_t)(addr - 0x4000), val, debug_current_pc);
        bank_read_count++;
        last_logged_bank = bankN_rom_;
    }
}

1C. Correlation with TileData (0x8000-0x97FF)

Added in VRAM counting section (Step 0391):

if (addr >= 0x8000 && addr<= 0x97FF) {
    vram_tiledata_nonzero_writes_++;
    
    // --- Step 0407: Correlación TileData con ROM banking ---
    static int tiledata_correlation_count = 0;
    const int TILEDATA_CORRELATION_LIMIT = 50;
    if (tiledata_correlation_count < TILEDATA_CORRELATION_LIMIT) {
        printf("[TILEDATA-0407] addr:0x%04X val:0x%02X | PC:0x%04X | "
               "bank0:%d bankN:%d | MBC:%d | count:%d\n",
               addr, value, debug_current_pc, bank0_rom_, bankN_rom_,
               static_cast<int>(mbc_type_), vram_tiledata_nonzero_writes_);
        tiledata_correlation_count++;
    }
}

Task 2: Immediate Fix - Robust Normalization

2A. Normalization with Warnings

Improved functionnormalize_rom_bank():

uint16_t MMU::normalize_rom_bank(uint16_t bank) const {
    if (rom_bank_count_ == 0) {
        returnbank;
    }
    
    // Warning for out-of-range banks
    if (bank >= rom_bank_count_) {
        static int normalize_warn_count = 0;
        if (normalize_warn_count< 10) {
            printf("[MBC-WARN-0407] Banco solicitado %d >= rom_bank_count_ %zu\n",
                   bank, rom_bank_count_);
            normalize_warn_count++;
        }
    }
    
    uint16_t normalized = static_cast<uint16_t>(bank % rom_bank_count_);
    return normalized;
}

2B. Post-Normalization Verification

Added verification inupdate_bank_mapping()to ensure that:

  • bankN_rom_never be 0 (it is forced to 1)
  • Banks are in valid range[0, rom_bank_count_-1]
// For MBC1, MBC3, MBC5:
if (bankN_rom_ == 0) {
    bankN_rom_ = 1;
    printf("[MBC-FIX-0407] bankN_rom_ was 0, forced to 1\n");
}

// Final validation
if (bankN_rom_ >= rom_bank_count_) {
    bankN_rom_ = 1;
    printf("[MBC-CLAMP-0407] bankN_rom_ >= rom_bank_count_, clamped to 1\n");
}

Tests and Verification

Compilation

cd /media/fabini/8CD1-4C30/ViboyColor
python3 setup.py build_ext --inplace > build_log_step0407.txt 2>&1

Result:✅ Compilation successful (Exit code: 0)

Controlled Tests (30s with timeout)

#DMG target
timeout 30s python3 main.py roms/pkmn.gb > logs/step0407_pkmn_banking.log 2>&1

# CGB target
timeout 30s python3 main.py roms/Oro.gbc > logs/step0407_oro_banking.log 2>&1

# Baseline CGB
timeout 30s python3 main.py roms/tetris_dx.gbc > logs/step0407_tetris_dx_baseline.log 2>&1

Diagnostic Results

1. pkmn.gb (DMG - MBC3)

  • Banking works:MBC3 detected, bank changes successful (1→2→4→6→19→28)
  • ROM Reads:They work correctly in all banks
  • Tilemap:808 non-zero writes (12.6%)
  • TileData: tiledata_nonzero=0 - NO non-zero writing
  • Gameplay: gameplay_state=NO

Log extract:

[MBC-WRITE-0407] BANK-LOW | addr:0x2000 val:0x02 | PC:0x23F0 | MBC:3 | bank0:0 bankN:1 | mode:0
[BANK-READ-0407] addr:0x5876 bank:2 offset:0x1876 -> val:0xEA | PC:0x23F8
[VRAM-SUMMARY] tiledata_nonzero=0 tilemap_nonzero=808 total=9000

2. Oro.gbc (CGB - MBC3)

  • Banking works:MBC3 with RTC, multiple banks (1, 2, 5, 58...)
  • ROM Reads:They work correctly
  • Tilemap:2048/2048 (100%)
  • TileData: tiledata_nonzero=0 - NO non-zero writing
  • Gameplay: gameplay_state=NO

Log extract:

[MBC-WRITE-0407] RAM-ENABLE | addr:0x0000 val:0x0A | PC:0x40B3 | MBC:3 | bank0:0 bankN:5 | mode:0
[MBC-WRITE-0407] MODE/LATCH | addr:0x6000 val:0x01 | PC:0x0463 | MBC:3 | bank0:0 bankN:5 | mode:0
[VRAM-REGIONS] Frame 1200 | tiledata_nonzero=0/6144 (0.0%) | tilemap_nonzero=2048/2048 (100.0%) | gameplay_state=NO

3. tetris_dx.gbc (BASELINE - MBC1)

  • Banking works:MBC1, mode 0
  • ROM Reads:They work correctly
  • TileData: tiledata_nonzero=7521 - Correct charging
  • Gameplay: gameplay_state=YES
  • No regression:Step 0407 didn't break anything

Log extract:

[TILEDATA-0407] addr:0x8820 val:0xFF | PC:0x12C1 | bank0:0 bankN:30 | MBC:1 | count:1
[TILEDATA-0407] addr:0x8822 val:0xFF | PC:0x12C1 | bank0:0 bankN:30 | MBC:1 | count:2
[VRAM-SUMMARY] tiledata_nonzero=2058 tilemap_nonzero=0 total=3000
[VRAM-REGIONS] Frame 720 | tiledata_nonzero=1416/6144 (23.0%) | gameplay_state=YES

Critical Finding

⚠️ Banking is NOT the problem

The diagnosis confirms that the systemMBC/Banking works perfectlyso much in MBC1 (Tetris DX) and in MBC3 (Pokémon/Gold).

Comparative Table

ROM MBC Banking TileData Tilemap Gameplay
pkmn.gb MBC3 ✅ It works ❌ 0 bytes (0.0%) ✅ 808 (12.6%) ❌ NO
Gold.gbc MBC3+RTC ✅ It works ❌ 0 bytes (0.0%) ✅ 2048 (100%) ❌ NO
tetris_dx.gbc MBC1 ✅ It works ✅ 7521 bytes (56.6%) ✅ 2012 (98.2%) ✅ YES

Conclusion

The real problem is thatPokémon Red and Gold NEVER write non-zero data to TileData (0x8000-0x97FF), while Tetris DX does it from PC:0x12C1 in bank 30.

Possible Causes (for future Steps)

  1. VBLANK/STAT Timing:Games expect specific timing conditions that are not met
  2. MBC3 RTC:Pokémon/Gold uses MBC3 with RTC, which could have an incomplete stub
  3. DMG vs CGB Initialization:Differences in hardware initialization
  4. Wait Loops:Games are stuck in waiting loops waiting for specific conditions
  5. Tiles decompression:Games use decompression routines that fail silently

Modified Files

  • src/core/cpp/MMU.cpp- MBC Instrumentation + Robust Normalization
  • logs/step0407_pkmn_banking.log- pkmn.gb diagnostic log
  • logs/step0407_oro_banking.log- Oro.gbc diagnostic log
  • logs/step0407_tetris_dx_baseline.log- Tetris DX baseline log
  • build_log_step0407.txt- Compilation log

Next Steps

  1. Step 0408:Investigate VBLANK/STAT timing conditions that block TileData from loading
  2. Step 0409:Complete MBC3 RTC stub (if necessary)
  3. Step 0410:Analyze tile decompression routines in target ROMs