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