aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mikisabate@gmail.com>2025-09-12 21:17:39 +0200
committerMiquel Sabaté Solà <mikisabate@gmail.com>2025-09-12 21:55:16 +0200
commit85f8afab8c6e6bd26509c54c765c542279e26675 (patch)
treefc213702bdc6c78eb331f0aca1d30ce700e93a03 /lib
parentc479fee73b691032e0e08097f9af354ececa68db (diff)
downloadtools.nes-85f8afab8c6e6bd26509c54c765c542279e26675.tar.gz
tools.nes-85f8afab8c6e6bd26509c54c765c542279e26675.zip
header: Add RAM and CPU/PPU Timing detection
Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
Diffstat (limited to 'lib')
-rw-r--r--lib/header/src/lib.rs143
1 files changed, 141 insertions, 2 deletions
diff --git a/lib/header/src/lib.rs b/lib/header/src/lib.rs
index b4103b8..7a858ea 100644
--- a/lib/header/src/lib.rs
+++ b/lib/header/src/lib.rs
@@ -23,6 +23,20 @@ pub enum NameTableArrangement {
Unknown,
}
+impl NameTableArrangement {
+ /// Returns the mirroring identifier for the nametable arrangement. Note
+ /// that mirroring is not exactly the same as nametable arrangement.
+ pub fn mirroring(&self) -> &str {
+ match self {
+ NameTableArrangement::Horizontal => "vertical",
+ NameTableArrangement::Vertical => "horizontal",
+ NameTableArrangement::OneScreen => "1-screen",
+ NameTableArrangement::FourScreen => "4-screen",
+ NameTableArrangement::Unknown => "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)]
@@ -77,6 +91,49 @@ impl std::fmt::Display for Mapper {
}
}
+/// The kind of RAM chip available on the cartridge.
+#[derive(Debug)]
+pub enum RamKind {
+ Ram,
+ Nvram,
+}
+
+impl std::fmt::Display for RamKind {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match self {
+ Self::Ram => write!(f, "RAM"),
+ Self::Nvram => write!(f, "NVRAM"),
+ }
+ }
+}
+
+/// The kind and size of the external RAM chip available on the cartridge.
+#[derive(Debug)]
+pub struct RamDefinition {
+ pub kind: RamKind,
+ pub size: usize,
+}
+
+/// The CPU/PPU timing architecture.
+#[derive(Debug)]
+pub enum Timing {
+ Ntsc,
+ Pal,
+ MultipleRegion,
+ Dendy,
+}
+
+impl std::fmt::Display for Timing {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match self {
+ Self::Ntsc => write!(f, "NTSC"),
+ Self::Pal => write!(f, "PAL"),
+ Self::MultipleRegion => write!(f, "multi-region"),
+ Self::Dendy => write!(f, "Dendy"),
+ }
+ }
+}
+
/// 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.
@@ -98,9 +155,21 @@ pub struct Header {
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).
+ /// battery-backed and handled through a mapper like MMC1). For the exact
+ /// kind of RAM definition check the `external_ram_definition` member.
pub has_persistent_memory: bool,
+ /// If `has_persistent_memory` is set to true, then it will contain a Some
+ /// value with the kind of PRG-RAM chip available and its size.
+ pub prg_ram_definition: Option<RamDefinition>,
+
+ /// If `has_persistent_memory` is set to true, then it will contain a Some
+ /// value with the kind of CHR-RAM chip available and its size.
+ pub chr_ram_definition: Option<RamDefinition>,
+
+ /// The CPU/PPU timing detected from the header.
+ pub timing: Timing,
+
/// Whether there is a 512-byte trainer at $7000-$71FF or not.
pub has_trainer: bool,
@@ -118,6 +187,7 @@ impl TryFrom<&[u8]> for Header {
let kind = get_rom_kind(bytes)?;
let ninth = bytes.get(9).unwrap_or(&0);
let mapper = parse_mapper(bytes.get(6), bytes.get(7), bytes.get(8));
+ let has_persistent_memory = (bytes.get(6).unwrap_or(&0) & 0x2) == 0x2;
Ok(Self {
prg_rom_size: if matches!(kind, Kind::Nes20) {
@@ -131,14 +201,75 @@ impl TryFrom<&[u8]> for Header {
bytes[5] as usize
},
nametable_arrangement: parse_nametable(bytes.get(6), &mapper),
- has_persistent_memory: (bytes.get(6).unwrap_or(&0) & 0x2) == 0x2,
+ has_persistent_memory,
has_trainer: (bytes.get(6).unwrap_or(&0) & 0x4) == 0x4,
mapper,
+ prg_ram_definition: parse_ram_definition(&kind, bytes.get(10), has_persistent_memory),
+ chr_ram_definition: parse_ram_definition(&kind, bytes.get(11), has_persistent_memory),
+ timing: parse_timing(&kind, bytes.get(12)),
kind,
})
}
}
+// Parse the given `byte` by assuming it's a RAM definition. It will just return
+// `None` if `has_persistent_memory` is false or the `kind` is not NES 2.0.
+fn parse_ram_definition(
+ kind: &Kind,
+ byte: Option<&u8>,
+ has_persistent_memory: bool,
+) -> Option<RamDefinition> {
+ // If the bit for persistent memory was not set, then we don't even
+ // bother. If there was something relevant here, then the ROM does not have
+ // a proper format.\
+ //
+ // Moreover, if this is not NES 2.0 format, then we don't even bother as
+ // defining this on iNES v1 is not recommended.
+ if !has_persistent_memory || !matches!(kind, Kind::Nes20) {
+ return None;
+ }
+ let definition = byte?;
+
+ let mut shift = definition & 0x0F;
+ if shift > 0 {
+ return Some(RamDefinition {
+ kind: RamKind::Ram,
+ size: 64 << (shift as usize),
+ });
+ }
+
+ shift = definition & 0xF0;
+ if shift > 0 {
+ shift >>= 4;
+ return Some(RamDefinition {
+ kind: RamKind::Nvram,
+ size: 64 << (shift as usize),
+ });
+ }
+
+ None
+}
+
+// Parse the CPU/PPU timing in case it's in the NES 2.0 format.
+fn parse_timing(kind: &Kind, byte: Option<&u8>) -> Timing {
+ // By default assume NTSC.
+ if matches!(kind, Kind::INes) {
+ return Timing::Ntsc;
+ }
+ let Some(definition) = byte else {
+ return Timing::Ntsc;
+ };
+
+ // A 0 value is NTSC, but Rust here is not clever enough to assume that
+ // masking out 0x03 only gives 4 possible outcomes.
+ match *definition & 0x03 {
+ 0x01 => Timing::Pal,
+ 0x02 => Timing::MultipleRegion,
+ 0x03 => Timing::Dendy,
+ _ => Timing::Ntsc,
+ }
+}
+
// Detect the nametable arrangement given the relevant `byte` value. In some
// cases the `mapper` might be needed to weed out some ambiguities.
fn parse_nametable(byte: Option<&u8>, mapper: &Mapper) -> NameTableArrangement {
@@ -389,5 +520,13 @@ mod tests {
assert!(!header.has_trainer);
assert!(matches!(header.mapper, Mapper::Mmc3Sharp));
assert!(matches!(header.kind, Kind::Nes20));
+
+ let prg_ram = header.prg_ram_definition.unwrap();
+ assert_eq!(prg_ram.size, 0x2000);
+ assert!(matches!(prg_ram.kind, RamKind::Nvram));
+
+ let chr_ram = header.chr_ram_definition.unwrap();
+ assert_eq!(chr_ram.size, 0x2000);
+ assert!(matches!(chr_ram.kind, RamKind::Ram));
}
}