Initial commit: NES emulator with GTK4 desktop frontend
Some checks failed
CI / rust (push) Has been cancelled
Some checks failed
CI / rust (push) Has been cancelled
Full NES emulation: CPU, PPU, APU, 47 mappers, iNES/NES 2.0 parsing. GTK4 desktop client with HeaderBar, pixel-perfect Cairo rendering, drag-and-drop ROM loading, and keyboard shortcuts. 187 tests covering core emulation, mappers, and runtime.
This commit is contained in:
253
src/native_core/mapper/core.rs
Normal file
253
src/native_core/mapper/core.rs
Normal file
@@ -0,0 +1,253 @@
|
||||
use crate::native_core::ines::Mirroring;
|
||||
|
||||
const MAPPER_STATE_SECTION_MAGIC: [u8; 4] = *b"MSS1";
|
||||
const MAPPER_STATE_SECTION_VERSION: u8 = 1;
|
||||
|
||||
pub trait Mapper {
|
||||
fn cpu_read(&self, addr: u16) -> u8;
|
||||
fn cpu_write(&mut self, addr: u16, value: u8);
|
||||
fn cpu_read_low(&self, _addr: u16) -> Option<u8> {
|
||||
None
|
||||
}
|
||||
fn cpu_write_low(&mut self, _addr: u16, _value: u8) -> bool {
|
||||
false
|
||||
}
|
||||
fn ppu_read(&self, addr: u16) -> u8;
|
||||
fn ppu_write(&mut self, addr: u16, value: u8);
|
||||
fn mirroring(&self) -> Mirroring;
|
||||
fn map_nametable_addr(&self, _addr: u16) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
fn clock_cpu(&mut self, _cycles: u8) {}
|
||||
fn clock_scanline(&mut self) {}
|
||||
fn needs_ppu_a12_clock(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn poll_irq(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
fn save_state(&self, out: &mut Vec<u8>);
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String>;
|
||||
}
|
||||
|
||||
pub(super) struct MapperStateSectionWriter<'a> {
|
||||
out: &'a mut Vec<u8>,
|
||||
}
|
||||
|
||||
impl<'a> MapperStateSectionWriter<'a> {
|
||||
pub(super) fn new(out: &'a mut Vec<u8>) -> Self {
|
||||
Self { out }
|
||||
}
|
||||
|
||||
pub(super) fn write_bytes(&mut self, bytes: &[u8]) {
|
||||
// Unified section envelope:
|
||||
// magic (4), version (1), payload_len (4), payload (N).
|
||||
self.out.extend_from_slice(&MAPPER_STATE_SECTION_MAGIC);
|
||||
self.out.push(MAPPER_STATE_SECTION_VERSION);
|
||||
self.out
|
||||
.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
|
||||
self.out.extend_from_slice(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct MapperStateSectionReader<'a> {
|
||||
data: &'a [u8],
|
||||
cursor: usize,
|
||||
}
|
||||
|
||||
impl<'a> MapperStateSectionReader<'a> {
|
||||
pub(super) fn new(data: &'a [u8]) -> Self {
|
||||
Self { data, cursor: 0 }
|
||||
}
|
||||
|
||||
pub(super) fn read_bytes(&mut self) -> Result<&'a [u8], String> {
|
||||
let rem = self.data.len().saturating_sub(self.cursor);
|
||||
let (len, header_len) =
|
||||
if rem >= 9 && self.data[self.cursor..self.cursor + 4] == MAPPER_STATE_SECTION_MAGIC {
|
||||
let version = self.data[self.cursor + 4];
|
||||
if version != MAPPER_STATE_SECTION_VERSION {
|
||||
return Err(format!(
|
||||
"unsupported mapper state section version {}",
|
||||
version
|
||||
));
|
||||
}
|
||||
let len = u32::from_le_bytes([
|
||||
self.data[self.cursor + 5],
|
||||
self.data[self.cursor + 6],
|
||||
self.data[self.cursor + 7],
|
||||
self.data[self.cursor + 8],
|
||||
]) as usize;
|
||||
(len, 9usize)
|
||||
} else if rem >= 4 {
|
||||
// Backward-compatible legacy section envelope:
|
||||
// payload_len (4), payload (N).
|
||||
let len = u32::from_le_bytes([
|
||||
self.data[self.cursor],
|
||||
self.data[self.cursor + 1],
|
||||
self.data[self.cursor + 2],
|
||||
self.data[self.cursor + 3],
|
||||
]) as usize;
|
||||
(len, 4usize)
|
||||
} else {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
};
|
||||
self.cursor += header_len;
|
||||
let end = self
|
||||
.cursor
|
||||
.checked_add(len)
|
||||
.ok_or_else(|| "mapper state cursor overflow".to_string())?;
|
||||
if end > self.data.len() {
|
||||
return Err("mapper state payload has invalid length".to_string());
|
||||
}
|
||||
let out = &self.data[self.cursor..end];
|
||||
self.cursor = end;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub(super) fn at_end(&self) -> bool {
|
||||
self.cursor == self.data.len()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn safe_mod(value: usize, modulo: usize) -> usize {
|
||||
if modulo == 0 { 0 } else { value % modulo }
|
||||
}
|
||||
|
||||
pub(super) fn read_bank(data: &[u8], bank_size: usize, bank: usize, offset: usize) -> u8 {
|
||||
if data.is_empty() || bank_size == 0 {
|
||||
return 0;
|
||||
}
|
||||
let total_banks = data.len() / bank_size;
|
||||
let bank_idx = safe_mod(bank, total_banks.max(1));
|
||||
let idx = bank_idx * bank_size + safe_mod(offset, bank_size);
|
||||
data.get(idx).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
pub(super) fn write_state_bytes(out: &mut Vec<u8>, bytes: &[u8]) {
|
||||
MapperStateSectionWriter::new(out).write_bytes(bytes);
|
||||
}
|
||||
|
||||
pub(super) fn read_state_bytes<'a>(data: &'a [u8], cursor: &mut usize) -> Result<&'a [u8], String> {
|
||||
let mut rd = MapperStateSectionReader {
|
||||
data,
|
||||
cursor: *cursor,
|
||||
};
|
||||
let payload = rd.read_bytes()?;
|
||||
*cursor = rd.cursor;
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
pub(super) fn write_chr_state(out: &mut Vec<u8>, chr_data: &[u8]) {
|
||||
write_state_bytes(out, chr_data);
|
||||
}
|
||||
|
||||
pub(super) fn load_chr_state(chr_data: &mut [u8], data: &[u8]) -> Result<(), String> {
|
||||
let mut rd = MapperStateSectionReader::new(data);
|
||||
let payload = rd.read_bytes()?;
|
||||
if payload.len() != chr_data.len() {
|
||||
return Err("mapper state does not match loaded ROM".to_string());
|
||||
}
|
||||
chr_data.copy_from_slice(payload);
|
||||
if !rd.at_end() {
|
||||
return Err("mapper state has trailing bytes".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn encode_mirroring(mirroring: Mirroring) -> u8 {
|
||||
match mirroring {
|
||||
Mirroring::Horizontal => 0,
|
||||
Mirroring::Vertical => 1,
|
||||
Mirroring::FourScreen => 2,
|
||||
Mirroring::OneScreenLow => 3,
|
||||
Mirroring::OneScreenHigh => 4,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn decode_mirroring(value: u8) -> Mirroring {
|
||||
match value {
|
||||
1 => Mirroring::Vertical,
|
||||
2 => Mirroring::FourScreen,
|
||||
3 => Mirroring::OneScreenLow,
|
||||
4 => Mirroring::OneScreenHigh,
|
||||
_ => Mirroring::Horizontal,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct VrcIrqRegisters<'a> {
|
||||
pub latch: &'a mut u8,
|
||||
pub counter: &'a mut u8,
|
||||
pub enabled: &'a mut bool,
|
||||
pub enabled_after_ack: &'a mut bool,
|
||||
pub mode_cpu: &'a mut bool,
|
||||
pub pending: &'a mut bool,
|
||||
pub prescaler: &'a mut i16,
|
||||
}
|
||||
|
||||
pub(super) fn vrc_irq_tick(counter: &mut u8, latch: u8, pending: &mut bool) {
|
||||
if *counter == 0xFF {
|
||||
*counter = latch;
|
||||
*pending = true;
|
||||
} else {
|
||||
*counter = counter.wrapping_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn vrc_irq_write_control(value: u8, irq: VrcIrqRegisters<'_>) {
|
||||
let VrcIrqRegisters {
|
||||
latch,
|
||||
counter,
|
||||
enabled,
|
||||
enabled_after_ack,
|
||||
mode_cpu,
|
||||
pending,
|
||||
prescaler,
|
||||
} = irq;
|
||||
*mode_cpu = (value & 0x04) != 0;
|
||||
*enabled = (value & 0x02) != 0;
|
||||
*enabled_after_ack = (value & 0x01) != 0;
|
||||
*pending = false;
|
||||
*prescaler = 341;
|
||||
if *enabled {
|
||||
*counter = *latch;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn vrc_irq_ack(irq: VrcIrqRegisters<'_>) {
|
||||
let VrcIrqRegisters {
|
||||
enabled,
|
||||
enabled_after_ack,
|
||||
pending,
|
||||
prescaler,
|
||||
..
|
||||
} = irq;
|
||||
*pending = false;
|
||||
*enabled = *enabled_after_ack;
|
||||
*prescaler = 341;
|
||||
}
|
||||
|
||||
pub(super) fn vrc_irq_clock(cycles: u8, irq: VrcIrqRegisters<'_>) {
|
||||
let VrcIrqRegisters {
|
||||
latch,
|
||||
counter,
|
||||
enabled,
|
||||
mode_cpu,
|
||||
pending,
|
||||
prescaler,
|
||||
..
|
||||
} = irq;
|
||||
if !*enabled {
|
||||
return;
|
||||
}
|
||||
for _ in 0..cycles {
|
||||
if *mode_cpu {
|
||||
vrc_irq_tick(counter, *latch, pending);
|
||||
} else {
|
||||
*prescaler -= 3;
|
||||
if *prescaler <= 0 {
|
||||
*prescaler += 341;
|
||||
vrc_irq_tick(counter, *latch, pending);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
91
src/native_core/mapper/mappers/axrom.rs
Normal file
91
src/native_core/mapper/mappers/axrom.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Axrom {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
bank_select: u8,
|
||||
one_screen_hi: bool,
|
||||
bus_conflicts_and: bool,
|
||||
}
|
||||
|
||||
impl Axrom {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
let bus_conflicts_and = rom.header.mapper == 7 && rom.header.submapper == 2;
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
bank_select: 0,
|
||||
one_screen_hi: false,
|
||||
bus_conflicts_and,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Axrom {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x8000,
|
||||
self.bank_select as usize,
|
||||
(addr as usize) - 0x8000,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr >= 0x8000 {
|
||||
let latched = if self.bus_conflicts_and {
|
||||
value & self.cpu_read(addr)
|
||||
} else {
|
||||
value
|
||||
};
|
||||
self.bank_select = latched & 0x07;
|
||||
self.one_screen_hi = (latched & 0x10) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
if self.one_screen_hi {
|
||||
Mirroring::OneScreenHigh
|
||||
} else {
|
||||
Mirroring::OneScreenLow
|
||||
}
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.bank_select);
|
||||
out.push(u8::from(self.one_screen_hi));
|
||||
out.push(u8::from(self.bus_conflicts_and));
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 3 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.bank_select = data[0];
|
||||
self.one_screen_hi = data[1] != 0;
|
||||
self.bus_conflicts_and = data[2] != 0;
|
||||
load_chr_state(&mut self.chr_data, &data[3..])
|
||||
}
|
||||
}
|
||||
119
src/native_core/mapper/mappers/bandai70_152.rs
Normal file
119
src/native_core/mapper/mappers/bandai70_152.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Bandai70_152 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring_default: Mirroring,
|
||||
prg_bank: u8,
|
||||
chr_bank: u8,
|
||||
one_screen_hi: Option<bool>,
|
||||
}
|
||||
|
||||
impl Bandai70_152 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring_default: rom.header.mirroring,
|
||||
prg_bank: 0,
|
||||
chr_bank: 0,
|
||||
one_screen_hi: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn prg_bank_count_16k(&self) -> usize {
|
||||
(self.prg_rom.len() / 0x4000).max(1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Bandai70_152 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
if addr < 0xC000 {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.prg_bank as usize,
|
||||
(addr as usize) - 0x8000,
|
||||
)
|
||||
} else {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.prg_bank_count_16k().saturating_sub(1),
|
||||
(addr as usize) - 0xC000,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr < 0x8000 {
|
||||
return;
|
||||
}
|
||||
self.prg_bank = value & 0x0F;
|
||||
self.chr_bank = (value >> 4) & 0x0F;
|
||||
self.one_screen_hi = Some((value & 0x80) != 0);
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
0x2000,
|
||||
self.chr_bank as usize,
|
||||
addr as usize,
|
||||
)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let total_banks = (self.chr_data.len() / 0x2000).max(1);
|
||||
let bank_idx = safe_mod(self.chr_bank as usize, total_banks);
|
||||
let idx = bank_idx * 0x2000 + ((addr as usize) & 0x1FFF);
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
match self.one_screen_hi {
|
||||
Some(true) => Mirroring::OneScreenHigh,
|
||||
Some(false) => Mirroring::OneScreenLow,
|
||||
None => self.mirroring_default,
|
||||
}
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.prg_bank);
|
||||
out.push(self.chr_bank);
|
||||
out.push(match self.one_screen_hi {
|
||||
Some(true) => 2,
|
||||
Some(false) => 1,
|
||||
None => 0,
|
||||
});
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 3 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.prg_bank = data[0];
|
||||
self.chr_bank = data[1];
|
||||
self.one_screen_hi = match data[2] {
|
||||
0 => None,
|
||||
1 => Some(false),
|
||||
2 => Some(true),
|
||||
_ => None,
|
||||
};
|
||||
load_chr_state(&mut self.chr_data, &data[3..])
|
||||
}
|
||||
}
|
||||
112
src/native_core/mapper/mappers/bnrom34.rs
Normal file
112
src/native_core/mapper/mappers/bnrom34.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Bnrom34 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring: Mirroring,
|
||||
prg_bank: u8,
|
||||
chr_bank_0_4k: u8,
|
||||
chr_bank_1_4k: u8,
|
||||
}
|
||||
|
||||
impl Bnrom34 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring: rom.header.mirroring,
|
||||
prg_bank: 0,
|
||||
chr_bank_0_4k: 0,
|
||||
chr_bank_1_4k: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Bnrom34 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x8000,
|
||||
self.prg_bank as usize,
|
||||
(addr as usize) - 0x8000,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr >= 0x8000 {
|
||||
self.prg_bank = value & 0x0F;
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||
match addr {
|
||||
0x7FFD => {
|
||||
self.prg_bank = value & 0x0F;
|
||||
true
|
||||
}
|
||||
0x7FFE => {
|
||||
self.chr_bank_0_4k = value & 0x0F;
|
||||
true
|
||||
}
|
||||
0x7FFF => {
|
||||
self.chr_bank_1_4k = value & 0x0F;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
let bank = if addr < 0x1000 {
|
||||
self.chr_bank_0_4k as usize
|
||||
} else {
|
||||
self.chr_bank_1_4k as usize
|
||||
};
|
||||
read_bank(&self.chr_data, 0x1000, bank, (addr as usize) & 0x0FFF)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let bank = if addr < 0x1000 {
|
||||
self.chr_bank_0_4k as usize
|
||||
} else {
|
||||
self.chr_bank_1_4k as usize
|
||||
};
|
||||
let total = (self.chr_data.len() / 0x1000).max(1);
|
||||
let idx = safe_mod(bank, total) * 0x1000 + ((addr as usize) & 0x0FFF);
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.prg_bank);
|
||||
out.push(self.chr_bank_0_4k);
|
||||
out.push(self.chr_bank_1_4k);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 3 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.prg_bank = data[0];
|
||||
self.chr_bank_0_4k = data[1];
|
||||
self.chr_bank_1_4k = data[2];
|
||||
load_chr_state(&mut self.chr_data, &data[3..])
|
||||
}
|
||||
}
|
||||
114
src/native_core/mapper/mappers/camerica71.rs
Normal file
114
src/native_core/mapper/mappers/camerica71.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Camerica71 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
submapper: u8,
|
||||
mirroring_default: Mirroring,
|
||||
prg_bank: u8,
|
||||
one_screen_hi: Option<bool>,
|
||||
}
|
||||
|
||||
impl Camerica71 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
let submapper = rom.header.submapper;
|
||||
let one_screen_hi = if submapper == 1 { Some(false) } else { None };
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
submapper,
|
||||
mirroring_default: rom.header.mirroring,
|
||||
prg_bank: 0,
|
||||
one_screen_hi,
|
||||
}
|
||||
}
|
||||
|
||||
fn prg_bank_count_16k(&self) -> usize {
|
||||
(self.prg_rom.len() / 0x4000).max(1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Camerica71 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
if addr < 0xC000 {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.prg_bank as usize,
|
||||
(addr as usize) - 0x8000,
|
||||
)
|
||||
} else {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.prg_bank_count_16k() - 1,
|
||||
(addr as usize) - 0xC000,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if self.submapper == 1 && (0x9000..=0x9FFF).contains(&addr) {
|
||||
self.one_screen_hi = Some((value & 0x10) != 0);
|
||||
return;
|
||||
}
|
||||
if addr >= 0x8000 {
|
||||
self.prg_bank = value & 0x0F;
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
match self.one_screen_hi {
|
||||
Some(true) => Mirroring::OneScreenHigh,
|
||||
Some(false) => Mirroring::OneScreenLow,
|
||||
None => self.mirroring_default,
|
||||
}
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.submapper);
|
||||
out.push(self.prg_bank);
|
||||
out.push(match self.one_screen_hi {
|
||||
Some(true) => 2,
|
||||
Some(false) => 1,
|
||||
None => 0,
|
||||
});
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 3 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.submapper = data[0];
|
||||
self.prg_bank = data[1];
|
||||
self.one_screen_hi = match data[2] {
|
||||
0 => None,
|
||||
1 => Some(false),
|
||||
2 => Some(true),
|
||||
_ => None,
|
||||
};
|
||||
load_chr_state(&mut self.chr_data, &data[3..])
|
||||
}
|
||||
}
|
||||
100
src/native_core/mapper/mappers/cnrom.rs
Normal file
100
src/native_core/mapper/mappers/cnrom.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Cnrom {
|
||||
submapper: u8,
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
chr_bank: u8,
|
||||
bus_conflicts_and: bool,
|
||||
mirroring: Mirroring,
|
||||
}
|
||||
|
||||
impl Cnrom {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
let bus_conflicts_and = rom.header.mapper == 3 && rom.header.submapper == 2;
|
||||
Self {
|
||||
submapper: rom.header.submapper,
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
chr_bank: 0,
|
||||
bus_conflicts_and,
|
||||
mirroring: rom.header.mirroring,
|
||||
}
|
||||
}
|
||||
|
||||
fn chr_banks(&self) -> usize {
|
||||
(self.chr_data.len() / 0x2000).max(1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Cnrom {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
((addr - 0x8000) as usize) / 0x4000,
|
||||
addr as usize,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr >= 0x8000 {
|
||||
let value = if self.bus_conflicts_and {
|
||||
value & self.cpu_read(addr)
|
||||
} else {
|
||||
value
|
||||
};
|
||||
let max = self.chr_banks() as u8;
|
||||
self.chr_bank = if max == 0 { 0 } else { value % max };
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
0x2000,
|
||||
self.chr_bank as usize,
|
||||
addr as usize,
|
||||
)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let bank = safe_mod(self.chr_bank as usize, self.chr_banks());
|
||||
let idx = bank * 0x2000 + (addr as usize & 0x1FFF);
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.chr_bank);
|
||||
out.push(self.submapper);
|
||||
out.push(u8::from(self.bus_conflicts_and));
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 3 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.chr_bank = data[0];
|
||||
self.submapper = data[1];
|
||||
self.bus_conflicts_and = data[2] != 0;
|
||||
load_chr_state(&mut self.chr_data, &data[3..])
|
||||
}
|
||||
}
|
||||
88
src/native_core/mapper/mappers/color_dreams11.rs
Normal file
88
src/native_core/mapper/mappers/color_dreams11.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct ColorDreams11 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring: Mirroring,
|
||||
prg_bank: u8,
|
||||
chr_bank: u8,
|
||||
}
|
||||
|
||||
impl ColorDreams11 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring: rom.header.mirroring,
|
||||
prg_bank: 0,
|
||||
chr_bank: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for ColorDreams11 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x8000,
|
||||
self.prg_bank as usize,
|
||||
(addr as usize) - 0x8000,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr >= 0x8000 {
|
||||
let latched = value & self.cpu_read(addr);
|
||||
self.prg_bank = latched & 0x03;
|
||||
self.chr_bank = (latched >> 4) & 0x0F;
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
0x2000,
|
||||
self.chr_bank as usize,
|
||||
addr as usize,
|
||||
)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let total_banks = (self.chr_data.len() / 0x2000).max(1);
|
||||
let bank_idx = safe_mod(self.chr_bank as usize, total_banks);
|
||||
let idx = bank_idx * 0x2000 + (addr as usize & 0x1FFF);
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.prg_bank);
|
||||
out.push(self.chr_bank);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 2 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.prg_bank = data[0];
|
||||
self.chr_bank = data[1];
|
||||
load_chr_state(&mut self.chr_data, &data[2..])
|
||||
}
|
||||
}
|
||||
88
src/native_core/mapper/mappers/cprom13.rs
Normal file
88
src/native_core/mapper/mappers/cprom13.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Cprom13 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_ram: Vec<u8>,
|
||||
mirroring: Mirroring,
|
||||
chr_bank_hi_4k: u8,
|
||||
}
|
||||
|
||||
impl Cprom13 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
let mut chr_ram = rom.chr_data;
|
||||
if chr_ram.len() < 0x4000 {
|
||||
chr_ram.resize(0x4000, 0);
|
||||
}
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_ram,
|
||||
mirroring: rom.header.mirroring,
|
||||
chr_bank_hi_4k: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Cprom13 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
((addr - 0x8000) as usize) / 0x4000,
|
||||
addr as usize,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr >= 0x8000 {
|
||||
self.chr_bank_hi_4k = value & 0x03;
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
let bank = if addr < 0x1000 {
|
||||
0usize
|
||||
} else {
|
||||
self.chr_bank_hi_4k as usize
|
||||
};
|
||||
read_bank(&self.chr_ram, 0x1000, bank, (addr as usize) & 0x0FFF)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let bank = if addr < 0x1000 {
|
||||
0usize
|
||||
} else {
|
||||
self.chr_bank_hi_4k as usize
|
||||
};
|
||||
let total = (self.chr_ram.len() / 0x1000).max(1);
|
||||
let idx = safe_mod(bank, total) * 0x1000 + ((addr as usize) & 0x0FFF);
|
||||
if let Some(cell) = self.chr_ram.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.chr_bank_hi_4k);
|
||||
write_chr_state(out, &self.chr_ram);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.is_empty() {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.chr_bank_hi_4k = data[0];
|
||||
load_chr_state(&mut self.chr_ram, &data[1..])
|
||||
}
|
||||
}
|
||||
78
src/native_core/mapper/mappers/crazy_climber180.rs
Normal file
78
src/native_core/mapper/mappers/crazy_climber180.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct CrazyClimber180 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring: Mirroring,
|
||||
bank_select: u8,
|
||||
}
|
||||
|
||||
impl CrazyClimber180 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring: rom.header.mirroring,
|
||||
bank_select: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for CrazyClimber180 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
if addr < 0xC000 {
|
||||
read_bank(&self.prg_rom, 0x4000, 0, (addr as usize) - 0x8000)
|
||||
} else {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.bank_select as usize,
|
||||
(addr as usize) - 0xC000,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr >= 0x8000 {
|
||||
self.bank_select = value & 0x07;
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.bank_select);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.is_empty() {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.bank_select = data[0];
|
||||
load_chr_state(&mut self.chr_data, &data[1..])
|
||||
}
|
||||
}
|
||||
19
src/native_core/mapper/mappers/fme7.rs
Normal file
19
src/native_core/mapper/mappers/fme7.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Fme7 {
|
||||
pub(super) prg_rom: Vec<u8>,
|
||||
pub(super) chr_data: Vec<u8>,
|
||||
pub(super) chr_is_ram: bool,
|
||||
pub(super) mirroring: Mirroring,
|
||||
pub(super) command: u8,
|
||||
pub(super) chr_banks: [u8; 8],
|
||||
pub(super) prg_banks: [u8; 3],
|
||||
pub(super) low_bank: u8,
|
||||
pub(super) low_is_ram: bool,
|
||||
pub(super) low_ram_enabled: bool,
|
||||
pub(super) low_ram: Vec<u8>,
|
||||
pub(super) irq_counter: u16,
|
||||
pub(super) irq_enabled: bool,
|
||||
pub(super) irq_counter_enabled: bool,
|
||||
pub(super) irq_pending: bool,
|
||||
}
|
||||
85
src/native_core/mapper/mappers/gxrom.rs
Normal file
85
src/native_core/mapper/mappers/gxrom.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Gxrom {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
prg_bank: u8,
|
||||
chr_bank: u8,
|
||||
mirroring: Mirroring,
|
||||
}
|
||||
|
||||
impl Gxrom {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
prg_bank: 0,
|
||||
chr_bank: 0,
|
||||
mirroring: rom.header.mirroring,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Gxrom {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x8000,
|
||||
self.prg_bank as usize,
|
||||
(addr as usize) - 0x8000,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr >= 0x8000 {
|
||||
self.prg_bank = (value >> 4) & 0x03;
|
||||
self.chr_bank = value & 0x03;
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
0x2000,
|
||||
self.chr_bank as usize,
|
||||
addr as usize,
|
||||
)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let total = (self.chr_data.len() / 0x2000).max(1);
|
||||
let idx = safe_mod(self.chr_bank as usize, total) * 0x2000 + (addr as usize & 0x1FFF);
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.prg_bank);
|
||||
out.push(self.chr_bank);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() != 2 {
|
||||
return Err("mapper state payload has invalid length".to_string());
|
||||
}
|
||||
self.prg_bank = data[0];
|
||||
self.chr_bank = data[1];
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
295
src/native_core/mapper/mappers/mapper105.rs
Normal file
295
src/native_core/mapper/mappers/mapper105.rs
Normal file
@@ -0,0 +1,295 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct InesMapper105 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
prg_ram: Vec<u8>,
|
||||
shift_reg: u8,
|
||||
shift_count: u8,
|
||||
control: u8,
|
||||
reg_a: u8,
|
||||
reg_b: u8,
|
||||
wram_disabled: bool,
|
||||
prg_unlocked: bool,
|
||||
saw_i_low: bool,
|
||||
irq_counter: u32, // 30-bit counter
|
||||
irq_pending: bool,
|
||||
}
|
||||
|
||||
impl InesMapper105 {
|
||||
pub(crate) const IRQ_THRESHOLD: u32 = 0x2800_0000; // Official Nintendo World Championships DIP setting.
|
||||
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: if rom.chr_data.is_empty() {
|
||||
vec![0; 0x2000]
|
||||
} else {
|
||||
rom.chr_data
|
||||
},
|
||||
prg_ram: vec![0; 0x2000],
|
||||
shift_reg: 0,
|
||||
shift_count: 0,
|
||||
control: 0x0C,
|
||||
reg_a: 0x10, // I bit high after reset keeps timer halted.
|
||||
reg_b: 0,
|
||||
wram_disabled: false,
|
||||
prg_unlocked: false,
|
||||
saw_i_low: false,
|
||||
irq_counter: 0,
|
||||
irq_pending: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn prg_mode(&self) -> u8 {
|
||||
(self.control >> 2) & 0x03
|
||||
}
|
||||
|
||||
fn timer_halted(&self) -> bool {
|
||||
(self.reg_a & 0x10) != 0
|
||||
}
|
||||
|
||||
fn outer_chip_selected(&self) -> bool {
|
||||
(self.reg_a & 0x08) != 0
|
||||
}
|
||||
|
||||
fn outer_32k_bank(&self) -> usize {
|
||||
((self.reg_a >> 1) & 0x03) as usize
|
||||
}
|
||||
|
||||
fn prg_bank_value(&self) -> usize {
|
||||
(self.reg_b & 0x0F) as usize
|
||||
}
|
||||
|
||||
fn prg_bank_count_16k(&self) -> usize {
|
||||
(self.prg_rom.len() / 0x4000).max(1)
|
||||
}
|
||||
|
||||
fn first_chip_banks_16k(&self) -> usize {
|
||||
(self.prg_bank_count_16k() / 2).max(1)
|
||||
}
|
||||
|
||||
fn second_chip_base_16k(&self) -> usize {
|
||||
self.first_chip_banks_16k()
|
||||
}
|
||||
|
||||
fn second_chip_banks_16k(&self) -> usize {
|
||||
self.prg_bank_count_16k()
|
||||
.saturating_sub(self.second_chip_base_16k())
|
||||
.max(1)
|
||||
}
|
||||
|
||||
fn on_reg_a_write(&mut self, value: u8) {
|
||||
let previous_i = (self.reg_a & 0x10) != 0;
|
||||
let next_i = (value & 0x10) != 0;
|
||||
self.reg_a = value & 0x1F;
|
||||
|
||||
if !next_i {
|
||||
self.saw_i_low = true;
|
||||
}
|
||||
if previous_i && !next_i {
|
||||
self.irq_pending = false;
|
||||
}
|
||||
if !previous_i && next_i && self.saw_i_low {
|
||||
self.prg_unlocked = true;
|
||||
self.irq_pending = false;
|
||||
}
|
||||
if next_i {
|
||||
self.irq_counter = 0;
|
||||
self.irq_pending = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn write_serial(&mut self, addr: u16, value: u8) {
|
||||
if (value & 0x80) != 0 {
|
||||
self.shift_reg = 0;
|
||||
self.shift_count = 0;
|
||||
self.control |= 0x0C;
|
||||
return;
|
||||
}
|
||||
|
||||
self.shift_reg |= (value & 1) << self.shift_count;
|
||||
self.shift_count = self.shift_count.wrapping_add(1);
|
||||
if self.shift_count < 5 {
|
||||
return;
|
||||
}
|
||||
|
||||
let latched = self.shift_reg & 0x1F;
|
||||
match (addr >> 13) & 0x03 {
|
||||
0 => self.control = latched,
|
||||
1 => self.on_reg_a_write(latched),
|
||||
2 => {}
|
||||
_ => {
|
||||
self.reg_b = latched;
|
||||
self.wram_disabled = (latched & 0x10) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
self.shift_reg = 0;
|
||||
self.shift_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for InesMapper105 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if !self.prg_unlocked {
|
||||
// Reset/power-on maps a fixed 32KiB window from the first PRG chip.
|
||||
return read_bank(
|
||||
&self.prg_rom,
|
||||
0x8000,
|
||||
0,
|
||||
(addr as usize).saturating_sub(0x8000),
|
||||
);
|
||||
}
|
||||
|
||||
if !self.outer_chip_selected() {
|
||||
let bank32 = self.outer_32k_bank();
|
||||
return read_bank(
|
||||
&self.prg_rom,
|
||||
0x8000,
|
||||
bank32,
|
||||
(addr as usize).saturating_sub(0x8000),
|
||||
);
|
||||
}
|
||||
|
||||
let second_base = self.second_chip_base_16k();
|
||||
let second_count = self.second_chip_banks_16k();
|
||||
let prg_bank = self.prg_bank_value();
|
||||
|
||||
let bank16 = match self.prg_mode() {
|
||||
0 | 1 => {
|
||||
let bank32 = prg_bank >> 1;
|
||||
if addr < 0xC000 {
|
||||
second_base + safe_mod(bank32 * 2, second_count)
|
||||
} else {
|
||||
second_base + safe_mod(bank32 * 2 + 1, second_count)
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
if addr < 0xC000 {
|
||||
second_base
|
||||
} else {
|
||||
second_base + safe_mod(prg_bank, second_count)
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if addr < 0xC000 {
|
||||
second_base + safe_mod(prg_bank, second_count)
|
||||
} else {
|
||||
second_base + second_count.saturating_sub(1)
|
||||
}
|
||||
}
|
||||
};
|
||||
read_bank(&self.prg_rom, 0x4000, bank16, (addr as usize) & 0x3FFF)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr >= 0x8000 {
|
||||
self.write_serial(addr, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
if (0x6000..=0x7FFF).contains(&addr) && !self.wram_disabled {
|
||||
let idx = (addr as usize) & 0x1FFF;
|
||||
return self.prg_ram.get(idx).copied();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||
if (0x6000..=0x7FFF).contains(&addr) && !self.wram_disabled {
|
||||
let idx = (addr as usize) & 0x1FFF;
|
||||
if let Some(cell) = self.prg_ram.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
match self.control & 0x03 {
|
||||
0 => Mirroring::OneScreenLow,
|
||||
1 => Mirroring::OneScreenHigh,
|
||||
2 => Mirroring::Vertical,
|
||||
_ => Mirroring::Horizontal,
|
||||
}
|
||||
}
|
||||
|
||||
fn clock_cpu(&mut self, cycles: u8) {
|
||||
if self.timer_halted() || self.irq_pending {
|
||||
return;
|
||||
}
|
||||
let previous = self.irq_counter;
|
||||
self.irq_counter = (self.irq_counter.wrapping_add(cycles as u32)) & 0x3FFF_FFFF;
|
||||
if previous < Self::IRQ_THRESHOLD && self.irq_counter >= Self::IRQ_THRESHOLD {
|
||||
self.irq_pending = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_irq(&mut self) -> bool {
|
||||
let out = self.irq_pending;
|
||||
self.irq_pending = false;
|
||||
out
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.shift_reg);
|
||||
out.push(self.shift_count);
|
||||
out.push(self.control);
|
||||
out.push(self.reg_a);
|
||||
out.push(self.reg_b);
|
||||
out.push(u8::from(self.wram_disabled));
|
||||
out.push(u8::from(self.prg_unlocked));
|
||||
out.push(u8::from(self.saw_i_low));
|
||||
out.extend_from_slice(&self.irq_counter.to_le_bytes());
|
||||
out.push(u8::from(self.irq_pending));
|
||||
write_state_bytes(out, &self.prg_ram);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 13 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.shift_reg = data[0];
|
||||
self.shift_count = data[1];
|
||||
self.control = data[2];
|
||||
self.reg_a = data[3];
|
||||
self.reg_b = data[4];
|
||||
self.wram_disabled = data[5] != 0;
|
||||
self.prg_unlocked = data[6] != 0;
|
||||
self.saw_i_low = data[7] != 0;
|
||||
self.irq_counter = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) & 0x3FFF_FFFF;
|
||||
self.irq_pending = data[12] != 0;
|
||||
|
||||
let mut cursor = 13usize;
|
||||
let prg_ram = read_state_bytes(data, &mut cursor)?;
|
||||
if prg_ram.len() != self.prg_ram.len() {
|
||||
return Err("mapper state does not match loaded ROM".to_string());
|
||||
}
|
||||
self.prg_ram.copy_from_slice(prg_ram);
|
||||
load_chr_state(&mut self.chr_data, &data[cursor..])
|
||||
}
|
||||
}
|
||||
77
src/native_core/mapper/mappers/mapper118.rs
Normal file
77
src/native_core/mapper/mappers/mapper118.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct InesMapper118 {
|
||||
mmc3: Mmc3,
|
||||
}
|
||||
|
||||
impl InesMapper118 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
mmc3: Mmc3::new(rom),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for InesMapper118 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
self.mmc3.cpu_read(addr)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
self.mmc3.cpu_write(addr, value);
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
self.mmc3.cpu_read_low(addr)
|
||||
}
|
||||
|
||||
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||
self.mmc3.cpu_write_low(addr, value)
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
self.mmc3.ppu_read(addr)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
self.mmc3.ppu_write(addr, value);
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mmc3.mirroring()
|
||||
}
|
||||
|
||||
fn map_nametable_addr(&self, addr: u16) -> Option<usize> {
|
||||
if !(0x2000..=0x3EFF).contains(&addr) {
|
||||
return None;
|
||||
}
|
||||
// TxSROM-class boards route CHR bank bit 7 (A17) to CIRAM A10 for NT fetches.
|
||||
// The board responds to $2000-$2FFF the same as MMC3's $0000-$0FFF CHR decode.
|
||||
let rel = (addr - 0x2000) & 0x0FFF;
|
||||
let page = (rel / 0x0400) as usize; // NT0..NT3 -> 1KB pages 0..3
|
||||
let offset = (rel & 0x03FF) as usize;
|
||||
let bank = self.mmc3.chr_bank_for_1k_page(page) as u8;
|
||||
let ciram_page = ((bank >> 7) & 1) as usize;
|
||||
Some(ciram_page * 0x0400 + offset)
|
||||
}
|
||||
|
||||
fn clock_scanline(&mut self) {
|
||||
self.mmc3.clock_scanline();
|
||||
}
|
||||
|
||||
fn needs_ppu_a12_clock(&self) -> bool {
|
||||
self.mmc3.needs_ppu_a12_clock()
|
||||
}
|
||||
|
||||
fn poll_irq(&mut self) -> bool {
|
||||
self.mmc3.poll_irq()
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
self.mmc3.save_state(out);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
self.mmc3.load_state(data)
|
||||
}
|
||||
}
|
||||
90
src/native_core/mapper/mappers/mapper140.rs
Normal file
90
src/native_core/mapper/mappers/mapper140.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct InesMapper140 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring: Mirroring,
|
||||
prg_bank: u8,
|
||||
chr_bank: u8,
|
||||
}
|
||||
|
||||
impl InesMapper140 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring: rom.header.mirroring,
|
||||
prg_bank: 0,
|
||||
chr_bank: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for InesMapper140 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x8000,
|
||||
self.prg_bank as usize,
|
||||
(addr as usize) - 0x8000,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||
|
||||
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||
if !(0x6000..=0x7FFF).contains(&addr) {
|
||||
return false;
|
||||
}
|
||||
self.chr_bank = value & 0x0F;
|
||||
self.prg_bank = (value >> 4) & 0x03;
|
||||
true
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
0x2000,
|
||||
self.chr_bank as usize,
|
||||
addr as usize,
|
||||
)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let total = (self.chr_data.len() / 0x2000).max(1);
|
||||
let idx = safe_mod(self.chr_bank as usize, total) * 0x2000 + ((addr as usize) & 0x1FFF);
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.prg_bank);
|
||||
out.push(self.chr_bank);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 2 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.prg_bank = data[0];
|
||||
self.chr_bank = data[1];
|
||||
load_chr_state(&mut self.chr_data, &data[2..])
|
||||
}
|
||||
}
|
||||
253
src/native_core/mapper/mappers/mapper155.rs
Normal file
253
src/native_core/mapper/mappers/mapper155.rs
Normal file
@@ -0,0 +1,253 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct InesMapper155 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
prg_ram: Vec<u8>,
|
||||
shift_reg: u8,
|
||||
shift_count: u8,
|
||||
control: u8,
|
||||
chr_bank0: u8,
|
||||
chr_bank1: u8,
|
||||
prg_bank: u8,
|
||||
mirroring_default: Mirroring,
|
||||
}
|
||||
|
||||
impl InesMapper155 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
prg_ram: vec![0; 0x2000],
|
||||
shift_reg: 0,
|
||||
shift_count: 0,
|
||||
control: 0x0C,
|
||||
chr_bank0: 0,
|
||||
chr_bank1: 0,
|
||||
prg_bank: 0,
|
||||
mirroring_default: rom.header.mirroring,
|
||||
}
|
||||
}
|
||||
|
||||
fn prg_mode(&self) -> u8 {
|
||||
(self.control >> 2) & 0x03
|
||||
}
|
||||
|
||||
fn chr_mode(&self) -> u8 {
|
||||
(self.control >> 4) & 1
|
||||
}
|
||||
|
||||
fn prg_bank_count_16k(&self) -> usize {
|
||||
(self.prg_rom.len() / 0x4000).max(1)
|
||||
}
|
||||
|
||||
fn outer_a17_override_enabled(&self) -> bool {
|
||||
(self.prg_bank & 0x10) != 0
|
||||
}
|
||||
|
||||
fn outer_bank_half(&self) -> usize {
|
||||
((self.prg_bank >> 3) & 1) as usize
|
||||
}
|
||||
|
||||
fn map_bank_16k_with_outer(&self, inner_bank: usize) -> usize {
|
||||
let total = self.prg_bank_count_16k();
|
||||
if !self.outer_a17_override_enabled() || total <= 8 {
|
||||
return safe_mod(inner_bank, total);
|
||||
}
|
||||
let half = (total / 2).max(1);
|
||||
let base = safe_mod(self.outer_bank_half(), 2) * half;
|
||||
base + safe_mod(inner_bank, half)
|
||||
}
|
||||
|
||||
fn write_serial(&mut self, addr: u16, value: u8) {
|
||||
if (value & 0x80) != 0 {
|
||||
self.shift_reg = 0;
|
||||
self.shift_count = 0;
|
||||
self.control |= 0x0C;
|
||||
return;
|
||||
}
|
||||
|
||||
self.shift_reg |= (value & 1) << self.shift_count;
|
||||
self.shift_count = self.shift_count.wrapping_add(1);
|
||||
if self.shift_count < 5 {
|
||||
return;
|
||||
}
|
||||
|
||||
let reg = (addr >> 13) & 0x03;
|
||||
match reg {
|
||||
0 => self.control = self.shift_reg & 0x1F,
|
||||
1 => self.chr_bank0 = self.shift_reg & 0x1F,
|
||||
2 => self.chr_bank1 = self.shift_reg & 0x1F,
|
||||
_ => self.prg_bank = self.shift_reg & 0x1F, // MMC1A uses bit 4 for A17 behavior
|
||||
}
|
||||
self.shift_reg = 0;
|
||||
self.shift_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for InesMapper155 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let prg_mode = self.prg_mode();
|
||||
if prg_mode <= 1 {
|
||||
let bank32 = (self.prg_bank as usize) >> 1;
|
||||
let bank16 = bank32 * 2 + usize::from(addr >= 0xC000);
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.map_bank_16k_with_outer(bank16),
|
||||
(addr as usize) & 0x3FFF,
|
||||
)
|
||||
} else if prg_mode == 2 {
|
||||
if addr < 0xC000 {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.map_bank_16k_with_outer(0),
|
||||
(addr as usize) - 0x8000,
|
||||
)
|
||||
} else {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.map_bank_16k_with_outer((self.prg_bank & 0x0F) as usize),
|
||||
(addr as usize) - 0xC000,
|
||||
)
|
||||
}
|
||||
} else if addr < 0xC000 {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.map_bank_16k_with_outer((self.prg_bank & 0x0F) as usize),
|
||||
(addr as usize) - 0x8000,
|
||||
)
|
||||
} else {
|
||||
let fixed_last = if self.outer_a17_override_enabled() && self.prg_bank_count_16k() > 8 {
|
||||
self.prg_bank_count_16k() / 2 - 1
|
||||
} else {
|
||||
self.prg_bank_count_16k().saturating_sub(1)
|
||||
};
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.map_bank_16k_with_outer(fixed_last),
|
||||
(addr as usize) - 0xC000,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr >= 0x8000 {
|
||||
self.write_serial(addr, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
if (0x6000..=0x7FFF).contains(&addr) {
|
||||
return self.prg_ram.get((addr as usize) & 0x1FFF).copied();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||
if (0x6000..=0x7FFF).contains(&addr) {
|
||||
if let Some(cell) = self.prg_ram.get_mut((addr as usize) & 0x1FFF) {
|
||||
*cell = value;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
if self.chr_mode() == 0 {
|
||||
let bank = (self.chr_bank0 as usize) >> 1;
|
||||
read_bank(&self.chr_data, 0x2000, bank, addr as usize)
|
||||
} else if addr < 0x1000 {
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
0x1000,
|
||||
self.chr_bank0 as usize,
|
||||
addr as usize,
|
||||
)
|
||||
} else {
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
0x1000,
|
||||
self.chr_bank1 as usize,
|
||||
(addr as usize) - 0x1000,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let idx = if self.chr_mode() == 0 {
|
||||
let total_banks = (self.chr_data.len() / 0x2000).max(1);
|
||||
let bank = safe_mod((self.chr_bank0 as usize) >> 1, total_banks);
|
||||
bank * 0x2000 + ((addr as usize) & 0x1FFF)
|
||||
} else {
|
||||
let total_banks = (self.chr_data.len() / 0x1000).max(1);
|
||||
let bank = if addr < 0x1000 {
|
||||
self.chr_bank0 as usize
|
||||
} else {
|
||||
self.chr_bank1 as usize
|
||||
};
|
||||
safe_mod(bank, total_banks) * 0x1000 + ((addr as usize) & 0x0FFF)
|
||||
};
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
match self.control & 0x03 {
|
||||
0 => Mirroring::OneScreenLow,
|
||||
1 => Mirroring::OneScreenHigh,
|
||||
2 => Mirroring::Vertical,
|
||||
3 => Mirroring::Horizontal,
|
||||
_ => self.mirroring_default,
|
||||
}
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.shift_reg);
|
||||
out.push(self.shift_count);
|
||||
out.push(self.control);
|
||||
out.push(self.chr_bank0);
|
||||
out.push(self.chr_bank1);
|
||||
out.push(self.prg_bank);
|
||||
write_state_bytes(out, &self.prg_ram);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 6 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.shift_reg = data[0];
|
||||
self.shift_count = data[1];
|
||||
self.control = data[2];
|
||||
self.chr_bank0 = data[3];
|
||||
self.chr_bank1 = data[4];
|
||||
self.prg_bank = data[5];
|
||||
|
||||
let mut cursor = 6usize;
|
||||
let prg_ram = read_state_bytes(data, &mut cursor)?;
|
||||
if prg_ram.len() != self.prg_ram.len() {
|
||||
return Err("mapper state does not match loaded ROM".to_string());
|
||||
}
|
||||
self.prg_ram.copy_from_slice(prg_ram);
|
||||
load_chr_state(&mut self.chr_data, &data[cursor..])
|
||||
}
|
||||
}
|
||||
75
src/native_core/mapper/mappers/mapper158.rs
Normal file
75
src/native_core/mapper/mappers/mapper158.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct InesMapper158 {
|
||||
base: InesMapper64,
|
||||
}
|
||||
|
||||
impl InesMapper158 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
base: InesMapper64::new(rom),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for InesMapper158 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
self.base.cpu_read(addr)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
// Mapper 158 uses CHR-bank controlled nametable routing instead of $A000 mirroring writes.
|
||||
if (0xA000..=0xBFFF).contains(&addr) && (addr & 1) == 0 {
|
||||
return;
|
||||
}
|
||||
self.base.cpu_write(addr, value);
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
self.base.ppu_read(addr)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
self.base.ppu_write(addr, value);
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
Mirroring::Horizontal
|
||||
}
|
||||
|
||||
fn map_nametable_addr(&self, addr: u16) -> Option<usize> {
|
||||
if !(0x2000..=0x3EFF).contains(&addr) {
|
||||
return None;
|
||||
}
|
||||
let rel = (addr - 0x2000) & 0x0FFF;
|
||||
let page = (rel / 0x0400) as usize; // NT0..NT3
|
||||
let offset = (rel & 0x03FF) as usize;
|
||||
let chr_bank = self.base.chr_bank_for_page(page) as u8;
|
||||
let ciram_page = ((chr_bank >> 7) & 1) as usize;
|
||||
Some(ciram_page * 0x0400 + offset)
|
||||
}
|
||||
|
||||
fn clock_cpu(&mut self, cycles: u8) {
|
||||
self.base.clock_cpu(cycles);
|
||||
}
|
||||
|
||||
fn clock_scanline(&mut self) {
|
||||
self.base.clock_scanline();
|
||||
}
|
||||
|
||||
fn needs_ppu_a12_clock(&self) -> bool {
|
||||
self.base.needs_ppu_a12_clock()
|
||||
}
|
||||
|
||||
fn poll_irq(&mut self) -> bool {
|
||||
self.base.poll_irq()
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
self.base.save_state(out);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
self.base.load_state(data)
|
||||
}
|
||||
}
|
||||
95
src/native_core/mapper/mappers/mapper184.rs
Normal file
95
src/native_core/mapper/mappers/mapper184.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct InesMapper184 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring: Mirroring,
|
||||
chr_bank_lo_4k: u8,
|
||||
chr_bank_hi_4k: u8,
|
||||
}
|
||||
|
||||
impl InesMapper184 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring: rom.header.mirroring,
|
||||
chr_bank_lo_4k: 0,
|
||||
chr_bank_hi_4k: 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for InesMapper184 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
((addr - 0x8000) as usize) / 0x4000,
|
||||
addr as usize,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||
|
||||
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||
if !(0x6000..=0x7FFF).contains(&addr) {
|
||||
return false;
|
||||
}
|
||||
self.chr_bank_lo_4k = value & 0x07;
|
||||
self.chr_bank_hi_4k = 0x04 | ((value >> 4) & 0x07);
|
||||
true
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
let bank = if addr < 0x1000 {
|
||||
self.chr_bank_lo_4k as usize
|
||||
} else {
|
||||
self.chr_bank_hi_4k as usize
|
||||
};
|
||||
read_bank(&self.chr_data, 0x1000, bank, (addr as usize) & 0x0FFF)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let bank = if addr < 0x1000 {
|
||||
self.chr_bank_lo_4k as usize
|
||||
} else {
|
||||
self.chr_bank_hi_4k as usize
|
||||
};
|
||||
let total = (self.chr_data.len() / 0x1000).max(1);
|
||||
let idx = safe_mod(bank, total) * 0x1000 + ((addr as usize) & 0x0FFF);
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.chr_bank_lo_4k);
|
||||
out.push(self.chr_bank_hi_4k);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 2 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.chr_bank_lo_4k = data[0];
|
||||
self.chr_bank_hi_4k = data[1];
|
||||
load_chr_state(&mut self.chr_data, &data[2..])
|
||||
}
|
||||
}
|
||||
92
src/native_core/mapper/mappers/mapper185.rs
Normal file
92
src/native_core/mapper/mappers/mapper185.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct InesMapper185 {
|
||||
submapper: u8,
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring: Mirroring,
|
||||
latch: u8,
|
||||
}
|
||||
|
||||
impl InesMapper185 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
submapper: rom.header.submapper,
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring: rom.header.mirroring,
|
||||
latch: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn chr_enabled(&self) -> bool {
|
||||
match self.submapper {
|
||||
4..=7 => (self.latch & 0x03) == (self.submapper - 4),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for InesMapper185 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
((addr - 0x8000) as usize) / 0x4000,
|
||||
addr as usize,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr >= 0x8000 {
|
||||
// Mapper 185 always has AND bus conflicts.
|
||||
self.latch = value & self.cpu_read(addr);
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
if !self.chr_enabled() {
|
||||
return 0xFF;
|
||||
}
|
||||
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
if !self.chr_enabled() {
|
||||
return;
|
||||
}
|
||||
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.submapper);
|
||||
out.push(self.latch);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 2 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.submapper = data[0];
|
||||
self.latch = data[1];
|
||||
load_chr_state(&mut self.chr_data, &data[2..])
|
||||
}
|
||||
}
|
||||
127
src/native_core/mapper/mappers/mapper206.rs
Normal file
127
src/native_core/mapper/mappers/mapper206.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct InesMapper206 {
|
||||
pub(super) prg_rom: Vec<u8>,
|
||||
pub(super) chr_data: Vec<u8>,
|
||||
pub(super) chr_is_ram: bool,
|
||||
pub(super) mirroring: Mirroring,
|
||||
pub(super) bank_regs: [u8; 8],
|
||||
pub(super) bank_select: u8,
|
||||
}
|
||||
|
||||
impl InesMapper206 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring: rom.header.mirroring,
|
||||
bank_regs: [0; 8],
|
||||
bank_select: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn prg_bank_count_8k(&self) -> usize {
|
||||
(self.prg_rom.len() / 0x2000).max(1)
|
||||
}
|
||||
|
||||
pub(super) fn prg_bank_for_slot(&self, slot: usize) -> usize {
|
||||
let last = self.prg_bank_count_8k() - 1;
|
||||
let second_last = last.saturating_sub(1);
|
||||
match slot {
|
||||
0 => self.bank_regs[6] as usize,
|
||||
1 => self.bank_regs[7] as usize,
|
||||
2 => second_last,
|
||||
3 => last,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn chr_bank_for_1k_page(&self, page: usize) -> u8 {
|
||||
let regs = &self.bank_regs;
|
||||
match page {
|
||||
0 => regs[0] & !1,
|
||||
1 => (regs[0] & !1).wrapping_add(1),
|
||||
2 => regs[1] & !1,
|
||||
3 => (regs[1] & !1).wrapping_add(1),
|
||||
4 => regs[2],
|
||||
5 => regs[3],
|
||||
6 => regs[4],
|
||||
_ => regs[5],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for InesMapper206 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
let slot = ((addr - 0x8000) / 0x2000) as usize;
|
||||
let bank = self.prg_bank_for_slot(slot);
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x2000,
|
||||
bank,
|
||||
((addr as usize) - 0x8000) & 0x1FFF,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
match addr {
|
||||
0x8000..=0x9FFF if (addr & 1) == 0 => self.bank_select = value & 0x07,
|
||||
0x8000..=0x9FFF => {
|
||||
let reg = (self.bank_select & 0x07) as usize;
|
||||
self.bank_regs[reg] = value;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
let page = (addr / 0x0400) as usize;
|
||||
let bank = self.chr_bank_for_1k_page(page);
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
0x0400,
|
||||
bank as usize,
|
||||
(addr as usize) & 0x03FF,
|
||||
)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let page = (addr / 0x0400) as usize;
|
||||
let bank = self.chr_bank_for_1k_page(page) as usize;
|
||||
let total_banks = (self.chr_data.len() / 0x0400).max(1);
|
||||
let bank_idx = safe_mod(bank, total_banks);
|
||||
let idx = bank_idx * 0x0400 + ((addr as usize) & 0x03FF);
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(&self.bank_regs);
|
||||
out.push(self.bank_select);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 9 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.bank_regs.copy_from_slice(&data[0..8]);
|
||||
self.bank_select = data[8];
|
||||
load_chr_state(&mut self.chr_data, &data[9..])
|
||||
}
|
||||
}
|
||||
63
src/native_core/mapper/mappers/mapper206_submapper1.rs
Normal file
63
src/native_core/mapper/mappers/mapper206_submapper1.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct InesMapper206Submapper1 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring: Mirroring,
|
||||
}
|
||||
|
||||
impl InesMapper206Submapper1 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring: rom.header.mirroring,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for InesMapper206Submapper1 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x8000,
|
||||
0,
|
||||
((addr as usize) - 0x8000) & 0x7FFF,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
load_chr_state(&mut self.chr_data, data)
|
||||
}
|
||||
}
|
||||
313
src/native_core/mapper/mappers/mapper253.rs
Normal file
313
src/native_core/mapper/mappers/mapper253.rs
Normal file
@@ -0,0 +1,313 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct InesMapper253 {
|
||||
base: Vrc2_23,
|
||||
chr_ram_2k: [u8; 0x800], // PPU pages 4/5 ($1000-$17FF): on-cart CHR-RAM overlay
|
||||
}
|
||||
|
||||
impl InesMapper253 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
let mut base = Vrc2_23::new_with_submapper(rom, 2); // VRC4e-style decode
|
||||
base.mapper_id = 23;
|
||||
Self {
|
||||
base,
|
||||
chr_ram_2k: [0; 0x800],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for InesMapper253 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
self.base.cpu_read(addr)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
self.base.cpu_write(addr, value);
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
self.base.cpu_read_low(addr)
|
||||
}
|
||||
|
||||
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||
self.base.cpu_write_low(addr, value)
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
if (0x1000..0x1800).contains(&addr) {
|
||||
return self.chr_ram_2k[(addr as usize) - 0x1000];
|
||||
}
|
||||
self.base.ppu_read(addr)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
if (0x1000..0x1800).contains(&addr) {
|
||||
self.chr_ram_2k[(addr as usize) - 0x1000] = value;
|
||||
return;
|
||||
}
|
||||
self.base.ppu_write(addr, value);
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.base.mirroring()
|
||||
}
|
||||
|
||||
fn clock_cpu(&mut self, cycles: u8) {
|
||||
self.base.clock_cpu(cycles);
|
||||
}
|
||||
|
||||
fn poll_irq(&mut self) -> bool {
|
||||
self.base.poll_irq()
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
let mut base_state = Vec::new();
|
||||
self.base.save_state(&mut base_state);
|
||||
write_state_bytes(out, &base_state);
|
||||
out.extend_from_slice(&self.chr_ram_2k);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
let mut cursor = 0usize;
|
||||
let base_state = read_state_bytes(data, &mut cursor)?;
|
||||
if data.len().saturating_sub(cursor) != self.chr_ram_2k.len() {
|
||||
return Err("mapper state does not match loaded ROM".to_string());
|
||||
}
|
||||
self.base.load_state(base_state)?;
|
||||
self.chr_ram_2k.copy_from_slice(&data[cursor..]);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Fme7 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring: rom.header.mirroring,
|
||||
command: 0,
|
||||
chr_banks: [0; 8],
|
||||
prg_banks: [0, 1, 0xFE],
|
||||
low_bank: 0,
|
||||
low_is_ram: false,
|
||||
low_ram_enabled: false,
|
||||
low_ram: vec![0; 0x8000],
|
||||
irq_counter: 0,
|
||||
irq_enabled: false,
|
||||
irq_counter_enabled: false,
|
||||
irq_pending: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn prg_bank_count_8k(&self) -> usize {
|
||||
(self.prg_rom.len() / 0x2000).max(1)
|
||||
}
|
||||
|
||||
fn low_ram_index(&self, addr: u16) -> usize {
|
||||
let bank = (self.low_bank & 0x03) as usize;
|
||||
bank * 0x2000 + ((addr as usize) & 0x1FFF)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Fme7 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
let bank = match ((addr - 0x8000) / 0x2000) as usize {
|
||||
0 => self.prg_banks[0] as usize,
|
||||
1 => self.prg_banks[1] as usize,
|
||||
2 => self.prg_banks[2] as usize,
|
||||
_ => self.prg_bank_count_8k().saturating_sub(1),
|
||||
};
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x2000,
|
||||
bank,
|
||||
((addr as usize) - 0x8000) & 0x1FFF,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if (0x8000..=0x9FFF).contains(&addr) {
|
||||
self.command = value & 0x0F;
|
||||
return;
|
||||
}
|
||||
if !(0xA000..=0xBFFF).contains(&addr) {
|
||||
return;
|
||||
}
|
||||
|
||||
match self.command {
|
||||
0x0..=0x7 => self.chr_banks[self.command as usize] = value,
|
||||
0x8 => {
|
||||
self.low_bank = value & 0x3F;
|
||||
self.low_is_ram = (value & 0x40) != 0;
|
||||
self.low_ram_enabled = (value & 0x80) != 0;
|
||||
}
|
||||
0x9 => self.prg_banks[0] = value & 0x3F,
|
||||
0xA => self.prg_banks[1] = value & 0x3F,
|
||||
0xB => self.prg_banks[2] = value & 0x3F,
|
||||
0xC => {
|
||||
self.mirroring = match value & 0x03 {
|
||||
0 => Mirroring::Vertical,
|
||||
1 => Mirroring::Horizontal,
|
||||
2 => Mirroring::OneScreenLow,
|
||||
_ => Mirroring::OneScreenHigh,
|
||||
};
|
||||
}
|
||||
0xD => {
|
||||
self.irq_enabled = (value & 0x01) != 0;
|
||||
self.irq_counter_enabled = (value & 0x80) != 0;
|
||||
if !self.irq_enabled {
|
||||
self.irq_pending = false;
|
||||
}
|
||||
}
|
||||
0xE => {
|
||||
self.irq_counter = (self.irq_counter & 0xFF00) | value as u16;
|
||||
self.irq_pending = false;
|
||||
}
|
||||
0xF => {
|
||||
self.irq_counter = (self.irq_counter & 0x00FF) | ((value as u16) << 8);
|
||||
self.irq_pending = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
if !(0x6000..=0x7FFF).contains(&addr) {
|
||||
return None;
|
||||
}
|
||||
if self.low_is_ram && self.low_ram_enabled {
|
||||
return Some(self.low_ram[self.low_ram_index(addr)]);
|
||||
}
|
||||
if self.low_is_ram {
|
||||
return Some(0);
|
||||
}
|
||||
Some(read_bank(
|
||||
&self.prg_rom,
|
||||
0x2000,
|
||||
self.low_bank as usize,
|
||||
(addr as usize) & 0x1FFF,
|
||||
))
|
||||
}
|
||||
|
||||
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||
if !(0x6000..=0x7FFF).contains(&addr) {
|
||||
return false;
|
||||
}
|
||||
if self.low_is_ram && self.low_ram_enabled {
|
||||
let idx = self.low_ram_index(addr);
|
||||
self.low_ram[idx] = value;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
let page = (addr / 0x0400) as usize;
|
||||
let bank = self.chr_banks[page] as usize;
|
||||
read_bank(&self.chr_data, 0x0400, bank, (addr as usize) & 0x03FF)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let page = (addr / 0x0400) as usize;
|
||||
let bank = self.chr_banks[page] as usize;
|
||||
let total_banks = (self.chr_data.len() / 0x0400).max(1);
|
||||
let bank_idx = safe_mod(bank, total_banks);
|
||||
let idx = bank_idx * 0x0400 + ((addr as usize) & 0x03FF);
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn clock_cpu(&mut self, cycles: u8) {
|
||||
if !self.irq_counter_enabled {
|
||||
return;
|
||||
}
|
||||
for _ in 0..cycles {
|
||||
if self.irq_counter == 0 {
|
||||
self.irq_counter = 0xFFFF;
|
||||
if self.irq_enabled {
|
||||
self.irq_pending = true;
|
||||
}
|
||||
} else {
|
||||
self.irq_counter = self.irq_counter.wrapping_sub(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_irq(&mut self) -> bool {
|
||||
let out = self.irq_pending;
|
||||
self.irq_pending = false;
|
||||
out
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.command);
|
||||
out.extend_from_slice(&self.chr_banks);
|
||||
out.extend_from_slice(&self.prg_banks);
|
||||
out.push(self.low_bank);
|
||||
out.push(u8::from(self.low_is_ram));
|
||||
out.push(u8::from(self.low_ram_enabled));
|
||||
out.extend_from_slice(&self.irq_counter.to_le_bytes());
|
||||
out.push(u8::from(self.irq_enabled));
|
||||
out.push(u8::from(self.irq_counter_enabled));
|
||||
out.push(u8::from(self.irq_pending));
|
||||
out.push(encode_mirroring(self.mirroring));
|
||||
write_state_bytes(out, &self.low_ram);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 21 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
let mut cursor = 0usize;
|
||||
self.command = data[cursor];
|
||||
cursor += 1;
|
||||
self.chr_banks.copy_from_slice(&data[cursor..cursor + 8]);
|
||||
cursor += 8;
|
||||
self.prg_banks.copy_from_slice(&data[cursor..cursor + 3]);
|
||||
cursor += 3;
|
||||
self.low_bank = data[cursor];
|
||||
cursor += 1;
|
||||
self.low_is_ram = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.low_ram_enabled = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.irq_counter = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
|
||||
cursor += 2;
|
||||
self.irq_enabled = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.irq_counter_enabled = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.irq_pending = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.mirroring = decode_mirroring(data[cursor]);
|
||||
cursor += 1;
|
||||
let low_ram_payload = read_state_bytes(data, &mut cursor)?;
|
||||
if low_ram_payload.len() != self.low_ram.len() {
|
||||
return Err("mapper state does not match loaded ROM".to_string());
|
||||
}
|
||||
self.low_ram.copy_from_slice(low_ram_payload);
|
||||
load_chr_state(&mut self.chr_data, &data[cursor..])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
268
src/native_core/mapper/mappers/mapper64.rs
Normal file
268
src/native_core/mapper/mappers/mapper64.rs
Normal file
@@ -0,0 +1,268 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct InesMapper64 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring: Mirroring,
|
||||
bank_select: u8,
|
||||
bank_regs: [u8; 16], // R0..RF
|
||||
irq_latch: u8,
|
||||
irq_counter: u8,
|
||||
irq_reload: bool, // set by $C001; applied on next clock
|
||||
irq_enabled: bool,
|
||||
irq_pending: bool,
|
||||
irq_delay: u8, // pending IRQ assert delay in CPU cycles
|
||||
cycle_prescaler: u8, // CPU-cycle mode clocks every 4 cycles
|
||||
}
|
||||
|
||||
impl InesMapper64 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring: rom.header.mirroring,
|
||||
bank_select: 0,
|
||||
bank_regs: [0; 16],
|
||||
irq_latch: 0,
|
||||
irq_counter: 0,
|
||||
irq_reload: false,
|
||||
irq_enabled: false,
|
||||
irq_pending: false,
|
||||
irq_delay: 0,
|
||||
cycle_prescaler: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn prg_bank_count_8k(&self) -> usize {
|
||||
(self.prg_rom.len() / 0x2000).max(1)
|
||||
}
|
||||
|
||||
fn chr_bank_count_1k(&self) -> usize {
|
||||
(self.chr_data.len() / 0x0400).max(1)
|
||||
}
|
||||
|
||||
fn prg_mode(&self) -> bool {
|
||||
(self.bank_select & 0x40) != 0
|
||||
}
|
||||
|
||||
fn chr_invert(&self) -> bool {
|
||||
(self.bank_select & 0x80) != 0
|
||||
}
|
||||
|
||||
fn chr_full_1k_mode(&self) -> bool {
|
||||
(self.bank_select & 0x20) != 0
|
||||
}
|
||||
|
||||
fn irq_cycle_mode(&self) -> bool {
|
||||
(self.bank_select & 0x01) != 0
|
||||
}
|
||||
|
||||
fn prg_bank_for_slot(&self, slot: usize) -> usize {
|
||||
let last = self.prg_bank_count_8k().saturating_sub(1);
|
||||
let r6 = self.bank_regs[0x6] as usize;
|
||||
let r7 = self.bank_regs[0x7] as usize;
|
||||
let rf = self.bank_regs[0xF] as usize;
|
||||
match (self.prg_mode(), slot) {
|
||||
(false, 0) => r6,
|
||||
(false, 1) => r7,
|
||||
(false, 2) => rf,
|
||||
(false, 3) => last,
|
||||
(true, 0) => rf,
|
||||
(true, 1) => r7,
|
||||
(true, 2) => r6,
|
||||
(true, 3) => last,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn chr_bank_for_page(&self, page: usize) -> usize {
|
||||
let r = &self.bank_regs;
|
||||
let k = self.chr_full_1k_mode();
|
||||
let r0_lo = (r[0x0] & !1) as usize;
|
||||
let r1_lo = (r[0x1] & !1) as usize;
|
||||
|
||||
let mut layout = [0usize; 8];
|
||||
layout[0] = r0_lo;
|
||||
layout[1] = if k { r[0x8] as usize } else { r0_lo + 1 };
|
||||
layout[2] = r1_lo;
|
||||
layout[3] = if k { r[0x9] as usize } else { r1_lo + 1 };
|
||||
layout[4] = r[0x2] as usize;
|
||||
layout[5] = r[0x3] as usize;
|
||||
layout[6] = r[0x4] as usize;
|
||||
layout[7] = r[0x5] as usize;
|
||||
|
||||
if self.chr_invert() {
|
||||
layout.rotate_left(4);
|
||||
}
|
||||
layout[page]
|
||||
}
|
||||
|
||||
fn clock_counter(&mut self) {
|
||||
if self.irq_reload {
|
||||
self.irq_counter = self.irq_latch;
|
||||
if self.irq_counter != 0 {
|
||||
self.irq_counter |= 1;
|
||||
}
|
||||
self.irq_reload = false;
|
||||
} else if self.irq_counter == 0 {
|
||||
self.irq_counter = self.irq_latch;
|
||||
} else {
|
||||
self.irq_counter = self.irq_counter.wrapping_sub(1);
|
||||
}
|
||||
|
||||
if self.irq_enabled && self.irq_counter == 0 {
|
||||
self.irq_delay = 4;
|
||||
}
|
||||
}
|
||||
|
||||
fn tick_irq_delay(&mut self, cycles: u8) {
|
||||
if self.irq_delay == 0 {
|
||||
return;
|
||||
}
|
||||
if cycles >= self.irq_delay {
|
||||
self.irq_delay = 0;
|
||||
self.irq_pending = true;
|
||||
} else {
|
||||
self.irq_delay -= cycles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for InesMapper64 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
let slot = ((addr - 0x8000) / 0x2000) as usize;
|
||||
let bank = self.prg_bank_for_slot(slot);
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x2000,
|
||||
bank,
|
||||
((addr as usize) - 0x8000) & 0x1FFF,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
match addr {
|
||||
0x8000..=0x9FFF if (addr & 1) == 0 => self.bank_select = value,
|
||||
0x8000..=0x9FFF => {
|
||||
let reg = (self.bank_select & 0x0F) as usize;
|
||||
match reg {
|
||||
0x0..=0x9 | 0xF => self.bank_regs[reg] = value,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
0xA000..=0xBFFF if (addr & 1) == 0 => {
|
||||
self.mirroring = if (value & 1) == 0 {
|
||||
Mirroring::Vertical
|
||||
} else {
|
||||
Mirroring::Horizontal
|
||||
};
|
||||
}
|
||||
0xC000..=0xDFFF if (addr & 1) == 0 => self.irq_latch = value,
|
||||
0xC000..=0xDFFF => {
|
||||
self.irq_counter = 0;
|
||||
self.irq_reload = true;
|
||||
self.cycle_prescaler = 0;
|
||||
}
|
||||
0xE000..=0xFFFF if (addr & 1) == 0 => {
|
||||
self.irq_enabled = false;
|
||||
self.irq_pending = false;
|
||||
self.irq_delay = 0;
|
||||
}
|
||||
0xE000..=0xFFFF => self.irq_enabled = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
let page = (addr / 0x0400) as usize;
|
||||
let bank = self.chr_bank_for_page(page);
|
||||
read_bank(&self.chr_data, 0x0400, bank, (addr as usize) & 0x03FF)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let page = (addr / 0x0400) as usize;
|
||||
let bank = self.chr_bank_for_page(page);
|
||||
let total = self.chr_bank_count_1k();
|
||||
let idx = safe_mod(bank, total) * 0x0400 + ((addr as usize) & 0x03FF);
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn clock_cpu(&mut self, cycles: u8) {
|
||||
self.tick_irq_delay(cycles);
|
||||
if !self.irq_cycle_mode() {
|
||||
return;
|
||||
}
|
||||
for _ in 0..cycles {
|
||||
self.cycle_prescaler = self.cycle_prescaler.wrapping_add(1);
|
||||
if self.cycle_prescaler >= 4 {
|
||||
self.cycle_prescaler = 0;
|
||||
self.clock_counter();
|
||||
}
|
||||
self.tick_irq_delay(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn clock_scanline(&mut self) {
|
||||
if !self.irq_cycle_mode() {
|
||||
self.clock_counter();
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_ppu_a12_clock(&self) -> bool {
|
||||
!self.irq_cycle_mode()
|
||||
}
|
||||
|
||||
fn poll_irq(&mut self) -> bool {
|
||||
let out = self.irq_pending;
|
||||
self.irq_pending = false;
|
||||
out
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.bank_select);
|
||||
out.extend_from_slice(&self.bank_regs);
|
||||
out.push(encode_mirroring(self.mirroring));
|
||||
out.push(self.irq_latch);
|
||||
out.push(self.irq_counter);
|
||||
out.push(u8::from(self.irq_reload));
|
||||
out.push(u8::from(self.irq_enabled));
|
||||
out.push(u8::from(self.irq_pending));
|
||||
out.push(self.irq_delay);
|
||||
out.push(self.cycle_prescaler);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 25 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.bank_select = data[0];
|
||||
self.bank_regs.copy_from_slice(&data[1..17]);
|
||||
self.mirroring = decode_mirroring(data[17]);
|
||||
self.irq_latch = data[18];
|
||||
self.irq_counter = data[19];
|
||||
self.irq_reload = data[20] != 0;
|
||||
self.irq_enabled = data[21] != 0;
|
||||
self.irq_pending = data[22] != 0;
|
||||
self.irq_delay = data[23];
|
||||
self.cycle_prescaler = data[24];
|
||||
load_chr_state(&mut self.chr_data, &data[25..])
|
||||
}
|
||||
}
|
||||
123
src/native_core/mapper/mappers/mapper78.rs
Normal file
123
src/native_core/mapper/mappers/mapper78.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct InesMapper78 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
submapper: u8,
|
||||
hv_mirroring_mode: bool,
|
||||
prg_bank: u8,
|
||||
chr_bank: u8,
|
||||
mirror_select: bool,
|
||||
}
|
||||
|
||||
impl InesMapper78 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
let hv_mirroring_mode = match rom.header.submapper {
|
||||
1 => false, // Uchuusen / Cosmo Carrier: 1scA/1scB
|
||||
3 => true, // Holy Diver: H/V
|
||||
_ => matches!(rom.header.mirroring, Mirroring::Vertical),
|
||||
};
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
submapper: rom.header.submapper,
|
||||
hv_mirroring_mode,
|
||||
prg_bank: 0,
|
||||
chr_bank: 0,
|
||||
mirror_select: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for InesMapper78 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
if addr < 0xC000 {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.prg_bank as usize,
|
||||
(addr as usize) - 0x8000,
|
||||
)
|
||||
} else {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
(self.prg_rom.len() / 0x4000).saturating_sub(1),
|
||||
(addr as usize) - 0xC000,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr < 0x8000 {
|
||||
return;
|
||||
}
|
||||
let latched = value & self.cpu_read(addr); // Mapper 78 has bus conflicts.
|
||||
self.prg_bank = latched & 0x07;
|
||||
self.mirror_select = (latched & 0x08) != 0;
|
||||
self.chr_bank = (latched >> 4) & 0x0F;
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
0x2000,
|
||||
self.chr_bank as usize,
|
||||
addr as usize,
|
||||
)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let total = (self.chr_data.len() / 0x2000).max(1);
|
||||
let idx = safe_mod(self.chr_bank as usize, total) * 0x2000 + ((addr as usize) & 0x1FFF);
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
if self.hv_mirroring_mode {
|
||||
if self.mirror_select {
|
||||
Mirroring::Vertical
|
||||
} else {
|
||||
Mirroring::Horizontal
|
||||
}
|
||||
} else if self.mirror_select {
|
||||
Mirroring::OneScreenHigh
|
||||
} else {
|
||||
Mirroring::OneScreenLow
|
||||
}
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.prg_bank);
|
||||
out.push(self.chr_bank);
|
||||
out.push(self.mirror_select as u8);
|
||||
out.push(self.submapper);
|
||||
out.push(self.hv_mirroring_mode as u8);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 5 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.prg_bank = data[0];
|
||||
self.chr_bank = data[1];
|
||||
self.mirror_select = (data[2] & 1) != 0;
|
||||
self.submapper = data[3];
|
||||
self.hv_mirroring_mode = (data[4] & 1) != 0;
|
||||
load_chr_state(&mut self.chr_data, &data[5..])
|
||||
}
|
||||
}
|
||||
87
src/native_core/mapper/mappers/mapper87.rs
Normal file
87
src/native_core/mapper/mappers/mapper87.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct InesMapper87 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring: Mirroring,
|
||||
chr_bank: u8,
|
||||
}
|
||||
|
||||
impl InesMapper87 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring: rom.header.mirroring,
|
||||
chr_bank: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for InesMapper87 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
((addr - 0x8000) as usize) / 0x4000,
|
||||
addr as usize,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||
|
||||
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||
if (0x6000..=0x7FFF).contains(&addr) {
|
||||
let low = value & 0x01;
|
||||
let high = (value >> 1) & 0x01;
|
||||
self.chr_bank = (low << 1) | high;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
0x2000,
|
||||
self.chr_bank as usize,
|
||||
addr as usize,
|
||||
)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let total_banks = (self.chr_data.len() / 0x2000).max(1);
|
||||
let idx = safe_mod(self.chr_bank as usize, total_banks) * 0x2000 + (addr as usize & 0x1FFF);
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.chr_bank);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.is_empty() {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.chr_bank = data[0];
|
||||
load_chr_state(&mut self.chr_data, &data[1..])
|
||||
}
|
||||
}
|
||||
64
src/native_core/mapper/mappers/mapper88.rs
Normal file
64
src/native_core/mapper/mappers/mapper88.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct InesMapper88 {
|
||||
base: InesMapper206,
|
||||
}
|
||||
|
||||
impl InesMapper88 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
base: InesMapper206::new(rom),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for InesMapper88 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
self.base.cpu_read(addr)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
self.base.cpu_write(addr, value);
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
let page = (addr / 0x0400) as usize;
|
||||
let mut bank = self.base.chr_bank_for_1k_page(page) as usize;
|
||||
if (addr & 0x1000) != 0 {
|
||||
bank |= 0x40;
|
||||
}
|
||||
read_bank(&self.base.chr_data, 0x0400, bank, (addr as usize) & 0x03FF)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.base.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let page = (addr / 0x0400) as usize;
|
||||
let mut bank = self.base.chr_bank_for_1k_page(page) as usize;
|
||||
if (addr & 0x1000) != 0 {
|
||||
bank |= 0x40;
|
||||
}
|
||||
let total_banks = (self.base.chr_data.len() / 0x0400).max(1);
|
||||
let bank_idx = safe_mod(bank, total_banks);
|
||||
let idx = bank_idx * 0x0400 + ((addr as usize) & 0x03FF);
|
||||
if let Some(cell) = self.base.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.base.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
self.base.save_state(out);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
self.base.load_state(data)
|
||||
}
|
||||
}
|
||||
87
src/native_core/mapper/mappers/mapper93.rs
Normal file
87
src/native_core/mapper/mappers/mapper93.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct InesMapper93 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring: Mirroring,
|
||||
prg_bank: u8,
|
||||
}
|
||||
|
||||
impl InesMapper93 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring: rom.header.mirroring,
|
||||
prg_bank: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn prg_banks_16k(&self) -> usize {
|
||||
(self.prg_rom.len() / 0x4000).max(1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for InesMapper93 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
if addr < 0xC000 {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.prg_bank as usize,
|
||||
(addr as usize) - 0x8000,
|
||||
)
|
||||
} else {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.prg_banks_16k().saturating_sub(1),
|
||||
(addr as usize) - 0xC000,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr >= 0x8000 {
|
||||
self.prg_bank = (value >> 4) & 0x07;
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.prg_bank);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.is_empty() {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.prg_bank = data[0];
|
||||
load_chr_state(&mut self.chr_data, &data[1..])
|
||||
}
|
||||
}
|
||||
55
src/native_core/mapper/mappers/mapper95.rs
Normal file
55
src/native_core/mapper/mappers/mapper95.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct InesMapper95 {
|
||||
base: InesMapper206,
|
||||
}
|
||||
|
||||
impl InesMapper95 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
base: InesMapper206::new(rom),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for InesMapper95 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
self.base.cpu_read(addr)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
self.base.cpu_write(addr, value);
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
self.base.ppu_read(addr)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
self.base.ppu_write(addr, value);
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
Mirroring::Horizontal
|
||||
}
|
||||
|
||||
fn map_nametable_addr(&self, addr: u16) -> Option<usize> {
|
||||
if !(0x2000..=0x3EFF).contains(&addr) {
|
||||
return None;
|
||||
}
|
||||
let rel = (addr - 0x2000) & 0x0FFF;
|
||||
let page = (rel / 0x0400) as usize;
|
||||
let offset = (rel & 0x03FF) as usize;
|
||||
let bank = self.base.chr_bank_for_1k_page(page);
|
||||
let ciram_page = ((bank >> 5) & 1) as usize;
|
||||
Some(ciram_page * 0x0400 + offset)
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
self.base.save_state(out);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
self.base.load_state(data)
|
||||
}
|
||||
}
|
||||
192
src/native_core/mapper/mappers/mmc1.rs
Normal file
192
src/native_core/mapper/mappers/mmc1.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Mmc1 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
shift_reg: u8,
|
||||
shift_count: u8,
|
||||
control: u8,
|
||||
chr_bank0: u8,
|
||||
chr_bank1: u8,
|
||||
prg_bank: u8,
|
||||
mirroring_default: Mirroring,
|
||||
}
|
||||
|
||||
impl Mmc1 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
shift_reg: 0,
|
||||
shift_count: 0,
|
||||
control: 0x0C,
|
||||
chr_bank0: 0,
|
||||
chr_bank1: 0,
|
||||
prg_bank: 0,
|
||||
mirroring_default: rom.header.mirroring,
|
||||
}
|
||||
}
|
||||
|
||||
fn prg_mode(&self) -> u8 {
|
||||
(self.control >> 2) & 0x03
|
||||
}
|
||||
|
||||
fn chr_mode(&self) -> u8 {
|
||||
(self.control >> 4) & 1
|
||||
}
|
||||
|
||||
fn prg_bank_count(&self) -> usize {
|
||||
(self.prg_rom.len() / 0x4000).max(1)
|
||||
}
|
||||
|
||||
fn write_serial(&mut self, addr: u16, value: u8) {
|
||||
if (value & 0x80) != 0 {
|
||||
self.shift_reg = 0;
|
||||
self.shift_count = 0;
|
||||
self.control |= 0x0C;
|
||||
return;
|
||||
}
|
||||
|
||||
self.shift_reg |= (value & 1) << self.shift_count;
|
||||
self.shift_count = self.shift_count.wrapping_add(1);
|
||||
if self.shift_count < 5 {
|
||||
return;
|
||||
}
|
||||
|
||||
let reg = (addr >> 13) & 0x03;
|
||||
match reg {
|
||||
0 => self.control = self.shift_reg & 0x1F,
|
||||
1 => self.chr_bank0 = self.shift_reg & 0x1F,
|
||||
2 => self.chr_bank1 = self.shift_reg & 0x1F,
|
||||
_ => self.prg_bank = self.shift_reg & 0x0F,
|
||||
}
|
||||
self.shift_reg = 0;
|
||||
self.shift_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Mmc1 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let prg_mode = self.prg_mode();
|
||||
if prg_mode <= 1 {
|
||||
let bank32 = (self.prg_bank as usize) >> 1;
|
||||
read_bank(&self.prg_rom, 0x8000, bank32, (addr as usize) - 0x8000)
|
||||
} else if prg_mode == 2 {
|
||||
if addr < 0xC000 {
|
||||
read_bank(&self.prg_rom, 0x4000, 0, (addr as usize) - 0x8000)
|
||||
} else {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.prg_bank as usize,
|
||||
(addr as usize) - 0xC000,
|
||||
)
|
||||
}
|
||||
} else if addr < 0xC000 {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.prg_bank as usize,
|
||||
(addr as usize) - 0x8000,
|
||||
)
|
||||
} else {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.prg_bank_count() - 1,
|
||||
(addr as usize) - 0xC000,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr >= 0x8000 {
|
||||
self.write_serial(addr, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
if self.chr_mode() == 0 {
|
||||
let bank = (self.chr_bank0 as usize) >> 1;
|
||||
read_bank(&self.chr_data, 0x2000, bank, addr as usize)
|
||||
} else if addr < 0x1000 {
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
0x1000,
|
||||
self.chr_bank0 as usize,
|
||||
addr as usize,
|
||||
)
|
||||
} else {
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
0x1000,
|
||||
self.chr_bank1 as usize,
|
||||
(addr as usize) - 0x1000,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let idx = if self.chr_mode() == 0 {
|
||||
let total_banks = (self.chr_data.len() / 0x2000).max(1);
|
||||
let bank = safe_mod((self.chr_bank0 as usize) >> 1, total_banks);
|
||||
bank * 0x2000 + ((addr as usize) & 0x1FFF)
|
||||
} else {
|
||||
let total_banks = (self.chr_data.len() / 0x1000).max(1);
|
||||
let bank = if addr < 0x1000 {
|
||||
self.chr_bank0 as usize
|
||||
} else {
|
||||
self.chr_bank1 as usize
|
||||
};
|
||||
safe_mod(bank, total_banks) * 0x1000 + ((addr as usize) & 0x0FFF)
|
||||
};
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
match self.control & 0x03 {
|
||||
0 => Mirroring::OneScreenLow,
|
||||
1 => Mirroring::OneScreenHigh,
|
||||
2 => Mirroring::Vertical,
|
||||
3 => Mirroring::Horizontal,
|
||||
_ => self.mirroring_default,
|
||||
}
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.shift_reg);
|
||||
out.push(self.shift_count);
|
||||
out.push(self.control);
|
||||
out.push(self.chr_bank0);
|
||||
out.push(self.chr_bank1);
|
||||
out.push(self.prg_bank);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 6 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.shift_reg = data[0];
|
||||
self.shift_count = data[1];
|
||||
self.control = data[2];
|
||||
self.chr_bank0 = data[3];
|
||||
self.chr_bank1 = data[4];
|
||||
self.prg_bank = data[5];
|
||||
load_chr_state(&mut self.chr_data, &data[6..])
|
||||
}
|
||||
}
|
||||
178
src/native_core/mapper/mappers/mmc2.rs
Normal file
178
src/native_core/mapper/mappers/mmc2.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Mmc2Latch {
|
||||
Fd,
|
||||
Fe,
|
||||
}
|
||||
|
||||
pub(crate) struct Mmc2 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
mirroring: Mirroring,
|
||||
prg_bank_8k: u8,
|
||||
chr_fd_0000: u8,
|
||||
chr_fe_0000: u8,
|
||||
chr_fd_1000: u8,
|
||||
chr_fe_1000: u8,
|
||||
latch_0000: Cell<Mmc2Latch>,
|
||||
latch_1000: Cell<Mmc2Latch>,
|
||||
}
|
||||
|
||||
impl Mmc2 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
mirroring: rom.header.mirroring,
|
||||
prg_bank_8k: 0,
|
||||
chr_fd_0000: 0,
|
||||
chr_fe_0000: 0,
|
||||
chr_fd_1000: 0,
|
||||
chr_fe_1000: 0,
|
||||
latch_0000: Cell::new(Mmc2Latch::Fd),
|
||||
latch_1000: Cell::new(Mmc2Latch::Fd),
|
||||
}
|
||||
}
|
||||
|
||||
fn prg_bank_count_8k(&self) -> usize {
|
||||
(self.prg_rom.len() / 0x2000).max(1)
|
||||
}
|
||||
|
||||
fn chr_bank_0000(&self) -> usize {
|
||||
match self.latch_0000.get() {
|
||||
Mmc2Latch::Fd => self.chr_fd_0000 as usize,
|
||||
Mmc2Latch::Fe => self.chr_fe_0000 as usize,
|
||||
}
|
||||
}
|
||||
|
||||
fn chr_bank_1000(&self) -> usize {
|
||||
match self.latch_1000.get() {
|
||||
Mmc2Latch::Fd => self.chr_fd_1000 as usize,
|
||||
Mmc2Latch::Fe => self.chr_fe_1000 as usize,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_latches_for_ppu_addr(&self, addr: u16) {
|
||||
if (0x0FD8..=0x0FDF).contains(&addr) {
|
||||
self.latch_0000.set(Mmc2Latch::Fd);
|
||||
} else if (0x0FE8..=0x0FEF).contains(&addr) {
|
||||
self.latch_0000.set(Mmc2Latch::Fe);
|
||||
} else if (0x1FD8..=0x1FDF).contains(&addr) {
|
||||
self.latch_1000.set(Mmc2Latch::Fd);
|
||||
} else if (0x1FE8..=0x1FEF).contains(&addr) {
|
||||
self.latch_1000.set(Mmc2Latch::Fe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Mmc2 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
let slot = ((addr - 0x8000) / 0x2000) as usize;
|
||||
let bank = match slot {
|
||||
0 => self.prg_bank_8k as usize,
|
||||
1 => self.prg_bank_count_8k().saturating_sub(3),
|
||||
2 => self.prg_bank_count_8k().saturating_sub(2),
|
||||
_ => self.prg_bank_count_8k().saturating_sub(1),
|
||||
};
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x2000,
|
||||
bank,
|
||||
((addr as usize) - 0x8000) & 0x1FFF,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
match addr {
|
||||
0xA000..=0xAFFF => self.prg_bank_8k = value & 0x0F,
|
||||
0xB000..=0xBFFF => self.chr_fd_0000 = value & 0x1F,
|
||||
0xC000..=0xCFFF => self.chr_fe_0000 = value & 0x1F,
|
||||
0xD000..=0xDFFF => self.chr_fd_1000 = value & 0x1F,
|
||||
0xE000..=0xEFFF => self.chr_fe_1000 = value & 0x1F,
|
||||
0xF000..=0xFFFF => {
|
||||
self.mirroring = if (value & 1) == 0 {
|
||||
Mirroring::Vertical
|
||||
} else {
|
||||
Mirroring::Horizontal
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
let value = if addr < 0x1000 {
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
0x1000,
|
||||
self.chr_bank_0000(),
|
||||
addr as usize & 0x0FFF,
|
||||
)
|
||||
} else {
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
0x1000,
|
||||
self.chr_bank_1000(),
|
||||
(addr as usize) & 0x0FFF,
|
||||
)
|
||||
};
|
||||
// MMC2 latches update after the triggering tile fetch.
|
||||
self.update_latches_for_ppu_addr(addr);
|
||||
value
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, _value: u8) {
|
||||
self.update_latches_for_ppu_addr(addr);
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.prg_bank_8k);
|
||||
out.push(self.chr_fd_0000);
|
||||
out.push(self.chr_fe_0000);
|
||||
out.push(self.chr_fd_1000);
|
||||
out.push(self.chr_fe_1000);
|
||||
out.push(match self.latch_0000.get() {
|
||||
Mmc2Latch::Fd => 0,
|
||||
Mmc2Latch::Fe => 1,
|
||||
});
|
||||
out.push(match self.latch_1000.get() {
|
||||
Mmc2Latch::Fd => 0,
|
||||
Mmc2Latch::Fe => 1,
|
||||
});
|
||||
out.push(encode_mirroring(self.mirroring));
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() != 8 {
|
||||
return Err("mapper state payload has invalid length".to_string());
|
||||
}
|
||||
self.prg_bank_8k = data[0];
|
||||
self.chr_fd_0000 = data[1];
|
||||
self.chr_fe_0000 = data[2];
|
||||
self.chr_fd_1000 = data[3];
|
||||
self.chr_fe_1000 = data[4];
|
||||
self.latch_0000.set(if data[5] == 0 {
|
||||
Mmc2Latch::Fd
|
||||
} else {
|
||||
Mmc2Latch::Fe
|
||||
});
|
||||
self.latch_1000.set(if data[6] == 0 {
|
||||
Mmc2Latch::Fd
|
||||
} else {
|
||||
Mmc2Latch::Fe
|
||||
});
|
||||
self.mirroring = decode_mirroring(data[7]);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
250
src/native_core/mapper/mappers/mmc3.rs
Normal file
250
src/native_core/mapper/mappers/mmc3.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Mmc3 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
prg_ram: Vec<u8>,
|
||||
prg_ram_enabled: bool,
|
||||
prg_ram_write_protect: bool,
|
||||
mirroring: Mirroring,
|
||||
bank_regs: [u8; 8],
|
||||
bank_select: u8,
|
||||
irq_latch: u8,
|
||||
irq_counter: u8,
|
||||
irq_reload: bool,
|
||||
irq_enabled: bool,
|
||||
irq_pending: bool,
|
||||
}
|
||||
|
||||
impl Mmc3 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
prg_ram: vec![0; 0x2000],
|
||||
prg_ram_enabled: true,
|
||||
prg_ram_write_protect: false,
|
||||
mirroring: rom.header.mirroring,
|
||||
bank_regs: [0; 8],
|
||||
bank_select: 0,
|
||||
irq_latch: 0,
|
||||
irq_counter: 0,
|
||||
irq_reload: false,
|
||||
irq_enabled: false,
|
||||
irq_pending: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn prg_bank_count_8k(&self) -> usize {
|
||||
(self.prg_rom.len() / 0x2000).max(1)
|
||||
}
|
||||
|
||||
fn prg_mode(&self) -> bool {
|
||||
(self.bank_select & 0x40) != 0
|
||||
}
|
||||
|
||||
fn chr_invert(&self) -> bool {
|
||||
(self.bank_select & 0x80) != 0
|
||||
}
|
||||
|
||||
fn prg_bank_for_slot(&self, slot: usize) -> usize {
|
||||
let last = self.prg_bank_count_8k() - 1;
|
||||
let second_last = last.saturating_sub(1);
|
||||
match (self.prg_mode(), slot) {
|
||||
(false, 0) => self.bank_regs[6] as usize,
|
||||
(false, 1) => self.bank_regs[7] as usize,
|
||||
(false, 2) => second_last,
|
||||
(false, 3) => last,
|
||||
(true, 0) => second_last,
|
||||
(true, 1) => self.bank_regs[7] as usize,
|
||||
(true, 2) => self.bank_regs[6] as usize,
|
||||
(true, 3) => last,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn chr_bank_for_1k_page(&self, page: usize) -> usize {
|
||||
let regs = &self.bank_regs;
|
||||
let mut layout = [0usize; 8];
|
||||
|
||||
layout[0] = (regs[0] as usize) & !1;
|
||||
layout[1] = layout[0] + 1;
|
||||
layout[2] = (regs[1] as usize) & !1;
|
||||
layout[3] = layout[2] + 1;
|
||||
layout[4] = regs[2] as usize;
|
||||
layout[5] = regs[3] as usize;
|
||||
layout[6] = regs[4] as usize;
|
||||
layout[7] = regs[5] as usize;
|
||||
|
||||
if self.chr_invert() {
|
||||
layout.rotate_left(4);
|
||||
}
|
||||
layout[page]
|
||||
}
|
||||
|
||||
fn clock_irq_scanline(&mut self) {
|
||||
if self.irq_reload || self.irq_counter == 0 {
|
||||
self.irq_counter = self.irq_latch;
|
||||
self.irq_reload = false;
|
||||
} else {
|
||||
self.irq_counter = self.irq_counter.wrapping_sub(1);
|
||||
}
|
||||
if self.irq_enabled && self.irq_counter == 0 {
|
||||
self.irq_pending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Mmc3 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
let slot = ((addr - 0x8000) / 0x2000) as usize;
|
||||
let bank = self.prg_bank_for_slot(slot);
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x2000,
|
||||
bank,
|
||||
((addr as usize) - 0x8000) & 0x1FFF,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
match addr {
|
||||
0x8000..=0x9FFF if (addr & 1) == 0 => self.bank_select = value,
|
||||
0x8000..=0x9FFF => {
|
||||
let reg = (self.bank_select & 0x07) as usize;
|
||||
self.bank_regs[reg] = value;
|
||||
}
|
||||
0xA000..=0xBFFF if (addr & 1) == 0 => {
|
||||
self.mirroring = if (value & 1) == 0 {
|
||||
Mirroring::Vertical
|
||||
} else {
|
||||
Mirroring::Horizontal
|
||||
};
|
||||
}
|
||||
0xA000..=0xBFFF => {
|
||||
self.prg_ram_enabled = (value & 0x80) != 0;
|
||||
self.prg_ram_write_protect = (value & 0x40) != 0;
|
||||
}
|
||||
0xC000..=0xDFFF if (addr & 1) == 0 => self.irq_latch = value,
|
||||
0xC000..=0xDFFF => {
|
||||
self.irq_counter = 0;
|
||||
self.irq_reload = true;
|
||||
}
|
||||
0xE000..=0xFFFF if (addr & 1) == 0 => {
|
||||
self.irq_enabled = false;
|
||||
self.irq_pending = false;
|
||||
}
|
||||
0xE000..=0xFFFF => self.irq_enabled = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
if (0x6000..=0x7FFF).contains(&addr) {
|
||||
if self.prg_ram_enabled {
|
||||
Some(self.prg_ram[(addr as usize) - 0x6000])
|
||||
} else {
|
||||
Some(0)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||
if (0x6000..=0x7FFF).contains(&addr) {
|
||||
if self.prg_ram_enabled && !self.prg_ram_write_protect {
|
||||
self.prg_ram[(addr as usize) - 0x6000] = value;
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
let page = (addr / 0x0400) as usize;
|
||||
let bank = self.chr_bank_for_1k_page(page);
|
||||
read_bank(&self.chr_data, 0x0400, bank, (addr as usize) & 0x03FF)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let page = (addr / 0x0400) as usize;
|
||||
let bank = self.chr_bank_for_1k_page(page);
|
||||
let total_banks = (self.chr_data.len() / 0x0400).max(1);
|
||||
let bank_idx = safe_mod(bank, total_banks);
|
||||
let idx = bank_idx * 0x0400 + ((addr as usize) & 0x03FF);
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn clock_scanline(&mut self) {
|
||||
self.clock_irq_scanline();
|
||||
}
|
||||
|
||||
fn needs_ppu_a12_clock(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn poll_irq(&mut self) -> bool {
|
||||
let out = self.irq_pending;
|
||||
self.irq_pending = false;
|
||||
out
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(&self.bank_regs);
|
||||
out.push(self.bank_select);
|
||||
out.push(encode_mirroring(self.mirroring));
|
||||
out.push(self.irq_latch);
|
||||
out.push(self.irq_counter);
|
||||
out.push(u8::from(self.irq_reload));
|
||||
out.push(u8::from(self.irq_enabled));
|
||||
out.push(u8::from(self.irq_pending));
|
||||
out.push(u8::from(self.prg_ram_enabled));
|
||||
out.push(u8::from(self.prg_ram_write_protect));
|
||||
write_state_bytes(out, &self.prg_ram);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 17 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.bank_regs.copy_from_slice(&data[0..8]);
|
||||
self.bank_select = data[8];
|
||||
self.mirroring = decode_mirroring(data[9]);
|
||||
self.irq_latch = data[10];
|
||||
self.irq_counter = data[11];
|
||||
self.irq_reload = data[12] != 0;
|
||||
self.irq_enabled = data[13] != 0;
|
||||
self.irq_pending = data[14] != 0;
|
||||
let mut cursor = 15usize;
|
||||
self.prg_ram_enabled = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.prg_ram_write_protect = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
let prg_ram = read_state_bytes(data, &mut cursor)?;
|
||||
if prg_ram.len() != self.prg_ram.len() {
|
||||
return Err("mapper state does not match loaded ROM".to_string());
|
||||
}
|
||||
self.prg_ram.copy_from_slice(prg_ram);
|
||||
load_chr_state(&mut self.chr_data, &data[cursor..])
|
||||
}
|
||||
}
|
||||
180
src/native_core/mapper/mappers/mmc4.rs
Normal file
180
src/native_core/mapper/mappers/mmc4.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Mmc2Latch {
|
||||
Fd,
|
||||
Fe,
|
||||
}
|
||||
|
||||
pub(crate) struct Mmc4 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
mirroring: Mirroring,
|
||||
prg_bank_16k: u8,
|
||||
chr_fd_0000: u8,
|
||||
chr_fe_0000: u8,
|
||||
chr_fd_1000: u8,
|
||||
chr_fe_1000: u8,
|
||||
latch_0000: Cell<Mmc2Latch>,
|
||||
latch_1000: Cell<Mmc2Latch>,
|
||||
}
|
||||
|
||||
impl Mmc4 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
mirroring: rom.header.mirroring,
|
||||
prg_bank_16k: 0,
|
||||
chr_fd_0000: 0,
|
||||
chr_fe_0000: 0,
|
||||
chr_fd_1000: 0,
|
||||
chr_fe_1000: 0,
|
||||
latch_0000: Cell::new(Mmc2Latch::Fd),
|
||||
latch_1000: Cell::new(Mmc2Latch::Fd),
|
||||
}
|
||||
}
|
||||
|
||||
fn prg_bank_count_16k(&self) -> usize {
|
||||
(self.prg_rom.len() / 0x4000).max(1)
|
||||
}
|
||||
|
||||
fn chr_bank_0000(&self) -> usize {
|
||||
match self.latch_0000.get() {
|
||||
Mmc2Latch::Fd => self.chr_fd_0000 as usize,
|
||||
Mmc2Latch::Fe => self.chr_fe_0000 as usize,
|
||||
}
|
||||
}
|
||||
|
||||
fn chr_bank_1000(&self) -> usize {
|
||||
match self.latch_1000.get() {
|
||||
Mmc2Latch::Fd => self.chr_fd_1000 as usize,
|
||||
Mmc2Latch::Fe => self.chr_fe_1000 as usize,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_latches_for_ppu_addr(&self, addr: u16) {
|
||||
if (0x0FD8..=0x0FDF).contains(&addr) {
|
||||
self.latch_0000.set(Mmc2Latch::Fd);
|
||||
} else if (0x0FE8..=0x0FEF).contains(&addr) {
|
||||
self.latch_0000.set(Mmc2Latch::Fe);
|
||||
} else if (0x1FD8..=0x1FDF).contains(&addr) {
|
||||
self.latch_1000.set(Mmc2Latch::Fd);
|
||||
} else if (0x1FE8..=0x1FEF).contains(&addr) {
|
||||
self.latch_1000.set(Mmc2Latch::Fe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Mmc4 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
if addr < 0xC000 {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.prg_bank_16k as usize,
|
||||
(addr as usize) - 0x8000,
|
||||
)
|
||||
} else {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.prg_bank_count_16k().saturating_sub(1),
|
||||
(addr as usize) - 0xC000,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
match addr {
|
||||
0xA000..=0xAFFF => self.prg_bank_16k = value & 0x0F,
|
||||
0xB000..=0xBFFF => self.chr_fd_0000 = value & 0x1F,
|
||||
0xC000..=0xCFFF => self.chr_fe_0000 = value & 0x1F,
|
||||
0xD000..=0xDFFF => self.chr_fd_1000 = value & 0x1F,
|
||||
0xE000..=0xEFFF => self.chr_fe_1000 = value & 0x1F,
|
||||
0xF000..=0xFFFF => {
|
||||
self.mirroring = if (value & 1) == 0 {
|
||||
Mirroring::Vertical
|
||||
} else {
|
||||
Mirroring::Horizontal
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
let value = if addr < 0x1000 {
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
0x1000,
|
||||
self.chr_bank_0000(),
|
||||
addr as usize & 0x0FFF,
|
||||
)
|
||||
} else {
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
0x1000,
|
||||
self.chr_bank_1000(),
|
||||
(addr as usize) & 0x0FFF,
|
||||
)
|
||||
};
|
||||
// MMC4 latches update after the triggering tile fetch.
|
||||
self.update_latches_for_ppu_addr(addr);
|
||||
value
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, _value: u8) {
|
||||
self.update_latches_for_ppu_addr(addr);
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.prg_bank_16k);
|
||||
out.push(self.chr_fd_0000);
|
||||
out.push(self.chr_fe_0000);
|
||||
out.push(self.chr_fd_1000);
|
||||
out.push(self.chr_fe_1000);
|
||||
out.push(match self.latch_0000.get() {
|
||||
Mmc2Latch::Fd => 0,
|
||||
Mmc2Latch::Fe => 1,
|
||||
});
|
||||
out.push(match self.latch_1000.get() {
|
||||
Mmc2Latch::Fd => 0,
|
||||
Mmc2Latch::Fe => 1,
|
||||
});
|
||||
out.push(encode_mirroring(self.mirroring));
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() != 8 {
|
||||
return Err("mapper state payload has invalid length".to_string());
|
||||
}
|
||||
self.prg_bank_16k = data[0];
|
||||
self.chr_fd_0000 = data[1];
|
||||
self.chr_fe_0000 = data[2];
|
||||
self.chr_fd_1000 = data[3];
|
||||
self.chr_fe_1000 = data[4];
|
||||
self.latch_0000.set(if data[5] == 0 {
|
||||
Mmc2Latch::Fd
|
||||
} else {
|
||||
Mmc2Latch::Fe
|
||||
});
|
||||
self.latch_1000.set(if data[6] == 0 {
|
||||
Mmc2Latch::Fd
|
||||
} else {
|
||||
Mmc2Latch::Fe
|
||||
});
|
||||
self.mirroring = decode_mirroring(data[7]);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
359
src/native_core/mapper/mappers/mmc5.rs
Normal file
359
src/native_core/mapper/mappers/mmc5.rs
Normal file
@@ -0,0 +1,359 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Mmc5 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring_default: Mirroring,
|
||||
prg_mode: u8,
|
||||
chr_mode: u8,
|
||||
prg_regs_8k: [u8; 4],
|
||||
prg_ram_bank: u8,
|
||||
prg_ram: Vec<u8>,
|
||||
chr_banks_1k: [u16; 8],
|
||||
nt_mapping: u8,
|
||||
ram_protect_1: u8,
|
||||
ram_protect_2: u8,
|
||||
chr_upper_bits: u8,
|
||||
multiplier_a: u8,
|
||||
multiplier_b: u8,
|
||||
irq_scanline: u8,
|
||||
irq_enable: bool,
|
||||
irq_pending: bool,
|
||||
irq_cycles: u32,
|
||||
}
|
||||
|
||||
impl Mmc5 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
let prg_banks_8k = (rom.prg_rom.len() / 0x2000).max(1);
|
||||
let last = prg_banks_8k.saturating_sub(1) as u8;
|
||||
let second_last = prg_banks_8k.saturating_sub(2) as u8;
|
||||
let mut chr_banks_1k = [0u16; 8];
|
||||
for (i, bank) in chr_banks_1k.iter_mut().enumerate() {
|
||||
*bank = i as u16;
|
||||
}
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring_default: rom.header.mirroring,
|
||||
prg_mode: 3,
|
||||
chr_mode: 3,
|
||||
prg_regs_8k: [0, 1, second_last, last],
|
||||
prg_ram_bank: 0,
|
||||
prg_ram: vec![0; 0x10000],
|
||||
chr_banks_1k,
|
||||
nt_mapping: 0x50,
|
||||
ram_protect_1: 0,
|
||||
ram_protect_2: 0,
|
||||
chr_upper_bits: 0,
|
||||
multiplier_a: 0,
|
||||
multiplier_b: 0,
|
||||
irq_scanline: 0,
|
||||
irq_enable: false,
|
||||
irq_pending: false,
|
||||
irq_cycles: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn chr_bank_count_1k(&self) -> usize {
|
||||
(self.chr_data.len() / 0x0400).max(1)
|
||||
}
|
||||
|
||||
fn prg_bank_8k_for_slot(&self, slot: usize) -> usize {
|
||||
let regs = &self.prg_regs_8k;
|
||||
match self.prg_mode & 0x03 {
|
||||
0 => {
|
||||
let bank32 = (regs[3] as usize) >> 2;
|
||||
bank32 * 4 + slot
|
||||
}
|
||||
1 => {
|
||||
let bank16_lo = (regs[1] as usize) >> 1;
|
||||
let bank16_hi = (regs[3] as usize) >> 1;
|
||||
if slot < 2 {
|
||||
bank16_lo * 2 + slot
|
||||
} else {
|
||||
bank16_hi * 2 + (slot - 2)
|
||||
}
|
||||
}
|
||||
2 => match slot {
|
||||
0 | 1 => {
|
||||
let bank16 = (regs[1] as usize) >> 1;
|
||||
bank16 * 2 + slot
|
||||
}
|
||||
2 => regs[2] as usize,
|
||||
_ => regs[3] as usize,
|
||||
},
|
||||
_ => regs[slot] as usize,
|
||||
}
|
||||
}
|
||||
|
||||
fn chr_bank_1k_for_page(&self, page: usize) -> usize {
|
||||
let mut banks = [0usize; 8];
|
||||
match self.chr_mode & 0x03 {
|
||||
0 => {
|
||||
let base = (self.chr_banks_1k[7] as usize) & !7;
|
||||
for (i, bank) in banks.iter_mut().enumerate() {
|
||||
*bank = base + i;
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
let b0 = (self.chr_banks_1k[3] as usize) & !3;
|
||||
let b1 = (self.chr_banks_1k[7] as usize) & !3;
|
||||
for i in 0..4usize {
|
||||
banks[i] = b0 + i;
|
||||
banks[i + 4] = b1 + i;
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
let b0 = (self.chr_banks_1k[1] as usize) & !1;
|
||||
let b1 = (self.chr_banks_1k[3] as usize) & !1;
|
||||
let b2 = (self.chr_banks_1k[5] as usize) & !1;
|
||||
let b3 = (self.chr_banks_1k[7] as usize) & !1;
|
||||
banks[0] = b0;
|
||||
banks[1] = b0 + 1;
|
||||
banks[2] = b1;
|
||||
banks[3] = b1 + 1;
|
||||
banks[4] = b2;
|
||||
banks[5] = b2 + 1;
|
||||
banks[6] = b3;
|
||||
banks[7] = b3 + 1;
|
||||
}
|
||||
_ => {
|
||||
for (i, bank) in banks.iter_mut().enumerate() {
|
||||
*bank = self.chr_banks_1k[i] as usize;
|
||||
}
|
||||
}
|
||||
}
|
||||
banks[page]
|
||||
}
|
||||
|
||||
fn ram_writable(&self) -> bool {
|
||||
self.ram_protect_1 == 0x02 && self.ram_protect_2 == 0x01
|
||||
}
|
||||
|
||||
fn decode_mirroring(&self) -> Mirroring {
|
||||
let nt0 = self.nt_mapping & 0x03;
|
||||
let nt1 = (self.nt_mapping >> 2) & 0x03;
|
||||
let nt2 = (self.nt_mapping >> 4) & 0x03;
|
||||
let nt3 = (self.nt_mapping >> 6) & 0x03;
|
||||
if nt0 == 0 && nt1 == 1 && nt2 == 0 && nt3 == 1 {
|
||||
return Mirroring::Vertical;
|
||||
}
|
||||
if nt0 == 0 && nt1 == 0 && nt2 == 1 && nt3 == 1 {
|
||||
return Mirroring::Horizontal;
|
||||
}
|
||||
if nt0 == 0 && nt1 == 0 && nt2 == 0 && nt3 == 0 {
|
||||
return Mirroring::OneScreenLow;
|
||||
}
|
||||
if nt0 == 1 && nt1 == 1 && nt2 == 1 && nt3 == 1 {
|
||||
return Mirroring::OneScreenHigh;
|
||||
}
|
||||
self.mirroring_default
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Mmc5 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
let slot = ((addr - 0x8000) / 0x2000) as usize;
|
||||
let bank = self.prg_bank_8k_for_slot(slot);
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x2000,
|
||||
bank,
|
||||
((addr as usize) - 0x8000) & 0x1FFF,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
match addr {
|
||||
0x5204 => {
|
||||
let mut status = 0u8;
|
||||
if self.irq_pending {
|
||||
status |= 0x80;
|
||||
}
|
||||
if self.irq_enable {
|
||||
status |= 0x40;
|
||||
}
|
||||
Some(status)
|
||||
}
|
||||
0x5205 => Some(((self.multiplier_a as u16 * self.multiplier_b as u16) & 0x00FF) as u8),
|
||||
0x5206 => Some(((self.multiplier_a as u16 * self.multiplier_b as u16) >> 8) as u8),
|
||||
0x6000..=0x7FFF => {
|
||||
let bank = (self.prg_ram_bank & 0x07) as usize;
|
||||
let idx = bank * 0x2000 + ((addr as usize) & 0x1FFF);
|
||||
Some(self.prg_ram.get(idx).copied().unwrap_or(0))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||
match addr {
|
||||
0x5100 => self.prg_mode = value & 0x03,
|
||||
0x5101 => self.chr_mode = value & 0x03,
|
||||
0x5102 => self.ram_protect_1 = value & 0x03,
|
||||
0x5103 => self.ram_protect_2 = value & 0x03,
|
||||
0x5105 => self.nt_mapping = value,
|
||||
0x5113 => self.prg_ram_bank = value & 0x07,
|
||||
0x5114..=0x5117 => {
|
||||
let reg = (addr - 0x5114) as usize;
|
||||
self.prg_regs_8k[reg] = value & 0x7F;
|
||||
}
|
||||
0x5120..=0x5127 => {
|
||||
let reg = (addr - 0x5120) as usize;
|
||||
let bank = (((self.chr_upper_bits & 0x03) as u16) << 8) | value as u16;
|
||||
self.chr_banks_1k[reg] = bank;
|
||||
}
|
||||
0x5128..=0x512B => {
|
||||
let reg = (addr - 0x5128) as usize;
|
||||
let bank = (((self.chr_upper_bits & 0x03) as u16) << 8) | value as u16;
|
||||
let base = reg * 2;
|
||||
self.chr_banks_1k[base] = bank & !1;
|
||||
self.chr_banks_1k[base + 1] = (bank & !1).wrapping_add(1);
|
||||
}
|
||||
0x5130 => self.chr_upper_bits = value & 0x03,
|
||||
0x5203 => self.irq_scanline = value,
|
||||
0x5204 => {
|
||||
self.irq_enable = (value & 0x80) != 0;
|
||||
if !self.irq_enable {
|
||||
self.irq_pending = false;
|
||||
}
|
||||
}
|
||||
0x5205 => self.multiplier_a = value,
|
||||
0x5206 => self.multiplier_b = value,
|
||||
0x6000..=0x7FFF => {
|
||||
if self.ram_writable() {
|
||||
let bank = (self.prg_ram_bank & 0x07) as usize;
|
||||
let idx = bank * 0x2000 + ((addr as usize) & 0x1FFF);
|
||||
if let Some(cell) = self.prg_ram.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
let page = (addr / 0x0400) as usize;
|
||||
let bank = safe_mod(self.chr_bank_1k_for_page(page), self.chr_bank_count_1k());
|
||||
read_bank(&self.chr_data, 0x0400, bank, (addr as usize) & 0x03FF)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let page = (addr / 0x0400) as usize;
|
||||
let bank = safe_mod(self.chr_bank_1k_for_page(page), self.chr_bank_count_1k());
|
||||
let idx = bank * 0x0400 + ((addr as usize) & 0x03FF);
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.decode_mirroring()
|
||||
}
|
||||
|
||||
fn clock_cpu(&mut self, cycles: u8) {
|
||||
if !self.irq_enable {
|
||||
return;
|
||||
}
|
||||
self.irq_cycles = self.irq_cycles.saturating_add(cycles as u32);
|
||||
let threshold = (self.irq_scanline as u32 + 1).saturating_mul(113);
|
||||
if threshold != 0 && self.irq_cycles >= threshold {
|
||||
self.irq_cycles %= threshold;
|
||||
self.irq_pending = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_irq(&mut self) -> bool {
|
||||
let out = self.irq_pending;
|
||||
self.irq_pending = false;
|
||||
out
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.prg_mode);
|
||||
out.push(self.chr_mode);
|
||||
out.extend_from_slice(&self.prg_regs_8k);
|
||||
out.push(self.prg_ram_bank);
|
||||
out.push(self.nt_mapping);
|
||||
out.push(self.ram_protect_1);
|
||||
out.push(self.ram_protect_2);
|
||||
out.push(self.chr_upper_bits);
|
||||
out.push(self.multiplier_a);
|
||||
out.push(self.multiplier_b);
|
||||
out.push(self.irq_scanline);
|
||||
out.push(u8::from(self.irq_enable));
|
||||
out.push(u8::from(self.irq_pending));
|
||||
out.extend_from_slice(&self.irq_cycles.to_le_bytes());
|
||||
for bank in self.chr_banks_1k {
|
||||
out.extend_from_slice(&bank.to_le_bytes());
|
||||
}
|
||||
write_state_bytes(out, &self.prg_ram);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 23 + 16 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
let mut cursor = 0usize;
|
||||
self.prg_mode = data[cursor];
|
||||
cursor += 1;
|
||||
self.chr_mode = data[cursor];
|
||||
cursor += 1;
|
||||
self.prg_regs_8k.copy_from_slice(&data[cursor..cursor + 4]);
|
||||
cursor += 4;
|
||||
self.prg_ram_bank = data[cursor];
|
||||
cursor += 1;
|
||||
self.nt_mapping = data[cursor];
|
||||
cursor += 1;
|
||||
self.ram_protect_1 = data[cursor];
|
||||
cursor += 1;
|
||||
self.ram_protect_2 = data[cursor];
|
||||
cursor += 1;
|
||||
self.chr_upper_bits = data[cursor];
|
||||
cursor += 1;
|
||||
self.multiplier_a = data[cursor];
|
||||
cursor += 1;
|
||||
self.multiplier_b = data[cursor];
|
||||
cursor += 1;
|
||||
self.irq_scanline = data[cursor];
|
||||
cursor += 1;
|
||||
self.irq_enable = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.irq_pending = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.irq_cycles = u32::from_le_bytes([
|
||||
data[cursor],
|
||||
data[cursor + 1],
|
||||
data[cursor + 2],
|
||||
data[cursor + 3],
|
||||
]);
|
||||
cursor += 4;
|
||||
for i in 0..8usize {
|
||||
self.chr_banks_1k[i] = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
|
||||
cursor += 2;
|
||||
}
|
||||
let prg_ram_payload = read_state_bytes(data, &mut cursor)?;
|
||||
if prg_ram_payload.len() != self.prg_ram.len() {
|
||||
return Err("mapper state does not match loaded ROM".to_string());
|
||||
}
|
||||
self.prg_ram.copy_from_slice(prg_ram_payload);
|
||||
load_chr_state(&mut self.chr_data, &data[cursor..])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
84
src/native_core/mapper/mappers/mod.rs
Normal file
84
src/native_core/mapper/mappers/mod.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use super::core::*;
|
||||
use super::types::*;
|
||||
use crate::native_core::ines::{InesRom, Mirroring};
|
||||
use std::cell::Cell;
|
||||
|
||||
mod axrom;
|
||||
mod bandai70_152;
|
||||
mod bnrom34;
|
||||
mod camerica71;
|
||||
mod cnrom;
|
||||
mod color_dreams11;
|
||||
mod cprom13;
|
||||
mod crazy_climber180;
|
||||
mod fme7;
|
||||
mod gxrom;
|
||||
mod mapper105;
|
||||
mod mapper118;
|
||||
mod mapper140;
|
||||
mod mapper155;
|
||||
mod mapper158;
|
||||
mod mapper184;
|
||||
mod mapper185;
|
||||
mod mapper206;
|
||||
mod mapper206_submapper1;
|
||||
mod mapper253;
|
||||
mod mapper64;
|
||||
mod mapper78;
|
||||
mod mapper87;
|
||||
mod mapper88;
|
||||
mod mapper93;
|
||||
mod mapper95;
|
||||
mod mmc1;
|
||||
mod mmc2;
|
||||
mod mmc3;
|
||||
mod mmc4;
|
||||
mod mmc5;
|
||||
mod namco163_19;
|
||||
mod nina79;
|
||||
mod nrom;
|
||||
mod tqrom119;
|
||||
mod un1rom94;
|
||||
mod unrom512_30;
|
||||
mod uxrom;
|
||||
mod vrc;
|
||||
|
||||
pub(super) use axrom::Axrom;
|
||||
pub(super) use bandai70_152::Bandai70_152;
|
||||
pub(super) use bnrom34::Bnrom34;
|
||||
pub(super) use camerica71::Camerica71;
|
||||
pub(super) use cnrom::Cnrom;
|
||||
pub(super) use color_dreams11::ColorDreams11;
|
||||
pub(super) use cprom13::Cprom13;
|
||||
pub(super) use crazy_climber180::CrazyClimber180;
|
||||
pub(super) use fme7::Fme7;
|
||||
pub(super) use gxrom::Gxrom;
|
||||
pub(super) use mapper64::InesMapper64;
|
||||
pub(super) use mapper78::InesMapper78;
|
||||
pub(super) use mapper87::InesMapper87;
|
||||
pub(super) use mapper88::InesMapper88;
|
||||
pub(super) use mapper93::InesMapper93;
|
||||
pub(super) use mapper95::InesMapper95;
|
||||
pub(super) use mapper105::InesMapper105;
|
||||
pub(super) use mapper118::InesMapper118;
|
||||
pub(super) use mapper140::InesMapper140;
|
||||
pub(super) use mapper155::InesMapper155;
|
||||
pub(super) use mapper158::InesMapper158;
|
||||
pub(super) use mapper184::InesMapper184;
|
||||
pub(super) use mapper185::InesMapper185;
|
||||
pub(super) use mapper206::InesMapper206;
|
||||
pub(super) use mapper206_submapper1::InesMapper206Submapper1;
|
||||
pub(super) use mapper253::InesMapper253;
|
||||
pub(super) use mmc1::Mmc1;
|
||||
pub(super) use mmc2::Mmc2;
|
||||
pub(super) use mmc3::Mmc3;
|
||||
pub(super) use mmc4::Mmc4;
|
||||
pub(super) use mmc5::Mmc5;
|
||||
pub(super) use namco163_19::Namco163_19;
|
||||
pub(super) use nina79::Nina79;
|
||||
pub(super) use nrom::Nrom;
|
||||
pub(super) use tqrom119::Tqrom119;
|
||||
pub(super) use un1rom94::Un1rom94;
|
||||
pub(super) use unrom512_30::Unrom512_30;
|
||||
pub(super) use uxrom::Uxrom;
|
||||
pub(super) use vrc::{Vrc1_75, Vrc2_23, Vrc6_24, Vrc7_85};
|
||||
202
src/native_core/mapper/mappers/namco163_19.rs
Normal file
202
src/native_core/mapper/mappers/namco163_19.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Namco163_19 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring: Mirroring,
|
||||
prg_ram: Vec<u8>,
|
||||
prg_banks_8k: [u8; 3],
|
||||
chr_banks_1k: [u8; 8],
|
||||
audio_ram: [u8; 0x80],
|
||||
irq_counter: u16,
|
||||
irq_enabled: bool,
|
||||
irq_pending: bool,
|
||||
}
|
||||
|
||||
impl Namco163_19 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring: rom.header.mirroring,
|
||||
prg_ram: vec![0; 0x2000],
|
||||
prg_banks_8k: [0, 1, 2],
|
||||
chr_banks_1k: [0, 1, 2, 3, 4, 5, 6, 7],
|
||||
audio_ram: [0; 0x80],
|
||||
irq_counter: 0,
|
||||
irq_enabled: false,
|
||||
irq_pending: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn prg_bank_count_8k(&self) -> usize {
|
||||
(self.prg_rom.len() / 0x2000).max(1)
|
||||
}
|
||||
|
||||
fn chr_bank_count_1k(&self) -> usize {
|
||||
(self.chr_data.len() / 0x0400).max(1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Namco163_19 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
let slot = ((addr - 0x8000) / 0x2000) as usize;
|
||||
let bank = match slot {
|
||||
0 => self.prg_banks_8k[0] as usize,
|
||||
1 => self.prg_banks_8k[1] as usize,
|
||||
2 => self.prg_banks_8k[2] as usize,
|
||||
_ => self.prg_bank_count_8k().saturating_sub(1),
|
||||
};
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x2000,
|
||||
bank,
|
||||
((addr as usize) - 0x8000) & 0x1FFF,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
match addr {
|
||||
0x8000..=0xDFFF => {
|
||||
let reg = ((addr - 0x8000) / 0x0800) as usize;
|
||||
if reg < 8 {
|
||||
self.chr_banks_1k[reg] = value;
|
||||
}
|
||||
}
|
||||
0xE000..=0xE7FF => self.prg_banks_8k[0] = value & 0x3F,
|
||||
0xE800..=0xEFFF => self.prg_banks_8k[1] = value & 0x3F,
|
||||
0xF000..=0xF7FF => self.prg_banks_8k[2] = value & 0x3F,
|
||||
0xF800..=0xFFFF => {
|
||||
self.mirroring = if (value & 1) == 0 {
|
||||
Mirroring::Vertical
|
||||
} else {
|
||||
Mirroring::Horizontal
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
match addr {
|
||||
0x4800..=0x487F => Some(self.audio_ram[(addr as usize) & 0x7F]),
|
||||
0x5000 => Some((self.irq_counter & 0x00FF) as u8),
|
||||
0x5800 => {
|
||||
let mut hi = ((self.irq_counter >> 8) as u8) & 0x7F;
|
||||
if self.irq_enabled {
|
||||
hi |= 0x80;
|
||||
}
|
||||
Some(hi)
|
||||
}
|
||||
0x6000..=0x7FFF => Some(self.prg_ram[(addr as usize) - 0x6000]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||
match addr {
|
||||
0x4800..=0x487F => self.audio_ram[(addr as usize) & 0x7F] = value,
|
||||
0x5000 => {
|
||||
self.irq_counter = (self.irq_counter & 0xFF00) | value as u16;
|
||||
self.irq_pending = false;
|
||||
}
|
||||
0x5800 => {
|
||||
self.irq_counter = (self.irq_counter & 0x00FF) | (((value & 0x7F) as u16) << 8);
|
||||
self.irq_enabled = (value & 0x80) != 0;
|
||||
if !self.irq_enabled {
|
||||
self.irq_pending = false;
|
||||
}
|
||||
}
|
||||
0x6000..=0x7FFF => self.prg_ram[(addr as usize) - 0x6000] = value,
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
let page = (addr / 0x0400) as usize;
|
||||
let bank = safe_mod(self.chr_banks_1k[page] as usize, self.chr_bank_count_1k());
|
||||
read_bank(&self.chr_data, 0x0400, bank, (addr as usize) & 0x03FF)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let page = (addr / 0x0400) as usize;
|
||||
let bank = safe_mod(self.chr_banks_1k[page] as usize, self.chr_bank_count_1k());
|
||||
let idx = bank * 0x0400 + ((addr as usize) & 0x03FF);
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn clock_cpu(&mut self, cycles: u8) {
|
||||
if !self.irq_enabled {
|
||||
return;
|
||||
}
|
||||
let sum = self.irq_counter as u32 + cycles as u32;
|
||||
if sum > 0x7FFF {
|
||||
self.irq_pending = true;
|
||||
}
|
||||
self.irq_counter = (sum as u16) & 0x7FFF;
|
||||
}
|
||||
|
||||
fn poll_irq(&mut self) -> bool {
|
||||
let out = self.irq_pending;
|
||||
self.irq_pending = false;
|
||||
out
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(encode_mirroring(self.mirroring));
|
||||
out.extend_from_slice(&self.prg_banks_8k);
|
||||
out.extend_from_slice(&self.chr_banks_1k);
|
||||
out.extend_from_slice(&self.audio_ram);
|
||||
out.extend_from_slice(&self.irq_counter.to_le_bytes());
|
||||
out.push(u8::from(self.irq_enabled));
|
||||
out.push(u8::from(self.irq_pending));
|
||||
write_state_bytes(out, &self.prg_ram);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 1 + 3 + 8 + 0x80 + 4 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
let mut cursor = 0usize;
|
||||
self.mirroring = decode_mirroring(data[cursor]);
|
||||
cursor += 1;
|
||||
self.prg_banks_8k.copy_from_slice(&data[cursor..cursor + 3]);
|
||||
cursor += 3;
|
||||
self.chr_banks_1k.copy_from_slice(&data[cursor..cursor + 8]);
|
||||
cursor += 8;
|
||||
self.audio_ram.copy_from_slice(&data[cursor..cursor + 0x80]);
|
||||
cursor += 0x80;
|
||||
self.irq_counter = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
|
||||
cursor += 2;
|
||||
self.irq_enabled = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.irq_pending = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
let prg_ram_payload = read_state_bytes(data, &mut cursor)?;
|
||||
if prg_ram_payload.len() != self.prg_ram.len() {
|
||||
return Err("mapper state does not match loaded ROM".to_string());
|
||||
}
|
||||
self.prg_ram.copy_from_slice(prg_ram_payload);
|
||||
load_chr_state(&mut self.chr_data, &data[cursor..])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
91
src/native_core/mapper/mappers/nina79.rs
Normal file
91
src/native_core/mapper/mappers/nina79.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Nina79 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring: Mirroring,
|
||||
prg_bank: u8,
|
||||
chr_bank: u8,
|
||||
}
|
||||
|
||||
impl Nina79 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring: rom.header.mirroring,
|
||||
prg_bank: 0,
|
||||
chr_bank: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Nina79 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x8000,
|
||||
self.prg_bank as usize,
|
||||
(addr as usize) - 0x8000,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||
|
||||
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||
// NINA-003/006 latch: 010x xxx1 xxxx xxxx ($4100/$4300/.../$5F00).
|
||||
if (addr & 0xE100) == 0x4100 {
|
||||
self.prg_bank = (value >> 3) & 0x01;
|
||||
self.chr_bank = value & 0x07;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
0x2000,
|
||||
self.chr_bank as usize,
|
||||
addr as usize,
|
||||
)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let total_banks = (self.chr_data.len() / 0x2000).max(1);
|
||||
let idx = safe_mod(self.chr_bank as usize, total_banks) * 0x2000 + (addr as usize & 0x1FFF);
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.prg_bank);
|
||||
out.push(self.chr_bank);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 2 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.prg_bank = data[0];
|
||||
self.chr_bank = data[1];
|
||||
load_chr_state(&mut self.chr_data, &data[2..])
|
||||
}
|
||||
}
|
||||
73
src/native_core/mapper/mappers/nrom.rs
Normal file
73
src/native_core/mapper/mappers/nrom.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Nrom {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring: Mirroring,
|
||||
}
|
||||
|
||||
impl Nrom {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring: rom.header.mirroring,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Nrom {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
((addr - 0x8000) as usize) / 0x4000,
|
||||
addr as usize,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
let _ = addr;
|
||||
None
|
||||
}
|
||||
|
||||
fn cpu_write_low(&mut self, addr: u16, _value: u8) -> bool {
|
||||
let _ = addr;
|
||||
false
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
load_chr_state(&mut self.chr_data, data)
|
||||
}
|
||||
}
|
||||
269
src/native_core/mapper/mappers/tqrom119.rs
Normal file
269
src/native_core/mapper/mappers/tqrom119.rs
Normal file
@@ -0,0 +1,269 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Tqrom119 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_rom: Vec<u8>,
|
||||
chr_ram: Vec<u8>,
|
||||
prg_ram: Vec<u8>,
|
||||
prg_ram_enabled: bool,
|
||||
prg_ram_write_protect: bool,
|
||||
mirroring: Mirroring,
|
||||
bank_regs: [u8; 8],
|
||||
bank_select: u8,
|
||||
irq_latch: u8,
|
||||
irq_counter: u8,
|
||||
irq_reload: bool,
|
||||
irq_enabled: bool,
|
||||
irq_pending: bool,
|
||||
}
|
||||
|
||||
impl Tqrom119 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_rom: rom.chr_data,
|
||||
chr_ram: vec![0; 0x2000],
|
||||
prg_ram: vec![0; 0x2000],
|
||||
prg_ram_enabled: true,
|
||||
prg_ram_write_protect: false,
|
||||
mirroring: rom.header.mirroring,
|
||||
bank_regs: [0; 8],
|
||||
bank_select: 0,
|
||||
irq_latch: 0,
|
||||
irq_counter: 0,
|
||||
irq_reload: false,
|
||||
irq_enabled: false,
|
||||
irq_pending: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn prg_bank_count_8k(&self) -> usize {
|
||||
(self.prg_rom.len() / 0x2000).max(1)
|
||||
}
|
||||
|
||||
fn prg_mode(&self) -> bool {
|
||||
(self.bank_select & 0x40) != 0
|
||||
}
|
||||
|
||||
fn chr_invert(&self) -> bool {
|
||||
(self.bank_select & 0x80) != 0
|
||||
}
|
||||
|
||||
fn prg_bank_for_slot(&self, slot: usize) -> usize {
|
||||
let last = self.prg_bank_count_8k() - 1;
|
||||
let second_last = last.saturating_sub(1);
|
||||
match (self.prg_mode(), slot) {
|
||||
(false, 0) => self.bank_regs[6] as usize,
|
||||
(false, 1) => self.bank_regs[7] as usize,
|
||||
(false, 2) => second_last,
|
||||
(false, 3) => last,
|
||||
(true, 0) => second_last,
|
||||
(true, 1) => self.bank_regs[7] as usize,
|
||||
(true, 2) => self.bank_regs[6] as usize,
|
||||
(true, 3) => last,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn chr_bank_for_1k_page(&self, page: usize) -> u8 {
|
||||
let regs = &self.bank_regs;
|
||||
let mut layout = [0u8; 8];
|
||||
|
||||
layout[0] = regs[0] & !1;
|
||||
layout[1] = layout[0].wrapping_add(1);
|
||||
layout[2] = regs[1] & !1;
|
||||
layout[3] = layout[2].wrapping_add(1);
|
||||
layout[4] = regs[2];
|
||||
layout[5] = regs[3];
|
||||
layout[6] = regs[4];
|
||||
layout[7] = regs[5];
|
||||
|
||||
if self.chr_invert() {
|
||||
layout.rotate_left(4);
|
||||
}
|
||||
layout[page]
|
||||
}
|
||||
|
||||
fn clock_irq_scanline(&mut self) {
|
||||
if self.irq_reload || self.irq_counter == 0 {
|
||||
self.irq_counter = self.irq_latch;
|
||||
self.irq_reload = false;
|
||||
} else {
|
||||
self.irq_counter = self.irq_counter.wrapping_sub(1);
|
||||
}
|
||||
if self.irq_enabled && self.irq_counter == 0 {
|
||||
self.irq_pending = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn chr_read_page(&self, bank: u8, offset: usize) -> u8 {
|
||||
if (bank & 0x40) != 0 {
|
||||
read_bank(&self.chr_ram, 0x0400, (bank & 0x07) as usize, offset)
|
||||
} else {
|
||||
read_bank(&self.chr_rom, 0x0400, (bank & 0x3F) as usize, offset)
|
||||
}
|
||||
}
|
||||
|
||||
fn chr_write_page(&mut self, bank: u8, offset: usize, value: u8) {
|
||||
if (bank & 0x40) == 0 {
|
||||
return;
|
||||
}
|
||||
let page = (bank & 0x07) as usize;
|
||||
let idx = page * 0x0400 + (offset & 0x03FF);
|
||||
if let Some(cell) = self.chr_ram.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Tqrom119 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
let slot = ((addr - 0x8000) / 0x2000) as usize;
|
||||
let bank = self.prg_bank_for_slot(slot);
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x2000,
|
||||
bank,
|
||||
((addr as usize) - 0x8000) & 0x1FFF,
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
match addr {
|
||||
0x8000..=0x9FFF if (addr & 1) == 0 => self.bank_select = value,
|
||||
0x8000..=0x9FFF => {
|
||||
let reg = (self.bank_select & 0x07) as usize;
|
||||
self.bank_regs[reg] = value;
|
||||
}
|
||||
0xA000..=0xBFFF if (addr & 1) == 0 => {
|
||||
self.mirroring = if (value & 1) == 0 {
|
||||
Mirroring::Vertical
|
||||
} else {
|
||||
Mirroring::Horizontal
|
||||
};
|
||||
}
|
||||
0xA000..=0xBFFF => {
|
||||
self.prg_ram_enabled = (value & 0x80) != 0;
|
||||
self.prg_ram_write_protect = (value & 0x40) != 0;
|
||||
}
|
||||
0xC000..=0xDFFF if (addr & 1) == 0 => self.irq_latch = value,
|
||||
0xC000..=0xDFFF => {
|
||||
self.irq_counter = 0;
|
||||
self.irq_reload = true;
|
||||
}
|
||||
0xE000..=0xFFFF if (addr & 1) == 0 => {
|
||||
self.irq_enabled = false;
|
||||
self.irq_pending = false;
|
||||
}
|
||||
0xE000..=0xFFFF => self.irq_enabled = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
if (0x6000..=0x7FFF).contains(&addr) {
|
||||
if self.prg_ram_enabled {
|
||||
Some(self.prg_ram[(addr as usize) - 0x6000])
|
||||
} else {
|
||||
Some(0)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||
if (0x6000..=0x7FFF).contains(&addr) {
|
||||
if self.prg_ram_enabled && !self.prg_ram_write_protect {
|
||||
self.prg_ram[(addr as usize) - 0x6000] = value;
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
let page = (addr / 0x0400) as usize;
|
||||
let bank = self.chr_bank_for_1k_page(page);
|
||||
self.chr_read_page(bank, (addr as usize) & 0x03FF)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let page = (addr / 0x0400) as usize;
|
||||
let bank = self.chr_bank_for_1k_page(page);
|
||||
self.chr_write_page(bank, (addr as usize) & 0x03FF, value);
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn clock_scanline(&mut self) {
|
||||
self.clock_irq_scanline();
|
||||
}
|
||||
|
||||
fn needs_ppu_a12_clock(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn poll_irq(&mut self) -> bool {
|
||||
let out = self.irq_pending;
|
||||
self.irq_pending = false;
|
||||
out
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(&self.bank_regs);
|
||||
out.push(self.bank_select);
|
||||
out.push(encode_mirroring(self.mirroring));
|
||||
out.push(self.irq_latch);
|
||||
out.push(self.irq_counter);
|
||||
out.push(u8::from(self.irq_reload));
|
||||
out.push(u8::from(self.irq_enabled));
|
||||
out.push(u8::from(self.irq_pending));
|
||||
out.push(u8::from(self.prg_ram_enabled));
|
||||
out.push(u8::from(self.prg_ram_write_protect));
|
||||
write_state_bytes(out, &self.prg_ram);
|
||||
write_state_bytes(out, &self.chr_ram);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 17 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.bank_regs.copy_from_slice(&data[0..8]);
|
||||
self.bank_select = data[8];
|
||||
self.mirroring = decode_mirroring(data[9]);
|
||||
self.irq_latch = data[10];
|
||||
self.irq_counter = data[11];
|
||||
self.irq_reload = data[12] != 0;
|
||||
self.irq_enabled = data[13] != 0;
|
||||
self.irq_pending = data[14] != 0;
|
||||
let mut cursor = 15usize;
|
||||
self.prg_ram_enabled = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.prg_ram_write_protect = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
let prg_ram = read_state_bytes(data, &mut cursor)?;
|
||||
if prg_ram.len() != self.prg_ram.len() {
|
||||
return Err("mapper state does not match loaded ROM".to_string());
|
||||
}
|
||||
self.prg_ram.copy_from_slice(prg_ram);
|
||||
let chr_ram = read_state_bytes(data, &mut cursor)?;
|
||||
if chr_ram.len() != self.chr_ram.len() || cursor != data.len() {
|
||||
return Err("mapper state does not match loaded ROM".to_string());
|
||||
}
|
||||
self.chr_ram.copy_from_slice(chr_ram);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
87
src/native_core/mapper/mappers/un1rom94.rs
Normal file
87
src/native_core/mapper/mappers/un1rom94.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Un1rom94 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring: Mirroring,
|
||||
bank_select: u8,
|
||||
}
|
||||
|
||||
impl Un1rom94 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring: rom.header.mirroring,
|
||||
bank_select: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn prg_banks(&self) -> usize {
|
||||
(self.prg_rom.len() / 0x4000).max(1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Un1rom94 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
if addr < 0xC000 {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.bank_select as usize,
|
||||
(addr as usize) - 0x8000,
|
||||
)
|
||||
} else {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.prg_banks() - 1,
|
||||
(addr as usize) - 0xC000,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr >= 0x8000 {
|
||||
self.bank_select = (value >> 2) & 0x07;
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.bank_select);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.is_empty() {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.bank_select = data[0];
|
||||
load_chr_state(&mut self.chr_data, &data[1..])
|
||||
}
|
||||
}
|
||||
123
src/native_core/mapper/mappers/unrom512_30.rs
Normal file
123
src/native_core/mapper/mappers/unrom512_30.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Unrom512_30 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring_default: Mirroring,
|
||||
prg_bank: u8,
|
||||
chr_bank: u8,
|
||||
one_screen_hi: Option<bool>,
|
||||
}
|
||||
|
||||
impl Unrom512_30 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
let mut chr_data = rom.chr_data;
|
||||
if rom.chr_is_ram && chr_data.len() < 0x8000 {
|
||||
chr_data.resize(0x8000, 0);
|
||||
}
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring_default: rom.header.mirroring,
|
||||
prg_bank: 0,
|
||||
chr_bank: 0,
|
||||
one_screen_hi: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn prg_bank_count_16k(&self) -> usize {
|
||||
(self.prg_rom.len() / 0x4000).max(1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Unrom512_30 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
if addr < 0xC000 {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.prg_bank as usize,
|
||||
(addr as usize) - 0x8000,
|
||||
)
|
||||
} else {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.prg_bank_count_16k().saturating_sub(1),
|
||||
(addr as usize) - 0xC000,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr < 0x8000 {
|
||||
return;
|
||||
}
|
||||
self.prg_bank = value & 0x1F;
|
||||
self.chr_bank = (value >> 5) & 0x03;
|
||||
self.one_screen_hi = Some((value & 0x80) != 0);
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
0x2000,
|
||||
self.chr_bank as usize,
|
||||
addr as usize,
|
||||
)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let total_banks = (self.chr_data.len() / 0x2000).max(1);
|
||||
let bank_idx = safe_mod(self.chr_bank as usize, total_banks);
|
||||
let idx = bank_idx * 0x2000 + ((addr as usize) & 0x1FFF);
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
match self.one_screen_hi {
|
||||
Some(true) => Mirroring::OneScreenHigh,
|
||||
Some(false) => Mirroring::OneScreenLow,
|
||||
None => self.mirroring_default,
|
||||
}
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.prg_bank);
|
||||
out.push(self.chr_bank);
|
||||
out.push(match self.one_screen_hi {
|
||||
Some(true) => 2,
|
||||
Some(false) => 1,
|
||||
None => 0,
|
||||
});
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 3 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.prg_bank = data[0];
|
||||
self.chr_bank = data[1];
|
||||
self.one_screen_hi = match data[2] {
|
||||
0 => None,
|
||||
1 => Some(false),
|
||||
2 => Some(true),
|
||||
_ => None,
|
||||
};
|
||||
load_chr_state(&mut self.chr_data, &data[3..])
|
||||
}
|
||||
}
|
||||
87
src/native_core/mapper/mappers/uxrom.rs
Normal file
87
src/native_core/mapper/mappers/uxrom.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Uxrom {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring: Mirroring,
|
||||
bank_select: u8,
|
||||
}
|
||||
|
||||
impl Uxrom {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring: rom.header.mirroring,
|
||||
bank_select: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn prg_banks(&self) -> usize {
|
||||
(self.prg_rom.len() / 0x4000).max(1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Uxrom {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
if addr < 0xC000 {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.bank_select as usize,
|
||||
(addr as usize) - 0x8000,
|
||||
)
|
||||
} else {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
0x4000,
|
||||
self.prg_banks() - 1,
|
||||
(addr as usize) - 0xC000,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr >= 0x8000 {
|
||||
self.bank_select = value & 0x0F;
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.bank_select);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.is_empty() {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.bank_select = data[0];
|
||||
load_chr_state(&mut self.chr_data, &data[1..])
|
||||
}
|
||||
}
|
||||
11
src/native_core/mapper/mappers/vrc/mod.rs
Normal file
11
src/native_core/mapper/mappers/vrc/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use super::*;
|
||||
|
||||
mod vrc1_75;
|
||||
mod vrc2_23;
|
||||
mod vrc6_24;
|
||||
mod vrc7_85;
|
||||
|
||||
pub(crate) use vrc1_75::Vrc1_75;
|
||||
pub(crate) use vrc2_23::Vrc2_23;
|
||||
pub(crate) use vrc6_24::Vrc6_24;
|
||||
pub(crate) use vrc7_85::Vrc7_85;
|
||||
122
src/native_core/mapper/mappers/vrc/vrc1_75.rs
Normal file
122
src/native_core/mapper/mappers/vrc/vrc1_75.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Vrc1_75 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring: Mirroring,
|
||||
prg_bank_8k_8000: u8,
|
||||
prg_bank_8k_a000: u8,
|
||||
prg_bank_8k_c000: u8,
|
||||
chr_bank_0: u8,
|
||||
chr_bank_1: u8,
|
||||
}
|
||||
|
||||
impl Vrc1_75 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring: rom.header.mirroring,
|
||||
prg_bank_8k_8000: 0,
|
||||
prg_bank_8k_a000: 1,
|
||||
prg_bank_8k_c000: 2,
|
||||
chr_bank_0: 0,
|
||||
chr_bank_1: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Vrc1_75 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
let bank = if addr < 0xA000 {
|
||||
self.prg_bank_8k_8000 as usize
|
||||
} else if addr < 0xC000 {
|
||||
self.prg_bank_8k_a000 as usize
|
||||
} else if addr < 0xE000 {
|
||||
self.prg_bank_8k_c000 as usize
|
||||
} else {
|
||||
(self.prg_rom.len() / 0x2000).max(1).saturating_sub(1)
|
||||
};
|
||||
read_bank(&self.prg_rom, 0x2000, bank, (addr as usize) & 0x1FFF)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
match addr {
|
||||
0x8000..=0x8FFF => self.prg_bank_8k_8000 = value & 0x0F,
|
||||
0x9000..=0x9FFF => {
|
||||
self.mirroring = if (value & 1) == 0 {
|
||||
Mirroring::Vertical
|
||||
} else {
|
||||
Mirroring::Horizontal
|
||||
};
|
||||
self.chr_bank_0 = (self.chr_bank_0 & 0x0F) | ((value & 0x02) << 3);
|
||||
self.chr_bank_1 = (self.chr_bank_1 & 0x0F) | ((value & 0x04) << 2);
|
||||
}
|
||||
0xA000..=0xAFFF => self.prg_bank_8k_a000 = value & 0x0F,
|
||||
0xC000..=0xCFFF => self.prg_bank_8k_c000 = value & 0x0F,
|
||||
0xE000..=0xEFFF => self.chr_bank_0 = (self.chr_bank_0 & 0x10) | (value & 0x0F),
|
||||
0xF000..=0xFFFF => self.chr_bank_1 = (self.chr_bank_1 & 0x10) | (value & 0x0F),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
let bank = if addr < 0x1000 {
|
||||
self.chr_bank_0 as usize
|
||||
} else {
|
||||
self.chr_bank_1 as usize
|
||||
};
|
||||
read_bank(&self.chr_data, 0x1000, bank, (addr as usize) & 0x0FFF)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let bank = if addr < 0x1000 {
|
||||
self.chr_bank_0 as usize
|
||||
} else {
|
||||
self.chr_bank_1 as usize
|
||||
};
|
||||
let total = (self.chr_data.len() / 0x1000).max(1);
|
||||
let idx = safe_mod(bank, total) * 0x1000 + ((addr as usize) & 0x0FFF);
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.prg_bank_8k_8000);
|
||||
out.push(self.prg_bank_8k_a000);
|
||||
out.push(self.prg_bank_8k_c000);
|
||||
out.push(self.chr_bank_0);
|
||||
out.push(self.chr_bank_1);
|
||||
out.push(encode_mirroring(self.mirroring));
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 6 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
self.prg_bank_8k_8000 = data[0];
|
||||
self.prg_bank_8k_a000 = data[1];
|
||||
self.prg_bank_8k_c000 = data[2];
|
||||
self.chr_bank_0 = data[3];
|
||||
self.chr_bank_1 = data[4];
|
||||
self.mirroring = decode_mirroring(data[5]);
|
||||
load_chr_state(&mut self.chr_data, &data[6..])
|
||||
}
|
||||
}
|
||||
399
src/native_core/mapper/mappers/vrc/vrc2_23.rs
Normal file
399
src/native_core/mapper/mappers/vrc/vrc2_23.rs
Normal file
@@ -0,0 +1,399 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Vrc2_23 {
|
||||
pub(crate) mapper_id: u16,
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
prg_ram: Vec<u8>,
|
||||
mirroring: Mirroring,
|
||||
has_irq: bool,
|
||||
submapper: u8,
|
||||
prg_swap: bool,
|
||||
prg_bank_8000: u8,
|
||||
prg_bank_a000: u8,
|
||||
chr_banks_1k: [u16; 8],
|
||||
irq_latch: u8,
|
||||
irq_counter: u8,
|
||||
irq_enabled: bool,
|
||||
irq_enabled_after_ack: bool,
|
||||
irq_mode_cpu: bool,
|
||||
irq_pending: bool,
|
||||
irq_prescaler: i16,
|
||||
}
|
||||
|
||||
impl Vrc2_23 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
let mapper_id = rom.header.mapper;
|
||||
let submapper = rom.header.submapper;
|
||||
let has_irq = mapper_id != 22 && !matches!(submapper, 3 | 4);
|
||||
let mut chr_banks_1k = [0u16; 8];
|
||||
for (i, bank) in chr_banks_1k.iter_mut().enumerate() {
|
||||
*bank = i as u16;
|
||||
}
|
||||
let prg_ram_size = if rom.header.prg_ram_shift > 0 {
|
||||
64usize << rom.header.prg_ram_shift
|
||||
} else {
|
||||
8192 // Default 8KB WRAM for VRC2/VRC4 boards
|
||||
};
|
||||
Self {
|
||||
mapper_id,
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
prg_ram: vec![0; prg_ram_size],
|
||||
mirroring: rom.header.mirroring,
|
||||
has_irq,
|
||||
submapper,
|
||||
prg_swap: false,
|
||||
prg_bank_8000: 0,
|
||||
prg_bank_a000: 1,
|
||||
chr_banks_1k,
|
||||
irq_latch: 0,
|
||||
irq_counter: 0,
|
||||
irq_enabled: false,
|
||||
irq_enabled_after_ack: false,
|
||||
irq_mode_cpu: false,
|
||||
irq_pending: false,
|
||||
irq_prescaler: 341,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_with_submapper(rom: InesRom, forced_submapper: u8) -> Self {
|
||||
let mut mapper = Self::new(rom);
|
||||
mapper.submapper = forced_submapper;
|
||||
mapper
|
||||
}
|
||||
|
||||
fn chr_bank_count_1k(&self) -> usize {
|
||||
(self.chr_data.len() / CHR_BANK_1K).max(1)
|
||||
}
|
||||
|
||||
fn set_chr_nibble(&mut self, bank: usize, high: bool, value: u8) {
|
||||
if bank >= 8 {
|
||||
return;
|
||||
}
|
||||
let cur = self.chr_banks_1k[bank];
|
||||
self.chr_banks_1k[bank] = if high {
|
||||
(cur & 0x000F) | (((value & 0x0F) as u16) << 4)
|
||||
} else {
|
||||
(cur & 0x01F0) | ((value & 0x0F) as u16)
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_subindex(addr: u16, a0_bit: u8, a1_bit: u8) -> usize {
|
||||
let a0 = ((addr >> a0_bit) & 1) as usize;
|
||||
let a1 = ((addr >> a1_bit) & 1) as usize;
|
||||
a0 | (a1 << 1)
|
||||
}
|
||||
|
||||
fn lower_wiring_bits(&self) -> (u8, u8) {
|
||||
match self.mapper_id {
|
||||
21 => (1, 2), // VRC4a-style
|
||||
22 => (1, 0), // VRC2a (swapped A0/A1)
|
||||
23 => (0, 1), // VRC2b/VRC4f
|
||||
25 => (1, 0), // VRC2c/VRC4d (swapped)
|
||||
_ => (0, 1),
|
||||
}
|
||||
}
|
||||
|
||||
fn higher_wiring_bits(&self) -> (u8, u8) {
|
||||
match self.mapper_id {
|
||||
21 => (6, 7), // VRC4c-style
|
||||
22 => (1, 0), // VRC2a has a single decode variant
|
||||
23 => (2, 3), // VRC4e
|
||||
25 => (3, 2), // VRC4d (swapped)
|
||||
_ => (0, 1),
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_subindices(&self, addr: u16) -> ([usize; 2], usize) {
|
||||
if self.mapper_id == 22 {
|
||||
let (a0, a1) = self.lower_wiring_bits();
|
||||
return ([Self::decode_subindex(addr, a0, a1), 0], 1);
|
||||
}
|
||||
|
||||
let (lower_a0, lower_a1) = self.lower_wiring_bits();
|
||||
let lower = Self::decode_subindex(addr, lower_a0, lower_a1);
|
||||
match self.submapper {
|
||||
1 | 3 => ([lower, 0], 1), // lower-variant wiring only
|
||||
2 | 4 => {
|
||||
let (higher_a0, higher_a1) = self.higher_wiring_bits();
|
||||
([Self::decode_subindex(addr, higher_a0, higher_a1), 0], 1)
|
||||
}
|
||||
_ => {
|
||||
let (higher_a0, higher_a1) = self.higher_wiring_bits();
|
||||
// iNES and NES2 submapper 0: support both known address families,
|
||||
// but choose one decode per write to avoid conflicting double-updates.
|
||||
let higher_selected =
|
||||
((addr >> higher_a0) & 1) != 0 || ((addr >> higher_a1) & 1) != 0;
|
||||
if higher_selected {
|
||||
([Self::decode_subindex(addr, higher_a0, higher_a1), 0], 1)
|
||||
} else {
|
||||
([lower, 0], 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn effective_chr_bank(&self, raw_bank: u16) -> usize {
|
||||
if self.mapper_id == 22 {
|
||||
// VRC2a (mapper 22): CHR bank value is shifted right by 1.
|
||||
(raw_bank >> 1) as usize
|
||||
} else {
|
||||
raw_bank as usize
|
||||
}
|
||||
}
|
||||
|
||||
fn irq_state(&mut self) -> VrcIrqRegisters<'_> {
|
||||
VrcIrqRegisters {
|
||||
latch: &mut self.irq_latch,
|
||||
counter: &mut self.irq_counter,
|
||||
enabled: &mut self.irq_enabled,
|
||||
enabled_after_ack: &mut self.irq_enabled_after_ack,
|
||||
mode_cpu: &mut self.irq_mode_cpu,
|
||||
pending: &mut self.irq_pending,
|
||||
prescaler: &mut self.irq_prescaler,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Vrc2_23 {
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
if (0x6000..=0x7FFF).contains(&addr) && !self.prg_ram.is_empty() {
|
||||
Some(self.prg_ram[(addr as usize - 0x6000) % self.prg_ram.len()])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||
if (0x6000..=0x7FFF).contains(&addr) && !self.prg_ram.is_empty() {
|
||||
let idx = (addr as usize - 0x6000) % self.prg_ram.len();
|
||||
self.prg_ram[idx] = value;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
let slot = ((addr - 0x8000) / 0x2000) as usize;
|
||||
let last = (self.prg_rom.len() / PRG_BANK_8K).max(1).saturating_sub(1);
|
||||
let fixed_second_last = last.saturating_sub(1);
|
||||
let bank0 = if self.has_irq && self.prg_swap {
|
||||
fixed_second_last
|
||||
} else {
|
||||
self.prg_bank_8000 as usize
|
||||
};
|
||||
let bank2 = if self.has_irq && self.prg_swap {
|
||||
self.prg_bank_8000 as usize
|
||||
} else {
|
||||
fixed_second_last
|
||||
};
|
||||
let bank = match slot {
|
||||
0 => bank0,
|
||||
1 => self.prg_bank_a000 as usize,
|
||||
2 => bank2,
|
||||
_ => last,
|
||||
};
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
PRG_BANK_8K,
|
||||
bank,
|
||||
((addr as usize) - 0x8000) & (PRG_BANK_8K - 1),
|
||||
)
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
match addr {
|
||||
0x8000..=0x8FFF => self.prg_bank_8000 = value & 0x1F,
|
||||
0x9000..=0x9FFF => {
|
||||
let (subs, count) = self.decode_subindices(addr);
|
||||
for i in 0..count {
|
||||
let sub = subs[i];
|
||||
if i > 0 && sub == subs[0] {
|
||||
continue;
|
||||
}
|
||||
if sub == 0 {
|
||||
self.mirroring = if self.has_irq {
|
||||
match value & 0x03 {
|
||||
0 => Mirroring::Vertical,
|
||||
1 => Mirroring::Horizontal,
|
||||
2 => Mirroring::OneScreenLow,
|
||||
_ => Mirroring::OneScreenHigh,
|
||||
}
|
||||
} else if (value & 0x01) == 0 {
|
||||
Mirroring::Vertical
|
||||
} else {
|
||||
Mirroring::Horizontal
|
||||
};
|
||||
} else if sub == 2 && self.has_irq {
|
||||
self.prg_swap = (value & 0x02) != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
0xA000..=0xAFFF => self.prg_bank_a000 = value & 0x1F,
|
||||
0xB000..=0xEFFF => {
|
||||
let group = ((addr - 0xB000) / 0x1000) as usize;
|
||||
let (subs, count) = self.decode_subindices(addr);
|
||||
for i in 0..count {
|
||||
let sub = subs[i];
|
||||
if i > 0 && sub == subs[0] {
|
||||
continue;
|
||||
}
|
||||
let bank = group * 2 + ((sub >> 1) & 1);
|
||||
let high = (sub & 1) != 0;
|
||||
self.set_chr_nibble(bank, high, value);
|
||||
}
|
||||
}
|
||||
0xF000..=0xFFFF if self.has_irq => {
|
||||
let (subs, count) = self.decode_subindices(addr);
|
||||
for i in 0..count {
|
||||
let sub = subs[i];
|
||||
if i > 0 && sub == subs[0] {
|
||||
continue;
|
||||
}
|
||||
match sub {
|
||||
0 => self.irq_latch = (self.irq_latch & 0xF0) | (value & 0x0F),
|
||||
1 => self.irq_latch = (self.irq_latch & 0x0F) | ((value & 0x0F) << 4),
|
||||
2 => vrc_irq_write_control(value, self.irq_state()),
|
||||
_ => vrc_irq_ack(self.irq_state()),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
let page = (addr / CHR_BANK_1K as u16) as usize;
|
||||
let bank = safe_mod(
|
||||
self.effective_chr_bank(self.chr_banks_1k[page]),
|
||||
self.chr_bank_count_1k(),
|
||||
);
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
CHR_BANK_1K,
|
||||
bank,
|
||||
(addr as usize) & (CHR_BANK_1K - 1),
|
||||
)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let page = (addr / CHR_BANK_1K as u16) as usize;
|
||||
let bank = safe_mod(
|
||||
self.effective_chr_bank(self.chr_banks_1k[page]),
|
||||
self.chr_bank_count_1k(),
|
||||
);
|
||||
let idx = bank * CHR_BANK_1K + (addr as usize & (CHR_BANK_1K - 1));
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring
|
||||
}
|
||||
|
||||
fn clock_cpu(&mut self, cycles: u8) {
|
||||
if !self.has_irq {
|
||||
return;
|
||||
}
|
||||
vrc_irq_clock(cycles, self.irq_state());
|
||||
}
|
||||
|
||||
fn poll_irq(&mut self) -> bool {
|
||||
let out = self.irq_pending;
|
||||
self.irq_pending = false;
|
||||
out
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(self.prg_bank_8000);
|
||||
out.push(self.prg_bank_a000);
|
||||
out.push(encode_mirroring(self.mirroring));
|
||||
out.push(u8::from(self.has_irq));
|
||||
out.push(u8::from(self.chr_is_ram));
|
||||
out.push(self.submapper);
|
||||
out.push(u8::from(self.prg_swap));
|
||||
out.push(self.irq_latch);
|
||||
out.push(self.irq_counter);
|
||||
out.push(u8::from(self.irq_enabled));
|
||||
out.push(u8::from(self.irq_enabled_after_ack));
|
||||
out.push(u8::from(self.irq_mode_cpu));
|
||||
out.push(u8::from(self.irq_pending));
|
||||
out.extend_from_slice(&self.irq_prescaler.to_le_bytes());
|
||||
for bank in self.chr_banks_1k {
|
||||
out.extend_from_slice(&bank.to_le_bytes());
|
||||
}
|
||||
write_chr_state(out, &self.chr_data);
|
||||
write_state_bytes(out, &self.prg_ram);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 35 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
let mut cursor = 0usize;
|
||||
self.prg_bank_8000 = data[cursor];
|
||||
cursor += 1;
|
||||
self.prg_bank_a000 = data[cursor];
|
||||
cursor += 1;
|
||||
self.mirroring = decode_mirroring(data[cursor]);
|
||||
cursor += 1;
|
||||
|
||||
self.has_irq = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.chr_is_ram = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.submapper = data[cursor];
|
||||
cursor += 1;
|
||||
self.prg_swap = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.irq_latch = data[cursor];
|
||||
cursor += 1;
|
||||
self.irq_counter = data[cursor];
|
||||
cursor += 1;
|
||||
self.irq_enabled = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.irq_enabled_after_ack = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.irq_mode_cpu = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.irq_pending = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.irq_prescaler = i16::from_le_bytes([data[cursor], data[cursor + 1]]);
|
||||
cursor += 2;
|
||||
if data.len().saturating_sub(cursor) < 16 + 4 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
for i in 0..8usize {
|
||||
let off = cursor + i * 2;
|
||||
self.chr_banks_1k[i] = u16::from_le_bytes([data[off], data[off + 1]]);
|
||||
}
|
||||
cursor += 16;
|
||||
let chr_payload = read_state_bytes(data, &mut cursor)?;
|
||||
if chr_payload.len() != self.chr_data.len() {
|
||||
return Err("mapper CHR state does not match loaded ROM".to_string());
|
||||
}
|
||||
self.chr_data.copy_from_slice(chr_payload);
|
||||
// Try to load PRG-RAM if present (backwards compatible)
|
||||
if cursor + 4 <= data.len() {
|
||||
let ram_payload = read_state_bytes(data, &mut cursor)?;
|
||||
if ram_payload.len() == self.prg_ram.len() {
|
||||
self.prg_ram.copy_from_slice(ram_payload);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
259
src/native_core/mapper/mappers/vrc/vrc6_24.rs
Normal file
259
src/native_core/mapper/mappers/vrc/vrc6_24.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Vrc6_24 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
mirroring_default: Mirroring,
|
||||
mapper26_wiring: bool,
|
||||
prg_bank_16k: u8,
|
||||
prg_bank_8k: u8,
|
||||
chr_banks_1k: [u8; 8],
|
||||
control: u8,
|
||||
prg_ram: Vec<u8>,
|
||||
irq_latch: u8,
|
||||
irq_counter: u8,
|
||||
irq_enabled: bool,
|
||||
irq_enabled_after_ack: bool,
|
||||
irq_mode_cpu: bool,
|
||||
irq_pending: bool,
|
||||
irq_prescaler: i16,
|
||||
}
|
||||
|
||||
impl Vrc6_24 {
|
||||
pub(crate) fn new(rom: InesRom, mapper26_wiring: bool) -> Self {
|
||||
let mut chr_banks_1k = [0u8; 8];
|
||||
for (i, bank) in chr_banks_1k.iter_mut().enumerate() {
|
||||
*bank = i as u8;
|
||||
}
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
mirroring_default: rom.header.mirroring,
|
||||
mapper26_wiring,
|
||||
prg_bank_16k: 0,
|
||||
prg_bank_8k: 0,
|
||||
chr_banks_1k,
|
||||
control: 0,
|
||||
prg_ram: vec![0; PRG_RAM_8K],
|
||||
irq_latch: 0,
|
||||
irq_counter: 0,
|
||||
irq_enabled: false,
|
||||
irq_enabled_after_ack: false,
|
||||
irq_mode_cpu: false,
|
||||
irq_pending: false,
|
||||
irq_prescaler: 341,
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_register(&self, addr: u16) -> u16 {
|
||||
let hi = addr & 0xF000;
|
||||
let mut lo = addr & 0x0003;
|
||||
if self.mapper26_wiring {
|
||||
lo = match lo {
|
||||
1 => 2,
|
||||
2 => 1,
|
||||
_ => lo,
|
||||
};
|
||||
}
|
||||
hi | lo
|
||||
}
|
||||
|
||||
fn chr_page_bank(&self, page: usize) -> usize {
|
||||
let mode = self.control & 0x03;
|
||||
let idx = match mode {
|
||||
0 => page,
|
||||
1 => [0, 1, 1, 3, 2, 5, 3, 7][page],
|
||||
_ => [0, 0, 1, 1, 2, 2, 3, 3][page],
|
||||
};
|
||||
self.chr_banks_1k[idx] as usize
|
||||
}
|
||||
|
||||
fn irq_state(&mut self) -> VrcIrqRegisters<'_> {
|
||||
VrcIrqRegisters {
|
||||
latch: &mut self.irq_latch,
|
||||
counter: &mut self.irq_counter,
|
||||
enabled: &mut self.irq_enabled,
|
||||
enabled_after_ack: &mut self.irq_enabled_after_ack,
|
||||
mode_cpu: &mut self.irq_mode_cpu,
|
||||
pending: &mut self.irq_pending,
|
||||
prescaler: &mut self.irq_prescaler,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Vrc6_24 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
if addr < 0x8000 {
|
||||
return 0;
|
||||
}
|
||||
if addr < 0xC000 {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
PRG_BANK_16K,
|
||||
self.prg_bank_16k as usize,
|
||||
(addr as usize) - 0x8000,
|
||||
)
|
||||
} else if addr < 0xE000 {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
PRG_BANK_8K,
|
||||
self.prg_bank_8k as usize,
|
||||
(addr as usize) - 0xC000,
|
||||
)
|
||||
} else {
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
PRG_BANK_8K,
|
||||
(self.prg_rom.len() / PRG_BANK_8K).max(1).saturating_sub(1),
|
||||
(addr as usize) - 0xE000,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
if addr < 0x8000 {
|
||||
return;
|
||||
}
|
||||
match self.decode_register(addr) {
|
||||
0x8000..=0x8003 => self.prg_bank_16k = value & 0x0F,
|
||||
0x9003 => self.control = value,
|
||||
0xC000..=0xC003 => self.prg_bank_8k = value & 0x1F,
|
||||
0xD000 => self.chr_banks_1k[0] = value,
|
||||
0xD001 => self.chr_banks_1k[1] = value,
|
||||
0xD002 => self.chr_banks_1k[2] = value,
|
||||
0xD003 => self.chr_banks_1k[3] = value,
|
||||
0xE000 => self.chr_banks_1k[4] = value,
|
||||
0xE001 => self.chr_banks_1k[5] = value,
|
||||
0xE002 => self.chr_banks_1k[6] = value,
|
||||
0xE003 => self.chr_banks_1k[7] = value,
|
||||
0xF000 => self.irq_latch = value,
|
||||
0xF001 => vrc_irq_write_control(value, self.irq_state()),
|
||||
0xF002 => vrc_irq_ack(self.irq_state()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
if (0x6000..=0x7FFF).contains(&addr) && (self.control & 0x80) != 0 {
|
||||
Some(self.prg_ram[(addr as usize) - 0x6000])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||
if (0x6000..=0x7FFF).contains(&addr) && (self.control & 0x80) != 0 {
|
||||
self.prg_ram[(addr as usize) - 0x6000] = value;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
let page = (addr / CHR_BANK_1K as u16) as usize;
|
||||
let bank = self.chr_page_bank(page);
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
CHR_BANK_1K,
|
||||
bank,
|
||||
(addr as usize) & (CHR_BANK_1K - 1),
|
||||
)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let page = (addr / CHR_BANK_1K as u16) as usize;
|
||||
let bank = safe_mod(
|
||||
self.chr_page_bank(page),
|
||||
(self.chr_data.len() / CHR_BANK_1K).max(1),
|
||||
);
|
||||
let idx = bank * CHR_BANK_1K + ((addr as usize) & (CHR_BANK_1K - 1));
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
match (self.control >> 2) & 0x03 {
|
||||
0 => Mirroring::Vertical,
|
||||
1 => Mirroring::Horizontal,
|
||||
2 => Mirroring::OneScreenLow,
|
||||
3 => Mirroring::OneScreenHigh,
|
||||
_ => self.mirroring_default,
|
||||
}
|
||||
}
|
||||
|
||||
fn clock_cpu(&mut self, cycles: u8) {
|
||||
vrc_irq_clock(cycles, self.irq_state());
|
||||
}
|
||||
|
||||
fn poll_irq(&mut self) -> bool {
|
||||
let out = self.irq_pending;
|
||||
self.irq_pending = false;
|
||||
out
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.push(u8::from(self.mapper26_wiring));
|
||||
out.push(self.prg_bank_16k);
|
||||
out.push(self.prg_bank_8k);
|
||||
out.extend_from_slice(&self.chr_banks_1k);
|
||||
out.push(self.control);
|
||||
out.push(self.irq_latch);
|
||||
out.push(self.irq_counter);
|
||||
out.push(u8::from(self.irq_enabled));
|
||||
out.push(u8::from(self.irq_enabled_after_ack));
|
||||
out.push(u8::from(self.irq_mode_cpu));
|
||||
out.push(u8::from(self.irq_pending));
|
||||
out.extend_from_slice(&self.irq_prescaler.to_le_bytes());
|
||||
write_state_bytes(out, &self.prg_ram);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 1 + 1 + 1 + 8 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 2 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
let mut cursor = 0usize;
|
||||
self.mapper26_wiring = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.prg_bank_16k = data[cursor];
|
||||
cursor += 1;
|
||||
self.prg_bank_8k = data[cursor];
|
||||
cursor += 1;
|
||||
self.chr_banks_1k.copy_from_slice(&data[cursor..cursor + 8]);
|
||||
cursor += 8;
|
||||
self.control = data[cursor];
|
||||
cursor += 1;
|
||||
self.irq_latch = data[cursor];
|
||||
cursor += 1;
|
||||
self.irq_counter = data[cursor];
|
||||
cursor += 1;
|
||||
self.irq_enabled = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.irq_enabled_after_ack = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.irq_mode_cpu = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.irq_pending = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
self.irq_prescaler = i16::from_le_bytes([data[cursor], data[cursor + 1]]);
|
||||
cursor += 2;
|
||||
|
||||
let prg_ram = read_state_bytes(data, &mut cursor)?;
|
||||
if prg_ram.len() != self.prg_ram.len() {
|
||||
return Err("mapper state does not match loaded ROM".to_string());
|
||||
}
|
||||
self.prg_ram.copy_from_slice(prg_ram);
|
||||
|
||||
load_chr_state(&mut self.chr_data, &data[cursor..])
|
||||
}
|
||||
}
|
||||
309
src/native_core/mapper/mappers/vrc/vrc7_85.rs
Normal file
309
src/native_core/mapper/mappers/vrc/vrc7_85.rs
Normal file
@@ -0,0 +1,309 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) struct Vrc7_85 {
|
||||
prg_rom: Vec<u8>,
|
||||
chr_data: Vec<u8>,
|
||||
chr_is_ram: bool,
|
||||
submapper: u8,
|
||||
mirroring_default: Mirroring,
|
||||
prg_ram: Vec<u8>,
|
||||
prg_ram_enabled: bool,
|
||||
prg_banks_8k: [u8; 3],
|
||||
chr_banks_1k: [u8; 8],
|
||||
irq_latch: u8,
|
||||
irq_counter: u8,
|
||||
irq_enabled: bool,
|
||||
irq_enabled_after_ack: bool,
|
||||
irq_mode_cpu: bool,
|
||||
irq_pending: bool,
|
||||
irq_prescaler: i16,
|
||||
}
|
||||
|
||||
impl Vrc7_85 {
|
||||
pub(crate) fn new(rom: InesRom) -> Self {
|
||||
let mut chr_banks_1k = [0u8; 8];
|
||||
for (i, bank) in chr_banks_1k.iter_mut().enumerate() {
|
||||
*bank = i as u8;
|
||||
}
|
||||
Self {
|
||||
prg_rom: rom.prg_rom,
|
||||
chr_data: rom.chr_data,
|
||||
chr_is_ram: rom.chr_is_ram,
|
||||
submapper: rom.header.submapper,
|
||||
mirroring_default: rom.header.mirroring,
|
||||
prg_ram: vec![0; PRG_RAM_8K],
|
||||
prg_ram_enabled: false,
|
||||
prg_banks_8k: [0, 1, 2],
|
||||
chr_banks_1k,
|
||||
irq_latch: 0,
|
||||
irq_counter: 0,
|
||||
irq_enabled: false,
|
||||
irq_enabled_after_ack: false,
|
||||
irq_mode_cpu: false,
|
||||
irq_pending: false,
|
||||
irq_prescaler: 341,
|
||||
}
|
||||
}
|
||||
|
||||
fn prg_bank_count_8k(&self) -> usize {
|
||||
(self.prg_rom.len() / PRG_BANK_8K).max(1)
|
||||
}
|
||||
|
||||
fn chr_bank_count_1k(&self) -> usize {
|
||||
(self.chr_data.len() / CHR_BANK_1K).max(1)
|
||||
}
|
||||
|
||||
fn is_secondary_addr(&self, addr: u16) -> bool {
|
||||
let a3 = (addr & 0x0008) != 0;
|
||||
let a4 = (addr & 0x0010) != 0;
|
||||
match self.submapper {
|
||||
1 => a3,
|
||||
2 => a4,
|
||||
_ => a3 || a4,
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_chr_reg(&self, addr: u16) -> Option<usize> {
|
||||
let group = ((addr >> 12) & 0x0F) as usize;
|
||||
if !(0x0A..=0x0D).contains(&group) {
|
||||
return None;
|
||||
}
|
||||
let base = (group - 0x0A) * 2;
|
||||
let odd = usize::from(self.is_secondary_addr(addr));
|
||||
Some(base + odd)
|
||||
}
|
||||
|
||||
fn load_native_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
let min_len = PRG_RAM_8K
|
||||
+ 1 // submapper
|
||||
+ 1 // prg_ram_enabled
|
||||
+ 3 // prg banks
|
||||
+ 8 // chr banks
|
||||
+ 1 // mirroring
|
||||
+ 1 // irq_latch
|
||||
+ 1 // irq_counter
|
||||
+ 1 // irq_enabled
|
||||
+ 1 // irq_enabled_after_ack
|
||||
+ 1 // irq_mode_cpu
|
||||
+ 1 // irq_pending
|
||||
+ 2 // irq_prescaler
|
||||
+ 4; // chr blob length
|
||||
if data.len() < min_len {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
|
||||
let mut cursor = 0usize;
|
||||
let mut prg_ram = vec![0u8; PRG_RAM_8K];
|
||||
prg_ram.copy_from_slice(&data[cursor..cursor + PRG_RAM_8K]);
|
||||
cursor += PRG_RAM_8K;
|
||||
|
||||
let submapper = data[cursor];
|
||||
cursor += 1;
|
||||
let prg_ram_enabled = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
|
||||
let mut prg_banks_8k = [0u8; 3];
|
||||
prg_banks_8k.copy_from_slice(&data[cursor..cursor + 3]);
|
||||
cursor += 3;
|
||||
|
||||
let mut chr_banks_1k = [0u8; 8];
|
||||
chr_banks_1k.copy_from_slice(&data[cursor..cursor + 8]);
|
||||
cursor += 8;
|
||||
|
||||
let mirroring_default = decode_mirroring(data[cursor]);
|
||||
cursor += 1;
|
||||
let irq_latch = data[cursor];
|
||||
cursor += 1;
|
||||
let irq_counter = data[cursor];
|
||||
cursor += 1;
|
||||
let irq_enabled = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
let irq_enabled_after_ack = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
let irq_mode_cpu = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
let irq_pending = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
let irq_prescaler = i16::from_le_bytes([data[cursor], data[cursor + 1]]);
|
||||
cursor += 2;
|
||||
|
||||
let mut chr_data = self.chr_data.clone();
|
||||
load_chr_state(&mut chr_data, &data[cursor..])?;
|
||||
|
||||
self.prg_ram = prg_ram;
|
||||
self.submapper = submapper;
|
||||
self.prg_ram_enabled = prg_ram_enabled;
|
||||
self.prg_banks_8k = prg_banks_8k;
|
||||
self.chr_banks_1k = chr_banks_1k;
|
||||
self.mirroring_default = mirroring_default;
|
||||
self.irq_latch = irq_latch;
|
||||
self.irq_counter = irq_counter;
|
||||
self.irq_enabled = irq_enabled;
|
||||
self.irq_enabled_after_ack = irq_enabled_after_ack;
|
||||
self.irq_mode_cpu = irq_mode_cpu;
|
||||
self.irq_pending = irq_pending;
|
||||
self.irq_prescaler = irq_prescaler;
|
||||
self.chr_data = chr_data;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn irq_state(&mut self) -> VrcIrqRegisters<'_> {
|
||||
VrcIrqRegisters {
|
||||
latch: &mut self.irq_latch,
|
||||
counter: &mut self.irq_counter,
|
||||
enabled: &mut self.irq_enabled,
|
||||
enabled_after_ack: &mut self.irq_enabled_after_ack,
|
||||
mode_cpu: &mut self.irq_mode_cpu,
|
||||
pending: &mut self.irq_pending,
|
||||
prescaler: &mut self.irq_prescaler,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mapper for Vrc7_85 {
|
||||
fn cpu_read(&self, addr: u16) -> u8 {
|
||||
match addr {
|
||||
0x6000..=0x7FFF => {
|
||||
if self.prg_ram_enabled {
|
||||
self.prg_ram[(addr as usize) - 0x6000]
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
0x8000..=0xFFFF => {
|
||||
let slot = ((addr - 0x8000) / 0x2000) as usize;
|
||||
let last = self.prg_bank_count_8k().saturating_sub(1);
|
||||
let bank = match slot {
|
||||
0 => self.prg_banks_8k[0] as usize,
|
||||
1 => self.prg_banks_8k[1] as usize,
|
||||
2 => self.prg_banks_8k[2] as usize,
|
||||
_ => last,
|
||||
};
|
||||
read_bank(
|
||||
&self.prg_rom,
|
||||
PRG_BANK_8K,
|
||||
bank,
|
||||
((addr as usize) - 0x8000) & (PRG_BANK_8K - 1),
|
||||
)
|
||||
}
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||
match addr {
|
||||
0x6000..=0x7FFF => {
|
||||
if self.prg_ram_enabled {
|
||||
self.prg_ram[(addr as usize) - 0x6000] = value;
|
||||
}
|
||||
}
|
||||
0x8000..=0xFFFF => {
|
||||
let masked = addr & 0xF018;
|
||||
match masked {
|
||||
0x8000 => self.prg_banks_8k[0] = value & 0x3F,
|
||||
0x9000 => self.prg_banks_8k[2] = value & 0x3F,
|
||||
0x8008 | 0x8010 | 0x8018 => self.prg_banks_8k[1] = value & 0x3F,
|
||||
0xA000 | 0xA008 | 0xA010 | 0xA018 | 0xB000 | 0xB008 | 0xB010 | 0xB018
|
||||
| 0xC000 | 0xC008 | 0xC010 | 0xC018 | 0xD000 | 0xD008 | 0xD010 | 0xD018 => {
|
||||
if let Some(reg) = self.decode_chr_reg(addr) {
|
||||
self.chr_banks_1k[reg] = value;
|
||||
}
|
||||
}
|
||||
0xE000 => {
|
||||
self.prg_ram_enabled = (value & 0x80) != 0;
|
||||
self.mirroring_default = match value & 0x03 {
|
||||
0 => Mirroring::Vertical,
|
||||
1 => Mirroring::Horizontal,
|
||||
2 => Mirroring::OneScreenLow,
|
||||
_ => Mirroring::OneScreenHigh,
|
||||
};
|
||||
}
|
||||
0xE008 | 0xE010 | 0xE018 => self.irq_latch = value,
|
||||
0xF000 => vrc_irq_write_control(value, self.irq_state()),
|
||||
0xF008 | 0xF010 | 0xF018 => vrc_irq_ack(self.irq_state()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
if (0x6000..=0x7FFF).contains(&addr) && self.prg_ram_enabled {
|
||||
Some(self.prg_ram[(addr as usize) - 0x6000])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||
if (0x6000..=0x7FFF).contains(&addr) {
|
||||
if self.prg_ram_enabled {
|
||||
self.prg_ram[(addr as usize) - 0x6000] = value;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn ppu_read(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
let page = (addr / CHR_BANK_1K as u16) as usize;
|
||||
let bank = safe_mod(self.chr_banks_1k[page] as usize, self.chr_bank_count_1k());
|
||||
read_bank(
|
||||
&self.chr_data,
|
||||
CHR_BANK_1K,
|
||||
bank,
|
||||
(addr as usize) & (CHR_BANK_1K - 1),
|
||||
)
|
||||
}
|
||||
|
||||
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||
if !self.chr_is_ram || addr > 0x1FFF {
|
||||
return;
|
||||
}
|
||||
let page = (addr / CHR_BANK_1K as u16) as usize;
|
||||
let bank = safe_mod(self.chr_banks_1k[page] as usize, self.chr_bank_count_1k());
|
||||
let idx = bank * CHR_BANK_1K + (addr as usize & (CHR_BANK_1K - 1));
|
||||
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||
*cell = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn mirroring(&self) -> Mirroring {
|
||||
self.mirroring_default
|
||||
}
|
||||
|
||||
fn clock_cpu(&mut self, cycles: u8) {
|
||||
vrc_irq_clock(cycles, self.irq_state());
|
||||
}
|
||||
|
||||
fn poll_irq(&mut self) -> bool {
|
||||
let out = self.irq_pending;
|
||||
self.irq_pending = false;
|
||||
out
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(&self.prg_ram);
|
||||
out.push(self.submapper);
|
||||
out.push(u8::from(self.prg_ram_enabled));
|
||||
out.extend_from_slice(&self.prg_banks_8k);
|
||||
out.extend_from_slice(&self.chr_banks_1k);
|
||||
out.push(encode_mirroring(self.mirroring_default));
|
||||
out.push(self.irq_latch);
|
||||
out.push(self.irq_counter);
|
||||
out.push(u8::from(self.irq_enabled));
|
||||
out.push(u8::from(self.irq_enabled_after_ack));
|
||||
out.push(u8::from(self.irq_mode_cpu));
|
||||
out.push(u8::from(self.irq_pending));
|
||||
out.extend_from_slice(&self.irq_prescaler.to_le_bytes());
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
self.load_native_state(data)
|
||||
}
|
||||
}
|
||||
70
src/native_core/mapper/mod.rs
Normal file
70
src/native_core/mapper/mod.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
mod core;
|
||||
mod mappers;
|
||||
mod types;
|
||||
|
||||
pub use core::Mapper;
|
||||
|
||||
use crate::native_core::ines::InesRom;
|
||||
use mappers::*;
|
||||
|
||||
pub fn create_mapper(rom: InesRom) -> Result<Box<dyn Mapper + Send>, String> {
|
||||
match rom.header.mapper {
|
||||
0 => Ok(Box::new(Nrom::new(rom))),
|
||||
1 => Ok(Box::new(Mmc1::new(rom))),
|
||||
2 => Ok(Box::new(Uxrom::new(rom))),
|
||||
3 => Ok(Box::new(Cnrom::new(rom))),
|
||||
4 => Ok(Box::new(Mmc3::new(rom))),
|
||||
5 => Ok(Box::new(Mmc5::new(rom))),
|
||||
7 => Ok(Box::new(Axrom::new(rom))),
|
||||
9 => Ok(Box::new(Mmc2::new(rom))),
|
||||
10 => Ok(Box::new(Mmc4::new(rom))),
|
||||
11 => Ok(Box::new(ColorDreams11::new(rom))),
|
||||
13 => Ok(Box::new(Cprom13::new(rom))),
|
||||
19 => Ok(Box::new(Namco163_19::new(rom))),
|
||||
21 => Ok(Box::new(Vrc2_23::new(rom))),
|
||||
22 => Ok(Box::new(Vrc2_23::new_with_submapper(rom, 2))),
|
||||
23 => Ok(Box::new(Vrc2_23::new(rom))),
|
||||
24 => Ok(Box::new(Vrc6_24::new(rom, false))),
|
||||
25 => Ok(Box::new(Vrc2_23::new(rom))),
|
||||
26 => Ok(Box::new(Vrc6_24::new(rom, true))),
|
||||
30 => Ok(Box::new(Unrom512_30::new(rom))),
|
||||
66 => Ok(Box::new(Gxrom::new(rom))),
|
||||
69 => Ok(Box::new(Fme7::new(rom))),
|
||||
70 => Ok(Box::new(Bandai70_152::new(rom))),
|
||||
71 => Ok(Box::new(Camerica71::new(rom))),
|
||||
78 => Ok(Box::new(InesMapper78::new(rom))),
|
||||
79 => Ok(Box::new(Nina79::new(rom))),
|
||||
85 => Ok(Box::new(Vrc7_85::new(rom))),
|
||||
87 => Ok(Box::new(InesMapper87::new(rom))),
|
||||
88 => Ok(Box::new(InesMapper88::new(rom))),
|
||||
89 => Ok(Box::new(Bandai70_152::new(rom))),
|
||||
93 => Ok(Box::new(InesMapper93::new(rom))),
|
||||
94 => Ok(Box::new(Un1rom94::new(rom))),
|
||||
95 => Ok(Box::new(InesMapper95::new(rom))),
|
||||
105 => Ok(Box::new(InesMapper105::new(rom))),
|
||||
140 => Ok(Box::new(InesMapper140::new(rom))),
|
||||
155 => Ok(Box::new(InesMapper155::new(rom))),
|
||||
75 => Ok(Box::new(Vrc1_75::new(rom))),
|
||||
118 => Ok(Box::new(InesMapper118::new(rom))), // TxSROM / TLSROM / TKSROM
|
||||
119 => Ok(Box::new(Tqrom119::new(rom))),
|
||||
152 => Ok(Box::new(Bandai70_152::new(rom))),
|
||||
34 => Ok(Box::new(Bnrom34::new(rom))),
|
||||
158 => Ok(Box::new(InesMapper158::new(rom))),
|
||||
64 => Ok(Box::new(InesMapper64::new(rom))),
|
||||
180 => Ok(Box::new(CrazyClimber180::new(rom))),
|
||||
184 => Ok(Box::new(InesMapper184::new(rom))),
|
||||
185 => Ok(Box::new(InesMapper185::new(rom))),
|
||||
206 => {
|
||||
if rom.header.submapper == 1 {
|
||||
Ok(Box::new(InesMapper206Submapper1::new(rom)))
|
||||
} else {
|
||||
Ok(Box::new(InesMapper206::new(rom)))
|
||||
}
|
||||
}
|
||||
253 => Ok(Box::new(InesMapper253::new(rom))),
|
||||
mapper => Err(format!("unsupported mapper: {mapper}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
32
src/native_core/mapper/tests.rs
Normal file
32
src/native_core/mapper/tests.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use super::{InesMapper105, Mapper, create_mapper};
|
||||
use crate::native_core::ines::{InesRom, Mirroring};
|
||||
use crate::native_core::test_support::MapperRomBuilder;
|
||||
|
||||
fn test_rom(mapper: u16, prg_banks_16k: u16, chr_banks_8k: u16) -> InesRom {
|
||||
MapperRomBuilder::new(mapper)
|
||||
.prg_banks_16k(prg_banks_16k)
|
||||
.chr_banks_8k(chr_banks_8k)
|
||||
.build()
|
||||
}
|
||||
|
||||
fn test_rom_with_submapper(
|
||||
mapper: u16,
|
||||
submapper: u8,
|
||||
prg_banks_16k: u16,
|
||||
chr_banks_8k: u16,
|
||||
) -> InesRom {
|
||||
MapperRomBuilder::new(mapper)
|
||||
.submapper(submapper)
|
||||
.prg_banks_16k(prg_banks_16k)
|
||||
.chr_banks_8k(chr_banks_8k)
|
||||
.mirroring(Mirroring::Horizontal)
|
||||
.build()
|
||||
}
|
||||
|
||||
mod basic_bank_switch;
|
||||
mod chr_ram_and_conflicts;
|
||||
mod mmc1_105_155_5_19;
|
||||
mod mmc3_vrc_core;
|
||||
mod property_invariants;
|
||||
mod rambo_dxrom_others;
|
||||
mod vrc_variants_mmc2_4;
|
||||
139
src/native_core/mapper/tests/basic_bank_switch/discrete.rs
Normal file
139
src/native_core/mapper/tests/basic_bank_switch/discrete.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
use super::*;
|
||||
#[test]
|
||||
fn mapper11_switches_prg_and_chr_banks() {
|
||||
let mut rom = test_rom(11, 8, 8);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.chr_data.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x10; // PRG bank 0
|
||||
rom.prg_rom[0x0001] = 0x32; // bus-conflict source for write at $8001
|
||||
rom.prg_rom[0x10000] = 0x20; // PRG bank 2 (2 * 32K)
|
||||
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||
rom.chr_data[0x6000] = 0x07; // CHR bank 3 (3 * 8K)
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||
mapper.cpu_write(0x8001, 0x32); // PRG=2, CHR=3 with bus-conflict-safe source byte
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x20);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x07);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper11_applies_bus_conflicts_when_latching_bank_bits() {
|
||||
let mut rom = test_rom(11, 8, 8);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.chr_data.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x11; // $8000 currently reads 0x11
|
||||
rom.prg_rom[0x8000] = 0x20; // PRG bank 1
|
||||
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||
rom.chr_data[0x2000] = 0x02; // CHR bank 1
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x33); // 0x33 & 0x11 => 0x11
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x20);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x02);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper71_switches_lower_prg_bank() {
|
||||
let mut rom = test_rom(71, 8, 0);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x11; // bank 0
|
||||
rom.prg_rom[0x8000] = 0x22; // bank 2
|
||||
rom.prg_rom[0x1C000] = 0xFF; // last bank fixed
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x11);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0xFF);
|
||||
mapper.cpu_write(0xC000, 0x02);
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x22);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0xFF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper71_submapper0_ignores_9000_mirroring_control() {
|
||||
let mut rom = test_rom_with_submapper(71, 0, 8, 1);
|
||||
rom.header.mirroring = Mirroring::Vertical;
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.mirroring(), Mirroring::Vertical);
|
||||
mapper.cpu_write(0x9000, 0x10);
|
||||
assert_eq!(mapper.mirroring(), Mirroring::Vertical);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper71_submapper1_uses_single_screen_mirroring_control() {
|
||||
let rom = test_rom_with_submapper(71, 1, 8, 1);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.mirroring(), Mirroring::OneScreenLow);
|
||||
mapper.cpu_write(0x9000, 0x10);
|
||||
assert_eq!(mapper.mirroring(), Mirroring::OneScreenHigh);
|
||||
mapper.cpu_write(0x9000, 0x00);
|
||||
assert_eq!(mapper.mirroring(), Mirroring::OneScreenLow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper94_uses_shifted_bank_value() {
|
||||
let mut rom = test_rom(94, 8, 0);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x10; // bank 0
|
||||
rom.prg_rom[0xC000] = 0x40; // bank 3
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||
mapper.cpu_write(0x8000, 0x0C); // 0x0C >> 2 = 3
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x40);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper180_switches_upper_prg_bank() {
|
||||
let mut rom = test_rom(180, 8, 0);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x11; // fixed first bank
|
||||
rom.prg_rom[0x8000] = 0x33; // bank 2 at upper window
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x11);
|
||||
mapper.cpu_write(0x8000, 0x02);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0x33);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper34_switches_32k_prg_bank() {
|
||||
let mut rom = test_rom(34, 8, 0);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x11; // 32K bank 0
|
||||
rom.prg_rom[0x10000] = 0x44; // 32K bank 2
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x11);
|
||||
mapper.cpu_write(0x8000, 0x02);
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x44);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper34_supports_nina001_low_registers() {
|
||||
let mut rom = test_rom(34, 8, 8);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.chr_data.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x10; // PRG bank 0 (32K)
|
||||
rom.prg_rom[0x8000] = 0x20; // PRG bank 1 (32K)
|
||||
rom.chr_data[0x0000] = 0x01; // CHR 4K bank 0
|
||||
rom.chr_data[0x1000] = 0x02; // CHR 4K bank 1
|
||||
rom.chr_data[0x3000] = 0x04; // CHR 4K bank 3
|
||||
rom.chr_data[0x4000] = 0x05; // CHR 4K bank 4
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||
assert_eq!(mapper.ppu_read(0x1000), 0x02);
|
||||
|
||||
assert!(mapper.cpu_write_low(0x7FFD, 0x01)); // PRG bank
|
||||
assert!(mapper.cpu_write_low(0x7FFE, 0x03)); // CHR low 4K
|
||||
assert!(mapper.cpu_write_low(0x7FFF, 0x04)); // CHR high 4K
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x20);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x04);
|
||||
assert_eq!(mapper.ppu_read(0x1000), 0x05);
|
||||
}
|
||||
46
src/native_core/mapper/tests/basic_bank_switch/mapper78.rs
Normal file
46
src/native_core/mapper/tests/basic_bank_switch/mapper78.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use super::*;
|
||||
#[test]
|
||||
fn mapper78_switches_prg_chr_and_mirroring() {
|
||||
let mut rom = test_rom(78, 8, 8);
|
||||
rom.prg_rom.fill(0xFF);
|
||||
rom.chr_data.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x10; // PRG bank 0 lower window
|
||||
rom.prg_rom[0x8000] = 0x20; // PRG bank 2 lower window
|
||||
rom.prg_rom[0x1C000] = 0xFE; // fixed last bank
|
||||
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||
rom.chr_data[0x6000] = 0x07; // CHR bank 3
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0xFE);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||
mapper.cpu_write(0x8001, 0x3A); // avoid bus conflict masking at $8000 byte
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x20);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x07);
|
||||
assert_eq!(mapper.mirroring(), Mirroring::OneScreenHigh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper78_submapper3_uses_horizontal_vertical_mirroring() {
|
||||
let mut rom = test_rom_with_submapper(78, 3, 8, 2);
|
||||
rom.prg_rom.fill(0xFF);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x00);
|
||||
assert_eq!(mapper.mirroring(), Mirroring::Horizontal);
|
||||
mapper.cpu_write(0x8000, 0x08);
|
||||
assert_eq!(mapper.mirroring(), Mirroring::Vertical);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper78_applies_bus_conflicts() {
|
||||
let mut rom = test_rom_with_submapper(78, 1, 8, 2);
|
||||
rom.prg_rom.fill(0xFF);
|
||||
// At $8000 lower PRG bank maps to byte 0x07, so write $08 becomes $00.
|
||||
rom.prg_rom[0x0000] = 0x07;
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x08);
|
||||
assert_eq!(mapper.mirroring(), Mirroring::OneScreenLow);
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x07);
|
||||
}
|
||||
5
src/native_core/mapper/tests/basic_bank_switch/mod.rs
Normal file
5
src/native_core/mapper/tests/basic_bank_switch/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use super::*;
|
||||
|
||||
mod discrete;
|
||||
mod mapper78;
|
||||
mod smoke_and_fme7;
|
||||
138
src/native_core/mapper/tests/basic_bank_switch/smoke_and_fme7.rs
Normal file
138
src/native_core/mapper/tests/basic_bank_switch/smoke_and_fme7.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
use super::*;
|
||||
#[test]
|
||||
fn supports_known_mapper_ids() {
|
||||
for mapper in [
|
||||
0u16, 1, 2, 3, 4, 5, 7, 9, 10, 11, 13, 19, 21, 22, 23, 24, 25, 26, 30, 34, 64, 66, 69, 70,
|
||||
71, 75, 78, 79, 85, 87, 88, 89, 93, 94, 95, 105, 118, 119, 140, 152, 155, 158, 180, 184,
|
||||
185, 206, 253,
|
||||
] {
|
||||
let rom = test_rom(mapper, 8, 4);
|
||||
let m = create_mapper(rom).expect("mapper should be created");
|
||||
let _ = m.cpu_read(0x8000);
|
||||
let _ = m.ppu_read(0x0000);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nrom_state_roundtrip() {
|
||||
let rom = test_rom(0, 1, 0);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
mapper.ppu_write(0x0010, 0xAB);
|
||||
|
||||
let mut saved = Vec::new();
|
||||
mapper.save_state(&mut saved);
|
||||
|
||||
let rom2 = test_rom(0, 1, 0);
|
||||
let mut restored = create_mapper(rom2).expect("must create mapper");
|
||||
restored.load_state(&saved).expect("state must load");
|
||||
assert_eq!(restored.ppu_read(0x0010), 0xAB);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nrom_16k_prg_mirrors_into_upper_half() {
|
||||
let mut rom = test_rom(0, 1, 1);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.prg_rom[0x0000] = 0xA1;
|
||||
rom.prg_rom[0x3FFF] = 0xB2;
|
||||
let mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0xA1);
|
||||
assert_eq!(mapper.cpu_read(0xBFFF), 0xB2);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0xA1);
|
||||
assert_eq!(mapper.cpu_read(0xFFFF), 0xB2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nrom_low_window_is_not_backend_prg_ram() {
|
||||
let rom = test_rom(0, 2, 1);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), None);
|
||||
assert!(!mapper.cpu_write_low(0x6000, 0xAB));
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uxrom_switches_bank() {
|
||||
let mut rom = test_rom(2, 4, 1);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x10;
|
||||
rom.prg_rom[0x4000] = 0x20;
|
||||
rom.prg_rom[0x8000] = 0x30;
|
||||
rom.prg_rom[0xC000] = 0x40;
|
||||
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0x40);
|
||||
|
||||
mapper.cpu_write(0x8000, 2);
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_mapper() {
|
||||
let rom = test_rom(255, 1, 1);
|
||||
assert!(create_mapper(rom).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fme7_accepts_command_and_data_across_port_ranges() {
|
||||
let mut rom = test_rom(69, 8, 1);
|
||||
for bank in 0..8usize {
|
||||
rom.chr_data[bank * 0x400] = bank as u8;
|
||||
}
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x9FFF, 0x00);
|
||||
mapper.cpu_write(0xB123, 0x03);
|
||||
|
||||
assert_eq!(mapper.ppu_read(0x0000), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fme7_chr_ram_write_uses_selected_bank() {
|
||||
let rom = test_rom(69, 8, 0);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x00);
|
||||
mapper.cpu_write(0xA000, 0x01);
|
||||
mapper.ppu_write(0x0012, 0xAA);
|
||||
|
||||
mapper.cpu_write(0x8000, 0x00);
|
||||
mapper.cpu_write(0xA000, 0x00);
|
||||
assert_eq!(mapper.ppu_read(0x0012), 0x00);
|
||||
|
||||
mapper.cpu_write(0x8000, 0x00);
|
||||
mapper.cpu_write(0xA000, 0x01);
|
||||
assert_eq!(mapper.ppu_read(0x0012), 0xAA);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fme7_low_window_defaults_to_prg_rom_when_ram_disabled() {
|
||||
let mut rom = test_rom(69, 8, 1);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x11; // 8K bank 0 @ $6000 when RAM disabled
|
||||
rom.prg_rom[0x2000] = 0x22; // 8K bank 1
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), Some(0x11));
|
||||
mapper.cpu_write(0x8000, 0x08);
|
||||
mapper.cpu_write(0xA000, 0x01); // RAM disabled, select ROM bank 1
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), Some(0x22));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fme7_low_window_uses_ram_only_when_enabled() {
|
||||
let mut rom = test_rom(69, 8, 1);
|
||||
rom.prg_rom.fill(0x33);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x08);
|
||||
mapper.cpu_write(0xA000, 0xC0); // Select RAM + enable RAM in $6000-$7FFF
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), Some(0x00));
|
||||
assert!(mapper.cpu_write_low(0x6123, 0xA5));
|
||||
assert_eq!(mapper.cpu_read_low(0x6123), Some(0xA5));
|
||||
|
||||
mapper.cpu_write(0xA000, 0x00); // Disable RAM -> back to ROM view
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), Some(0x33));
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mapper3_allows_chr_ram_write_when_chr_is_ram() {
|
||||
let mut rom = test_rom(3, 2, 0);
|
||||
rom.chr_data = vec![0; 0x4000];
|
||||
rom.chr_is_ram = true;
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x01);
|
||||
mapper.ppu_write(0x0012, 0xA5);
|
||||
assert_eq!(mapper.ppu_read(0x0012), 0xA5);
|
||||
mapper.cpu_write(0x8000, 0x00);
|
||||
assert_eq!(mapper.ppu_read(0x0012), 0x00);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper3_submapper2_applies_and_bus_conflicts() {
|
||||
let mut rom = test_rom_with_submapper(3, 2, 2, 4);
|
||||
rom.prg_rom.fill(0xFF);
|
||||
rom.prg_rom[0x0000] = 0x01; // $8000 bus value used for conflict mask
|
||||
rom.chr_data.fill(0);
|
||||
rom.chr_data[0x0000] = 0x10; // CHR bank 0 marker
|
||||
rom.chr_data[0x2000] = 0x11; // CHR bank 1 marker
|
||||
rom.chr_data[0x4000] = 0x12; // CHR bank 2 marker
|
||||
rom.chr_data[0x6000] = 0x13; // CHR bank 3 marker
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x03); // bus conflict -> 0x03 & 0x01 = 0x01
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper7_submapper2_applies_and_bus_conflicts() {
|
||||
let mut rom = test_rom_with_submapper(7, 2, 8, 1);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x02; // CPU sees this byte at $8000 during write
|
||||
rom.prg_rom[0x10000] = 0xA2; // 32K bank 2 marker at $8000
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x01); // with conflicts latched=0x00 -> bank 0
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x02);
|
||||
|
||||
mapper.cpu_write(0x8000, 0x13); // with conflicts latched=0x02 -> bank 2, one-screen low
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0xA2);
|
||||
assert_eq!(mapper.mirroring(), Mirroring::OneScreenLow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper7_submapper1_has_no_bus_conflicts() {
|
||||
let mut rom = test_rom_with_submapper(7, 1, 8, 1);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x02; // if conflicts existed, bit0 would be masked off
|
||||
rom.prg_rom[0x8000] = 0xA1; // 32K bank 1 marker at $8000
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x01); // no conflicts -> bank 1 selected
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0xA1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper3_chr_ram_state_roundtrip_uses_strict_payload() {
|
||||
let mut rom = test_rom(3, 2, 0);
|
||||
rom.chr_data = vec![0; 0x4000];
|
||||
rom.chr_is_ram = true;
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x01);
|
||||
mapper.ppu_write(0x0020, 0xA6);
|
||||
|
||||
let mut state = Vec::new();
|
||||
mapper.save_state(&mut state);
|
||||
assert!(
|
||||
state.len() > 8,
|
||||
"mapper 3 state must include CHR payload blob"
|
||||
);
|
||||
|
||||
let mut rom2 = test_rom(3, 2, 0);
|
||||
rom2.chr_data = vec![0; 0x4000];
|
||||
rom2.chr_is_ram = true;
|
||||
let mut restored = create_mapper(rom2).expect("must create mapper");
|
||||
restored.load_state(&state).expect("state load");
|
||||
assert_eq!(restored.ppu_read(0x0020), 0xA6);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mapper75_switches_prg_chr_and_mirroring() {
|
||||
let mut rom = test_rom(75, 16, 8);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.chr_data.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x10; // PRG bank 0
|
||||
rom.prg_rom[0x2000] = 0x11; // PRG bank 1
|
||||
rom.prg_rom[0x6000] = 0x13; // PRG bank 3
|
||||
rom.prg_rom[0x3E000] = 0x1F; // fixed last bank
|
||||
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||
rom.chr_data[0x2000] = 0x03; // CHR bank 2
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||
mapper.cpu_write(0x8000, 0x01);
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x11);
|
||||
mapper.cpu_write(0xC000, 0x03);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0x13);
|
||||
assert_eq!(mapper.cpu_read(0xE000), 0x1F);
|
||||
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||
mapper.cpu_write(0xE000, 0x02);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x03);
|
||||
|
||||
mapper.cpu_write(0x9000, 0x01);
|
||||
assert_eq!(mapper.mirroring(), Mirroring::Horizontal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper140_selects_prg_chr_from_low_window_write() {
|
||||
let mut rom = test_rom(140, 8, 16);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.chr_data.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x10; // 32K bank 0
|
||||
rom.prg_rom[0x8000] = 0x20; // 32K bank 1
|
||||
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||
rom.chr_data[0x6000] = 0x04; // CHR bank 3
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||
assert!(mapper.cpu_write_low(0x6000, 0x13)); // PRG=1 CHR=3
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x20);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x04);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper184_selects_two_4k_chr_banks_from_low_window_write() {
|
||||
let mut rom = test_rom(184, 2, 16);
|
||||
rom.chr_data.fill(0);
|
||||
rom.chr_data[0x0000] = 0x11; // bank 0
|
||||
rom.chr_data[0x5000] = 0x55; // bank 5
|
||||
rom.chr_data[0x6000] = 0x66; // bank 6
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert!(mapper.cpu_write_low(0x6000, 0x65)); // low=5, high=(6|4)=10
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x55);
|
||||
assert_eq!(mapper.ppu_read(0x1000), 0x66);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper79_switches_prg_and_chr_from_low_window_write() {
|
||||
let mut rom = test_rom(79, 4, 8);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.chr_data.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x11; // PRG 32K bank 0
|
||||
rom.prg_rom[0x8000] = 0x22; // PRG 32K bank 1
|
||||
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||
rom.chr_data[0x6000] = 0x07; // CHR bank 3
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x11);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||
assert!(mapper.cpu_write_low(0x4100, 0x0B)); // PRG=1 CHR=3
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x22);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x07);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper79_only_accepts_masked_low_register_addresses() {
|
||||
let mut rom = test_rom(79, 4, 8);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.chr_data.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x11; // PRG 32K bank 0
|
||||
rom.prg_rom[0x8000] = 0x22; // PRG 32K bank 1
|
||||
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||
rom.chr_data[0x6000] = 0x07; // CHR bank 3
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert!(!mapper.cpu_write_low(0x4200, 0x0B)); // A8=0 -> not a register
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x11);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||
|
||||
assert!(!mapper.cpu_write_low(0x6100, 0x0B)); // outside $4xxx/$5xxx
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x11);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||
|
||||
assert!(mapper.cpu_write_low(0x5F00, 0x0B)); // valid masked register
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x22);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x07);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper79_chr_ram_write_and_state_roundtrip() {
|
||||
let mut rom = test_rom(79, 4, 0);
|
||||
rom.chr_data = vec![0; 0x8000];
|
||||
rom.chr_is_ram = true;
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert!(mapper.cpu_write_low(0x4100, 0x09)); // PRG=1 CHR=1
|
||||
mapper.ppu_write(0x0010, 0xA5);
|
||||
assert_eq!(mapper.ppu_read(0x0010), 0xA5);
|
||||
|
||||
let mut state = Vec::new();
|
||||
mapper.save_state(&mut state);
|
||||
let mut rom2 = test_rom(79, 4, 0);
|
||||
rom2.chr_data = vec![0; 0x8000];
|
||||
rom2.chr_is_ram = true;
|
||||
let mut restored = create_mapper(rom2).expect("must create mapper");
|
||||
restored.load_state(&state).expect("state load");
|
||||
assert!(restored.cpu_write_low(0x4100, 0x09));
|
||||
assert_eq!(restored.ppu_read(0x0010), 0xA5);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
use super::*;
|
||||
#[test]
|
||||
fn mapper78_chr_ram_state_roundtrip() {
|
||||
let mut rom = test_rom_with_submapper(78, 1, 4, 0);
|
||||
rom.prg_rom.fill(0xFF);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x10); // CHR bank 1
|
||||
mapper.ppu_write(0x0123, 0x5A);
|
||||
assert_eq!(mapper.ppu_read(0x0123), 0x5A);
|
||||
|
||||
let mut state = Vec::new();
|
||||
mapper.save_state(&mut state);
|
||||
|
||||
let mut rom2 = test_rom_with_submapper(78, 1, 4, 0);
|
||||
rom2.prg_rom.fill(0xFF);
|
||||
let mut restored = create_mapper(rom2).expect("must create mapper");
|
||||
restored.load_state(&state).expect("state must load");
|
||||
assert_eq!(restored.ppu_read(0x0123), 0x5A);
|
||||
assert_eq!(restored.mirroring(), Mirroring::OneScreenLow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper87_switches_chr_bank() {
|
||||
let mut rom = test_rom(87, 2, 4);
|
||||
rom.chr_data.fill(0);
|
||||
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||
rom.chr_data[0x2000] = 0x02; // CHR bank 1
|
||||
rom.chr_data[0x4000] = 0x03; // CHR bank 2
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||
assert!(mapper.cpu_write_low(0x6000, 0x01)); // LH -> bank 2
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x03);
|
||||
assert!(mapper.cpu_write_low(0x6000, 0x02)); // LH -> bank 1
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x02);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper87_chr_ram_write_and_state_roundtrip() {
|
||||
let mut rom = test_rom(87, 2, 0);
|
||||
rom.chr_data = vec![0; 0x4000];
|
||||
rom.chr_is_ram = true;
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert!(mapper.cpu_write_low(0x6000, 0x02));
|
||||
mapper.ppu_write(0x0010, 0x7C);
|
||||
assert_eq!(mapper.ppu_read(0x0010), 0x7C);
|
||||
|
||||
let mut state = Vec::new();
|
||||
mapper.save_state(&mut state);
|
||||
let mut rom2 = test_rom(87, 2, 0);
|
||||
rom2.chr_data = vec![0; 0x4000];
|
||||
rom2.chr_is_ram = true;
|
||||
let mut restored = create_mapper(rom2).expect("must create mapper");
|
||||
restored.load_state(&state).expect("state load");
|
||||
assert!(restored.cpu_write_low(0x6000, 0x02));
|
||||
assert_eq!(restored.ppu_read(0x0010), 0x7C);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper87_ignores_cpu_write_high_window() {
|
||||
let mut rom = test_rom(87, 2, 4);
|
||||
rom.chr_data.fill(0);
|
||||
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||
rom.chr_data[0x2000] = 0x02; // CHR bank 1
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x01);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||
assert!(mapper.cpu_write_low(0x7000, 0x02));
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x02);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper93_switches_lower_prg_bank_using_high_nibble() {
|
||||
let mut rom = test_rom(93, 8, 0);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x10; // bank 0
|
||||
rom.prg_rom[0x8000] = 0x30; // bank 2
|
||||
rom.prg_rom[0x1C000] = 0xFF; // fixed last bank
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0xFF);
|
||||
mapper.cpu_write(0x8000, 0x20); // 0x2 in high nibble
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x30);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0xFF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper30_switches_prg_and_chr_ram_bank() {
|
||||
let mut rom = test_rom(30, 8, 0);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.chr_data.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x11; // PRG bank 0
|
||||
rom.prg_rom[0x8000] = 0x33; // PRG bank 2
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x11);
|
||||
mapper.cpu_write(0x8000, 0x22); // PRG=2, CHR=1
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x33);
|
||||
mapper.ppu_write(0x0010, 0xA5);
|
||||
assert_eq!(mapper.ppu_read(0x0010), 0xA5);
|
||||
mapper.cpu_write(0x8000, 0x02); // CHR bank 0
|
||||
assert_eq!(mapper.ppu_read(0x0010), 0x00);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper70_switches_prg_and_chr() {
|
||||
let mut rom = test_rom(70, 8, 8);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.chr_data.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x10; // PRG bank 0
|
||||
rom.prg_rom[0x8000] = 0x30; // PRG bank 2
|
||||
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||
rom.chr_data[0x4000] = 0x03; // CHR bank 2
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||
mapper.cpu_write(0x8000, 0x22); // CHR=2 PRG=2
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x30);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x03);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper13_switches_upper_4k_chr_ram_bank() {
|
||||
let mut rom = test_rom(13, 2, 0);
|
||||
rom.chr_data = vec![0; 0x4000];
|
||||
rom.chr_is_ram = true;
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.ppu_write(0x1000, 0xA1); // bank 0
|
||||
assert_eq!(mapper.ppu_read(0x1000), 0xA1);
|
||||
mapper.cpu_write(0x8000, 0x01);
|
||||
assert_eq!(mapper.ppu_read(0x1000), 0x00);
|
||||
mapper.ppu_write(0x1000, 0xB2); // bank 1
|
||||
assert_eq!(mapper.ppu_read(0x1000), 0xB2);
|
||||
mapper.cpu_write(0x8000, 0x00);
|
||||
assert_eq!(mapper.ppu_read(0x1000), 0xA1);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
use super::*;
|
||||
|
||||
mod mapper3_7;
|
||||
mod mapper79_and_friends;
|
||||
mod misc_chr_switch;
|
||||
222
src/native_core/mapper/tests/mmc1_105_155_5_19.rs
Normal file
222
src/native_core/mapper/tests/mmc1_105_155_5_19.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mmc1_control_mirroring_modes_match_hardware_layout() {
|
||||
let rom = test_rom(1, 2, 1);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
fn serial_write5(mapper: &mut Box<dyn super::Mapper + Send>, addr: u16, value: u8) {
|
||||
for i in 0..5 {
|
||||
mapper.cpu_write(addr, (value >> i) & 1);
|
||||
}
|
||||
}
|
||||
|
||||
serial_write5(&mut mapper, 0x8000, 0);
|
||||
assert_eq!(mapper.mirroring(), Mirroring::OneScreenLow);
|
||||
|
||||
serial_write5(&mut mapper, 0x8000, 1);
|
||||
assert_eq!(mapper.mirroring(), Mirroring::OneScreenHigh);
|
||||
|
||||
serial_write5(&mut mapper, 0x8000, 2);
|
||||
assert_eq!(mapper.mirroring(), Mirroring::Vertical);
|
||||
|
||||
serial_write5(&mut mapper, 0x8000, 3);
|
||||
assert_eq!(mapper.mirroring(), Mirroring::Horizontal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mmc1_chr_ram_write_respects_selected_4k_chr_banks() {
|
||||
let mut rom = test_rom(1, 2, 0);
|
||||
rom.chr_data = vec![0; 0x8000];
|
||||
rom.chr_is_ram = true;
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
fn serial_write5(mapper: &mut Box<dyn super::Mapper + Send>, addr: u16, value: u8) {
|
||||
for i in 0..5 {
|
||||
mapper.cpu_write(addr, (value >> i) & 1);
|
||||
}
|
||||
}
|
||||
|
||||
serial_write5(&mut mapper, 0x8000, 0x10); // CHR mode 1 (two 4KB banks)
|
||||
serial_write5(&mut mapper, 0xA000, 2); // $0000-$0FFF -> bank 2
|
||||
serial_write5(&mut mapper, 0xC000, 5); // $1000-$1FFF -> bank 5
|
||||
mapper.ppu_write(0x0123, 0xA1);
|
||||
mapper.ppu_write(0x1456, 0xB2);
|
||||
|
||||
serial_write5(&mut mapper, 0xA000, 3);
|
||||
serial_write5(&mut mapper, 0xC000, 6);
|
||||
assert_eq!(mapper.ppu_read(0x0123), 0x00);
|
||||
assert_eq!(mapper.ppu_read(0x1456), 0x00);
|
||||
|
||||
serial_write5(&mut mapper, 0xA000, 2);
|
||||
serial_write5(&mut mapper, 0xC000, 5);
|
||||
assert_eq!(mapper.ppu_read(0x0123), 0xA1);
|
||||
assert_eq!(mapper.ppu_read(0x1456), 0xB2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper105_requires_unlock_sequence_before_prg_switching() {
|
||||
let mut rom = test_rom(105, 16, 0);
|
||||
rom.prg_rom.fill(0);
|
||||
for bank in 0..16usize {
|
||||
rom.prg_rom[bank * 0x4000] = bank as u8;
|
||||
}
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
fn serial_write5(mapper: &mut Box<dyn super::Mapper + Send>, addr: u16, value: u8) {
|
||||
for i in 0..5 {
|
||||
mapper.cpu_write(addr, (value >> i) & 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Writes before the 0->1 transition of I must not unlock PRG switching.
|
||||
serial_write5(&mut mapper, 0xA000, 0x08);
|
||||
serial_write5(&mut mapper, 0x8000, 0x0C);
|
||||
serial_write5(&mut mapper, 0xE000, 0x02);
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x00);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0x01);
|
||||
|
||||
// NES-EVENT unlock sequence: I low, then high once, then mapper can switch.
|
||||
serial_write5(&mut mapper, 0xA000, 0x00);
|
||||
serial_write5(&mut mapper, 0xA000, 0x10);
|
||||
serial_write5(&mut mapper, 0xA000, 0x08);
|
||||
serial_write5(&mut mapper, 0x8000, 0x0C);
|
||||
serial_write5(&mut mapper, 0xE000, 0x02);
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x0A);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0x0F);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper105_wram_disable_bit_controls_6000_window() {
|
||||
let rom = test_rom(105, 16, 0);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
fn serial_write5(mapper: &mut Box<dyn super::Mapper + Send>, addr: u16, value: u8) {
|
||||
for i in 0..5 {
|
||||
mapper.cpu_write(addr, (value >> i) & 1);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(mapper.cpu_write_low(0x6000, 0x55));
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), Some(0x55));
|
||||
|
||||
serial_write5(&mut mapper, 0xE000, 0x10);
|
||||
assert!(!mapper.cpu_write_low(0x6000, 0xAA));
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), None);
|
||||
|
||||
serial_write5(&mut mapper, 0xE000, 0x00);
|
||||
assert!(mapper.cpu_write_low(0x6000, 0xAA));
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), Some(0xAA));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper105_irq_counter_reaches_threshold_from_restored_state() {
|
||||
let rom = test_rom(105, 16, 0);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
fn serial_write5(mapper: &mut Box<dyn super::Mapper + Send>, addr: u16, value: u8) {
|
||||
for i in 0..5 {
|
||||
mapper.cpu_write(addr, (value >> i) & 1);
|
||||
}
|
||||
}
|
||||
|
||||
serial_write5(&mut mapper, 0xA000, 0x00); // timer running (I=0)
|
||||
|
||||
let mut state = Vec::new();
|
||||
mapper.save_state(&mut state);
|
||||
let near_threshold = super::InesMapper105::IRQ_THRESHOLD - 1;
|
||||
state[3] = 0x00; // reg_a with I=0
|
||||
state[8..12].copy_from_slice(&near_threshold.to_le_bytes());
|
||||
state[12] = 0;
|
||||
mapper.load_state(&state).expect("state must load");
|
||||
|
||||
mapper.clock_cpu(1);
|
||||
assert!(mapper.poll_irq());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper155_keeps_wram_enabled_when_prg_bit4_is_set() {
|
||||
let rom = test_rom(155, 16, 0);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
fn serial_write5(mapper: &mut Box<dyn super::Mapper + Send>, addr: u16, value: u8) {
|
||||
for i in 0..5 {
|
||||
mapper.cpu_write(addr, (value >> i) & 1);
|
||||
}
|
||||
}
|
||||
|
||||
serial_write5(&mut mapper, 0xE000, 0x10);
|
||||
assert!(mapper.cpu_write_low(0x6000, 0x66));
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), Some(0x66));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper155_prg_bit4_with_bit3_controls_outer_fixed_bank_region() {
|
||||
let mut rom = test_rom(155, 16, 1);
|
||||
rom.prg_rom.fill(0);
|
||||
for bank in 0..16usize {
|
||||
rom.prg_rom[bank * 0x4000] = bank as u8;
|
||||
}
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
fn serial_write5(mapper: &mut Box<dyn super::Mapper + Send>, addr: u16, value: u8) {
|
||||
for i in 0..5 {
|
||||
mapper.cpu_write(addr, (value >> i) & 1);
|
||||
}
|
||||
}
|
||||
|
||||
serial_write5(&mut mapper, 0x8000, 0x08); // mode 2: fixed first @ $8000, switch @ $C000
|
||||
serial_write5(&mut mapper, 0xE000, 0x18); // bit4=1 (MMC1A mode), bit3=1 selects A17 upper half
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x08);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper5_supports_low_window_registers_and_prg_bank_switch() {
|
||||
let mut rom = test_rom(5, 16, 8);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x10; // 8K bank 0
|
||||
rom.prg_rom[0x2000] = 0x11; // 8K bank 1
|
||||
rom.prg_rom[0x4000] = 0x12; // 8K bank 2
|
||||
rom.prg_rom[0x6000] = 0x13; // 8K bank 3
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert!(mapper.cpu_write_low(0x5100, 0x03)); // 4x8K PRG mode
|
||||
assert!(mapper.cpu_write_low(0x5114, 0x00));
|
||||
assert!(mapper.cpu_write_low(0x5115, 0x01));
|
||||
assert!(mapper.cpu_write_low(0x5116, 0x02));
|
||||
assert!(mapper.cpu_write_low(0x5117, 0x03));
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||
assert_eq!(mapper.cpu_read(0xA000), 0x11);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0x12);
|
||||
assert_eq!(mapper.cpu_read(0xE000), 0x13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper19_switches_prg_chr_and_triggers_irq() {
|
||||
let mut rom = test_rom(19, 8, 8);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.chr_data.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x10; // 8K bank 0
|
||||
rom.prg_rom[0x2000] = 0x11; // 8K bank 1
|
||||
rom.prg_rom[0x4000] = 0x12; // 8K bank 2
|
||||
rom.prg_rom[0x1E000] = 0x17; // fixed last 8K bank
|
||||
rom.chr_data[0x0000] = 0x01;
|
||||
rom.chr_data[0x1000] = 0x05; // bank 4
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0xE000, 0x00);
|
||||
mapper.cpu_write(0xE800, 0x01);
|
||||
mapper.cpu_write(0xF000, 0x02);
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||
assert_eq!(mapper.cpu_read(0xA000), 0x11);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0x12);
|
||||
assert_eq!(mapper.cpu_read(0xE000), 0x17);
|
||||
|
||||
mapper.cpu_write(0x8000, 0x04);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x05);
|
||||
|
||||
assert!(mapper.cpu_write_low(0x5000, 0xFE));
|
||||
assert!(mapper.cpu_write_low(0x5800, 0xFF)); // enable IRQ
|
||||
mapper.clock_cpu(4);
|
||||
assert!(mapper.poll_irq());
|
||||
}
|
||||
297
src/native_core/mapper/tests/mmc3_vrc_core.rs
Normal file
297
src/native_core/mapper/tests/mmc3_vrc_core.rs
Normal file
@@ -0,0 +1,297 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mapper185_submapper4_chr_gate_uses_latch_low_bits() {
|
||||
let mut rom = test_rom_with_submapper(185, 4, 2, 1);
|
||||
rom.prg_rom.fill(0xFF);
|
||||
rom.chr_data.fill(0);
|
||||
rom.chr_data[0x0000] = 0x3C;
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x00);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x3C);
|
||||
|
||||
mapper.cpu_write(0x8000, 0x01);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0xFF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper185_always_applies_and_bus_conflicts() {
|
||||
let mut rom = test_rom_with_submapper(185, 4, 2, 1);
|
||||
rom.prg_rom.fill(0xFF);
|
||||
rom.prg_rom[0x0000] = 0x00; // force effective latch value to 0 on $8000 writes
|
||||
rom.chr_data.fill(0);
|
||||
rom.chr_data[0x0000] = 0x55;
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x03); // with conflicts latch remains 0 -> enabled for submapper 4
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x55);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper185_submapper0_does_not_use_startup_open_bus_reads() {
|
||||
let mut rom = test_rom_with_submapper(185, 0, 2, 1);
|
||||
rom.chr_data.fill(0);
|
||||
rom.chr_data[0x0000] = 0xA5;
|
||||
let mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0xA5);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0xA5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper66_allows_chr_ram_write_when_chr_is_ram() {
|
||||
let mut rom = test_rom(66, 4, 0);
|
||||
rom.chr_data = vec![0; 0x8000];
|
||||
rom.chr_is_ram = true;
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x01); // CHR bank 1
|
||||
mapper.ppu_write(0x0020, 0x5A);
|
||||
assert_eq!(mapper.ppu_read(0x0020), 0x5A);
|
||||
mapper.cpu_write(0x8000, 0x00); // CHR bank 0
|
||||
assert_eq!(mapper.ppu_read(0x0020), 0x00);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper_alias_206_behaves_like_mmc3_bank_select() {
|
||||
let mut rom = test_rom(206, 8, 4);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x10; // bank 0
|
||||
rom.prg_rom[0x2000] = 0x20; // bank 1
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
mapper.cpu_write(0x8000, 0x06); // select R6
|
||||
mapper.cpu_write(0x8001, 0x01); // put bank1 in first slot
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper118_routes_nametables_from_chr_bank_bit7() {
|
||||
let rom = test_rom(118, 8, 8);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
// R0 controls pages 0/1 (NT0/NT1); set bit7 => CIRAM page 1.
|
||||
mapper.cpu_write(0x8000, 0x00);
|
||||
mapper.cpu_write(0x8001, 0x80);
|
||||
// R1 controls pages 2/3 (NT2/NT3); keep bit7 cleared => CIRAM page 0.
|
||||
mapper.cpu_write(0x8000, 0x01);
|
||||
mapper.cpu_write(0x8001, 0x00);
|
||||
|
||||
assert_eq!(mapper.map_nametable_addr(0x2000), Some(0x0400));
|
||||
assert_eq!(mapper.map_nametable_addr(0x2400), Some(0x0400));
|
||||
assert_eq!(mapper.map_nametable_addr(0x2800), Some(0x0000));
|
||||
assert_eq!(mapper.map_nametable_addr(0x2C00), Some(0x0000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper118_nametable_mapping_is_independent_from_a000_mirroring_writes() {
|
||||
let rom = test_rom(118, 8, 8);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x00);
|
||||
mapper.cpu_write(0x8001, 0x80);
|
||||
let before = mapper.map_nametable_addr(0x2000);
|
||||
|
||||
mapper.cpu_write(0xA000, 0x00);
|
||||
mapper.cpu_write(0xA000, 0x01);
|
||||
let after = mapper.map_nametable_addr(0x2000);
|
||||
|
||||
assert_eq!(before, Some(0x0400));
|
||||
assert_eq!(after, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper95_routes_nametables_from_chr_bank_bit5() {
|
||||
let rom = test_rom(95, 8, 8);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
// R0 controls pages 0/1 (NT0/NT1); set bit5 => CIRAM page 1.
|
||||
mapper.cpu_write(0x8000, 0x00);
|
||||
mapper.cpu_write(0x8001, 0x20);
|
||||
// R1 controls pages 2/3 (NT2/NT3); keep bit5 clear => CIRAM page 0.
|
||||
mapper.cpu_write(0x8000, 0x01);
|
||||
mapper.cpu_write(0x8001, 0x00);
|
||||
|
||||
assert_eq!(mapper.map_nametable_addr(0x2000), Some(0x0400));
|
||||
assert_eq!(mapper.map_nametable_addr(0x2400), Some(0x0400));
|
||||
assert_eq!(mapper.map_nametable_addr(0x2800), Some(0x0000));
|
||||
assert_eq!(mapper.map_nametable_addr(0x2C00), Some(0x0000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper95_nametable_mapping_is_independent_from_a000_writes() {
|
||||
let rom = test_rom(95, 8, 8);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x00);
|
||||
mapper.cpu_write(0x8001, 0x20);
|
||||
let before = mapper.map_nametable_addr(0x2000);
|
||||
|
||||
mapper.cpu_write(0xA000, 0x00);
|
||||
mapper.cpu_write(0xA000, 0x01);
|
||||
let after = mapper.map_nametable_addr(0x2000);
|
||||
|
||||
assert_eq!(before, Some(0x0400));
|
||||
assert_eq!(after, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper4_mmc3_prg_ram_respects_a001_enable_and_write_protect() {
|
||||
let rom = test_rom(4, 8, 8);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert!(mapper.cpu_write_low(0x6000, 0x12));
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), Some(0x12));
|
||||
|
||||
mapper.cpu_write(0xA001, 0x00); // disable PRG-RAM
|
||||
assert!(mapper.cpu_write_low(0x6000, 0x34));
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), Some(0x00));
|
||||
|
||||
mapper.cpu_write(0xA001, 0x80); // enable writes
|
||||
assert!(mapper.cpu_write_low(0x6000, 0x34));
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), Some(0x34));
|
||||
|
||||
mapper.cpu_write(0xA001, 0xC0); // enable + write-protect
|
||||
assert!(mapper.cpu_write_low(0x6000, 0x56));
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), Some(0x34));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper119_tqrom_prg_ram_respects_a001_enable_and_write_protect() {
|
||||
let rom = test_rom(119, 8, 8);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert!(mapper.cpu_write_low(0x6000, 0xAB));
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), Some(0xAB));
|
||||
|
||||
mapper.cpu_write(0xA001, 0x00); // disable PRG-RAM
|
||||
assert!(mapper.cpu_write_low(0x6000, 0xCD));
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), Some(0x00));
|
||||
|
||||
mapper.cpu_write(0xA001, 0x80); // enable writes
|
||||
assert!(mapper.cpu_write_low(0x6000, 0xCD));
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), Some(0xCD));
|
||||
|
||||
mapper.cpu_write(0xA001, 0xC0); // enable + write-protect
|
||||
assert!(mapper.cpu_write_low(0x6000, 0xEF));
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), Some(0xCD));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper4_state_roundtrip_preserves_prg_ram_and_a001_bits() {
|
||||
let rom = test_rom(4, 8, 8);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
mapper.cpu_write(0xA001, 0x80); // enable PRG-RAM writes
|
||||
assert!(mapper.cpu_write_low(0x6001, 0x5A));
|
||||
mapper.cpu_write(0xA001, 0xC0); // write-protect on
|
||||
|
||||
let mut state = Vec::new();
|
||||
mapper.save_state(&mut state);
|
||||
|
||||
let rom2 = test_rom(4, 8, 8);
|
||||
let mut restored = create_mapper(rom2).expect("must create mapper");
|
||||
restored.load_state(&state).expect("state load");
|
||||
|
||||
assert_eq!(restored.cpu_read_low(0x6001), Some(0x5A));
|
||||
assert!(restored.cpu_write_low(0x6001, 0x99)); // ignored because write-protect
|
||||
assert_eq!(restored.cpu_read_low(0x6001), Some(0x5A));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper119_state_roundtrip_preserves_prg_ram_and_a001_bits() {
|
||||
let rom = test_rom(119, 8, 8);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
mapper.cpu_write(0xA001, 0x80); // enable PRG-RAM writes
|
||||
assert!(mapper.cpu_write_low(0x6002, 0xA7));
|
||||
mapper.cpu_write(0xA001, 0xC0); // write-protect on
|
||||
|
||||
let mut state = Vec::new();
|
||||
mapper.save_state(&mut state);
|
||||
|
||||
let rom2 = test_rom(119, 8, 8);
|
||||
let mut restored = create_mapper(rom2).expect("must create mapper");
|
||||
restored.load_state(&state).expect("state load");
|
||||
|
||||
assert_eq!(restored.cpu_read_low(0x6002), Some(0xA7));
|
||||
assert!(restored.cpu_write_low(0x6002, 0xBC)); // ignored because write-protect
|
||||
assert_eq!(restored.cpu_read_low(0x6002), Some(0xA7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper119_tqrom_can_map_chr_ram_via_bank_bit6() {
|
||||
let mut rom = test_rom(119, 8, 8);
|
||||
rom.chr_data.fill(0);
|
||||
rom.chr_data[0x0000] = 0x11; // ROM bank 0
|
||||
rom.chr_data[0x0400] = 0x22; // ROM bank 1
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
// Default CHR from ROM.
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x11);
|
||||
|
||||
// Select R2 (1K bank slot 4) and point it to CHR-RAM page 0x40.
|
||||
mapper.cpu_write(0x8000, 0x02);
|
||||
mapper.cpu_write(0x8001, 0x40);
|
||||
|
||||
mapper.ppu_write(0x1000, 0xA5); // page 4 starts at $1000
|
||||
assert_eq!(mapper.ppu_read(0x1000), 0xA5);
|
||||
|
||||
// Switch same slot back to ROM page 1 and verify RAM is not visible.
|
||||
mapper.cpu_write(0x8000, 0x02);
|
||||
mapper.cpu_write(0x8001, 0x01);
|
||||
assert_eq!(mapper.ppu_read(0x1000), 0x22);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper23_switches_prg_and_chr_and_mirroring() {
|
||||
let mut rom = test_rom(23, 4, 8);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.chr_data.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x10; // 8K bank 0 @ $8000
|
||||
rom.prg_rom[0x2000] = 0x11; // 8K bank 1 @ $A000
|
||||
rom.prg_rom[0xA000] = 0x15; // 8K bank 5
|
||||
rom.chr_data[0x0000] = 0x01; // 1K bank 0
|
||||
rom.chr_data[0x1400] = 0x05; // 1K bank 5
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||
assert_eq!(mapper.cpu_read(0xA000), 0x11);
|
||||
mapper.cpu_write(0x8000, 0x05);
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x15);
|
||||
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||
mapper.cpu_write(0xB000, 0x05); // bank0 low nibble
|
||||
mapper.cpu_write(0xB001, 0x00); // bank0 high nibble
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x05);
|
||||
|
||||
mapper.cpu_write(0x9000, 0x01);
|
||||
assert_eq!(mapper.mirroring(), Mirroring::Horizontal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper23_vrc_irq_can_raise_cpu_irq() {
|
||||
let rom = test_rom(23, 8, 8);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0xF000, 0x0E); // latch low nibble
|
||||
mapper.cpu_write(0xF001, 0x0F); // latch high nibble => 0xFE
|
||||
mapper.cpu_write(0xF002, 0x06); // enable IRQ + CPU cycle mode
|
||||
mapper.clock_cpu(2); // 0xFE -> 0xFF -> IRQ pending
|
||||
assert!(mapper.poll_irq());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper23_vrc4_prg_swap_moves_switchable_bank_to_c000() {
|
||||
let mut rom = test_rom(23, 4, 8);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.prg_rom[0x6000] = 0x33; // switchable bank 3
|
||||
rom.prg_rom[0xC000] = 0x66; // fixed second-last bank (6)
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x03);
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x33);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0x66);
|
||||
|
||||
mapper.cpu_write(0x9002, 0x02); // PRG swap on (VRC4 control)
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x66);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0x33);
|
||||
}
|
||||
49
src/native_core/mapper/tests/property_invariants.rs
Normal file
49
src/native_core/mapper/tests/property_invariants.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn uxrom_prg_bank_selection_is_periodic_by_bank_count() {
|
||||
let mut rom = test_rom(2, 8, 1);
|
||||
for bank in 0..8usize {
|
||||
rom.prg_rom[bank * 0x4000] = bank as u8;
|
||||
}
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
for value in 0u16..=255 {
|
||||
mapper.cpu_write(0x8000, value as u8);
|
||||
let observed = mapper.cpu_read(0x8000);
|
||||
assert_eq!(observed, (value as usize % 8) as u8, "value={value}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cnrom_chr_bank_selection_is_periodic_by_bank_count() {
|
||||
let mut rom = test_rom(3, 2, 4);
|
||||
for bank in 0..4usize {
|
||||
rom.chr_data[bank * 0x2000] = 0xA0 + bank as u8;
|
||||
}
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
for value in 0u16..=255 {
|
||||
mapper.cpu_write(0x8000, value as u8);
|
||||
let observed = mapper.ppu_read(0x0000);
|
||||
let expected = 0xA0 + (value as usize % 4) as u8;
|
||||
assert_eq!(observed, expected, "value={value}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper78_submapper3_mirroring_depends_only_on_control_bit3() {
|
||||
let mut rom = test_rom_with_submapper(78, 3, 8, 2);
|
||||
rom.prg_rom.fill(0xFF);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
for value in 0u16..=255 {
|
||||
mapper.cpu_write(0x8000, value as u8);
|
||||
let expected = if (value & 0x08) == 0 {
|
||||
Mirroring::Horizontal
|
||||
} else {
|
||||
Mirroring::Vertical
|
||||
};
|
||||
assert_eq!(mapper.mirroring(), expected, "value={value}");
|
||||
}
|
||||
}
|
||||
309
src/native_core/mapper/tests/rambo_dxrom_others.rs
Normal file
309
src/native_core/mapper/tests/rambo_dxrom_others.rs
Normal file
@@ -0,0 +1,309 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mapper64_rambo1_switches_prg_with_r6_r7_rf_and_prg_mode() {
|
||||
let mut rom = test_rom(64, 8, 8);
|
||||
rom.prg_rom.fill(0);
|
||||
for bank in 0..16usize {
|
||||
rom.prg_rom[bank * 0x2000] = 0x10 + bank as u8;
|
||||
}
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x06);
|
||||
mapper.cpu_write(0x8001, 0x02); // R6
|
||||
mapper.cpu_write(0x8000, 0x07);
|
||||
mapper.cpu_write(0x8001, 0x03); // R7
|
||||
mapper.cpu_write(0x8000, 0x0F);
|
||||
mapper.cpu_write(0x8001, 0x04); // RF
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x12);
|
||||
assert_eq!(mapper.cpu_read(0xA000), 0x13);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0x14);
|
||||
assert_eq!(mapper.cpu_read(0xE000), 0x1F);
|
||||
|
||||
mapper.cpu_write(0x8000, 0x46); // PRG mode=1
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x14);
|
||||
assert_eq!(mapper.cpu_read(0xA000), 0x13);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0x12);
|
||||
assert_eq!(mapper.cpu_read(0xE000), 0x1F);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper64_rambo1_chr_k_mode_and_invert_follow_register_table() {
|
||||
let mut rom = test_rom(64, 4, 16);
|
||||
rom.chr_data.fill(0);
|
||||
for bank in 0..128usize {
|
||||
let idx = bank * 0x0400;
|
||||
if idx < rom.chr_data.len() {
|
||||
rom.chr_data[idx] = bank as u8;
|
||||
}
|
||||
}
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x00);
|
||||
mapper.cpu_write(0x8001, 0x04); // R0
|
||||
mapper.cpu_write(0x8000, 0x01);
|
||||
mapper.cpu_write(0x8001, 0x08); // R1
|
||||
mapper.cpu_write(0x8000, 0x02);
|
||||
mapper.cpu_write(0x8001, 0x0A); // R2
|
||||
mapper.cpu_write(0x8000, 0x03);
|
||||
mapper.cpu_write(0x8001, 0x0B); // R3
|
||||
mapper.cpu_write(0x8000, 0x04);
|
||||
mapper.cpu_write(0x8001, 0x0C); // R4
|
||||
mapper.cpu_write(0x8000, 0x05);
|
||||
mapper.cpu_write(0x8001, 0x0D); // R5
|
||||
mapper.cpu_write(0x8000, 0x08);
|
||||
mapper.cpu_write(0x8001, 0x06); // R8
|
||||
mapper.cpu_write(0x8000, 0x09);
|
||||
mapper.cpu_write(0x8001, 0x0E); // R9
|
||||
|
||||
// K=0, C=0: 2KB pairs in the first half.
|
||||
mapper.cpu_write(0x8000, 0x00);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x04);
|
||||
assert_eq!(mapper.ppu_read(0x0400), 0x05);
|
||||
assert_eq!(mapper.ppu_read(0x0800), 0x08);
|
||||
assert_eq!(mapper.ppu_read(0x0C00), 0x09);
|
||||
assert_eq!(mapper.ppu_read(0x1000), 0x0A);
|
||||
|
||||
// K=1, C=0: R8/R9 replace second 1KB bank in each lower pair.
|
||||
mapper.cpu_write(0x8000, 0x20);
|
||||
assert_eq!(mapper.ppu_read(0x0400), 0x06);
|
||||
assert_eq!(mapper.ppu_read(0x0C00), 0x0E);
|
||||
|
||||
// K=1, C=1: lower/upper 4KB halves swap.
|
||||
mapper.cpu_write(0x8000, 0xA0);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x0A);
|
||||
assert_eq!(mapper.ppu_read(0x1000), 0x04);
|
||||
assert_eq!(mapper.ppu_read(0x1400), 0x06);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper64_rambo1_cycle_irq_mode_counts_every_four_cpu_cycles() {
|
||||
let rom = test_rom(64, 8, 8);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert!(mapper.needs_ppu_a12_clock());
|
||||
mapper.cpu_write(0x8000, 0x01); // cycle IRQ mode
|
||||
assert!(!mapper.needs_ppu_a12_clock());
|
||||
|
||||
mapper.cpu_write(0xC000, 0x02); // latch
|
||||
mapper.cpu_write(0xC001, 0x00); // request reload + reset prescaler
|
||||
mapper.cpu_write(0xE001, 0x00); // enable IRQ
|
||||
|
||||
mapper.clock_cpu(15);
|
||||
assert!(!mapper.poll_irq());
|
||||
mapper.clock_cpu(8);
|
||||
assert!(mapper.poll_irq());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper64_state_roundtrip_preserves_regs_and_irq_mode() {
|
||||
let mut rom = test_rom(64, 8, 8);
|
||||
rom.chr_data.fill(0);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
mapper.cpu_write(0x8000, 0x26); // K=1 + select R6
|
||||
mapper.cpu_write(0x8001, 0x03);
|
||||
mapper.cpu_write(0x8000, 0x09);
|
||||
mapper.cpu_write(0x8001, 0x07);
|
||||
mapper.cpu_write(0x8000, 0x01); // cycle mode
|
||||
mapper.cpu_write(0xC000, 0x01);
|
||||
mapper.cpu_write(0xC001, 0x00);
|
||||
mapper.cpu_write(0xE001, 0x00);
|
||||
mapper.clock_cpu(8);
|
||||
mapper.ppu_write(0x0400, 0x5A);
|
||||
|
||||
let mut state = Vec::new();
|
||||
mapper.save_state(&mut state);
|
||||
|
||||
let mut rom2 = test_rom(64, 8, 8);
|
||||
rom2.chr_data.fill(0);
|
||||
let mut restored = create_mapper(rom2).expect("must create mapper");
|
||||
restored.load_state(&state).expect("state must load");
|
||||
|
||||
assert_eq!(restored.cpu_read(0xC000), mapper.cpu_read(0xC000));
|
||||
assert_eq!(restored.ppu_read(0x0400), mapper.ppu_read(0x0400));
|
||||
assert_eq!(restored.needs_ppu_a12_clock(), mapper.needs_ppu_a12_clock());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper158_uses_chr_bit7_for_nametable_routing() {
|
||||
let mut rom = test_rom(158, 4, 32);
|
||||
rom.chr_data.fill(0);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
// Program CHR banks used by $0000-$0FFF pages.
|
||||
mapper.cpu_write(0x8000, 0x00);
|
||||
mapper.cpu_write(0x8001, 0x00); // page0 -> bank 0 (bit7=0)
|
||||
mapper.cpu_write(0x8000, 0x20 | 0x08);
|
||||
mapper.cpu_write(0x8001, 0x80); // page1 -> bank 0x80 (bit7=1) via R8 in K=1 mode
|
||||
mapper.cpu_write(0x8000, 0x20 | 0x01);
|
||||
mapper.cpu_write(0x8001, 0x00); // page2 base
|
||||
mapper.cpu_write(0x8000, 0x20 | 0x09);
|
||||
mapper.cpu_write(0x8001, 0x80); // page3 -> bank 0x80 (bit7=1) via R9
|
||||
|
||||
assert_eq!(mapper.map_nametable_addr(0x2000), Some(0x000)); // NT0 -> CIRAM page 0
|
||||
assert_eq!(mapper.map_nametable_addr(0x2400), Some(0x400)); // NT1 -> CIRAM page 1
|
||||
assert_eq!(mapper.map_nametable_addr(0x2800), Some(0x000)); // NT2 -> CIRAM page 0
|
||||
assert_eq!(mapper.map_nametable_addr(0x2C00), Some(0x400)); // NT3 -> CIRAM page 1
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper158_ignores_a000_mirroring_register() {
|
||||
let rom = test_rom(158, 4, 8);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.mirroring(), Mirroring::Horizontal);
|
||||
mapper.cpu_write(0xA000, 0x00); // would set vertical on mapper 64
|
||||
assert_eq!(mapper.mirroring(), Mirroring::Horizontal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper206_dxrom_has_no_cpu_low_window_mapping() {
|
||||
let rom = test_rom(206, 8, 8);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), None);
|
||||
assert!(!mapper.cpu_write_low(0x6000, 0x5A));
|
||||
assert_eq!(mapper.cpu_read_low(0x7000), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper206_uses_fixed_upper_prg_and_ignores_mmc3_mode_bits_irq_regs() {
|
||||
let mut rom = test_rom(206, 8, 8);
|
||||
rom.prg_rom.fill(0);
|
||||
// 8K PRG bank markers.
|
||||
for bank in 0..16usize {
|
||||
rom.prg_rom[bank * 0x2000] = 0x10 + bank as u8;
|
||||
}
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x06);
|
||||
mapper.cpu_write(0x8001, 0x02); // bank6 -> slot $8000
|
||||
mapper.cpu_write(0x8000, 0x07);
|
||||
mapper.cpu_write(0x8001, 0x03); // bank7 -> slot $A000
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x12);
|
||||
assert_eq!(mapper.cpu_read(0xA000), 0x13);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0x1E); // second last fixed
|
||||
assert_eq!(mapper.cpu_read(0xE000), 0x1F); // last fixed
|
||||
|
||||
// MMC3 PRG mode/invert bits must not affect mapper 206 slot layout.
|
||||
mapper.cpu_write(0x8000, 0x46); // would flip PRG mode on MMC3
|
||||
mapper.cpu_write(0x8001, 0x04);
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x14);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0x1E);
|
||||
|
||||
// IRQ and mirroring control registers are not implemented by mapper 206.
|
||||
mapper.cpu_write(0xA000, 0x01);
|
||||
mapper.cpu_write(0xC000, 0xFF);
|
||||
mapper.cpu_write(0xE001, 0x00);
|
||||
mapper.clock_cpu(16);
|
||||
assert_eq!(mapper.mirroring(), Mirroring::Horizontal);
|
||||
assert!(!mapper.poll_irq());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper206_submapper1_is_fixed_32k_and_ignores_cpu_mapper_writes() {
|
||||
let mut rom = test_rom_with_submapper(206, 1, 4, 1);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.chr_data.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x10; // first 32K, lower half
|
||||
rom.prg_rom[0x4000] = 0x11; // first 32K, upper half
|
||||
rom.prg_rom[0x8000] = 0x20; // second 32K, must stay unreachable
|
||||
rom.prg_rom[0xC000] = 0x21;
|
||||
rom.chr_data[0x0000] = 0x33;
|
||||
rom.chr_data[0x0400] = 0x44;
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0x11);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x33);
|
||||
assert_eq!(mapper.ppu_read(0x0400), 0x44);
|
||||
|
||||
mapper.cpu_write(0x8000, 0x06);
|
||||
mapper.cpu_write(0x8001, 0x0F);
|
||||
mapper.cpu_write(0xA000, 0x01);
|
||||
mapper.cpu_write(0xE001, 0x01);
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0x11);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x33);
|
||||
assert_eq!(mapper.ppu_read(0x0400), 0x44);
|
||||
assert!(!mapper.poll_irq());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper88_routes_chr_a16_from_ppu_a12() {
|
||||
let mut rom = test_rom(88, 8, 16);
|
||||
rom.chr_data.fill(0);
|
||||
rom.chr_data[0x0000] = 0x11; // bank 0
|
||||
rom.chr_data[64 * 0x0400] = 0x22; // bank 64 (A16=1 path)
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x8000, 0x00);
|
||||
mapper.cpu_write(0x8001, 0x00); // reg0
|
||||
mapper.cpu_write(0x8000, 0x02);
|
||||
mapper.cpu_write(0x8001, 0x00); // reg2
|
||||
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x11);
|
||||
assert_eq!(mapper.ppu_read(0x1000), 0x22);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper253_uses_vrc4e_prg_and_chr_ram_overlay_on_pages_4_5() {
|
||||
let mut rom = test_rom(253, 8, 128);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.chr_data.fill(0);
|
||||
// Distinguish 8K PRG banks for VRC4 layout.
|
||||
rom.prg_rom[0x0000] = 0x10; // bank 0 at $8000 initially
|
||||
rom.prg_rom[0x2000] = 0x20; // bank 1 at $A000 initially
|
||||
rom.prg_rom[14 * 0x2000] = 0x30; // fixed second-last bank at $C000
|
||||
rom.prg_rom[15 * 0x2000] = 0x40; // fixed last bank at $E000
|
||||
rom.prg_rom[0x8000] = 0x50; // bank 4 (switch target for $8000)
|
||||
rom.chr_data[0x0000] = 0x01; // page 0: CHR-ROM
|
||||
rom.chr_data[64 * 0x0400] = 0x02; // page 0 via different bank
|
||||
rom.chr_data[0x1800] = 0x77; // page 6 baseline
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||
assert_eq!(mapper.cpu_read(0xA000), 0x20);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0x30);
|
||||
assert_eq!(mapper.cpu_read(0xE000), 0x40);
|
||||
mapper.cpu_write(0x8000, 0x04); // VRC4: $8000 selects $8000 bank
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x50);
|
||||
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||
mapper.cpu_write(0xB000, 0x00); // CHR reg0 low nibble = 0
|
||||
mapper.cpu_write(0xB004, 0x04); // CHR reg0 high nibble = 4 -> bank 64 (VRC4e wiring)
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x02);
|
||||
|
||||
// Mapper 253 overlays page 4/5 with dedicated CHR-RAM.
|
||||
assert_eq!(mapper.ppu_read(0x1000), 0x00);
|
||||
mapper.ppu_write(0x1000, 0xAB);
|
||||
mapper.ppu_write(0x17FF, 0xCD);
|
||||
assert_eq!(mapper.ppu_read(0x1000), 0xAB);
|
||||
assert_eq!(mapper.ppu_read(0x17FF), 0xCD);
|
||||
// Neighboring page still uses CHR-ROM mapping.
|
||||
assert_eq!(mapper.ppu_read(0x1800), 0x77);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper253_state_roundtrip_preserves_chr_ram_overlay() {
|
||||
let rom = test_rom(253, 8, 2);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
mapper.ppu_write(0x1001, 0x5A);
|
||||
mapper.ppu_write(0x1402, 0xA5);
|
||||
mapper.cpu_write(0x8000, 0x03);
|
||||
mapper.cpu_write(0xA000, 0x04);
|
||||
|
||||
let mut state = Vec::new();
|
||||
mapper.save_state(&mut state);
|
||||
|
||||
let rom2 = test_rom(253, 8, 2);
|
||||
let mut restored = create_mapper(rom2).expect("must create mapper");
|
||||
restored.load_state(&state).expect("state must load");
|
||||
assert_eq!(restored.ppu_read(0x1001), 0x5A);
|
||||
assert_eq!(restored.ppu_read(0x1402), 0xA5);
|
||||
assert_eq!(restored.cpu_read(0x8000), mapper.cpu_read(0x8000));
|
||||
assert_eq!(restored.cpu_read(0xA000), mapper.cpu_read(0xA000));
|
||||
}
|
||||
263
src/native_core/mapper/tests/vrc_variants_mmc2_4.rs
Normal file
263
src/native_core/mapper/tests/vrc_variants_mmc2_4.rs
Normal file
@@ -0,0 +1,263 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mapper22_vrc2a_uses_hv_mirroring_and_no_irq() {
|
||||
let rom = test_rom(22, 8, 8);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0x9000, 0x02);
|
||||
assert_eq!(mapper.mirroring(), Mirroring::Vertical);
|
||||
mapper.cpu_write(0x9000, 0x03);
|
||||
assert_eq!(mapper.mirroring(), Mirroring::Horizontal);
|
||||
|
||||
mapper.cpu_write(0xF002, 0x06);
|
||||
mapper.clock_cpu(8);
|
||||
assert!(!mapper.poll_irq());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper22_vrc2a_chr_bank_value_is_shifted_right_by_one() {
|
||||
let mut rom = test_rom(22, 4, 8);
|
||||
rom.chr_data.fill(0);
|
||||
let chr_1k_banks = rom.chr_data.len() / 0x0400;
|
||||
for bank in 0..chr_1k_banks {
|
||||
rom.chr_data[bank * 0x0400] = bank as u8;
|
||||
}
|
||||
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
mapper.cpu_write(0xB000, 0x05); // bank0 low nibble
|
||||
mapper.cpu_write(0xB002, 0x00); // bank0 high nibble => raw value 0x05
|
||||
|
||||
// Mapper 22 uses value >> 1 for CHR bank selection.
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x02);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper25_submapper1_uses_lower_address_wiring_for_chr_registers() {
|
||||
let mut rom = test_rom_with_submapper(25, 1, 4, 8);
|
||||
rom.chr_data.fill(0);
|
||||
let chr_1k_banks = rom.chr_data.len() / 0x0400;
|
||||
for bank in 0..chr_1k_banks {
|
||||
rom.chr_data[bank * 0x0400] = bank as u8;
|
||||
}
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x00);
|
||||
assert_eq!(mapper.ppu_read(0x0400), 0x01);
|
||||
mapper.cpu_write(0xB001, 0x05); // lower wiring => subindex 2 => bank1 low nibble
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x00);
|
||||
assert_eq!(mapper.ppu_read(0x0400), 0x05);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper25_submapper2_uses_higher_address_wiring_for_chr_registers() {
|
||||
let mut rom = test_rom_with_submapper(25, 2, 4, 8);
|
||||
rom.chr_data.fill(0);
|
||||
let chr_1k_banks = rom.chr_data.len() / 0x0400;
|
||||
for bank in 0..chr_1k_banks {
|
||||
rom.chr_data[bank * 0x0400] = bank as u8;
|
||||
}
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x00);
|
||||
assert_eq!(mapper.ppu_read(0x0400), 0x01);
|
||||
mapper.cpu_write(0xB001, 0x05); // higher wiring => subindex 0 => bank0 low nibble
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x05);
|
||||
assert_eq!(mapper.ppu_read(0x0400), 0x01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper25_submapper3_disables_vrc_irq_block() {
|
||||
let rom = test_rom_with_submapper(25, 3, 8, 8);
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0xF000, 0x0E); // latch low nibble
|
||||
mapper.cpu_write(0xF001, 0x0F); // latch high nibble
|
||||
mapper.cpu_write(0xF002, 0x06); // would enable IRQ on VRC4 wiring
|
||||
mapper.clock_cpu(8);
|
||||
assert!(!mapper.poll_irq());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper24_vrc6_switches_prg_chr_and_irq() {
|
||||
let mut rom = test_rom(24, 16, 8);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.chr_data.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x10; // 16K bank 0
|
||||
rom.prg_rom[0x4000] = 0x20; // 16K bank 1
|
||||
rom.prg_rom[0xA000] = 0x33; // 8K bank 5
|
||||
rom.chr_data[0x0000] = 0x01; // CHR 1K bank 0
|
||||
rom.chr_data[0x0400] = 0x02; // CHR 1K bank 1
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||
mapper.cpu_write(0x8000, 0x01);
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x20);
|
||||
|
||||
mapper.cpu_write(0xC000, 0x05);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0x33);
|
||||
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||
mapper.cpu_write(0xD000, 0x01);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x02);
|
||||
|
||||
mapper.cpu_write(0xF000, 0xFE);
|
||||
mapper.cpu_write(0xF001, 0x06); // CPU mode + enabled
|
||||
mapper.clock_cpu(2);
|
||||
assert!(mapper.poll_irq());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper26_swaps_a0_a1_for_vrc6_register_decode() {
|
||||
let mut rom = test_rom(26, 16, 4);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.chr_data.fill(0);
|
||||
rom.chr_data[0x0000] = 0x11; // CHR bank 0
|
||||
rom.chr_data[0x0400] = 0x22; // CHR bank 1
|
||||
rom.chr_data[0x0800] = 0x33; // CHR bank 2
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.ppu_read(0x0400), 0x22);
|
||||
mapper.cpu_write(0xD001, 0x02); // mapper 26 swaps A0/A1 -> writes R2, not R1
|
||||
assert_eq!(mapper.ppu_read(0x0400), 0x22);
|
||||
assert_eq!(mapper.ppu_read(0x0800), 0x33);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper85_vrc7_switches_prg_chr_mirroring_and_irq() {
|
||||
let mut rom = test_rom(85, 4, 8);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.chr_data.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x10; // 8K bank 0
|
||||
rom.prg_rom[0x2000] = 0x11; // 8K bank 1
|
||||
rom.prg_rom[0x4000] = 0x12; // 8K bank 2
|
||||
rom.prg_rom[0x6000] = 0x13; // 8K bank 3
|
||||
rom.prg_rom[0xE000] = 0x17; // last fixed bank (7)
|
||||
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||
rom.chr_data[0x0400] = 0x02; // CHR bank 1
|
||||
rom.chr_data[0x1000] = 0x05; // CHR bank 4
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||
assert_eq!(mapper.cpu_read(0xA000), 0x11);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0x12);
|
||||
assert_eq!(mapper.cpu_read(0xE000), 0x17);
|
||||
|
||||
mapper.cpu_write(0x8000, 0x03);
|
||||
mapper.cpu_write(0x8008, 0x01);
|
||||
mapper.cpu_write(0x9000, 0x02);
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x13);
|
||||
assert_eq!(mapper.cpu_read(0xA000), 0x11);
|
||||
assert_eq!(mapper.cpu_read(0xC000), 0x12);
|
||||
|
||||
assert_eq!(mapper.ppu_read(0x0400), 0x02);
|
||||
mapper.cpu_write(0xA008, 0x04); // CHR bank 1 via secondary alias
|
||||
assert_eq!(mapper.ppu_read(0x0400), 0x05);
|
||||
|
||||
mapper.cpu_write(0xE000, 0x03);
|
||||
assert_eq!(mapper.mirroring(), Mirroring::OneScreenHigh);
|
||||
|
||||
mapper.cpu_write(0xE000, 0x80);
|
||||
assert!(mapper.cpu_write_low(0x6000, 0x5A));
|
||||
assert_eq!(mapper.cpu_read_low(0x6000), Some(0x5A));
|
||||
|
||||
mapper.cpu_write(0xE010, 0xFE);
|
||||
mapper.cpu_write(0xF000, 0x06); // CPU mode + enabled
|
||||
mapper.clock_cpu(2);
|
||||
assert!(mapper.poll_irq());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper85_submapper1_uses_a3_for_secondary_chr_registers() {
|
||||
let mut rom = test_rom_with_submapper(85, 1, 4, 8);
|
||||
rom.chr_data.fill(0);
|
||||
rom.chr_data[0x0000] = 0x01; // bank 0
|
||||
rom.chr_data[0x1000] = 0x05; // bank 4
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0xA008, 0x04); // submapper1: A3 selects odd register (reg1)
|
||||
assert_eq!(mapper.ppu_read(0x0400), 0x05);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper85_submapper2_uses_a4_for_secondary_chr_registers() {
|
||||
let mut rom = test_rom_with_submapper(85, 2, 4, 8);
|
||||
rom.chr_data.fill(0);
|
||||
rom.chr_data[0x0000] = 0x01; // bank 0
|
||||
rom.chr_data[0x1000] = 0x05; // bank 4
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0xA008, 0x04); // submapper2: A3 ignored for odd/even
|
||||
assert_eq!(mapper.ppu_read(0x0400), 0x00);
|
||||
mapper.cpu_write(0xA010, 0x04); // submapper2: A4 selects odd register (reg1)
|
||||
assert_eq!(mapper.ppu_read(0x0400), 0x05);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper9_mmc2_chr_latches_switch_banks() {
|
||||
let mut rom = test_rom(9, 8, 8);
|
||||
rom.chr_data.fill(0);
|
||||
rom.chr_data[0x0000] = 0x11; // bank 0
|
||||
rom.chr_data[0x2000] = 0x22; // bank 2
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0xB000, 0x00); // latch FD -> bank0
|
||||
mapper.cpu_write(0xC000, 0x02); // latch FE -> bank2
|
||||
mapper.ppu_write(0x0FD8, 0);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x11);
|
||||
mapper.ppu_write(0x0FE8, 0);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x22);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper9_mmc2_latches_also_switch_on_ppu_reads() {
|
||||
let mut rom = test_rom(9, 8, 8);
|
||||
rom.chr_data.fill(0);
|
||||
rom.chr_data[0x0000] = 0x11; // bank 0
|
||||
rom.chr_data[0x2000] = 0x22; // bank 2
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0xB000, 0x00); // latch FD -> bank0
|
||||
mapper.cpu_write(0xC000, 0x02); // latch FE -> bank2
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x11);
|
||||
let _ = mapper.ppu_read(0x0FE8); // read-trigger FE latch
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x22);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper10_mmc4_switches_prg_and_chr_latches() {
|
||||
let mut rom = test_rom(10, 8, 8);
|
||||
rom.prg_rom.fill(0);
|
||||
rom.chr_data.fill(0);
|
||||
rom.prg_rom[0x0000] = 0x10; // PRG bank 0 @ $8000
|
||||
rom.prg_rom[0x8000] = 0x30; // PRG bank 2 @ $8000
|
||||
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||
rom.chr_data[0x3000] = 0x03; // CHR bank 3
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||
mapper.cpu_write(0xA000, 0x02);
|
||||
assert_eq!(mapper.cpu_read(0x8000), 0x30);
|
||||
|
||||
mapper.cpu_write(0xB000, 0x00);
|
||||
mapper.cpu_write(0xC000, 0x03);
|
||||
mapper.ppu_write(0x0FD8, 0);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||
mapper.ppu_write(0x0FE8, 0);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x03);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapper10_mmc4_latches_also_switch_on_ppu_reads() {
|
||||
let mut rom = test_rom(10, 8, 8);
|
||||
rom.chr_data.fill(0);
|
||||
rom.chr_data[0x0000] = 0x01; // FD bank
|
||||
rom.chr_data[0x3000] = 0x03; // FE bank
|
||||
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||
|
||||
mapper.cpu_write(0xB000, 0x00);
|
||||
mapper.cpu_write(0xC000, 0x03);
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||
let _ = mapper.ppu_read(0x0FE8); // read-trigger FE latch
|
||||
assert_eq!(mapper.ppu_read(0x0000), 0x03);
|
||||
}
|
||||
4
src/native_core/mapper/types.rs
Normal file
4
src/native_core/mapper/types.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub(crate) const PRG_BANK_8K: usize = 0x2000;
|
||||
pub(crate) const PRG_BANK_16K: usize = 0x4000;
|
||||
pub(crate) const CHR_BANK_1K: usize = 0x0400;
|
||||
pub(crate) const PRG_RAM_8K: usize = 0x2000;
|
||||
Reference in New Issue
Block a user