Video summary
Gameboy Emulator Development - Part 16
Main summary
Key takeaways
Summary of the video (Gameboy Emulator Dev Part 16)
This installment expands the emulator to support MBC1 (Memory Bank Controller 1), enabling correct “bank switching” so many classic Game Boy games load and run properly (e.g., Super Mario Land, Mega Man, Zelda, etc.). Earlier parts already implemented the PPU; this video focuses on cartridge handling.
1) MBC1 concepts from the cartridge header
The speaker reviews the cartridge header fields needed to implement MBC1:
-
ROM banking (read-only)
- ROM banks are mapped into address ranges (a fixed “static” area and switchable banks).
-
RAM banking (battery-backed RAM)
- MBC1 can support up to 32KB RAM via multiple RAM banks.
- RAM is often battery backed, and the emulator uses the header’s battery info to decide whether to persist save data.
-
Register behavior
- Writes to specific address ranges act as register writes controlling:
- RAM enable/disable
- ROM bank number
- RAM bank number
- Banking mode select (ROM banking vs RAM banking / “advanced” mode)
- Important rules mentioned:
- Some register values are effectively masked to a limited bit width (bottom bits only).
- Writing zero to the ROM bank register is treated as bank one.
- Writes to specific address ranges act as register writes controlling:
-
Simplification
- The implementation deliberately ignores/doesn’t fully implement “advanced” or multi-cart special cases, aiming for ~99% of typical games.
2) Emulator code changes (cart.c / cart.h)
The speaker implements MBC1 support by extending the cartridge subsystem.
New state/fields added for MBC1
-
Flags like:
- ramEnabled
- ramBanking mode
-
Bank tracking:
romBankValue,ramBankValue- current selected
ramBank - an array/list of pointers for up to 16 RAM banks
-
Battery save support:
- whether the cartridge has a battery
- whether the emulator needs to save (dirty flag)
New helper functions (placeholders + detection)
cart_need_save(...)cart_mbc_one(...)- determines whether the cartridge is MBC1 (ROM type between 1 and 3, based on header values)
cart_battery(...)- checks header type to detect battery-backed behavior (speaker notes MBC1 battery-related type)
Bank setup
A function like cart_setup_banking(...):
- allocates RAM banks based on header RAM size codes, with mapping in the code:
0x02→ 1 bank (8KB)0x03→ 4 banks (32KB)0x04/0x05→ 16 banks- the speaker also notes that one code path corresponds to 8 banks, while the max supported configuration here is 16.
- initializes:
- RAM bank 0 as current
- ROM bank 1 as initial
romBankXpointer base
3) Read/write memory mapping logic
Reads (cart_read)
- 0x0000–0x3FFF: always return from the fixed ROM region:
ctx->romData[address]
- For other regions, the code branches if the cartridge is MBC1:
- 0xA000–0xBFFF:
- only returns RAM content if RAM is enabled
- uses current selected RAM bank
- switchable ROM bank region:
- uses
romBankX + (address - 0x4000)
- uses
- 0xA000–0xBFFF:
- If not MBC1, it returns error-like values (
0xFF).
The speaker later fixes a regression where non-MBC1 games (e.g., Tetris) were broken by an overly broad condition, then adjusts the condition so non-MBC cartridges behave correctly.
Writes (cart_write)
Writes are treated as register writes for MBC1:
-
0x0000–0x1FFF: RAM enable/disable
- writing
0x0Aenables RAM, writing0x00disables it
- writing
-
0x2000–0x3FFF: ROM bank number
- masks to 5 bits
- handles “0 becomes 1”
- updates
romBankXpointer to the selected bank
-
0x4000–0x5FFF: RAM bank number / upper bits
- masks to relevant bits (mentioned as 2-bit bank selection)
- bank switching happens depending on banking mode
- (speaker chooses simplified behavior)
-
0x6000–0x7FFF: banking mode select
- bottom bit selects mode
- in the simplified approach, it mostly controls whether RAM banking is active
-
0xA000–0xBFFF: write to RAM
- only if RAM bank is enabled
- marks the cartridge as needing save (dirty flag)
4) Battery save implementation (save/load .battery files)
The video completes persistent saves for battery-backed cartridges.
Key implementation details
-
Adds functions/placeholders:
cart_battery_loadcart_battery_save
-
Save file naming:
- battery save file is derived from the ROM filename:
rom_filename + ".battery"
- battery save file is derived from the ROM filename:
-
File format/behavior:
- Load uses
fopen(..., "rb")andfreadinto RAM bank storage - Save uses
fopen(..., "wb")andfwriteRAM bank contents
- Load uses
-
Triggering saves:
cart_battery_saveis called when needed (dirty flag set on RAM writes)- speaker mentions save checks tied to the emulator’s periodic loop (e.g., “once per second” via FPS/update logic)
Demonstration
- Starting Zelda shows no save at first
- After creating a save, restarting the emulator shows the save persists
- Resuming confirms saves are functioning correctly
5) Testing and bug fixes during development
- Zelda (MBC1 + battery) works and save persists after implementation.
- Regression fix: non-MBC games (example: Tetris) were broken due to an incorrect condition in cart read logic; adjusting the condition restored them.
- Additional MBC1 non-battery games (example: Batman, Mega Man, Contra) run successfully.
Unrelated gameplay bug observed: Contra diagonal shots
- Contra can’t shoot diagonally, likely due to gamepad input handling logic (
gamepad.c) usingelse ifso only one button press is detected at a time. - Proposed/fix approach:
- remove
else ifchains so multiple simultaneous button states can register.
- remove
6) What the series is likely to do next
The speaker suggests next steps may include:
- Save/restore emulator state (“save states”)
- described as “not too difficult” compared to battery RAM
- Possibly later:
- audio support (not guaranteed; depends on interest)
Main speakers/sources
- Primary speaker/source: the creator of “low-level-devil” (game boy emulator development series)
- External references: no other explicit sources are cited beyond referencing Game Boy cartridge header/spec concepts and MBC1 register behavior.