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:
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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user