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}