made mapper module to support the various NES cartridge types in the furyte, MMC1 support added

This commit is contained in:
2026-08-09 14:03:08 -05:00
parent a54d3ccc09
commit b4504bf9ce
5 changed files with 180 additions and 23 deletions
+3 -1
View File
@@ -23,7 +23,7 @@ impl Bus for NesBus {
0x2000..=0x3FFF => 0, //PPU registers stub, temporal
0x4000..=0x401F => 0, //APU controller stub, temporal
0x4020..=0x5FFF => 0, //Expansion ROM stub. temporal
0x6000..=0x7FFF => 0, //SRAM stub, no reading saves for you :3
0x6000..=0x7FFF => self.cart.read_prg_ram(address), // SRAM reads from the cartridge
0x8000..=0xFFFF => self.cart.read_prg(address), // this is te space PRG Rom gets "loaded" to in real hardware
}
}
@@ -31,6 +31,8 @@ impl Bus for NesBus {
fn write(&mut self, address: u16, value: u8) {
match address {
0x0000..=0x1FFF => self.ram[(address & 0x07FF) as usize] = value,
0x6000..=0x7FFF => self.cart.write_prg_ram(address, value),
0x8000..=0xFFFF => self.cart.write_prg(address, value),
_ => {} // Everything else dropped for now
}
}
+29 -18
View File
@@ -1,15 +1,14 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mirroring { Horizontal, Vertical, FourScreen }
use crate::mapper::{Mapper, Mirroring, Nrom, Mmc1};
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug)]
pub struct Cartridge {
prg_rom: Vec<u8>,
chr_rom: Vec<u8>,
mirroring: Mirroring,
prg_ram: Vec<u8>, // 8KB PRG RAM (battery-backed SRAM lives on the cartridge)
battery: bool,
trainer: bool,
four_screen: bool,
mapper: u8,
mapper: Box<dyn Mapper>,
}
impl Cartridge {
@@ -40,11 +39,13 @@ impl Cartridge {
let four_screen: bool = flags6 & 0b1000 != 0; // this is meant to override mirroring but both will be stored
let mapper: u8 = ((flags7 & 0xF0) >> 4) << 4 | ((flags6 & 0xF0) >> 4);
// Gate for Mapper as we are going NROM-only for now
// Gate for Mapper: NROM (0) and MMC1 (1) supported for now
if mapper != 0 {
return Err(format!("mapper {mapper} not supported yet, this is temporal"));
}
let mapper: Box<dyn Mapper> = match mapper {
0 => Box::new(Nrom::new(mirroring)),
1 => Box::new(Mmc1::new()),
_ => return Err(format!("mapper {mapper} not supported yet, this is temporal")),
};
// Compute the ROM offsets for future use
@@ -62,7 +63,7 @@ impl Cartridge {
Ok(Cartridge {
prg_rom: data[prg_start..prg_start + prg_len].to_vec(),
chr_rom: if chr_len == 0 { vec![0u8; 0x2000] } else { data[chr_start..chr_start + chr_len].to_vec() }, // Vector of 8KB filled with 0's or the actual CHR ROM data
mirroring,
prg_ram: vec![0u8; 0x2000],
battery,
trainer,
four_screen,
@@ -73,22 +74,31 @@ impl Cartridge {
// read_* operations are for the CPU to work with, letting it read the ROM chips on the emulated cartridge
pub fn read_prg(&self, address: u16) -> u8 {
debug_assert!(address >= 0x8000, "PRG read outside of $8000-$FFFF; Address from CPU: {address:#06X}"); // #06X makes the Address readable similar to $1FFF
let mut offset: u16 = address - 0x8000;
if self.prg_rom.len() == 0x4000 {
offset %= 0x4000;
}
self.prg_rom[offset as usize]
self.mapper.read_prg(&self.prg_rom, address)
}
pub fn write_prg(&mut self, address: u16, value: u8) {
self.mapper.write_prg(address, value);
}
pub fn read_chr(&self, address: u16) -> u8 {
debug_assert!(address < 0x2000, "CHR reads outside $0000-$1FFF; Address from CPU: {address:#06X}");
self.chr_rom[address as usize]
self.mapper.read_chr(&self.chr_rom, address)
}
pub fn write_chr(&mut self, address: u16, value: u8) {
debug_assert!(address < 0x2000, "CHR writes outside $0000-$1FFF; Address from CPU: {address:#06X}");
self.chr_rom[address as usize] = value
self.mapper.write_chr(&mut self.chr_rom, address, value);
}
pub fn read_prg_ram(&self, address: u16) -> u8 {
self.prg_ram[(address - 0x6000) as usize]
}
pub fn write_prg_ram(&mut self, address: u16, value: u8) {
self.prg_ram[(address - 0x6000) as usize] = value;
}
pub fn mirroring(&self) -> Mirroring {
self.mapper.mirroring()
}
}
/*
#[cfg(test)]
mod tests {
use super::*;
@@ -157,4 +167,5 @@ mod tests {
cart.write_chr(0x1234, 0xAB);
assert_eq!(cart.read_chr(0x1234), 0xAB);
}
}
}
*/
+1
View File
@@ -1,6 +1,7 @@
mod cartridge;
mod bus;
mod cpu;
mod mapper;
use crate::bus::{Bus, NesBus};
use crate::cartridge::Cartridge;
+133
View File
@@ -0,0 +1,133 @@
use std::fmt::Debug;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mirroring {
Horizontal,
Vertical,
OneScreenLow, // MMC1 control bits 0-1 = 0
OneScreenHigh, // MMC1 control bits 0-1 = 1
FourScreen,
}
pub trait Mapper: Debug {
fn read_prg(&self, prg: &[u8], addr: u16) -> u8;
fn write_prg(&mut self, addr: u16, val: u8);
fn read_chr(&self, chr: &[u8], addr: u16) -> u8;
fn write_chr(&mut self, chr: &mut [u8], addr: u16, val: u8);
fn mirroring(&self) -> Mirroring;
}
#[derive(Debug)]
pub struct Nrom { mirroring: Mirroring }
impl Nrom {
pub fn new(mirroring: Mirroring) -> Self {
Self { mirroring }
}
}
impl Mapper for Nrom {
fn read_prg(&self, prg: &[u8], addr: u16) -> u8 {
debug_assert!(addr >= 0x8000, "PRG read outside of $8000-$FFFF; Address from CPU: {addr:#06X}"); // #06X makes the Address readable similar to $1FFF
let mut offset = addr - 0x8000;
if prg.len() == 0x4000 { offset %= 0x4000 } // 16 KiB prg mirror
prg[offset as usize]
}
fn write_prg(&mut self, _addr: u16, _val: u8) { /* NROM has no registers */ }
fn read_chr(&self, chr: &[u8], addr: u16) -> u8 {
chr[ addr as usize ]
}
fn write_chr(&mut self, chr: &mut [u8], addr: u16, val: u8) {
debug_assert!(addr < 0x2000, "CHR writes outside $0000-$1FFF; Address from CPU: {addr:#06X}");
chr[ addr as usize ] = val;
}
fn mirroring(&self) -> Mirroring {
self.mirroring
}
}
#[derive(Debug)]
pub struct Mmc1 {
shift_reg: u8, shift_count: u8, control: u8, chr0: u8,
chr1: u8, prg_bank:u8,
}
impl Mmc1 {
pub fn new() -> Self {
Self { shift_reg: 0, shift_count: 0, control: 0x0C, chr0: 0, chr1: 0, prg_bank: 0 }
}
// shared CHR bank math for read/write; mode = control>>3 & 1
fn chr_offset(&self, chr: &[u8], addr: u16) -> usize {
if (self.control >> 3) & 1 == 0 {
// mode 0: 8K switch via chr0 (bit 0 ignored, bank even)
let count = (chr.len() / 0x2000).max(1);
let bank = ((self.chr0 & 0x0E) as usize) % count;
bank * 0x2000 + addr as usize
} else if addr < 0x1000 {
// mode 1, low 4K: chr0
let count = (chr.len() / 0x1000).max(1);
let bank = (self.chr0 as usize) % count;
bank * 0x1000 + addr as usize
} else {
// mode 1, high 4K: chr1
let count = (chr.len() / 0x1000).max(1);
let bank = (self.chr1 as usize) % count;
bank * 0x1000 + (addr - 0x1000) as usize
}
}
}
impl Mapper for Mmc1 {
// Shift-Register protocol
fn write_prg(&mut self, addr: u16, val: u8) {
if val & 0x80 != 0 {
self.shift_reg = 0;
self.shift_count = 0;
return;
}
self.shift_reg = (self.shift_reg >> 1) | ((val & 1) << 4);
self.shift_count += 1;
if self.shift_count == 5 {
match addr {
0x8000..=0x9FFF => self.control = self.shift_reg,
0xA000..=0xBFFF => self.chr0 = self.shift_reg,
0xC000..=0xDFFF => self.chr1 = self.shift_reg,
_ => self.prg_bank = self.shift_reg,
}
self.shift_reg = 0;
self.shift_count = 0;
}
}
// PRG banking
fn read_prg(&self, prg: &[u8], addr: u16) -> u8 {
let banks = prg.len() / 0x4000; // 16k bank count
let mode = (self.control >> 2) & 0x3;
let offset = if mode <= 1 {
// 32K at $8000
let bank = ((self.prg_bank & 0x0E) as usize) % banks;
bank * 0x4000 + (addr - 0x8000) as usize
} else if addr < 0xC000 {
// 16K mode at $8000
(addr - 0x8000) as usize
} else {
// 16K mode at $C000
let bank = self.prg_bank as usize % banks;
bank * 0x4000 + (addr - 0xC000) as usize
};
prg[offset]
}
// CHR banking
fn read_chr(&self, chr: &[u8], addr: u16) -> u8 { chr[self.chr_offset(chr, addr)] }
fn write_chr(&mut self, chr: &mut [u8], addr: u16, val: u8) { let o = self.chr_offset(chr, addr); chr[o] = val; }
fn mirroring(&self) -> Mirroring {
match self.control & 0x3 {
0 => Mirroring::OneScreenLow,
1 => Mirroring::OneScreenHigh,
2 => Mirroring::Vertical,
_ => Mirroring::Horizontal,
}
}
}