This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Step 0260: MBC1 ROM Banking
Summary
This Step implements basic MBC1 (Memory Bank Controller 1) support in the C++ MMU to allow large games (>32KB) to access their ROM banks. The Step 0259 diagnostic confirmed that Pokémon Red was writing zeros to VRAM because it was trying to read graphics from unmapped ROM banks. With MBC1 implemented, games can select ROM banks and read the correct data.
Problem Solved:Pokémon Red (1024KB ROM) was trying to copy graphics from bank 2, 3, etc., but our MMU only had bank 0 mapped. The game would read zeros or garbage, and copy those zeros to VRAM, resulting in a green screen. With MBC1, the game can select the correct bank and read the actual graphical data.
Hardware Concept
MBC1 (Memory Bank Controller 1)
Game Boy cartridges can have different ROM sizes:
- ROM ONLY (32KB):It fits integer in the address space `0x0000-0x7FFF`. You don't need MBC.
- MBC1 (>32KB):Use a Memory Bank Controller to swap ROM banks. The space `0x0000-0x3FFF` always maps to Bank 0 (fixed), but the space `0x4000-0x7FFF` can map to different banks (1, 2, 3, etc.) by writing to special registers of the MBC.
MBC1 Banking Control
The MBC1 controls bank switching by writing to the ROM range (which is normally read-only):
- 0x2000-0x3FFF:ROM bank selection. The written value (bits 0-4) selects the bank that will appear in `0x4000-0x7FFF`. Note: Bank 0 is treated as bank 1.
- 0x0000-0x1FFF:External RAM enable/disable (ignored in this basic implementation).
MBC1 Memory Mapping
- 0x0000-0x3FFF:Always maps to Bank 0 (fixed). It can't be changed.
- 0x4000-0x7FFF:Maps to the selected bank by writing to `0x2000-0x3FFF`. Each bank is 16KB (0x4000 bytes).
Fountain:Pan Docs - "MBC1", "Memory Bank Controllers", "Cartridge Types"
Implementation
1. Modification in `MMU.hpp`
Two private members were added to support MBC1:
/**
* --- Step 0260: MBC1 ROM BANKING ---
* Stores the entire ROM cartridge (can be >32KB).
* Used to access switchable ROM banks.
*/
std::vector<uint8_t> rom_data_;
/**
* --- Step 0260: MBC1 ROM BANKING ---
* ROM bank currently selected for range 0x4000-0x7FFF.
* Initialized to 1 (bank 0 is always mapped to 0x0000-0x3FFF).
*Note: In MBC1, bank 0 is treated as bank 1 when selected.
*/
uint8_t current_rom_bank_;
2. Modification in `MMU.cpp` (Constructor)
Initialize `current_rom_bank_ = 1` in the constructor:
MMU::MMU() : memory_(MEMORY_SIZE, 0), ppu_(nullptr), timer_(nullptr),
joypad_(nullptr), debug_current_pc(0), current_rom_bank_(1) {
3. Modification in `MMU.cpp` (`load_rom` Method)
Changed to load the entire ROM into `rom_data_` instead of just 32KB:
void MMU::load_rom(const uint8_t* data, size_t size) {
// Resize rom_data_ and copy the entire ROM
rom_data_.resize(size);
std::memcpy(rom_data_.data(), data, size);
// Also copy bank 0 (first 16KB) to memory_ for compatibility
size_t bank0_size = (size > 0x4000) ? 0x4000: size;
std::memcpy(memory_.data(), data, bank0_size);
// Initialize the current bank to 1
current_rom_bank_ = 1;
printf("[MBC1] ROM loaded: %zu bytes (%zu banks)\n", size, size / 0x4000);
}
4. Modification in `MMU.cpp` (`read` Method)
Added logic to read from the correct bank based on address:
// --- Step 0260: MBC1 ROM BANKING ---
// If there is ROM data loaded, use banking
if (!rom_data_.empty()) {
if (addr >= 0x0000 && addr<= 0x3FFF) {
// Banco 0 fijo: leer desde el principio de la ROM
if (addr < rom_data_.size()) {
return rom_data_[addr];
}
return 0x00; // Fuera de rango
} else if (addr >= 0x4000 && addr<= 0x7FFF) {
// Banco conmutable: calcular offset
// Offset = (banco * 0x4000) + (addr - 0x4000)
size_t bank_offset = static_cast<size_t>(current_rom_bank_) * 0x4000;
size_t rom_addr = bank_offset + (addr - 0x4000);
if (rom_addr < rom_data_.size()) {
return rom_data_[rom_addr];
}
return 0x00; // Fuera de rango
}
}
5. Modification in `MMU.cpp` (`write` Method)
Added logic to intercept writes to `0x2000-0x3FFF` and change the ROM bank:
// --- Step 0260: MBC1 ROM BANK SELECTION ---
// Intercept writes at 0x2000-0x3FFF to change ROM bank
if (addr >= 0x2000 && addr<= 0x3FFF) {
// Selección de banco ROM (bits 0-4 del valor escrito)
uint8_t bank = value & 0x1F; // Máscara para bits 0-4
// En MBC1, el banco 0 se trata como banco 1
if (bank == 0) {
bank = 1;
}
// Validar que el banco no exceda el tamaño de la ROM
size_t max_banks = rom_data_.size() / 0x4000;
if (max_banks >0 && bank >= max_banks) {
bank = max_banks - 1; // Limit to last available bank
}
current_rom_bank_ = bank;
// Diagnostic log (first 10 times only)
static int bank_change_counter = 0;
if (bank_change_counter< 10) {
printf("[MBC1] PC:%04X ->ROM Bank changed to %d (max: %zu)\n",
debug_current_pc, current_rom_bank_, max_banks);
bank_change_counter++;
}
}
Design Decisions
- Full ROM Storage:The entire ROM is stored in `rom_data_` to allow access to any bank, not just the first 32KB.
- Compatibility with existing code:Bank 0 is also copied to `memory_[0x0000-0x3FFF]` to maintain compatibility with code that directly accesses `memory_`.
- Bank validation:It is validated that the selected bank does not exceed the size of the ROM to avoid out-of-range accesses.
- Limited log:The bank change log is limited to the first 10 times so as not to saturate the output.
Affected Files
src/core/cpp/MMU.hpp- Added `rom_data_` and `current_rom_bank_` members to support MBC1 (Step 0260).src/core/cpp/MMU.cpp- Modified constructor, `load_rom()`, `read()` and `write()` to implement basic MBC1 (Step 0260).
Tests and Verification
MBC1 Validation:
- Recompilation:Execute
.\rebuild_cpp.ps1to recompile the C++ extension. - Execution:Execute
python main.py roms/pkmn.gb(Pokémon Red is ideal because it has 1024KB of ROM and requires MBC1). - Log observation:Search the log:
[MBC1] ROM loaded: X bytes (Y banks)- Confirm that the ROM was loaded successfully.[MBC1] PC:XXXX -> ROM Bank changed to N- Confirm that the game is changing banks.[VRAM] PC:XXXX -> Write VRAM [XXXX] = XX- The values should be other than `00` now.
- Visual Observation:If MBC1 is working correctly, you should see graphics on the screen (with the debug palette active).
Test Command:
.\rebuild_cpp.ps1
python main.py roms/pkmn.gb
Expected Result:
- The log should show that the ROM was loaded with multiple banks.
- The log should show bank changes when the game tries to access different parts of the ROM.
- VRAM logs should show values other than `00` (actual graphics data).
- If everything works, you should see the Pokémon intro on the screen.
C++ Compiled Module Validation:The code runs in compiled C++, so you need to recompile the extension before running. MBC1 runs in real time during emulation, allowing games to access correct ROM banks.
Sources consulted
- Bread Docs:MBC1
- Bread Docs:Cartridge Header
- Bread Docs:Memory Map
- Bread Docs:Memory Bank Controllers
Educational Integrity
What I Understand Now
- MBC1 Banking:Large cartridges (>32KB) use MBC1 to swap ROM banks. The space `0x0000-0x3FFF` always maps to Bank 0 (fixed), but the space `0x4000-0x7FFF` can map to different banks by writing to `0x2000-0x3FFF`.
- Empty VRAM issue:If the game tries to read graphics from bank 2, 3, etc., but only bank 0 was loaded, it will read garbage or zeros. The CPU copies those "zeros" to VRAM, resulting in a green screen.
- MBC1 Solution:By implementing MBC1, the game can select the correct bank and read the actual graphical data. This allows large games to load their graphics correctly.
- ROM Storage:To support MBC1, we need to store the entire ROM (not just 32KB) in a separate vector (`rom_data_`) and calculate the correct offset based on the selected bank.
What remains to be confirmed
- Real Operation:Run the diagnosis with Pokémon Red and verify that the graphics appear on the screen. If the graphs appear, we confirm that MBC1 is working correctly.
- Values in VRAM:Verify that the VRAM logs show values other than `00` (real graphics data) after implementing MBC1.
- RAM Banking:MBC1 also supports external RAM (for saving games), but for now we only implement ROM banking. RAM banking can be added later if necessary.
Hypotheses and Assumptions
Main Hypothesis:Pokémon Red was writing zeros to VRAM because it was trying to read graphics from unmapped ROM banks. With MBC1 implemented, the game can select the correct bank and read the actual graphics data, allowing the graphics to appear on screen.
Assumption:We assume that the basic implementation of MBC1 (ROM banking only, no RAM banking) is enough for large games to load their graphics. If there are problems, we can add support for RAM banking later.
Next Steps
- [ ] Recompile:
.\rebuild_cpp.ps1 - [ ] Execute:
python main.py roms/pkmn.gb(Pokémon Red is ideal because it has 1024KB of ROM). - [ ] Observe the logs:
- Does `[MBC1] ROM loaded: X bytes (Y banks)` appear?Confirm that the ROM was loaded successfully.
- Does `[MBC1] PC:XXXX -> ROM Bank changed to N` appear?Confirms that the game is changing banks.
- Do the `[VRAM]` logs show values other than `00`?Confirms that the CPU is copying actual graphics data.
- [ ] Visual Observation:If MBC1 is working correctly, you should see graphics on the screen (with the debug palette active). If you watch the Pokémon intro, MBC1 works!
- [ ] If there are problems:
- Verify that the selected bank does not exceed the size of the ROM.
- Verify that the bank offset calculation is correct.
- Verify that the game is writing to `0x2000-0x3FFF` to change banks.