use crate::nes::cartridge::NametableLayout;
use crate::nes::cartridge::base_mapper::BaseMapper;
use crate::nes::cartridge::mapper::{Mapper, MapperCapabilities, MapperContext};
const MAPPER_NUMBER: u16 = 183;
const PRG_BANK_SIZE: usize = 8 * 1024;
const CHR_BANK_SIZE: usize = 1024;
pub struct Mapper183 {
base: BaseMapper,
chr_regs: [u8; 8],
prg_banks: [u8; 3],
prg_reg: u8,
irq_counter: u8,
irq_scaler: u8,
irq_enabled: bool,
irq_active: bool,
need_irq: bool,
}
impl Mapper183 {
pub fn new(ctx: MapperContext) -> Self {
let capabilities = MapperCapabilities {
prg_bank_size_kb: PRG_BANK_SIZE / 1024,
chr_bank_size_kb: CHR_BANK_SIZE / 1024,
has_irq: true,
has_chr_banking: true,
has_dynamic_mirroring: true,
..Default::default()
};
let mut base = BaseMapper::new(&ctx, capabilities);
base.configure_prg_banking(PRG_BANK_SIZE);
base.configure_chr_banking(CHR_BANK_SIZE);
base.configure_prg_6000_banking();
let mut mapper = Self {
base,
chr_regs: [0; 8],
prg_banks: [0; 3],
prg_reg: 0,
irq_counter: 0,
irq_scaler: 0,
irq_enabled: false,
irq_active: false,
need_irq: false,
};
mapper.update_prg();
mapper
}
fn update_prg(&mut self) {
self.base.select_prg_6000_page(self.prg_reg as i16);
self.base.select_prg_page(3, -1);
}
fn update_chr(&mut self, slot: usize) {
self.base.select_chr_page(slot, self.chr_regs[slot] as i16);
}
fn write_chr_nibble(&mut self, addr: u16, value: u8) {
let addr32 = addr as u32;
let slot = ((((addr32 >> 11) - 6) | (addr32 >> 3)) & 0x07) as usize;
if (addr & 0x04) == 0 {
self.chr_regs[slot] = (self.chr_regs[slot] & 0xF0) | (value & 0x0F);
} else {
self.chr_regs[slot] = (self.chr_regs[slot] & 0x0F) | ((value & 0x0F) << 4);
}
self.update_chr(slot);
}
}
impl Mapper for Mapper183 {
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(&self, addr: u16) -> u8 {
if let Some(value) = self.base.try_read_prg_6000(addr) {
return value;
}
if (0x8000..=0xFFFF).contains(&addr) {
return self.base.read_prg_banked(addr);
}
0
}
fn write_prg(&mut self, addr: u16, value: u8) {
if (addr & 0xF800) == 0x6800 {
self.prg_reg = (addr & 0x3F) as u8;
self.update_prg();
return;
}
if (addr & 0xF800) >= 0xB000 && (addr & 0xF800) <= 0xE000 {
self.write_chr_nibble(addr, value);
return;
}
match addr & 0xF80C {
0x8800 => {
self.prg_banks[0] = value;
self.base.select_prg_page(0, value as i16);
}
0xA800 => {
self.prg_banks[1] = value;
self.base.select_prg_page(1, value as i16);
}
0xA000 => {
self.prg_banks[2] = value;
self.base.select_prg_page(2, value as i16);
}
0x9800 => {
let mirroring = match value & 0x03 {
0 => NametableLayout::Vertical,
1 => NametableLayout::Horizontal,
2 => NametableLayout::SingleScreenLower,
_ => NametableLayout::SingleScreenUpper,
};
self.base.set_mirroring(mirroring);
}
0xF000 => {
self.irq_counter = (self.irq_counter & 0xF0) | (value & 0x0F);
}
0xF004 => {
self.irq_counter = (self.irq_counter & 0x0F) | ((value & 0x0F) << 4);
}
0xF008 => {
self.irq_enabled = value != 0;
if !self.irq_enabled {
self.irq_scaler = 0;
}
self.irq_active = false;
self.need_irq = false;
}
_ => {}
}
}
fn cpu_cycle(&mut self) {
if self.need_irq {
self.irq_active = true;
self.need_irq = false;
}
self.irq_scaler = self.irq_scaler.wrapping_add(1);
if self.irq_scaler == 114 {
self.irq_scaler = 0;
if self.irq_enabled {
self.irq_counter = self.irq_counter.wrapping_add(1);
if self.irq_counter == 0 {
self.need_irq = true;
}
}
}
}
fn irq_pending(&self) -> bool {
self.irq_active
}
fn capabilities(&self) -> MapperCapabilities {
MapperCapabilities {
has_irq: true,
has_chr_banking: true,
has_dynamic_mirroring: true,
prg_bank_size_kb: PRG_BANK_SIZE / 1024,
chr_bank_size_kb: CHR_BANK_SIZE / 1024,
..Default::default()
}
}
fn registers_snapshot(&self) -> Vec<u8> {
let mut snap = vec![self.prg_reg, self.irq_counter, self.irq_scaler];
let flags = (self.irq_enabled as u8)
| ((self.irq_active as u8) << 1)
| ((self.need_irq as u8) << 2);
snap.push(flags);
snap.extend_from_slice(&self.prg_banks);
snap.extend_from_slice(&self.chr_regs);
snap.push(self.base.mirroring().to_snapshot_byte());
snap
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() < 15 {
return;
}
self.prg_reg = data[0];
self.irq_counter = data[1];
self.irq_scaler = data[2];
self.irq_enabled = (data[3] & 0x01) != 0;
self.irq_active = (data[3] & 0x02) != 0;
self.need_irq = (data[3] & 0x04) != 0;
self.prg_banks.copy_from_slice(&data[4..7]);
self.chr_regs.copy_from_slice(&data[7..15]);
self.update_prg();
for slot in 0..3 {
self.base.select_prg_page(slot, self.prg_banks[slot] as i16);
}
for slot in 0..8 {
self.update_chr(slot);
}
if let Some(&mir) = data.get(15) {
self.base
.set_mirroring(NametableLayout::from_snapshot_byte(mir));
}
}
fn reset(&mut self) {
self.chr_regs = [0; 8];
self.prg_banks = [0; 3];
self.prg_reg = 0;
self.irq_counter = 0;
self.irq_scaler = 0;
self.irq_enabled = false;
self.irq_active = false;
self.need_irq = false;
self.update_prg();
for slot in 0..3 {
self.base.select_prg_page(slot, 0);
}
for slot in 0..8 {
self.update_chr(slot);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nes::cartridge::NametableLayout;
use crate::nes::cartridge::mapper::{MapperContext, create_mapper};
use crate::nes::cartridge::test_helpers::banked_data;
const PRG_BANKS: usize = 8;
const CHR_1K_BANKS: usize = 16;
fn make_mapper() -> Mapper183 {
Mapper183::new(MapperContext::new_for_test(
MAPPER_NUMBER,
banked_data(PRG_BANK_SIZE, PRG_BANKS),
banked_data(CHR_BANK_SIZE, CHR_1K_BANKS),
NametableLayout::Vertical,
))
}
#[test]
fn mapper_183_is_registered_in_factory() {
let result = create_mapper(MapperContext::new_for_test(
MAPPER_NUMBER,
banked_data(PRG_BANK_SIZE, PRG_BANKS),
banked_data(CHR_BANK_SIZE, CHR_1K_BANKS),
NametableLayout::Vertical,
));
assert!(result.is_ok(), "Mapper 183 must be creatable via factory");
}
#[test]
fn power_on_e000_is_last_bank() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0xE000),
(PRG_BANKS - 1) as u8,
"slot 3 ($E000) must be fixed to last bank"
);
}
#[test]
fn power_on_6000_reads_bank_0() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0x6000),
0,
"$6000 must read bank 0 at power-on"
);
}
#[test]
fn prg_slot0_write_via_8800() {
let mut mapper = make_mapper();
mapper.write_prg(0x8800, 3);
assert_eq!(mapper.read_prg(0x8000), 3, "$8800 write must set slot 0");
}
#[test]
fn prg_slot1_write_via_a800() {
let mut mapper = make_mapper();
mapper.write_prg(0xA800, 2);
assert_eq!(mapper.read_prg(0xA000), 2, "$A800 write must set slot 1");
}
#[test]
fn prg_slot2_write_via_a000() {
let mut mapper = make_mapper();
mapper.write_prg(0xA000, 4);
assert_eq!(mapper.read_prg(0xC000), 4, "$A000 write must set slot 2");
}
#[test]
fn prg_6000_window_set_via_6800() {
let mut mapper = make_mapper();
mapper.write_prg(0x6803, 0);
assert_eq!(
mapper.read_prg(0x6000),
3,
"$6803 write must set $6000 bank to 3"
);
}
#[test]
fn chr_nibble_write_slot0_low_then_high() {
let mut mapper = make_mapper();
mapper.write_prg(0xB000, 0x05); mapper.write_prg(0xB004, 0x03); assert_eq!(mapper.chr_regs[0], 0x35);
assert_eq!(mapper.read_chr(0x0000), 5);
}
#[test]
fn chr_nibble_write_slot7() {
let mut mapper = make_mapper();
mapper.write_prg(0xB038, 0x09); mapper.write_prg(0xB03C, 0x00); assert_eq!(mapper.chr_regs[7], 0x09);
}
#[test]
fn mirroring_via_9800() {
let mut mapper = make_mapper();
mapper.write_prg(0x9800, 0x01); assert_eq!(mapper.base().mirroring(), NametableLayout::Horizontal);
mapper.write_prg(0x9800, 0x00); assert_eq!(mapper.base().mirroring(), NametableLayout::Vertical);
}
#[test]
fn irq_counter_nibble_writes() {
let mut mapper = make_mapper();
mapper.write_prg(0xF000, 0x05); mapper.write_prg(0xF004, 0x0A); assert_eq!(mapper.irq_counter, 0xA5);
}
#[test]
fn irq_fires_after_counter_overflow() {
let mut mapper = make_mapper();
mapper.write_prg(0xF000, 0x0F); mapper.write_prg(0xF004, 0x0F); mapper.write_prg(0xF008, 0x01);
assert!(!mapper.irq_pending(), "IRQ must not be pending yet");
for _ in 0..114 {
mapper.cpu_cycle();
}
mapper.cpu_cycle();
assert!(mapper.irq_pending(), "IRQ must fire after counter overflow");
}
#[test]
fn irq_cleared_by_f008_write() {
let mut mapper = make_mapper();
mapper.write_prg(0xF000, 0x0F);
mapper.write_prg(0xF004, 0x0F);
mapper.write_prg(0xF008, 0x01);
for _ in 0..115 {
mapper.cpu_cycle();
}
assert!(mapper.irq_pending());
mapper.write_prg(0xF008, 0x00); assert!(!mapper.irq_pending(), "IRQ must be cleared by $F008 write");
}
#[test]
fn reset_clears_all_state() {
let mut mapper = make_mapper();
mapper.write_prg(0x8800, 5);
mapper.write_prg(0x6803, 0);
mapper.reset();
assert_eq!(mapper.read_prg(0x6000), 0);
assert_eq!(mapper.prg_reg, 0);
assert!(!mapper.irq_active);
}
#[test]
fn snapshot_restore_round_trips() {
let mut mapper = make_mapper();
mapper.write_prg(0x8800, 3);
mapper.write_prg(0xB000, 0x05);
mapper.write_prg(0xB004, 0x02);
let snap = mapper.registers_snapshot();
let mut restored = make_mapper();
restored.restore_registers(&snap);
assert_eq!(restored.prg_reg, 0);
assert_eq!(restored.chr_regs[0], 0x25);
assert_eq!(restored.read_prg(0x8000), 3);
}
#[test]
fn snapshot_restores_mirroring() {
let mut mapper = make_mapper();
mapper.write_prg(0x9800, 0x01); let snap = mapper.registers_snapshot();
let mut restored = make_mapper();
restored.restore_registers(&snap);
assert_eq!(
restored.base().mirroring(),
NametableLayout::Horizontal,
"mirroring must survive snapshot round-trip"
);
}
#[test]
fn reset_resets_prg_slots_to_bank_0() {
let mut mapper = make_mapper();
mapper.write_prg(0x8800, 5); mapper.write_prg(0xA800, 3); mapper.write_prg(0xA000, 2); mapper.reset();
assert_eq!(
mapper.read_prg(0x8000),
0,
"slot 0 must be bank 0 after reset"
);
assert_eq!(
mapper.read_prg(0xA000),
0,
"slot 1 must be bank 0 after reset"
);
assert_eq!(
mapper.read_prg(0xC000),
0,
"slot 2 must be bank 0 after reset"
);
}
}