Make iNES parsing more robust, and add mapper 71

This commit is contained in:
Henry Sloan 2021-04-03 18:04:32 -04:00
parent 27cadf6f5f
commit 54a628bbf9
5 changed files with 193 additions and 46 deletions

View file

@ -1,5 +1,7 @@
// https://wiki.nesdev.com/w/index.php/INES
// https://wiki.nesdev.com/w/index.php/NES_2.0#Header
pub struct CartridgeMetadata {
pub is_nes2: bool,
pub n_prg_banks: u16,
pub n_chr_banks: u16,
pub hardwired_mirroring: Mirroring,
@ -8,28 +10,28 @@ pub struct CartridgeMetadata {
pub mapper_num: u16,
// Uncommon features
pub submapper_num: u16,
pub submapper_num: Option<u16>,
pub console_type: ConsoleType,
pub prg_ram_shifts: u8,
pub prg_nvram_shifts: u8,
pub chr_ram_shifts: u8,
pub chr_nvram_shifts: u8,
pub prg_ram_bytes: usize,
pub prg_nvram_bytes: usize,
pub chr_ram_bytes: usize,
pub chr_nvram_bytes: usize,
pub timing: ClockTiming,
pub vs_system_type: u8,
pub extended_console_type: u8,
pub n_misc_roms: u8,
pub default_expansion_device: u8,
pub vs_system_type: Option<u8>,
pub n_misc_roms: Option<u8>,
pub default_expansion_device: Option<u8>,
}
impl CartridgeMetadata {
pub fn from_header(header: Vec<u8>) -> Result<Self, &'static str> {
// TODO: Some garbage data in iNES 1.0 headers breaks this parsing
if header[0..=3] != [b'N', b'E', b'S', 0x1A] {
return Err("header does not begin with NES<EOF> identifier");
}
let n_prg_banks = (((header[9] & 0x0F) as u16) << 8) | (header[4] as u16);
let n_chr_banks = (((header[9] & 0xF0) as u16) << 4) | (header[5] as u16);
let is_nes2 = (header[7] & 0b1100) == 0b1000;
let mut n_prg_banks = header[4] as u16;
let mut n_chr_banks = header[5] as u16;
let hardwired_mirroring = if ((header[6] >> 3) & 1) == 1 {
Mirroring::FourScreen
@ -39,42 +41,108 @@ impl CartridgeMetadata {
_ => Mirroring::Vertical,
}
};
let has_battery = ((header[6] >> 1) & 1) == 1;
let has_trainer = ((header[6] >> 2) & 1) == 1;
let mut mapper_num = (header[6] >> 4) as u16;
let mapper_num = ((header[6] >> 4) as u16)
| ((header[7] & 0xF0) as u16)
| (((header[8] & 0x0F) as u16) << 8);
let submapper_num = (header[8] >> 4) as u16;
let mut submapper_num = None;
let mut timing = ClockTiming::NTSC;
let mut vs_system_type = None;
let mut n_misc_roms = None;
let mut default_expansion_device = None;
let console_type = match header[7] & 0b11 {
0 => ConsoleType::NESFamicom,
1 => ConsoleType::VsSystem,
2 => ConsoleType::Playchoice10,
_ => ConsoleType::Extended,
};
let (console_type, prg_ram_bytes, prg_nvram_bytes, chr_ram_bytes, chr_nvram_bytes) =
if is_nes2 {
let console_type = match header[7] & 0b11 {
0 => ConsoleType::NESFamicom,
1 => ConsoleType::VsSystem,
2 => ConsoleType::Playchoice10,
_ => match header[13] & 0b1111 {
0x0 => ConsoleType::NESFamicom,
0x1 => ConsoleType::VsSystem,
0x2 => ConsoleType::Playchoice10,
0x3 => ConsoleType::DecimalFamiclone,
0x4 => ConsoleType::VT01Monochrome,
0x5 => ConsoleType::VT01STN,
0x6 => ConsoleType::VT02,
0x7 => ConsoleType::VT03,
0x8 => ConsoleType::VT09,
0x9 => ConsoleType::VT32,
0xA => ConsoleType::VT369,
0xB => ConsoleType::UM6578,
_ => ConsoleType::Other,
},
};
let prg_ram_shifts = header[10] & 0x0F;
let prg_nvram_shifts = (header[10] & 0xF0) >> 4;
let chr_ram_shifts = header[11] & 0x0F;
let chr_nvram_shifts = (header[11] & 0xF0) >> 4;
mapper_num |= ((header[7] & 0xF0) as u16) | (((header[8] & 0x0F) as u16) << 8);
submapper_num = Some((header[8] >> 4) as u16);
let timing = match header[12] & 0b11 {
0 => ClockTiming::NTSC,
1 => ClockTiming::PAL,
2 => ClockTiming::MultiRegion,
_ => ClockTiming::Dendy,
};
n_prg_banks |= ((header[9] & 0x0F) as u16) << 8;
n_chr_banks |= ((header[9] & 0xF0) as u16) << 4;
// TODO: These can be enums
let vs_system_type = header[13];
let extended_console_type = header[13] & 0x0F;
let prg_ram_bytes = 64usize << (header[10] & 0x0F);
let prg_nvram_bytes = 64usize << ((header[10] & 0xF0) >> 4);
let chr_ram_bytes = 64usize << (header[11] & 0x0F);
let chr_nvram_bytes = 64usize << ((header[11] & 0xF0) >> 4);
let n_misc_roms = header[14] & 0b11;
let default_expansion_device = header[14] & 0b11_1111;
timing = match header[12] & 0b11 {
0 => ClockTiming::NTSC,
1 => ClockTiming::PAL,
2 => ClockTiming::MultiRegion,
_ => ClockTiming::Dendy,
};
// This could be an enum if it's ever used
vs_system_type = Some(header[13]);
n_misc_roms = Some(header[14] & 0b11);
default_expansion_device = Some(header[14] & 0b11_1111);
(
console_type,
prg_ram_bytes,
prg_nvram_bytes,
chr_ram_bytes,
chr_nvram_bytes,
)
} else {
let console_type = match header[7] & 0b11 {
0 => ConsoleType::NESFamicom,
1 => ConsoleType::VsSystem,
_ => ConsoleType::Playchoice10,
};
// Either PRG-RAM or PRG-NVRAM, depending on the battery flag in byte 6
let mut some_prg_ram_bytes = 0x2000;
// "A general rule of thumb: if the last 4 bytes are not all zero, and the header is not marked for NES 2.0 format,
// an emulator should either mask off the upper 4 bits of the mapper number or simply refuse to load the ROM."
if header[12] == 0 && header[13] == 0 && header[14] == 0 && header[15] == 0 {
mapper_num |= (header[7] & 0xF0) as u16;
// "Size of PRG RAM in 8 KB units (Value 0 infers 8 KB for compatibility; see PRG RAM circuit)"
some_prg_ram_bytes *= std::cmp::max(header[8], 1) as usize;
}
let chr_ram_bytes = if n_chr_banks == 0 { 0x2000 } else { 0 };
let (prg_ram_bytes, prg_nvram_bytes) = if has_battery {
(0, some_prg_ram_bytes)
} else {
(some_prg_ram_bytes, 0)
};
(
console_type,
prg_ram_bytes,
prg_nvram_bytes,
chr_ram_bytes,
0,
)
};
Ok(Self {
is_nes2,
n_prg_banks,
n_chr_banks,
hardwired_mirroring,
@ -83,13 +151,12 @@ impl CartridgeMetadata {
mapper_num,
submapper_num,
console_type,
prg_ram_shifts,
prg_nvram_shifts,
chr_ram_shifts,
chr_nvram_shifts,
prg_ram_bytes,
prg_nvram_bytes,
chr_ram_bytes,
chr_nvram_bytes,
timing,
vs_system_type,
extended_console_type,
n_misc_roms,
default_expansion_device,
})
@ -109,7 +176,16 @@ pub enum ConsoleType {
NESFamicom,
VsSystem,
Playchoice10,
Extended,
DecimalFamiclone,
VT01Monochrome,
VT01STN,
VT02,
VT03,
VT09,
VT32,
VT369,
UM6578,
Other,
}
pub enum ClockTiming {

View file

@ -196,7 +196,7 @@ impl Memory for Mapper1 {
} else if 0xC000 <= addr && addr <= 0xDFFF {
self.chr_bank_1 = self.shift_register;
} else {
self.prg_bank = self.shift_register;
self.prg_bank = (self.shift_register) % (self.n_prg_banks as u8);
}
self.shift_register = 0b10000;
self.shift_write_count = 0;

View file

@ -0,0 +1,67 @@
use crate::cartridge::Mapper;
use crate::cartridge::Mirroring;
use memory::Memory;
// https://wiki.nesdev.com/w/index.php/INES_Mapper_071
pub struct Mapper71 {
n_prg_banks: u16,
prg_rom: Vec<u8>,
chr_mem: Vec<u8>,
prg_bank: u8,
mirroring_option: Option<Mirroring>,
}
impl Mapper for Mapper71 {
fn get_nametable_mirroring(&self) -> Option<Mirroring> {
self.mirroring_option
}
}
impl Mapper71 {
pub fn new(n_prg_banks: u16, prg_data: Vec<u8>) -> Self {
Self {
n_prg_banks,
chr_mem: vec![0; 0x2000],
prg_rom: prg_data,
prg_bank: 0,
mirroring_option: None,
}
}
}
impl Memory for Mapper71 {
fn read(&mut self, addr: u16) -> u8 {
self.peek(addr)
}
fn peek(&self, addr: u16) -> u8 {
if addr <= 0x1FFF {
self.chr_mem[addr as usize % self.chr_mem.len()]
} else if 0x8000 <= addr && addr <= 0xBFFF {
self.prg_rom[self.prg_bank as usize * 0x4000 + (addr as usize - 0x8000)]
} else if 0xC000 <= addr {
self.prg_rom[(self.n_prg_banks as usize - 1) * 0x4000 + (addr as usize - 0xC000)]
} else {
0x0
}
}
fn write(&mut self, addr: u16, data: u8) {
if addr <= 0x1FFF {
let len = self.chr_mem.len();
self.chr_mem[addr as usize % len] = data;
} else if 0x9000 <= addr && addr <= 0x9FFF {
// "For compatibility without using a submapper, FCEUX begins all games with fixed mirroring,
// and applies single screen mirroring only once $9000-9FFF is written, ignoring writes to $8000-8FFF."
self.mirroring_option = Some(if (data >> 4) & 1 == 1 {
Mirroring::SingleScreenUpper
} else {
Mirroring::SingleScreenLower
});
} else if addr >= 0xC000 {
self.prg_bank = data % self.n_prg_banks as u8;
}
}
}

View file

@ -7,6 +7,7 @@ mod mapper2;
mod mapper3;
mod mapper4;
mod mapper7;
mod mapper71;
mod mapper9;
pub use self::mapper0::Mapper0;
@ -15,8 +16,10 @@ pub use self::mapper2::Mapper2;
pub use self::mapper3::Mapper3;
pub use self::mapper4::Mapper4;
pub use self::mapper7::Mapper7;
pub use self::mapper71::Mapper71;
pub use self::mapper9::Mapper9;
// TODO: Add reset to more mappers so NES::reset works
pub trait Mapper: Memory {
fn get_nametable_mirroring(&self) -> Option<Mirroring> {
None // Unless otherwise specified, mirroring is hard-wired

View file

@ -52,10 +52,11 @@ impl Cartridge {
n_chr_banks,
prg_data,
chr_data,
meta.submapper_num == 1,
meta.submapper_num == Some(1),
)),
7 => Box::from(Mapper7::new(n_prg_banks, prg_data)),
9 => Box::from(Mapper9::new(n_prg_banks, prg_data, chr_data)),
71 => Box::from(Mapper71::new(n_prg_banks, prg_data)),
_ => return Err("unsupported mapper"),
};