use super::cartridge::GbCartridge;
pub struct Mbc1 {
rom: Vec<u8>,
ram: Vec<u8>,
rom_bank: u8,
secondary_bank: u8,
mode: bool,
ram_enabled: bool,
}
impl Mbc1 {
pub fn new(rom: Vec<u8>, ram: Vec<u8>) -> Self {
Self {
rom,
ram,
rom_bank: 0,
secondary_bank: 0,
mode: false,
ram_enabled: false,
}
}
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 {
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 {
((self.secondary_bank as usize & 0x03) << 5) & (self.rom_bank_count() - 1)
} else {
0
}
}
fn ram_bank_index(&self) -> usize {
if self.mode {
(self.secondary_bank & 0x03) as usize
} 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),
_ => {}
}
}
}
#[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));
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));
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));
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));
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));
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));
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));
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));
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));
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);
}
}