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/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..])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user