use super::cartridge::GbCartridge;
const HEADER_LOGO_LEN: usize = 48;
const CANONICAL_HEADER_LOGO_CRC32: u32 = 0x4619_5417;
fn is_multicart_rom(rom: &[u8]) -> bool {
if rom.len() != 64 * 0x4000 {
return false;
}
let logo_offset = 0x10 * 0x4000 + 0x104;
rom.get(logo_offset..logo_offset + HEADER_LOGO_LEN)
.is_some_and(|slice| crate::platform::crc32::crc32(&[slice]) == CANONICAL_HEADER_LOGO_CRC32)
}
pub struct Mbc1 {
rom: Vec<u8>,
ram: Vec<u8>,
rom_bank: u8,
secondary_bank: u8,
mode: bool,
ram_enabled: bool,
multicart: bool,
battery: bool,
}
impl Mbc1 {
pub fn new(rom: Vec<u8>, ram: Vec<u8>, battery: bool) -> Self {
let multicart = is_multicart_rom(&rom);
Self {
multicart,
rom,
ram,
rom_bank: 0,
secondary_bank: 0,
mode: false,
ram_enabled: false,
battery,
}
}
fn rom_bank_count(&self) -> usize {
(self.rom.len() / 0x4000).max(1)
}
fn effective_rom_bank(&self) -> usize {
let raw = (self.rom_bank & 0x1F) as usize;
if raw == 0 { 1 } else { raw }
}
fn bank1_index(&self) -> usize {
if self.multicart {
let upper = (self.secondary_bank as usize & 0x03) << 4;
(upper | (self.effective_rom_bank() & 0x0F)) & (self.rom_bank_count() - 1)
} else {
let upper = (self.secondary_bank as usize & 0x03) << 5;
(upper | self.effective_rom_bank()) & (self.rom_bank_count() - 1)
}
}
fn bank0_index(&self) -> usize {
if self.mode {
let shift = if self.multicart { 4 } else { 5 };
((self.secondary_bank as usize & 0x03) << shift) & (self.rom_bank_count() - 1)
} else {
0
}
}
fn ram_bank_count(&self) -> usize {
(self.ram.len() / 0x2000).max(1)
}
fn ram_bank_index(&self) -> usize {
if self.mode {
(self.secondary_bank & 0x03) as usize & (self.ram_bank_count() - 1)
} else {
0
}
}
fn read_rom(&self, addr: u16) -> u8 {
let (bank, offset) = if addr < 0x4000 {
(self.bank0_index(), addr as usize)
} else {
(self.bank1_index(), addr as usize - 0x4000)
};
let idx = bank * 0x4000 + offset;
self.rom.get(idx).copied().unwrap_or(0xFF)
}
fn read_ram(&self, addr: u16) -> u8 {
if !self.ram_enabled || self.ram.is_empty() {
return 0xFF;
}
let offset = addr as usize - 0xA000;
let idx = self.ram_bank_index() * 0x2000 + offset;
self.ram.get(idx).copied().unwrap_or(0xFF)
}
fn write_registers(&mut self, addr: u16, val: u8) {
match addr {
0x0000..=0x1FFF => {
self.ram_enabled = (val & 0x0F) == 0x0A;
}
0x2000..=0x3FFF => {
self.rom_bank = val & 0x1F;
}
0x4000..=0x5FFF => {
self.secondary_bank = val & 0x03;
}
0x6000..=0x7FFF => {
self.mode = val & 0x01 != 0;
}
_ => {}
}
}
fn write_ram(&mut self, addr: u16, val: u8) {
if !self.ram_enabled || self.ram.is_empty() {
return;
}
let offset = addr as usize - 0xA000;
let idx = self.ram_bank_index() * 0x2000 + offset;
if let Some(byte) = self.ram.get_mut(idx) {
*byte = val;
}
}
}
impl GbCartridge for Mbc1 {
fn read(&self, addr: u16) -> u8 {
match addr {
0x0000..=0x7FFF => self.read_rom(addr),
0xA000..=0xBFFF => self.read_ram(addr),
_ => 0xFF,
}
}
fn write(&mut self, addr: u16, val: u8) {
match addr {
0x0000..=0x7FFF => self.write_registers(addr, val),
0xA000..=0xBFFF => self.write_ram(addr, val),
_ => {}
}
}
fn has_battery(&self) -> bool {
self.battery
}
fn ram_snapshot(&self) -> Vec<u8> {
self.ram.clone()
}
fn restore_ram(&mut self, data: &[u8]) {
let len = data.len().min(self.ram.len());
self.ram[..len].copy_from_slice(&data[..len]);
}
fn mbc_state_snapshot(&self) -> Vec<u8> {
vec![
self.rom_bank,
self.secondary_bank,
self.mode as u8,
self.ram_enabled as u8,
self.multicart as u8,
]
}
fn restore_mbc_state(&mut self, data: &[u8]) {
if data.len() >= 5 {
self.rom_bank = data[0];
self.secondary_bank = data[1];
self.mode = data[2] != 0;
self.ram_enabled = data[3] != 0;
self.multicart = data[4] != 0;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_mbc1_rom(bank_count: usize) -> Vec<u8> {
let mut rom = vec![0u8; bank_count * 0x4000];
for bank in 0..bank_count {
let start = bank * 0x4000;
let end = start + 0x4000;
rom[start..end].fill(bank as u8);
}
rom
}
fn make_mbc1_ram(bank_count: usize) -> Vec<u8> {
vec![0u8; bank_count * 0x2000]
}
#[test]
fn test_mbc1_reads_bank0_data_from_low_region_initially() {
let cart = Mbc1::new(make_mbc1_rom(4), make_mbc1_ram(0), false);
assert_eq!(cart.read(0x0000), 0x00);
assert_eq!(cart.read(0x3FFF), 0x00);
}
#[test]
fn test_mbc1_reads_bank1_data_from_high_region_initially() {
let cart = Mbc1::new(make_mbc1_rom(4), make_mbc1_ram(0), false);
assert_eq!(cart.read(0x4000), 0x01);
assert_eq!(cart.read(0x7FFF), 0x01);
}
#[test]
fn test_mbc1_rom_bank_switch_selects_correct_bank() {
let mut cart = Mbc1::new(make_mbc1_rom(4), make_mbc1_ram(0), false);
cart.write(0x2000, 0x02);
assert_eq!(cart.read(0x4000), 0x02);
}
#[test]
fn test_mbc1_writing_zero_to_bank_reg_selects_bank_1() {
let mut cart = Mbc1::new(make_mbc1_rom(4), make_mbc1_ram(0), false);
cart.write(0x2000, 0x00);
assert_eq!(cart.read(0x4000), 0x01);
}
#[test]
fn test_mbc1_secondary_bank_shifts_to_upper_rom_bits_in_mode0() {
let mut cart = Mbc1::new(make_mbc1_rom(64), make_mbc1_ram(0), false);
cart.write(0x4000, 0x01); cart.write(0x2000, 0x00); assert_eq!(cart.read(0x4000), 33u8);
}
#[test]
fn test_mbc1_mode1_bank0_region_uses_secondary_bank_offset() {
let mut cart = Mbc1::new(make_mbc1_rom(64), make_mbc1_ram(0), false);
cart.write(0x6000, 0x01); cart.write(0x4000, 0x01); assert_eq!(cart.read(0x0000), 32u8);
}
#[test]
fn test_mbc1_ram_read_returns_0xff_when_disabled() {
let cart = Mbc1::new(make_mbc1_rom(4), make_mbc1_ram(1), false);
assert_eq!(cart.read(0xA000), 0xFF);
}
#[test]
fn test_mbc1_ram_read_write_when_enabled() {
let mut cart = Mbc1::new(make_mbc1_rom(4), make_mbc1_ram(1), false);
cart.write(0x0000, 0x0A); cart.write(0xA000, 0x42);
assert_eq!(cart.read(0xA000), 0x42);
}
#[test]
fn test_mbc1_ram_bank_switching_in_mode1() {
let mut cart = Mbc1::new(make_mbc1_rom(4), make_mbc1_ram(4), false);
cart.write(0x0000, 0x0A); cart.write(0x6000, 0x01);
cart.write(0x4000, 0x00); cart.write(0xA000, 0xAA);
cart.write(0x4000, 0x01); cart.write(0xA000, 0xBB);
assert_eq!(cart.read(0xA000), 0xBB);
cart.write(0x4000, 0x00);
assert_eq!(cart.read(0xA000), 0xAA);
}
fn make_mbc1m_rom() -> Vec<u8> {
let mut rom = make_mbc1_rom(64);
let logo_offset = 0x10 * 0x4000 + 0x104;
rom[logo_offset..logo_offset + HEADER_LOGO_LEN].copy_from_slice(&fixture_header_logo());
rom
}
fn fixture_header_logo() -> [u8; HEADER_LOGO_LEN] {
let rom =
std::fs::read("roms/gb/automated_tests/daid/rom_and_ram.gb").expect("fixture ROM");
rom[0x0104..0x0134].try_into().expect("header logo bytes")
}
#[test]
fn test_is_multicart_rom_returns_true_for_1mb_with_logo_in_bank10() {
let rom = make_mbc1m_rom();
assert!(is_multicart_rom(&rom));
}
#[test]
fn test_is_multicart_rom_returns_false_for_non_1mb_rom() {
let mut rom = make_mbc1_rom(128);
let logo_offset = 0x10 * 0x4000 + 0x104;
rom[logo_offset..logo_offset + HEADER_LOGO_LEN].copy_from_slice(&fixture_header_logo());
assert!(!is_multicart_rom(&rom));
}
#[test]
fn test_is_multicart_rom_returns_false_for_1mb_without_logo() {
let rom = make_mbc1_rom(64);
assert!(!is_multicart_rom(&rom));
}
#[test]
fn test_mbc1m_bank1_maps_secondary_to_bits_4_5() {
let mut cart = Mbc1::new(make_mbc1m_rom(), make_mbc1_ram(0), false);
cart.write(0x4000, 0x01); cart.write(0x2000, 0x01); assert_eq!(cart.read(0x4000), 17u8);
}
#[test]
fn test_mbc1m_bank1_ignores_top_bit_of_rom_bank_register() {
let mut cart = Mbc1::new(make_mbc1m_rom(), make_mbc1_ram(0), false);
cart.write(0x4000, 0x00); cart.write(0x2000, 0x11); assert_eq!(cart.read(0x4000), 1u8);
}
#[test]
fn test_mbc1m_bank0_in_mode1_uses_secondary_shifted_4() {
let mut cart = Mbc1::new(make_mbc1m_rom(), make_mbc1_ram(0), false);
cart.write(0x6000, 0x01); cart.write(0x4000, 0x01); assert_eq!(cart.read(0x0000), 16u8);
}
#[test]
fn test_mbc1_ram_bank_masked_to_actual_count_with_1_bank() {
let mut cart = Mbc1::new(make_mbc1_rom(4), make_mbc1_ram(1), false);
cart.write(0x0000, 0x0A); cart.write(0x6000, 0x01); cart.write(0x4000, 0x00); cart.write(0xA000, 0x42); cart.write(0x4000, 0x02); assert_eq!(cart.read(0xA000), 0x42);
}
}