diff options
| -rw-r--r-- | Cargo.lock | 13 | ||||
| -rw-r--r-- | Cargo.toml | 3 | ||||
| -rw-r--r-- | README.md | 7 | ||||
| -rw-r--r-- | crates/nasm/src/main.rs | 2 | ||||
| -rw-r--r-- | crates/readrom/Cargo.toml | 12 | ||||
| -rw-r--r-- | crates/readrom/src/main.rs | 55 | ||||
| -rw-r--r-- | lib/header/Cargo.toml | 9 | ||||
| -rw-r--r-- | lib/header/src/lib.rs | 332 |
8 files changed, 431 insertions, 2 deletions
@@ -104,6 +104,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] +name = "header" +version = "0.1.0" + +[[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -163,6 +167,15 @@ dependencies = [ ] [[package]] +name = "readrom" +version = "1.0.0" +dependencies = [ + "anyhow", + "clap", + "header", +] + +[[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6,10 +6,11 @@ resolver = "2" rust-version = "1.82" edition = "2021" license = "GPLv3+" -authors = ["Miquel Sabaté Solà"] +authors = ["Miquel Sabaté Solà <mikisabate@gmail.com>"] [workspace.dependencies] xixanta = { path = "./lib/xixanta", version = "0.1.0" } +header = { path = "./lib/header", version = "0.1.0" } [profile.release] codegen-units = 1 @@ -19,3 +19,10 @@ changed with the `-c/--configuration` flag, which accepts the following values: `CODE`, `VECTORS`, and `CHARS`. - `nrom65`: same as `nrom` but it also has `STARTUP` for compatibility with the default linker configuration from [cc65](https://github.com/cc65/cc65). + +## `readrom` + +The `readrom` reads a given ROM file and shows all the information that can be +gathered from it. For now this only applies to information on the header, but in +the future we might want to add disassembling user-specified segments, for +example. diff --git a/crates/nasm/src/main.rs b/crates/nasm/src/main.rs index 9ad710b..105a995 100644 --- a/crates/nasm/src/main.rs +++ b/crates/nasm/src/main.rs @@ -5,7 +5,7 @@ use std::io::{self, Read, Write}; use xixanta::assembler::Assembler; use xixanta::mapping::{Mapping, EMPTY, NROM, NROM65}; -/// Assembler for the 6502 microprocessor that targets the NES. +/// Assembler for the 6502 microprocessor that targets the NES/Famicom. #[derive(ClapParser, Debug)] #[command(version, about, long_about = None)] struct Args { diff --git a/crates/readrom/Cargo.toml b/crates/readrom/Cargo.toml new file mode 100644 index 0000000..98a5106 --- /dev/null +++ b/crates/readrom/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "readrom" +version = "1.0.0" +rust-version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +anyhow = "^1" +clap = { version = "^4", features = ["derive"] } +header.workspace = true diff --git a/crates/readrom/src/main.rs b/crates/readrom/src/main.rs new file mode 100644 index 0000000..56e60e0 --- /dev/null +++ b/crates/readrom/src/main.rs @@ -0,0 +1,55 @@ +use anyhow::{bail, Result}; +use clap::Parser as ClapParser; +use header::{Header, Kind}; +use std::fs::File; +use std::io::{ErrorKind, Read}; + +/// Display information about NES/Famicom ROM files. +#[derive(ClapParser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// NES/Famicom ROM file from which to display information. + file: String, +} + +fn print_header(header: &Header) { + println!("Header:"); + + match header.kind { + Kind::INes => println!(" Kind:\t\tiNES"), + Kind::Nes20 => println!(" Kind:\t\tNES 2.0"), + } + + println!( + " PRG ROM size:\t{} bytes ({}KB)", + header.prg_rom_size * 16 * 1024, + header.prg_rom_size * 16 + ); + println!( + " CHR ROM size:\t{} bytes ({}KB)", + header.chr_rom_size * 8 * 1024, + header.chr_rom_size * 8 + ); + println!(" Mapper:\t{}", header.mapper); +} + +fn main() -> Result<()> { + let args = Args::parse(); + let mut input = File::open(args.file)?; + + let mut buf = vec![0u8; 0x10]; + if let Err(e) = input.read_exact(&mut buf) { + match e.kind() { + ErrorKind::UnexpectedEof => bail!("malformed ROM file."), + _ => return Err(e.into()), + } + } + + let header = match Header::try_from(buf.as_slice()) { + Ok(h) => h, + Err(e) => bail!("{}.", e), + }; + print_header(&header); + + Ok(()) +} diff --git a/lib/header/Cargo.toml b/lib/header/Cargo.toml new file mode 100644 index 0000000..56eb36c --- /dev/null +++ b/lib/header/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "header" +version = "0.1.0" +rust-version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] diff --git a/lib/header/src/lib.rs b/lib/header/src/lib.rs new file mode 100644 index 0000000..f7e2758 --- /dev/null +++ b/lib/header/src/lib.rs @@ -0,0 +1,332 @@ +/// ROM header formats. +#[derive(Debug)] +pub enum Kind { + /// Historical header used since the iNES emulator. + INes, + + /// Modern header which is backwards-compatible in regards to iNES. Using + /// this header is helpful to disambiguate certain mappers or to be more + /// specific about some of them (e.g. submappers inside of MMC3). + Nes20, +} + +/// Nametable arrangement as defined by the header. Typically it's either +/// Vertical or Horizontal, since this library does not support "alternative" +/// types. Hence, an "Unknown" value is most probably either a defect on the ROM +/// file, or a weird alternative arrangement what we don't care. +#[derive(Debug)] +pub enum NameTableArrangement { + Vertical, + Horizontal, + Unknown, +} + +/// Memory mappers. Note that not all of them are listed here, but they will be +/// added with time (and if I even care). +#[derive(Debug)] +pub enum Mapper { + Axrom, + BnromCombo, + BnromOnly, + Cnrom, + Mmc1, + Mmc2, + Mmc3Acc, + Mmc3c, + Mmc3Nec, + Mmc3Sharp, + Mmc3T9552, + Mmc4, + Mmc5, + Mmc6, + Nina001, + Nrom, + Unknown, + Unrom512, + Uxrom, +} + +impl std::fmt::Display for Mapper { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Mapper::Axrom => write!(f, "AxROM"), + Mapper::BnromCombo => write!(f, "BNROM / NINA-001"), + Mapper::BnromOnly => write!(f, "BNROM"), + Mapper::Cnrom => write!(f, "CNROM"), + Mapper::Mmc1 => write!(f, "MMC1"), + Mapper::Mmc2 => write!(f, "MMC2"), + Mapper::Mmc3Acc => write!(f, "MC-ACC"), + Mapper::Mmc3c => write!(f, "MMC3C"), + Mapper::Mmc3Nec => write!(f, "MMC3 (NEC)"), + Mapper::Mmc3Sharp => write!(f, "MMC3 (Sharp)"), + Mapper::Mmc3T9552 => write!(f, "MMC3 (variant with a T9552 scrambling chip)"), + Mapper::Mmc4 => write!(f, "MMC4"), + Mapper::Mmc5 => write!(f, "MMC5"), + Mapper::Mmc6 => write!(f, "MMC6"), + Mapper::Nina001 => write!(f, "NINA 001"), + Mapper::Nrom => write!(f, "NROM"), + Mapper::Unknown => write!(f, "unknown"), + Mapper::Unrom512 => write!(f, "UNROM 512"), + Mapper::Uxrom => write!( + f, + "UxROM (NES-UNROM, NES-UOROM, HVC-UN1ROM their HVC counterparts, and clone boards)" + ), + } + } +} + +/// Information that has been parsed from an NES/Famicom ROM header. The +/// `Header` struct implements the `TryFrom` trait. Hence, use +/// `Header::try_from` in order to parse a header. +#[derive(Debug)] +pub struct Header { + /// PRG ROM size in units of 16KB. That is, a "1" here actually means + /// "16KB". This is in accordance to the header itself, but callers that + /// need to display this information should perform the translation to be + /// more useful to the human eye. + pub prg_rom_size: usize, + + /// CHR ROM size in units of 8KB. That is, a "1" here actually means "8KB". + /// This is in accordance to the header itself, but callers that need to + /// display this information should perform the translation to be more + /// useful to the human eye. + pub chr_rom_size: usize, + + /// Nametable arrangement. + pub nametable_arrangement: NameTableArrangement, + + /// Whether the memory region $6000-$7FFF is persistent or not (e.g. + /// battery-backed and handled through a mapper like MMC1). + pub has_persistent_memory: bool, + + /// Whether there is a 512-byte trainer at $7000-$71FF or not. + pub has_trainer: bool, + + /// The memory mapper being used. + pub mapper: Mapper, + + /// The kind of the header. + pub kind: Kind, +} + +impl TryFrom<&[u8]> for Header { + type Error = &'static str; + + fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> { + let kind = get_rom_kind(bytes)?; + let ninth = bytes.get(9).unwrap_or(&0); + + Ok(Self { + prg_rom_size: if matches!(kind, Kind::Nes20) { + (((ninth & 0x0F) as usize) << 8) + bytes[4] as usize + } else { + bytes[4] as usize + }, + chr_rom_size: if matches!(kind, Kind::Nes20) { + (((ninth & 0xF0) as usize) << 4) + bytes[5] as usize + } else { + bytes[5] as usize + }, + nametable_arrangement: parse_nametable(bytes.get(6)), + has_persistent_memory: (bytes.get(6).unwrap_or(&0) & 0x2) == 0x2, + has_trainer: (bytes.get(6).unwrap_or(&0) & 0x4) == 0x4, + mapper: parse_mapper(bytes.get(6), bytes.get(7), bytes.get(8)), + kind, + }) + } +} + +fn parse_nametable(byte: Option<&u8>) -> NameTableArrangement { + match byte { + Some(b) => { + if b & 0x1 == 0 { + NameTableArrangement::Vertical + } else { + NameTableArrangement::Horizontal + } + } + None => NameTableArrangement::Unknown, + } +} + +fn parse_mapper(sixth: Option<&u8>, seventh: Option<&u8>, eighth: Option<&u8>) -> Mapper { + match sixth { + Some(l) => { + let byte = (l & 0xF0) >> 4; + let s = seventh.unwrap_or(&0); + let result = (s & 0xF0) | byte; + + // Is this NES 2.0 format? If so, then the eighth byte will contain + // further information on mapper/submapper. + if (s & 0x0C) == 0x08 { + let e = eighth.unwrap_or(&0); + get_mapper_from_id( + ((*e as usize & 0x0F) << 16) + result as usize, + (e & 0xF0) >> 4, + ) + } else { + get_mapper_from_id(result as usize, 0) + } + } + None => Mapper::Unknown, + } +} + +fn get_mapper_from_id(mapper_id: usize, submapper_id: u8) -> Mapper { + match mapper_id { + 0 => Mapper::Nrom, + 1 => Mapper::Mmc1, + 2 => Mapper::Uxrom, + 3 => Mapper::Cnrom, + 4 => match submapper_id { + 0 => Mapper::Mmc3Sharp, + 1 => Mapper::Mmc6, + 2 => Mapper::Mmc3c, + 3 => Mapper::Mmc3Acc, + 4 => Mapper::Mmc3Nec, + 5 => Mapper::Mmc3T9552, + _ => Mapper::Unknown, + }, + 5 => Mapper::Mmc5, + 7 => Mapper::Axrom, + 9 => Mapper::Mmc2, + 10 => Mapper::Mmc4, + 30 => Mapper::Unrom512, + 34 => match submapper_id { + 0 => Mapper::BnromCombo, + 1 => Mapper::BnromOnly, + 2 => Mapper::Nina001, + _ => Mapper::Unknown, + }, + _ => Mapper::Unknown, + } +} + +/// Get the Kind as parsed from the header expressed in `bytes`. +pub fn get_rom_kind(bytes: &[u8]) -> Result<Kind, &'static str> { + if bytes.len() < 6 { + return Err("given header is too short"); + } + if bytes[0] != b'N' || bytes[1] != b'E' || bytes[2] != b'S' || bytes[3] != 0x1A { + return Err("invalid magic value for header"); + } + + if (bytes.get(7).unwrap_or(&0) & 0x0C) == 0x08 { + Ok(Kind::Nes20) + } else { + Ok(Kind::INes) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_rom_kind_test() { + assert_eq!( + get_rom_kind(vec![].as_slice()).unwrap_err(), + "given header is too short" + ); + + assert_eq!( + get_rom_kind(vec![b'N', b'E', b'S', b'\0', 0x01, 0x00].as_slice()).unwrap_err(), + "invalid magic value for header" + ); + + // iNES without the seventh byte. + assert!(matches!( + get_rom_kind(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x00, 0x00].as_slice()).unwrap(), + Kind::INes + )); + + // iNES with seventh byte. + assert!(matches!( + get_rom_kind(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x00, 0x00, 0x00].as_slice()).unwrap(), + Kind::INes + )); + + assert!(matches!( + get_rom_kind(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x00, 0x00, 0x08].as_slice()).unwrap(), + Kind::Nes20 + )); + } + + #[test] + fn nrom_test() { + let mut header = + Header::try_from(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x01, 0x00, 0x00].as_slice()) + .unwrap(); + assert_eq!(header.prg_rom_size, 1); + assert_eq!(header.chr_rom_size, 1); + assert!(matches!( + header.nametable_arrangement, + NameTableArrangement::Vertical + )); + assert!(!header.has_persistent_memory); + assert!(!header.has_trainer); + assert!(matches!(header.mapper, Mapper::Nrom)); + assert!(matches!(header.kind, Kind::INes)); + + header = Header::try_from(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x01, 0x00, 0x08].as_slice()) + .unwrap(); + assert_eq!(header.prg_rom_size, 1); + assert_eq!(header.chr_rom_size, 1); + assert!(matches!( + header.nametable_arrangement, + NameTableArrangement::Vertical + )); + assert!(!header.has_persistent_memory); + assert!(!header.has_trainer); + assert!(matches!(header.mapper, Mapper::Nrom)); + assert!(matches!(header.kind, Kind::Nes20)); + } + + #[test] + fn mmc3_mmc6_test() { + // MMC3 Sharp in iNES format. + let mut header = + Header::try_from(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x01, 0x40, 0x00].as_slice()) + .unwrap(); + assert_eq!(header.prg_rom_size, 1); + assert_eq!(header.chr_rom_size, 1); + assert!(matches!( + header.nametable_arrangement, + NameTableArrangement::Vertical + )); + assert!(!header.has_persistent_memory); + assert!(!header.has_trainer); + assert!(matches!(header.mapper, Mapper::Mmc3Sharp)); + assert!(matches!(header.kind, Kind::INes)); + + // MMC3 Sharp but with NES 2.0 (notice explicit 0 submapper). + header = + Header::try_from(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x01, 0x40, 0x08, 0x00].as_slice()) + .unwrap(); + assert_eq!(header.prg_rom_size, 1); + assert_eq!(header.chr_rom_size, 1); + assert!(matches!( + header.nametable_arrangement, + NameTableArrangement::Vertical + )); + assert!(!header.has_persistent_memory); + assert!(!header.has_trainer); + assert!(matches!(header.mapper, Mapper::Mmc3Sharp)); + assert!(matches!(header.kind, Kind::Nes20)); + + // Disambiguate to MMC6 thanks to submapper + header = + Header::try_from(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x01, 0x40, 0x08, 0x10].as_slice()) + .unwrap(); + assert_eq!(header.prg_rom_size, 1); + assert_eq!(header.chr_rom_size, 1); + assert!(matches!( + header.nametable_arrangement, + NameTableArrangement::Vertical + )); + assert!(!header.has_persistent_memory); + assert!(!header.has_trainer); + assert!(matches!(header.mapper, Mapper::Mmc6)); + assert!(matches!(header.kind, Kind::Nes20)); + } +} |
