Skip to main content

rustynes_mappers/
cartridge.rs

1//! `Cartridge` value type and supporting enums plus the `RomError` returned
2//! by [`crate::parse`].
3//!
4//! The shape of the public type follows `docs/cartridge-format.md` §Public API
5//! and `docs/mappers.md` §Interfaces.
6
7use alloc::{boxed::Box, string::String};
8use thiserror::Error;
9
10/// Nametable mirroring layout selected by the cartridge wiring.
11///
12/// Per `docs/mappers.md` §Mirroring, this is *initial* mirroring for any
13/// non-trivial mapper; mappers that expose runtime mirroring control will
14/// override this from their internal state.
15#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
16pub enum Mirroring {
17    /// Horizontal arrangement (vertical mirroring on the address line).
18    Horizontal,
19    /// Vertical arrangement (horizontal mirroring on the address line).
20    Vertical,
21    /// Both nametables fetch from physical bank A.
22    SingleScreenA,
23    /// Both nametables fetch from physical bank B.
24    SingleScreenB,
25    /// Four-screen mode: cartridge supplies extra 2 KiB VRAM.
26    FourScreen,
27    /// Mapper supplies a runtime mirroring table; defer to the mapper.
28    MapperControlled,
29}
30
31impl Mirroring {
32    /// Resolve a logical nametable index (0..=3, in `$2000` / `$2400` /
33    /// `$2800` / `$2C00` order) to a 2 KiB-VRAM physical bank index (0 or 1)
34    /// under this mirroring mode. Used by every mapper's `nametable_offset`
35    /// helper to keep the mirroring table in one place.
36    #[must_use]
37    pub const fn physical_bank(self, logical_table: u8) -> usize {
38        match self {
39            // Horizontal arrangement: tables 0/1 -> bank 0, 2/3 -> bank 1.
40            Self::Horizontal => (logical_table >> 1) as usize & 1,
41            // Vertical arrangement: tables 0/2 -> bank 0, 1/3 -> bank 1.
42            // Also the fallback for FourScreen / MapperControlled headers
43            // that show up on mappers that don't actually support those.
44            Self::Vertical | Self::FourScreen | Self::MapperControlled => {
45                logical_table as usize & 1
46            }
47            // Single-screen: every logical table aliases to one physical bank.
48            Self::SingleScreenA => 0,
49            Self::SingleScreenB => 1,
50        }
51    }
52}
53
54/// Region governing CPU/PPU dividers, scanline counts, audio rate tables.
55///
56/// Mirrors `rustynes_core::Region` but lives in `rustynes-mappers` so the cartridge can
57/// surface region from the NES 2.0 header without depending on `rustynes-core`.
58/// `rustynes-core` re-exports a public alias so callers see one type.
59#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
60pub enum Region {
61    /// NTSC (Japan, North America, Australia). 60 Hz, 262 scanlines.
62    Ntsc,
63    /// PAL (Europe). 50 Hz, 312 scanlines.
64    Pal,
65    /// Multi-region: cartridge runs on either NTSC or PAL hardware.
66    Multi,
67    /// Dendy (Russian PAL famiclone). 50 Hz, PAL pixel clock + NTSC PPU layout.
68    Dendy,
69}
70
71/// Console type from NES 2.0 header byte 7 (bits 0-1).
72///
73/// iNES 1.0 cartridges always parse as [`ConsoleType::Nes`].
74#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
75pub enum ConsoleType {
76    /// Standard NES / Famicom.
77    Nes,
78    /// Nintendo Vs. System arcade hardware.
79    VsSystem,
80    /// Nintendo PlayChoice-10 arcade hardware.
81    Playchoice10,
82    /// Extended console family (see NES 2.0 byte 13 for the specific variant).
83    Extended,
84}
85
86/// Hardware RGB palette selected by a Vs. System / PlayChoice-10 PPU.
87///
88/// This mirrors `rustynes_ppu::PpuPalette` but lives in `rustynes-mappers` so the
89/// cartridge layer can surface the resolved palette from the NES 2.0 header
90/// without a dependency on `rustynes-ppu` (the workspace edge is `rustynes-ppu ->
91/// rustynes-mappers`, never the reverse). `rustynes-core` maps this to the PPU enum.
92#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)]
93pub enum VsPpuPalette {
94    /// Standard 2C02 composite palette (default NES/Famicom; never a Vs. PPU).
95    #[default]
96    Composite2C02,
97    /// 2C03 RGB PPU.
98    Rgb2C03,
99    /// RP2C04-0001 RGB PPU.
100    Rgb2C04_0001,
101    /// RP2C04-0002 RGB PPU.
102    Rgb2C04_0002,
103    /// RP2C04-0003 RGB PPU.
104    Rgb2C04_0003,
105    /// RP2C04-0004 RGB PPU.
106    Rgb2C04_0004,
107    /// 2C05 RGB PPU (shares the 2C03 master palette; adds register quirks).
108    Rgb2C05,
109}
110
111/// Vs. System PPU type from NES 2.0 header byte 13 low nibble.
112///
113/// Only meaningful when the console type (byte 7) is [`ConsoleType::VsSystem`];
114/// otherwise [`VsPpuType::None`]. Per nesdev "NES 2.0" §Vs. System Type and
115/// "PPU registers" §2C05 identifier.
116#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)]
117pub enum VsPpuType {
118    /// Not a Vs. System cart (the default for NES/Famicom + PlayChoice-10).
119    #[default]
120    None,
121    /// `$0`: any RP2C03/RC2C03 variant.
122    Rp2C03,
123    /// `$2`: RP2C04-0001.
124    Rp2C04_0001,
125    /// `$3`: RP2C04-0002.
126    Rp2C04_0002,
127    /// `$4`: RP2C04-0003.
128    Rp2C04_0003,
129    /// `$5`: RP2C04-0004.
130    Rp2C04_0004,
131    /// `$8`: RC2C05-01 (signature unknown; the sole known game does not check
132    /// `$2002`).
133    Rc2C05_01,
134    /// `$9`: RC2C05-02 (`$2002 AND $3F = $3D`).
135    Rc2C05_02,
136    /// `$A`: RC2C05-03 (`$2002 AND $1F = $1C`).
137    Rc2C05_03,
138    /// `$B`: RC2C05-04 (`$2002 AND $1F = $1B`).
139    Rc2C05_04,
140}
141
142impl VsPpuType {
143    /// Decode the NES 2.0 byte-13 low nibble into a Vs. PPU type. Reserved /
144    /// unknown nibbles fall back to [`VsPpuType::Rp2C03`] (the most common RGB
145    /// PPU), matching how most emulators treat unspecified Vs. carts.
146    #[must_use]
147    pub const fn from_byte13_low_nibble(nibble: u8) -> Self {
148        match nibble & 0x0F {
149            0x2 => Self::Rp2C04_0001,
150            0x3 => Self::Rp2C04_0002,
151            0x4 => Self::Rp2C04_0003,
152            0x5 => Self::Rp2C04_0004,
153            0x8 => Self::Rc2C05_01,
154            0x9 => Self::Rc2C05_02,
155            0xA => Self::Rc2C05_03,
156            0xB => Self::Rc2C05_04,
157            // $0 = any RP2C03/RC2C03 variant; $1, $6, $7, $C-$F are reserved.
158            // All resolve to the 2C03 (the most common RGB PPU).
159            _ => Self::Rp2C03,
160        }
161    }
162
163    /// Resolve to the output palette.
164    #[must_use]
165    pub const fn ppu_palette(self) -> VsPpuPalette {
166        match self {
167            Self::None => VsPpuPalette::Composite2C02,
168            Self::Rp2C03 => VsPpuPalette::Rgb2C03,
169            Self::Rp2C04_0001 => VsPpuPalette::Rgb2C04_0001,
170            Self::Rp2C04_0002 => VsPpuPalette::Rgb2C04_0002,
171            Self::Rp2C04_0003 => VsPpuPalette::Rgb2C04_0003,
172            Self::Rp2C04_0004 => VsPpuPalette::Rgb2C04_0004,
173            // All 2C05 variants share the 2C03 master palette.
174            Self::Rc2C05_01 | Self::Rc2C05_02 | Self::Rc2C05_03 | Self::Rc2C05_04 => {
175                VsPpuPalette::Rgb2C05
176            }
177        }
178    }
179
180    /// True for the 2C05 series ($2000/$2001 swap + $2002 identifier).
181    #[must_use]
182    pub const fn is_2c05(self) -> bool {
183        matches!(
184            self,
185            Self::Rc2C05_01 | Self::Rc2C05_02 | Self::Rc2C05_03 | Self::Rc2C05_04
186        )
187    }
188
189    /// The byte returned in the low 5 bits of `$2002` on a 2C05 (0 otherwise).
190    ///
191    /// The 2C05-01 signature is unknown (the only known game, Ninja
192    /// Jajamaru-kun, never reads it), so it returns 0.
193    #[must_use]
194    pub const fn ppu_2c05_id(self) -> u8 {
195        match self {
196            Self::Rc2C05_02 => 0x3D,
197            Self::Rc2C05_03 => 0x1C,
198            Self::Rc2C05_04 => 0x1B,
199            // 2C05-01 signature unknown -> 0; non-2C05 -> 0.
200            _ => 0x00,
201        }
202    }
203}
204
205/// Errors returned by [`crate::parse`].
206///
207/// Marked `#[non_exhaustive]` so new variants can be added without breaking
208/// downstream `match` arms.
209#[derive(Debug, Error)]
210#[non_exhaustive]
211pub enum RomError {
212    /// File ended before the header / declared sections finished loading.
213    #[error("rom is truncated: needed at least {needed} bytes, got {got}")]
214    Truncated {
215        /// Minimum number of bytes required to satisfy the header.
216        needed: usize,
217        /// Number of bytes actually present.
218        got: usize,
219    },
220
221    /// Magic bytes did not match `"NES\x1A"`.
222    #[error("rom magic bytes do not match \"NES\\x1A\"")]
223    BadMagic,
224
225    /// The file is a Famicom Disk System disk image (fwNES `"FDS\x1A"` header
226    /// or a raw `"*NINTENDO-HVC*"` disk side), handed to the CARTRIDGE parser.
227    /// FDS is supported since v2.2.0, but as a separate sub-platform: a disk
228    /// loads through `rustynes_core::Nes::from_disk` together with the
229    /// `disksys.rom` BIOS, which the desktop and web frontends do. It is
230    /// detected here so a host without that path (the mobile apps, which load
231    /// iNES / NES 2.0 only) shows a clear message instead of a generic
232    /// bad-magic error. Until v2.7.5 the message still said FDS was "planned
233    /// for v2.2.0" (core audit §4.7).
234    #[error(
235        "this is a Famicom Disk System disk image; it loads through the disk loader with a \
236         disksys.rom BIOS, not as a cartridge"
237    )]
238    FdsUnsupported,
239
240    /// Mapper id is outside the coverage matrix for this build.
241    #[error("mapper {0} is not yet implemented")]
242    UnsupportedMapper(u16),
243
244    /// The header parsed but encoded an internally inconsistent configuration
245    /// (e.g., trainer flag set on a NES 2.0 ROM that has no trainer bytes).
246    #[error("rom configuration is invalid: {0}")]
247    InvalidConfig(String),
248}
249
250/// Concrete iNES / NES 2.0 cartridge value.
251///
252/// `prg_rom` and `chr_rom` are read-only ROM banks. `prg_ram_size` and
253/// `chr_ram_size` are *requested* sizes; mapper implementations allocate the
254/// matching RAM buffers in their constructors.
255///
256/// Field set follows `docs/cartridge-format.md` §Public API. `mapper` (the
257/// boxed `dyn Mapper` from `docs/mappers.md`) is constructed by
258/// [`crate::parse`] and stored on the cartridge separately from this metadata
259/// header so the metadata is cheap to clone.
260///
261/// `#[non_exhaustive]` since v3.0.0 (T-API-EXTENSIBLE): outside this crate a
262/// cartridge comes from [`crate::parse`] (or the other loaders), or from
263/// [`Cartridge::synthetic`] for a device with no cartridge header; set the
264/// public fields that differ afterwards. A later header field is then not a
265/// break, as `nametable_wiring_bits` was at v2.9.9.
266#[derive(Debug, Clone)]
267#[allow(clippy::struct_excessive_bools)] // header flags map 1:1 to NES 2.0 bits
268#[non_exhaustive]
269pub struct Cartridge {
270    /// PRG-ROM bytes. Length is a multiple of 16 KiB for standard sizes; may
271    /// be irregular when the NES 2.0 exponent-multiplier encoding is used.
272    pub prg_rom: Box<[u8]>,
273    /// CHR-ROM bytes. Empty when the cartridge uses CHR-RAM (length 0).
274    pub chr_rom: Box<[u8]>,
275    /// 12-bit mapper id (iNES 1.0 only uses the low 8 bits).
276    pub mapper_id: u16,
277    /// 4-bit submapper id (NES 2.0 only; always 0 for iNES 1.0).
278    pub submapper: u8,
279    /// Initial mirroring as selected by the header.
280    pub mirroring: Mirroring,
281    /// Region from NES 2.0 byte 12; defaults to [`Region::Ntsc`] for iNES 1.0.
282    pub region: Region,
283    /// Console type from NES 2.0 byte 7; always [`ConsoleType::Nes`] for iNES 1.0.
284    pub console_type: ConsoleType,
285    /// Vs. System PPU type from NES 2.0 byte 13 (low nibble), valid only when
286    /// `console_type == ConsoleType::VsSystem`; [`VsPpuType::None`] otherwise.
287    pub vs_ppu_type: VsPpuType,
288    /// True when the header marks a Vs. `DualSystem` board (NES 2.0 byte-13
289    /// high nibble = Vs. hardware type 5/6). Detection only; the two-CPU/two-PPU
290    /// emulation is a documented v2.0 deferral.
291    pub vs_dual_system: bool,
292    /// Requested PRG-RAM size in bytes.
293    pub prg_ram_size: u32,
294    /// Requested CHR-RAM size in bytes (0 if the cart ships with CHR-ROM only).
295    pub chr_ram_size: u32,
296    /// True if the cartridge has non-volatile save data: battery-backed
297    /// PRG-RAM per the header, or (v2.9.6) a self-flashable board's flash
298    /// (mappers 30 and 111), set by the loader whatever the header's bit says.
299    pub has_battery: bool,
300    /// True if a 512-byte trainer was present in the file (loaded at $7000-$71FF).
301    pub has_trainer: bool,
302    /// True if the file format is NES 2.0 (vs. iNES 1.0).
303    pub is_nes2: bool,
304    /// The raw iNES byte-6 nametable bits, `byte6 & 0x09` (bit 3 four-screen,
305    /// bit 0 the arrangement bit), before [`Self::mirroring`] folds them.
306    /// Mappers 30 (UNROM 512) and 218 (Magic Floor) wire CIRAM from these
307    /// bits directly, so two headers that both say `FourScreen` can build
308    /// different machines; v2.9.9 carries the bits so the movie and netplay
309    /// board check can tell them apart (core re-audit NC-10). Zero for
310    /// formats without an iNES header (FDS, NSF).
311    pub nametable_wiring_bits: u8,
312}
313
314impl Cartridge {
315    /// v3.0.0 — metadata for a machine whose program does not come from a
316    /// cartridge header (the FDS, an NSF player): empty PRG/CHR-ROM, NTSC,
317    /// horizontal mirroring, a stock NES, no battery, trainer or NES 2.0
318    /// header, and the given mapper id and RAM sizes. The bus consults little
319    /// of it for such a machine; the device owns its own storage. Set any
320    /// field that differs after construction.
321    #[must_use]
322    pub fn synthetic(mapper_id: u16, prg_ram_size: u32, chr_ram_size: u32) -> Self {
323        Self {
324            prg_rom: Box::default(),
325            chr_rom: Box::default(),
326            mapper_id,
327            submapper: 0,
328            mirroring: Mirroring::Horizontal,
329            region: Region::Ntsc,
330            console_type: ConsoleType::Nes,
331            vs_ppu_type: VsPpuType::None,
332            vs_dual_system: false,
333            prg_ram_size,
334            chr_ram_size,
335            has_battery: false,
336            has_trainer: false,
337            is_nes2: false,
338            nametable_wiring_bits: 0,
339        }
340    }
341
342    /// Returns `true` when this cartridge ships PRG-ROM only (no CHR-ROM bank).
343    #[must_use]
344    pub fn uses_chr_ram(&self) -> bool {
345        self.chr_rom.is_empty()
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    #[test]
354    fn vs_ppu_type_decodes_byte13_nibble() {
355        assert_eq!(VsPpuType::from_byte13_low_nibble(0x0), VsPpuType::Rp2C03);
356        assert_eq!(
357            VsPpuType::from_byte13_low_nibble(0x2),
358            VsPpuType::Rp2C04_0001
359        );
360        assert_eq!(
361            VsPpuType::from_byte13_low_nibble(0x3),
362            VsPpuType::Rp2C04_0002
363        );
364        assert_eq!(
365            VsPpuType::from_byte13_low_nibble(0x4),
366            VsPpuType::Rp2C04_0003
367        );
368        assert_eq!(
369            VsPpuType::from_byte13_low_nibble(0x5),
370            VsPpuType::Rp2C04_0004
371        );
372        assert_eq!(VsPpuType::from_byte13_low_nibble(0x8), VsPpuType::Rc2C05_01);
373        assert_eq!(VsPpuType::from_byte13_low_nibble(0x9), VsPpuType::Rc2C05_02);
374        assert_eq!(VsPpuType::from_byte13_low_nibble(0xA), VsPpuType::Rc2C05_03);
375        assert_eq!(VsPpuType::from_byte13_low_nibble(0xB), VsPpuType::Rc2C05_04);
376        // Reserved nibbles fall back to a 2C03.
377        assert_eq!(VsPpuType::from_byte13_low_nibble(0x1), VsPpuType::Rp2C03);
378        assert_eq!(VsPpuType::from_byte13_low_nibble(0xF), VsPpuType::Rp2C03);
379    }
380
381    #[test]
382    fn vs_ppu_type_resolves_palette_and_quirks() {
383        assert_eq!(VsPpuType::None.ppu_palette(), VsPpuPalette::Composite2C02);
384        assert!(!VsPpuType::None.is_2c05());
385        assert_eq!(VsPpuType::Rp2C03.ppu_palette(), VsPpuPalette::Rgb2C03);
386        assert!(!VsPpuType::Rp2C03.is_2c05());
387        assert_eq!(
388            VsPpuType::Rp2C04_0002.ppu_palette(),
389            VsPpuPalette::Rgb2C04_0002
390        );
391        // All 2C05 variants share the 2C03 palette and are 2C05.
392        for t in [
393            VsPpuType::Rc2C05_01,
394            VsPpuType::Rc2C05_02,
395            VsPpuType::Rc2C05_03,
396            VsPpuType::Rc2C05_04,
397        ] {
398            assert_eq!(t.ppu_palette(), VsPpuPalette::Rgb2C05);
399            assert!(t.is_2c05());
400        }
401    }
402
403    #[test]
404    fn vs_2c05_signature_ids_match_nesdev() {
405        assert_eq!(VsPpuType::Rc2C05_01.ppu_2c05_id(), 0x00); // unknown
406        assert_eq!(VsPpuType::Rc2C05_02.ppu_2c05_id(), 0x3D);
407        assert_eq!(VsPpuType::Rc2C05_03.ppu_2c05_id(), 0x1C);
408        assert_eq!(VsPpuType::Rc2C05_04.ppu_2c05_id(), 0x1B);
409        // Non-2C05 PPUs report no id.
410        assert_eq!(VsPpuType::Rp2C03.ppu_2c05_id(), 0x00);
411        assert_eq!(VsPpuType::None.ppu_2c05_id(), 0x00);
412    }
413}