use crate::nes::cartridge::base_mapper::BaseMapper;
use crate::nes::cartridge::ines::NametableLayout;
use crate::nes::cartridge::mapper::{Mapper, MapperCapabilities};
const MAPPER_NUMBER: u16 = 150;
pub struct Mapper150 {
base: BaseMapper,
command: u8,
regs: [u8; 8],
}
impl Mapper150 {
pub fn new(ctx: crate::nes::cartridge::mapper::MapperContext) -> Self {
let capabilities = MapperCapabilities {
has_chr_banking: true,
has_dynamic_mirroring: true,
prg_bank_size_kb: 32,
chr_bank_size_kb: 8,
..Default::default()
};
let mut base = BaseMapper::new(&ctx, capabilities);
base.configure_prg_banking(32 * 1024);
base.configure_chr_banking(8 * 1024);
let mut mapper = Self {
base,
command: 0,
regs: [0; 8],
};
mapper.apply_state();
mapper
}
fn chr_bank(&self) -> usize {
((self.regs[4] & 1) as usize) << 2 | (self.regs[6] & 3) as usize
}
fn prg_bank(&self) -> usize {
(self.regs[5] & 3) as usize
}
fn apply_state(&mut self) {
self.base.select_prg_page(0, self.prg_bank() as i16);
self.base.select_chr_page(0, self.chr_bank() as i16);
let mirroring = match (self.regs[7] >> 1) & 3 {
1 => NametableLayout::Horizontal,
2 => NametableLayout::Vertical,
3 => NametableLayout::SingleScreenUpper,
_ => NametableLayout::SingleScreenLower, };
self.base.set_mirroring(mirroring);
}
}
impl Mapper for Mapper150 {
fn base(&self) -> &BaseMapper {
&self.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.base
}
fn mapper_number(&self) -> u16 {
MAPPER_NUMBER
}
fn read_prg_open_bus(&self, addr: u16, open_bus: u8) -> u8 {
if (addr & 0xC101) == 0x4101 {
return (open_bus & 0xF8) | (self.regs[self.command as usize] & 0x07);
}
open_bus
}
fn write_prg(&mut self, addr: u16, value: u8) {
if (addr & 0xC101) == 0x4100 {
self.command = value & 0x07;
} else if (addr & 0xC101) == 0x4101 {
self.regs[self.command as usize] = value & 0x07;
self.apply_state();
}
}
fn registers_snapshot(&self) -> Vec<u8> {
let mut v = vec![self.command];
v.extend_from_slice(&self.regs);
v
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() >= 9 {
self.command = data[0] & 0x07;
self.regs.copy_from_slice(&data[1..9]);
for r in &mut self.regs {
*r &= 0x07;
}
self.apply_state();
}
}
fn reset(&mut self) {
self.command = 0;
self.regs = [0; 8];
self.apply_state();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nes::cartridge::mapper::{MapperContext, create_mapper};
use crate::nes::cartridge::test_helpers::banked_data;
const PRG_BANKS_32K: usize = 4;
const CHR_BANKS_8K: usize = 8;
fn make_mapper() -> Mapper150 {
Mapper150::new(MapperContext::new_for_test(
MAPPER_NUMBER,
banked_data(32 * 1024, PRG_BANKS_32K),
banked_data(8 * 1024, CHR_BANKS_8K),
NametableLayout::Vertical,
))
}
#[test]
fn mapper_150_is_registered_in_factory() {
let result = create_mapper(MapperContext::new_for_test(
MAPPER_NUMBER,
banked_data(32 * 1024, PRG_BANKS_32K),
banked_data(8 * 1024, CHR_BANKS_8K),
NametableLayout::Vertical,
));
assert!(result.is_ok(), "Mapper 150 must be creatable via factory");
}
#[test]
fn power_on_maps_prg_bank_0() {
let mapper = make_mapper();
assert_eq!(mapper.read_prg(0x8000), 0, "PRG bank 0 on reset");
}
#[test]
fn power_on_maps_chr_bank_0() {
let mut mapper = make_mapper();
assert_eq!(mapper.read_chr(0x0000), 0, "CHR bank 0 on reset");
}
#[test]
fn r5_selects_prg_bank() {
let mut mapper = make_mapper();
mapper.write_prg(0x4100, 5);
mapper.write_prg(0x4101, 2);
assert_eq!(mapper.read_prg(0x8000), 2, "PRG bank 2 via R5=2");
mapper.write_prg(0x4101, 3);
assert_eq!(mapper.read_prg(0x8000), 3, "PRG bank 3 via R5=3");
}
#[test]
fn r5_only_uses_low_2_bits() {
let mut mapper = make_mapper();
mapper.write_prg(0x4100, 5);
mapper.write_prg(0x4101, 0xFF); assert_eq!(mapper.read_prg(0x8000), 3, "R5 masked to 2 bits → bank 3");
}
#[test]
fn r6_selects_chr_bank_low_bits() {
let mut mapper = make_mapper();
mapper.write_prg(0x4100, 6);
mapper.write_prg(0x4101, 3); assert_eq!(mapper.read_chr(0x0000), 3, "CHR bank 3 via R6=3");
}
#[test]
fn r4_sets_chr_bank_high_bit() {
let mut mapper = make_mapper();
mapper.write_prg(0x4100, 4);
mapper.write_prg(0x4101, 1); assert_eq!(mapper.read_chr(0x0000), 4, "CHR bank bit 2 set via R4[0]=1");
}
#[test]
fn chr_bank_combines_r4_and_r6() {
let mut mapper = make_mapper();
mapper.write_prg(0x4100, 4);
mapper.write_prg(0x4101, 1);
mapper.write_prg(0x4100, 6);
mapper.write_prg(0x4101, 3);
assert_eq!(mapper.read_chr(0x0000), 7, "CHR bank 7 = R4[0]=1, R6=3");
}
#[test]
fn index_register_uses_only_low_3_bits() {
let mut mapper = make_mapper();
mapper.write_prg(0x4100, 0xFF);
mapper.write_prg(0x4101, 0);
}
#[test]
fn data_register_is_readable() {
let mut mapper = make_mapper();
mapper.write_prg(0x4100, 5);
mapper.write_prg(0x4101, 2);
let val = mapper.read_prg_open_bus(0x4101, 0x00);
assert_eq!(
val & 0x07,
2,
"register read-back must return written value"
);
}
#[test]
fn r7_mode1_sets_horizontal_mirroring() {
let mut mapper = make_mapper();
mapper.write_prg(0x4100, 7);
mapper.write_prg(0x4101, 0x02); assert_eq!(
mapper.get_mirroring(),
NametableLayout::Horizontal,
"R7[2:1]=1 → Horizontal"
);
}
#[test]
fn r7_mode2_sets_vertical_mirroring() {
let mut mapper = make_mapper();
mapper.write_prg(0x4100, 7);
mapper.write_prg(0x4101, 0x04); assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"R7[2:1]=2 → Vertical"
);
}
#[test]
fn r7_mode3_sets_single_screen_upper() {
let mut mapper = make_mapper();
mapper.write_prg(0x4100, 7);
mapper.write_prg(0x4101, 0x06); assert_eq!(
mapper.get_mirroring(),
NametableLayout::SingleScreenUpper,
"R7[2:1]=3 → SingleScreenUpper"
);
}
#[test]
fn reset_clears_all_registers() {
let mut mapper = make_mapper();
mapper.write_prg(0x4100, 5);
mapper.write_prg(0x4101, 3);
mapper.reset();
assert_eq!(mapper.read_prg(0x8000), 0, "PRG bank must be 0 after reset");
assert_eq!(mapper.read_chr(0x0000), 0, "CHR bank must be 0 after reset");
}
#[test]
fn snapshot_restore_round_trips_registers() {
let mut mapper = make_mapper();
mapper.write_prg(0x4100, 5);
mapper.write_prg(0x4101, 2); mapper.write_prg(0x4100, 4);
mapper.write_prg(0x4101, 1); let snap = mapper.registers_snapshot();
let mut restored = make_mapper();
restored.restore_registers(&snap);
assert_eq!(restored.read_prg(0x8000), 2, "restored PRG bank must be 2");
assert_eq!(restored.read_chr(0x0000), 4, "restored CHR bank must be 4");
}
}