use crate::{
common::{Clocked, NesRegion, Powered},
mapper::{
m001_sxrom::Mmc1Revision, m024_m026_vrc6::Vrc6Revision, Axrom, Bf909x, Cnrom, Empty, Exrom,
Gxrom, MapRead, MapWrite, Mapped, MappedRead, MappedWrite, Mapper, Nrom, Pxrom, Sxrom,
Txrom, Uxrom, Vrc6,
},
memory::{MemRead, MemWrite, Memory, RamState},
ppu::Mirroring,
NesResult,
};
use anyhow::{anyhow, bail, Context};
use log::{debug, info};
use serde::{Deserialize, Serialize};
#[cfg(not(target_arch = "wasm32"))]
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
io::BufRead,
};
use std::{
fmt,
fs::File,
io::{BufReader, Read},
path::Path,
};
const PRG_ROM_BANK_SIZE: usize = 16 * 1024;
const CHR_ROM_BANK_SIZE: usize = 8 * 1024;
#[cfg(not(target_arch = "wasm32"))]
const GAME_DB: &[u8] = include_bytes!("../config/game_database.txt");
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[must_use]
pub enum RomSize {
S128, S256,
S512,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[must_use]
pub enum ChrMode {
Rom,
Ram,
}
#[derive(Default, Copy, Clone, PartialEq, Eq)]
#[must_use]
pub struct NesHeader {
pub version: u8, pub mapper_num: u16, pub submapper_num: u8, pub flags: u8, pub prg_rom_banks: u16, pub chr_rom_banks: u16, pub prg_ram_shift: u8, pub chr_ram_shift: u8, pub tv_mode: u8, pub vs_data: u8, }
#[derive(Default, Clone, Serialize, Deserialize)]
#[must_use]
pub struct Cart {
#[serde(skip)]
pub name: String,
#[serde(skip)]
pub header: NesHeader,
#[serde(skip)]
pub ram_state: RamState,
#[serde(skip)]
pub mirroring: Mirroring,
#[serde(skip)]
pub nes_region: NesRegion,
#[serde(skip)]
pub prg_rom: Memory, pub prg_ram: Memory, pub chr: Memory, pub mapper: Mapper,
pub open_bus: u8,
}
impl Cart {
#[inline]
pub fn new() -> Self {
Self {
name: String::new(),
ram_state: RamState::Random,
header: NesHeader::new(),
mirroring: Mirroring::default(),
nes_region: NesRegion::default(),
prg_rom: Memory::new(),
prg_ram: Memory::new(),
chr: Memory::new(),
mapper: Empty.into(),
open_bus: 0x00,
}
}
#[inline]
pub fn from_path<P: AsRef<Path>>(path: P, ram_state: RamState) -> NesResult<Self> {
let path = path.as_ref();
let mut rom = BufReader::new(
File::open(path).with_context(|| format!("failed to open rom {:?}", path))?,
);
Self::from_rom(&path.to_string_lossy(), &mut rom, ram_state)
}
pub fn from_rom<S, F>(name: S, mut rom_data: &mut F, ram_state: RamState) -> NesResult<Self>
where
S: ToString,
F: Read,
{
let name = name.to_string();
let header = NesHeader::load(&mut rom_data)?;
let prg_ram_size = Self::calculate_ram_size("prg", header.prg_ram_shift)?;
let chr_ram_size = Self::calculate_ram_size("chr", header.chr_ram_shift)?;
let mut prg_data = vec![0x00; (header.prg_rom_banks as usize) * PRG_ROM_BANK_SIZE];
rom_data.read_exact(&mut prg_data).with_context(|| {
let bytes_rem = rom_data
.read_to_end(&mut prg_data)
.map_or_else(|_| "unknown".to_string(), |rem| rem.to_string());
format!(
"invalid rom header \"{}\". prg-rom banks: {}. bytes remaining: {}",
name, header.prg_rom_banks, bytes_rem
)
})?;
let prg_rom = Memory::rom(prg_data);
#[cfg(not(target_arch = "wasm32"))]
let nes_region = {
let mut hasher = DefaultHasher::new();
prg_rom.hash(&mut hasher);
let hash = hasher.finish();
Self::lookup_region(hash)
};
#[cfg(target_arch = "wasm32")]
let nes_region = NesRegion::default();
let mut chr_data = vec![0x00; (header.chr_rom_banks as usize) * CHR_ROM_BANK_SIZE];
rom_data.read_exact(&mut chr_data).with_context(|| {
let bytes_rem = rom_data
.read_to_end(&mut chr_data)
.map_or_else(|_| "unknown".to_string(), |rem| rem.to_string());
format!(
"invalid rom header \"{}\". chr-rom banks: {}. bytes remaining: {}",
name, header.chr_rom_banks, bytes_rem,
)
})?;
let chr = if chr_data.is_empty() {
Memory::ram(chr_ram_size, ram_state)
} else {
Memory::rom(chr_data)
};
let mirroring = if header.flags & 0x08 == 0x08 {
Mirroring::FourScreen
} else {
match header.flags & 0x01 {
0 => Mirroring::Horizontal,
1 => Mirroring::Vertical,
_ => unreachable!("impossible mirroring"),
}
};
let mut cart = Self {
name,
header,
ram_state,
mirroring,
nes_region,
prg_rom,
prg_ram: Memory::ram(prg_ram_size, ram_state),
chr,
mapper: Mapper::default(),
open_bus: 0x00,
};
cart.mapper = match header.mapper_num {
0 => Nrom::load(&mut cart),
1 => Sxrom::load(&mut cart, Mmc1Revision::BC),
2 => Uxrom::load(&mut cart),
3 => Cnrom::load(&mut cart),
4 => Txrom::load(&mut cart),
5 => Exrom::load(&mut cart, nes_region),
7 => Axrom::load(&mut cart),
9 => Pxrom::load(&mut cart),
24 => Vrc6::load(&mut cart, Vrc6Revision::A),
26 => Vrc6::load(&mut cart, Vrc6Revision::B),
66 => Gxrom::load(&mut cart),
71 => Bf909x::load(&mut cart),
155 => Sxrom::load(&mut cart, Mmc1Revision::A),
_ => bail!("unsupported mapper number: {}", header.mapper_num),
};
info!("Loaded `{}`", cart);
debug!("{:?}", cart);
Ok(cart)
}
#[inline]
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[inline]
#[must_use]
pub fn sram(&self) -> &[u8] {
&self.prg_ram
}
#[inline]
pub fn load_sram(&mut self, sram: Vec<u8>) {
self.prg_ram.load(sram);
}
#[inline]
pub fn bus_read(&mut self, val: u8) {
self.open_bus = val;
}
#[inline]
#[must_use]
pub const fn battery_backed(&self) -> bool {
self.header.flags & 0x02 == 0x02
}
#[must_use]
pub const fn mapper_board(&self) -> &'static str {
self.header.mapper_board()
}
pub fn set_nes_region(&mut self, nes_region: NesRegion) {
if let Mapper::Exrom(ref mut exrom) = self.mapper {
exrom.dmc.set_nes_region(nes_region);
}
}
pub fn swap(&mut self, cart: &mut Self) {
std::mem::swap(&mut self.prg_ram, &mut cart.prg_ram);
if self.chr.writable() {
std::mem::swap(&mut self.chr, &mut cart.chr);
}
std::mem::swap(&mut self.mapper, &mut cart.mapper);
}
}
impl Cart {
#[inline]
fn calculate_ram_size(ram_type: &str, value: u8) -> NesResult<usize> {
if value > 0 {
64usize
.checked_shl(value.into())
.ok_or_else(|| anyhow!("invalid header {}-ram size: ${:02X}", ram_type, value))
} else {
Ok(0)
}
}
#[cfg(not(target_arch = "wasm32"))]
#[inline]
fn lookup_region(lookup_hash: u64) -> NesRegion {
let db = BufReader::new(GAME_DB);
let lines: Vec<String> = db.lines().filter_map(Result::ok).collect();
if let Ok(line) = lines.binary_search_by(|line| {
let hash = line
.split(',')
.next()
.map(|hash| hash.parse::<u64>().unwrap_or_default())
.unwrap_or_default();
hash.cmp(&lookup_hash)
}) {
let mut fields = lines[line].split(',').skip(1);
if let Some(region) = fields.next() {
return NesRegion::try_from(region).unwrap_or_default();
}
}
NesRegion::default()
}
}
impl Mapped for Cart {
#[inline]
fn irq_pending(&self) -> bool {
self.mapper.irq_pending()
}
#[inline]
fn mirroring(&self) -> Option<Mirroring> {
self.mapper.mirroring().or(Some(self.mirroring))
}
#[inline]
fn use_ciram(&self, addr: u16) -> bool {
self.mapper.use_ciram(addr)
}
#[inline]
fn nametable_page(&self, addr: u16) -> Option<u16> {
self.mapper.nametable_page(addr).or_else(|| {
self.mirroring().map(|mirroring| match mirroring {
Mirroring::Horizontal => (addr >> 11) & 1,
Mirroring::Vertical => (addr >> 10) & 1,
Mirroring::SingleScreenA => (addr >> 14) & 1,
Mirroring::SingleScreenB => (addr >> 13) & 1,
Mirroring::FourScreen => panic!("unhandled FourScreen mirroring"),
})
})
}
#[inline]
fn ppu_addr(&mut self, addr: u16) {
self.mapper.ppu_addr(addr);
}
#[inline]
fn ppu_read(&mut self, addr: u16) {
self.mapper.ppu_read(addr);
}
#[inline]
fn ppu_write(&mut self, addr: u16, val: u8) {
self.mapper.ppu_write(addr, val);
}
}
impl MemRead for Cart {
#[inline]
fn read(&mut self, addr: u16) -> u8 {
match self.mapper.map_read(addr) {
MappedRead::Chr(addr) => self.chr.readw(addr),
MappedRead::PrgRam(addr) => self.prg_ram.readw(addr),
MappedRead::PrgRom(addr) => self.prg_rom.readw(addr),
MappedRead::Data(data) => data,
MappedRead::None => self.open_bus,
}
}
#[inline]
fn peek(&self, addr: u16) -> u8 {
match self.mapper.map_peek(addr) {
MappedRead::Chr(addr) => self.chr.peekw(addr),
MappedRead::PrgRam(addr) => self.prg_ram.peekw(addr),
MappedRead::PrgRom(addr) => self.prg_rom.peekw(addr),
MappedRead::Data(data) => data,
MappedRead::None => self.open_bus,
}
}
}
impl MemWrite for Cart {
#[inline]
fn write(&mut self, addr: u16, val: u8) {
match self.mapper.map_write(addr, val) {
MappedWrite::Chr(addr, val) if self.chr.writable() => self.chr.writew(addr, val),
MappedWrite::PrgRam(addr, val) if self.prg_ram.writable() => {
self.prg_ram.writew(addr, val);
}
MappedWrite::PrgRamProtect(protect) => self.prg_ram.write_protect(protect),
_ => (),
}
}
}
impl Clocked for Cart {
fn clock(&mut self) -> usize {
self.mapper.clock()
}
}
impl Powered for Cart {
fn reset(&mut self) {
self.mapper.reset();
}
fn power_cycle(&mut self) {
self.mapper.power_cycle();
}
}
impl fmt::Display for Cart {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::result::Result<(), fmt::Error> {
write!(
f,
"{} - {}, CHR-{}: {}K, PRG-ROM: {}K, PRG-RAM: {}K, Mirroring: {:?}, Battery: {}",
self.name,
self.mapper_board(),
if self.chr.writable() { "RAM" } else { "ROM" },
self.chr.len() / 1024,
self.prg_rom.len() / 1024,
self.prg_ram.len() / 1024,
self.mirroring().unwrap_or_default(),
self.battery_backed(),
)
}
}
impl fmt::Debug for Cart {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::result::Result<(), fmt::Error> {
f.debug_struct("Cart")
.field("name", &self.name)
.field("header", &self.header)
.field("mirroring", &self.mirroring())
.field("battery_backed", &self.battery_backed())
.field("chr", &self.chr)
.field("prg_rom", &self.prg_rom)
.field("prg_ram", &self.prg_ram)
.field("mapper", &self.mapper)
.field("open_bus", &format_args!("${:02X}", &self.open_bus))
.finish()
}
}
impl NesHeader {
const fn new() -> Self {
Self {
version: 0x01,
mapper_num: 0x0000,
submapper_num: 0x00,
flags: 0x00,
prg_rom_banks: 0x0000,
chr_rom_banks: 0x0000,
prg_ram_shift: 0x00,
chr_ram_shift: 0x00,
tv_mode: 0x00,
vs_data: 0x00,
}
}
#[inline]
pub fn from_path<P: AsRef<Path>>(path: P) -> NesResult<Self> {
let path = path.as_ref();
let mut rom = BufReader::new(
File::open(path).with_context(|| format!("failed to open rom {:?}", path))?,
);
Self::load(&mut rom)
}
pub fn load<F: Read>(rom_data: &mut F) -> NesResult<Self> {
let mut header = [0u8; 16];
rom_data.read_exact(&mut header)?;
if header[0..4] != *b"NES\x1a" {
bail!("iNES header signature not found");
} else if (header[7] & 0x0C) == 0x04 {
bail!("header is corrupted by `DiskDude!`. repair and try again");
} else if (header[7] & 0x0C) == 0x0C {
bail!("unrecognized header format. repair and try again");
}
let mut prg_rom_banks = u16::from(header[4]);
let mut chr_rom_banks = u16::from(header[5]);
let mut mapper_num = u16::from(((header[6] & 0xF0) >> 4) | (header[7] & 0xF0));
let flags = (header[6] & 0x0F) | ((header[7] & 0x0F) << 4);
let mut version = 1; let mut submapper_num = 0;
let mut prg_ram_shift = 0;
let mut chr_ram_shift = 0;
let mut tv_mode = 0;
let mut vs_data = 0;
if header[7] & 0x0C == 0x08 {
version = 2;
mapper_num |= u16::from(header[8] & 0x0F) << 8;
submapper_num = (header[8] & 0xF0) >> 4;
prg_rom_banks |= u16::from(header[9] & 0x0F) << 8;
chr_rom_banks |= u16::from(header[9] & 0xF0) << 4;
prg_ram_shift = header[10];
chr_ram_shift = header[11];
tv_mode = header[12];
vs_data = header[13];
if prg_ram_shift & 0x0F == 0x0F || prg_ram_shift & 0xF0 == 0xF0 {
bail!("invalid prg-ram size in header");
} else if chr_ram_shift & 0x0F == 0x0F || chr_ram_shift & 0xF0 == 0xF0 {
bail!("invalid chr-ram size in header");
} else if chr_ram_shift & 0xF0 == 0xF0 {
bail!("battery-backed chr-ram is currently not supported");
} else if header[14] > 0 || header[15] > 0 {
bail!("unrecognized data found at header offsets 14-15");
}
} else {
for (i, header) in header.iter().enumerate().take(16).skip(8) {
if *header > 0 {
bail!(
"unrecogonized data found at header offset {}. repair and try again",
i,
);
}
}
}
if flags & 0x04 == 0x04 {
bail!("trained roms are currently not supported.");
}
Ok(Self {
version,
mapper_num,
submapper_num,
flags,
prg_rom_banks,
chr_rom_banks,
prg_ram_shift,
chr_ram_shift,
tv_mode,
vs_data,
})
}
#[must_use]
pub const fn mapper_board(&self) -> &'static str {
match self.mapper_num {
0 => "Mapper 000 - NROM",
1 => "Mapper 001 - SxROM/MMC1B/C",
2 => "Mapper 002 - UxROM",
3 => "Mapper 003 - CNROM",
4 => "Mapper 004 - TxROM/MMC3/MMC6",
5 => "Mapper 005 - ExROM/MMC5",
7 => "Mapper 007 - AxROM",
9 => "Mapper 009 - PxROM",
24 => "Mapper 024 - Vrc6a",
26 => "Mapper 026 - Vrc6b",
66 => "Mapper 066 - GxROM/MxROM",
71 => "Mapper 071 - Camerica/Codemasters/BF909x",
155 => "Mapper 155 - SxROM/MMC1A",
_ => "Unsupported Mapper",
}
}
}
impl fmt::Debug for NesHeader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::result::Result<(), fmt::Error> {
f.debug_struct("NesHeader")
.field("version", &self.version)
.field("mapper_num", &format_args!("{:03}", &self.mapper_num))
.field("submapper_num", &self.submapper_num)
.field("flags", &format_args!("0b{:08b}", &self.flags))
.field("prg_rom_banks", &self.prg_rom_banks)
.field("chr_rom_banks", &self.chr_rom_banks)
.field("prg_ram_shift", &self.prg_ram_shift)
.field("chr_ram_shift", &self.chr_ram_shift)
.field("tv_mode", &self.tv_mode)
.field("vs_data", &self.vs_data)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! test_headers {
($(($test:ident, $data:expr, $header:expr$(,)?)),*$(,)?) => {$(
#[test]
fn $test() {
let header = NesHeader::load(&mut $data.as_slice()).expect("valid header");
assert_eq!(header, $header);
}
)*};
}
#[rustfmt::skip]
test_headers!(
(
mapper000_horizontal,
[0x4E, 0x45, 0x53, 0x1A,
0x02, 0x01, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00],
NesHeader {
version: 1,
mapper_num: 0,
flags: 0b0000_0001,
prg_rom_banks: 2,
chr_rom_banks: 1,
..NesHeader::default()
},
),
(
mapper001_vertical,
[0x4E, 0x45, 0x53, 0x1A,
0x08, 0x00, 0x10, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00],
NesHeader {
version: 1,
mapper_num: 1,
flags: 0b0000_0000,
prg_rom_banks: 8,
chr_rom_banks: 0,
..NesHeader::default()
},
),
);
}