use crate::nes::cartridge::NametableLayout;
use crate::nes::cartridge::base_mapper::BaseMapper;
use crate::nes::cartridge::mapper::{Mapper, MapperCapabilities};
pub struct Mapper83 {
base: BaseMapper,
regs: [u8; 11],
ex_regs: [u8; 4],
mode: u8,
bank: u8,
is_2k_bank: bool,
force_chr_1k_mode: bool,
irq_counter: u16,
irq_enabled: bool,
irq_pending: bool,
}
impl Mapper83 {
const PRG_PAGE_SIZE: usize = 0x2000; const CHR_PAGE_SIZE: usize = 0x0400;
const MIRRORING_MASK: u8 = 0x03; const MODE_32KB_PRG: u8 = 0x40; const MODE_IRQ_LATCH: u8 = 0x80; const BANK_GROUP_MASK: u8 = 0x30; const PRG_BANK_MASK: u8 = 0x3F; const PRG_LAST_GROUP_MASK: u8 = 0x0F; const IRQ_COUNTER_RESET: u16 = 0xFFFF;
const SNAPSHOT_SIZE: usize = 22;
pub fn new(ctx: crate::nes::cartridge::mapper::MapperContext) -> Self {
let capabilities = MapperCapabilities {
has_irq: true,
has_chr_banking: true,
has_dynamic_mirroring: true,
prg_bank_size_kb: 8,
chr_bank_size_kb: 1,
..Default::default()
};
let mut base = BaseMapper::new(&ctx, capabilities);
base.configure_prg_banking(Self::PRG_PAGE_SIZE);
base.configure_chr_banking(Self::CHR_PAGE_SIZE);
let mut mapper = Self {
base,
regs: [0u8; 11],
ex_regs: [0u8; 4],
mode: 0,
bank: 0,
is_2k_bank: false,
force_chr_1k_mode: false,
irq_counter: 0,
irq_enabled: false,
irq_pending: false,
};
mapper.update_state();
mapper
}
fn apply_mirroring(&mut self) {
self.base
.set_mirroring(match self.mode & Self::MIRRORING_MASK {
0 => NametableLayout::Vertical,
1 => NametableLayout::Horizontal,
2 => NametableLayout::SingleScreenLower,
_ => NametableLayout::SingleScreenUpper,
});
}
fn apply_chr_banking(&mut self) {
if self.is_2k_bank && !self.force_chr_1k_mode {
let r0 = (self.regs[0] as i16) << 1;
let r1 = (self.regs[1] as i16) << 1;
let r6 = (self.regs[6] as i16) << 1;
let r7 = (self.regs[7] as i16) << 1;
self.base.select_chr_page(0, r0);
self.base.select_chr_page(1, r0 + 1);
self.base.select_chr_page(2, r1);
self.base.select_chr_page(3, r1 + 1);
self.base.select_chr_page(4, r6);
self.base.select_chr_page(5, r6 + 1);
self.base.select_chr_page(6, r7);
self.base.select_chr_page(7, r7 + 1);
} else {
let ext = ((self.bank & Self::BANK_GROUP_MASK) as i16) << 4;
for i in 0..8usize {
self.base.select_chr_page(i, (self.regs[i] as i16) | ext);
}
}
}
fn apply_prg_banking(&mut self) {
if self.mode & Self::MODE_32KB_PRG != 0 {
let base_bank = ((self.bank & Self::PRG_BANK_MASK) as i16) << 1;
let last_bank =
(((self.bank & Self::BANK_GROUP_MASK) | Self::PRG_LAST_GROUP_MASK) as i16) << 1;
self.base.select_prg_page(0, base_bank);
self.base.select_prg_page(1, base_bank + 1);
self.base.select_prg_page(2, last_bank);
self.base.select_prg_page(3, last_bank + 1);
} else {
self.base.select_prg_page(0, self.regs[8] as i16);
self.base.select_prg_page(1, self.regs[9] as i16);
self.base.select_prg_page(2, self.regs[10] as i16);
self.base.select_prg_page(3, -1);
}
}
fn update_state(&mut self) {
self.apply_mirroring();
self.apply_chr_banking();
self.apply_prg_banking();
}
}
impl Mapper for Mapper83 {
fn base(&self) -> &BaseMapper {
&self.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.base
}
fn read_prg(&self, addr: u16) -> u8 {
match addr {
0x5000 => {
0x00
}
0x5100..=0x5103 => self.ex_regs[(addr & 0x03) as usize],
0x8000..=0xFFFF => self.base.read_prg_rom(addr),
_ => 0,
}
}
fn write_prg(&mut self, addr: u16, value: u8) {
match addr {
0x5100..=0x5103 => {
self.ex_regs[(addr & 0x03) as usize] = value;
}
0x8000 | 0xB000 | 0xB0FF | 0xB1FF => {
self.is_2k_bank = true;
self.bank = value;
self.mode |= Self::MODE_32KB_PRG;
self.update_state();
}
0x8100 => {
self.mode = value | (self.mode & Self::MODE_32KB_PRG);
self.update_state();
}
0x8200 => {
self.irq_counter = (self.irq_counter & 0xFF00) | (value as u16);
self.irq_pending = false;
}
0x8201 => {
self.irq_enabled = (self.mode & Self::MODE_IRQ_LATCH) != 0;
self.irq_counter = (self.irq_counter & 0x00FF) | ((value as u16) << 8);
}
0x8300..=0x8302 => {
self.mode &= !Self::MODE_32KB_PRG; self.regs[(addr - 0x8300 + 8) as usize] = value;
self.update_state();
}
0x8310..=0x8317 => {
let idx = (addr - 0x8310) as usize;
self.regs[idx] = value;
if (0x8312..=0x8315).contains(&addr) {
self.force_chr_1k_mode = true;
}
self.update_state();
}
_ => {}
}
}
fn irq_pending(&self) -> bool {
self.irq_pending
}
fn cpu_cycle(&mut self) {
if self.irq_enabled {
self.irq_counter = self.irq_counter.wrapping_sub(1);
if self.irq_counter == 0 {
self.irq_enabled = false;
self.irq_counter = Self::IRQ_COUNTER_RESET;
self.irq_pending = true;
}
}
}
fn registers_snapshot(&self) -> Vec<u8> {
let irq_flags = (self.irq_enabled as u8) | ((self.irq_pending as u8) << 1);
let mut v = vec![
self.mode,
self.bank,
self.is_2k_bank as u8,
self.force_chr_1k_mode as u8,
irq_flags,
(self.irq_counter & 0xFF) as u8,
(self.irq_counter >> 8) as u8,
];
v.extend_from_slice(&self.regs);
v.extend_from_slice(&self.ex_regs);
v
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() < Self::SNAPSHOT_SIZE {
return;
}
self.mode = data[0];
self.bank = data[1];
self.is_2k_bank = data[2] != 0;
self.force_chr_1k_mode = data[3] != 0;
self.irq_enabled = (data[4] & 1) != 0;
self.irq_pending = (data[4] & 2) != 0;
self.irq_counter = (data[5] as u16) | ((data[6] as u16) << 8);
self.regs.copy_from_slice(&data[7..18]);
self.ex_regs.copy_from_slice(&data[18..22]);
self.update_state();
}
fn reset(&mut self) {
self.regs = [0u8; 11];
self.ex_regs = [0u8; 4];
self.mode = 0;
self.bank = 0;
self.is_2k_bank = false;
self.force_chr_1k_mode = false;
self.irq_counter = 0;
self.irq_enabled = false;
self.irq_pending = false;
self.update_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: usize = 33; const CHR_BANKS: usize = 33;
fn make_mapper() -> Mapper83 {
let prg = banked_data(8 * 1024, PRG_BANKS);
let chr = banked_data(1024, CHR_BANKS);
Mapper83::new(MapperContext::new_for_test(
83,
prg,
chr,
NametableLayout::Vertical,
))
}
#[test]
fn mapper_83_is_registered() {
let result = create_mapper(MapperContext::new_for_test(
83,
banked_data(8 * 1024, PRG_BANKS),
banked_data(1024, CHR_BANKS),
NametableLayout::Vertical,
));
assert!(
result.is_ok(),
"Mapper 83 must be registered in the factory"
);
}
#[test]
fn power_on_prg_slot0_is_bank0() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0x8000),
0,
"PRG slot 0 must start at bank 0"
);
}
#[test]
fn power_on_prg_slot3_is_last_bank() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0xE000),
(PRG_BANKS - 1) as u8,
"PRG slot 3 must be fixed to last bank at power-on"
);
}
#[test]
fn power_on_chr_slot0_is_bank0() {
let mut mapper = make_mapper();
assert_eq!(
mapper.read_chr(0x0000),
0,
"CHR slot 0 must start at bank 0"
);
}
#[test]
fn power_on_irq_not_pending() {
let mapper = make_mapper();
assert!(!mapper.irq_pending(), "IRQ must not be pending at power-on");
}
#[test]
fn power_on_mirroring_from_header() {
let mapper = make_mapper();
assert_eq!(mapper.get_mirroring(), NametableLayout::Vertical);
}
#[test]
fn prg_8kb_mode_reg8_controls_slot0() {
let mut mapper = make_mapper();
mapper.write_prg(0x8300, 5);
assert_eq!(
mapper.read_prg(0x8000),
5,
"PRG slot 0 must reflect reg[8] in 8KB mode"
);
}
#[test]
fn prg_8kb_mode_reg9_controls_slot1() {
let mut mapper = make_mapper();
mapper.write_prg(0x8301, 7);
assert_eq!(
mapper.read_prg(0xA000),
7,
"PRG slot 1 must reflect reg[9] in 8KB mode"
);
}
#[test]
fn prg_8kb_mode_reg10_controls_slot2() {
let mut mapper = make_mapper();
mapper.write_prg(0x8302, 3);
assert_eq!(
mapper.read_prg(0xC000),
3,
"PRG slot 2 must reflect reg[10] in 8KB mode"
);
}
#[test]
fn prg_8kb_mode_slot3_always_last() {
let mut mapper = make_mapper();
mapper.write_prg(0x8300, 5);
mapper.write_prg(0x8301, 7);
mapper.write_prg(0x8302, 3);
assert_eq!(
mapper.read_prg(0xE000),
(PRG_BANKS - 1) as u8,
"PRG slot 3 must always be fixed to last bank in 8KB mode"
);
}
#[test]
fn prg_8300_write_clears_32kb_mode() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 1); mapper.write_prg(0x8300, 2);
assert_eq!(
mapper.read_prg(0xE000),
(PRG_BANKS - 1) as u8,
"Writing $8300 must clear 32KB mode and fix slot 3 to last bank"
);
}
#[test]
fn prg_32kb_mode_set_by_8000_write() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 2);
assert_eq!(
mapper.read_prg(0x8000),
4,
"PRG slot 0 in 32KB mode must be (bank & 0x3F)*2"
);
assert_eq!(
mapper.read_prg(0xA000),
5,
"PRG slot 1 in 32KB mode must be (bank & 0x3F)*2 + 1"
);
}
#[test]
fn prg_32kb_mode_slots_2_3_from_last_group() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 2);
assert_eq!(
mapper.read_prg(0xC000),
30,
"PRG slot 2 in 32KB mode must be last-group bank"
);
assert_eq!(
mapper.read_prg(0xE000),
31,
"PRG slot 3 in 32KB mode must be last-group bank + 1"
);
}
#[test]
fn prg_32kb_mode_b000_alias() {
let mut mapper = make_mapper();
mapper.write_prg(0xB000, 2);
assert_eq!(
mapper.read_prg(0x8000),
4,
"$B000 must behave like $8000 for bank selection"
);
}
#[test]
fn chr_1kb_reg0_controls_slot0() {
let mut mapper = make_mapper();
mapper.write_prg(0x8312, 0);
mapper.write_prg(0x8310, 7);
assert_eq!(
mapper.read_chr(0x0000),
7,
"CHR slot 0 must reflect reg[0] in 1KB mode"
);
}
#[test]
fn chr_1kb_reg7_controls_slot7() {
let mut mapper = make_mapper();
mapper.write_prg(0x8312, 0);
mapper.write_prg(0x8317, 5);
assert_eq!(
mapper.read_chr(0x1C00),
5,
"CHR slot 7 must reflect reg[7] in 1KB mode"
);
}
#[test]
fn chr_1kb_all_slots_independent() {
let mut mapper = make_mapper();
mapper.write_prg(0x8312, 0); for i in 0..8u16 {
mapper.write_prg(0x8310 + i, (i * 4) as u8);
}
for i in 0..8u16 {
let bank = (i * 4) as u8 % CHR_BANKS as u8;
assert_eq!(
mapper.read_chr(i * 0x400),
bank,
"CHR slot {i} must be independently selectable"
);
}
}
#[test]
fn chr_2kb_mode_enabled_by_8000_write() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0);
mapper.write_prg(0x8310, 3);
assert_eq!(
mapper.read_chr(0x0000),
6,
"CHR slot 0 in 2KB mode must be reg[0]*2"
);
assert_eq!(
mapper.read_chr(0x0400),
7,
"CHR slot 1 in 2KB mode must be reg[0]*2 + 1"
);
}
#[test]
fn chr_2kb_mode_reg1_controls_slots_2_3() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0);
mapper.write_prg(0x8311, 4); assert_eq!(
mapper.read_chr(0x0800),
8,
"CHR slot 2 in 2KB mode must be reg[1]*2"
);
assert_eq!(
mapper.read_chr(0x0C00),
9,
"CHR slot 3 in 2KB mode must be reg[1]*2 + 1"
);
}
#[test]
fn chr_8312_write_disables_2kb_mode() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0); mapper.write_prg(0x8310, 3); mapper.write_prg(0x8312, 0);
assert_eq!(
mapper.read_chr(0x0000),
3,
"Writing $8312 must disable 2KB mode; slot 0 must use reg[0] directly"
);
}
#[test]
fn mirroring_vertical_from_mode_bits() {
let mut mapper = make_mapper();
mapper.write_prg(0x8100, 0x00); assert_eq!(mapper.get_mirroring(), NametableLayout::Vertical);
}
#[test]
fn mirroring_horizontal_from_mode_bits() {
let mut mapper = make_mapper();
mapper.write_prg(0x8100, 0x01); assert_eq!(mapper.get_mirroring(), NametableLayout::Horizontal);
}
#[test]
fn mirroring_single_screen_a_from_mode_bits() {
let mut mapper = make_mapper();
mapper.write_prg(0x8100, 0x02); assert_eq!(mapper.get_mirroring(), NametableLayout::SingleScreenLower);
}
#[test]
fn mirroring_single_screen_b_from_mode_bits() {
let mut mapper = make_mapper();
mapper.write_prg(0x8100, 0x03); assert_eq!(mapper.get_mirroring(), NametableLayout::SingleScreenUpper);
}
#[test]
fn mirroring_8100_preserves_32kb_mode_bit() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 2); mapper.write_prg(0x8100, 0x01); assert_eq!(
mapper.read_prg(0x8000),
4,
"32KB mode must persist after $8100 mirroring write"
);
}
#[test]
fn irq_not_pending_at_power_on() {
let mapper = make_mapper();
assert!(!mapper.irq_pending(), "IRQ must not fire at power-on");
}
#[test]
fn irq_fires_after_counter_reaches_zero() {
let mut mapper = make_mapper();
mapper.write_prg(0x8100, 0x80);
mapper.write_prg(0x8200, 3);
mapper.write_prg(0x8201, 0);
for _ in 0..2 {
assert!(!mapper.irq_pending());
mapper.cpu_cycle();
}
mapper.cpu_cycle(); assert!(mapper.irq_pending(), "IRQ must fire when counter reaches 0");
}
#[test]
fn irq_write_8200_clears_pending() {
let mut mapper = make_mapper();
mapper.write_prg(0x8100, 0x80);
mapper.write_prg(0x8200, 1);
mapper.write_prg(0x8201, 0);
mapper.cpu_cycle();
assert!(mapper.irq_pending());
mapper.write_prg(0x8200, 5); assert!(
!mapper.irq_pending(),
"Writing $8200 must clear the pending IRQ"
);
}
#[test]
fn irq_auto_disables_after_fire() {
let mut mapper = make_mapper();
mapper.write_prg(0x8100, 0x80);
mapper.write_prg(0x8200, 1);
mapper.write_prg(0x8201, 0);
mapper.cpu_cycle(); assert!(mapper.irq_pending());
mapper.write_prg(0x8200, 10);
for _ in 0..20 {
mapper.cpu_cycle();
}
assert!(
!mapper.irq_pending(),
"IRQ must not re-fire after auto-disable"
);
}
#[test]
fn irq_counter_resets_to_ffff_after_fire() {
let mut mapper = make_mapper();
mapper.write_prg(0x8100, 0x80);
mapper.write_prg(0x8200, 1);
mapper.write_prg(0x8201, 0);
mapper.cpu_cycle(); assert_eq!(
mapper.irq_counter, 0xFFFF,
"IRQ counter must reset to 0xFFFF after firing"
);
}
#[test]
fn irq_not_enabled_without_mode_bit7() {
let mut mapper = make_mapper();
mapper.write_prg(0x8100, 0x00); mapper.write_prg(0x8200, 3);
mapper.write_prg(0x8201, 0); for _ in 0..10 {
mapper.cpu_cycle();
}
assert!(
!mapper.irq_pending(),
"IRQ must not fire when mode bit 7 is clear"
);
}
#[test]
fn ex_regs_read_write_roundtrip() {
let mut mapper = make_mapper();
mapper.write_prg(0x5100, 0xAB);
mapper.write_prg(0x5101, 0xCD);
mapper.write_prg(0x5102, 0xEF);
mapper.write_prg(0x5103, 0x12);
assert_eq!(mapper.read_prg(0x5100), 0xAB);
assert_eq!(mapper.read_prg(0x5101), 0xCD);
assert_eq!(mapper.read_prg(0x5102), 0xEF);
assert_eq!(mapper.read_prg(0x5103), 0x12);
}
#[test]
fn registers_snapshot_round_trips() {
let mut mapper = make_mapper();
mapper.write_prg(0x8300, 5); mapper.write_prg(0x8301, 7); mapper.write_prg(0x8312, 3); mapper.write_prg(0x8100, 0x81); mapper.write_prg(0x8200, 0x34);
mapper.write_prg(0x8201, 0x12);
mapper.write_prg(0x5101, 0x55);
let snap = mapper.registers_snapshot();
let mut restored = make_mapper();
restored.restore_registers(&snap);
assert_eq!(
restored.read_prg(0x8000),
mapper.read_prg(0x8000),
"PRG slot 0 must be restored"
);
assert_eq!(
restored.read_prg(0xA000),
mapper.read_prg(0xA000),
"PRG slot 1 must be restored"
);
assert_eq!(
restored.get_mirroring(),
mapper.get_mirroring(),
"Mirroring must be restored"
);
assert_eq!(
restored.irq_counter, mapper.irq_counter,
"IRQ counter must be restored"
);
assert_eq!(
restored.read_prg(0x5101),
mapper.read_prg(0x5101),
"ex_reg[1] must be restored"
);
}
#[test]
fn reset_restores_power_on_state() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 3); mapper.write_prg(0x8100, 0x83);
mapper.write_prg(0x8200, 10);
mapper.write_prg(0x8201, 0);
mapper.reset();
assert_eq!(
mapper.read_prg(0x8000),
0,
"PRG slot 0 must be bank 0 after reset"
);
assert_eq!(
mapper.read_prg(0xE000),
(PRG_BANKS - 1) as u8,
"PRG slot 3 must be last bank after reset"
);
assert_eq!(mapper.read_chr(0x0000), 0, "CHR must be bank 0 after reset");
assert!(!mapper.irq_pending(), "IRQ must not be pending after reset");
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"Mirroring must reset to Vertical"
);
}
#[test]
fn chr_ram_writable_when_no_chr_rom() {
let prg = banked_data(8 * 1024, PRG_BANKS);
let mut mapper = Mapper83::new(MapperContext::new_for_test(
83,
prg,
vec![],
NametableLayout::Vertical,
));
mapper.write_chr(0x0200, 0xBE);
assert_eq!(
mapper.read_chr(0x0200),
0xBE,
"CHR-RAM must be writable when no CHR-ROM"
);
}
}