1mod mappers;
2
3use bincode::de::{BorrowDecoder, Decoder};
4use bincode::enc::Encoder;
5use bincode::error::{DecodeError, EncodeError};
6use bincode::{BorrowDecode, Decode, Encode};
7use jgenesis_common::frontend::{PartialClone, TimingMode};
8use jgenesis_common::num::GetBit;
9use jgenesis_proc_macros::MatchEachVariantMacro;
10use std::fmt::{Display, Formatter};
11use std::{io, mem};
12use thiserror::Error;
13
14use crate::bus::cartridge::mappers::action52::Action52;
15use crate::bus::cartridge::mappers::bandai::BandaiFcg;
16use crate::bus::cartridge::mappers::konami::{Vrc4, Vrc6, Vrc7};
17use crate::bus::cartridge::mappers::mmc1::Mmc1;
18use crate::bus::cartridge::mappers::mmc2::Mmc2;
19use crate::bus::cartridge::mappers::mmc3::Mmc3;
20use crate::bus::cartridge::mappers::mmc5::Mmc5;
21use crate::bus::cartridge::mappers::namco163::Namco163;
22use crate::bus::cartridge::mappers::namco175::Namco175;
23use crate::bus::cartridge::mappers::nrom::{Axrom, Bnrom, Cnrom, Gxrom, Nrom, Uxrom};
24use crate::bus::cartridge::mappers::sunsoft::Sunsoft;
25use crate::bus::cartridge::mappers::unrom512::Unrom512;
26use crate::bus::cartridge::mappers::{ChrType, NametableMirroring, PpuMapResult, unrom512};
27#[cfg(test)]
28pub(crate) use mappers::new_mmc1;
29
30#[derive(Debug, Clone, PartialClone)]
31pub struct Cartridge {
32    timing_mode: TimingMode,
33    #[partial_clone(default)]
34    prg_rom: Vec<u8>,
35    prg_ram: Vec<u8>,
36    has_ram_battery: bool,
37    prg_ram_dirty_bit: bool,
38    #[partial_clone(default)]
39    chr_rom: Vec<u8>,
40    chr_ram: Vec<u8>,
41}

Encode and Decode are implemented explicitly instead of using derive in order to avoid serializing ROM bytes as part of save states

45impl Encode for Cartridge {
46    fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
47        self.timing_mode.encode(encoder)?;
48        self.prg_ram.encode(encoder)?;
49        self.has_ram_battery.encode(encoder)?;
50        self.prg_ram_dirty_bit.encode(encoder)?;
51        self.chr_ram.encode(encoder)?;
52
53        Ok(())
54    }
55}
57impl<Context> Decode<Context> for Cartridge {
58    fn decode<D: Decoder<Context = Context>>(decoder: &mut D) -> Result<Self, DecodeError> {
59        let timing_mode = Decode::decode(decoder)?;
60        let prg_ram = Decode::decode(decoder)?;
61        let has_ram_battery = Decode::decode(decoder)?;
62        let prg_ram_dirty_bit = Decode::decode(decoder)?;
63        let chr_ram = Decode::decode(decoder)?;
64
65        Ok(Self {
66            timing_mode,
67            prg_rom: vec![],
68            prg_ram,
69            has_ram_battery,
70            prg_ram_dirty_bit,
71            chr_rom: vec![],
72            chr_ram,
73        })
74    }
75}
76
77impl<'de, Context> BorrowDecode<'de, Context> for Cartridge {
78    fn borrow_decode<D: BorrowDecoder<'de, Context = Context>>(
79        decoder: &mut D,
80    ) -> Result<Self, DecodeError> {
81        let timing_mode = BorrowDecode::borrow_decode(decoder)?;
82        let prg_ram = BorrowDecode::borrow_decode(decoder)?;
83        let has_ram_battery = BorrowDecode::borrow_decode(decoder)?;
84        let prg_ram_dirty_bit = BorrowDecode::borrow_decode(decoder)?;
85        let chr_ram = BorrowDecode::borrow_decode(decoder)?;
86
87        Ok(Self {
88            timing_mode,
89            prg_rom: vec![],
90            prg_ram,
91            has_ram_battery,
92            prg_ram_dirty_bit,
93            chr_rom: vec![],
94            chr_ram,
95        })
96    }
97}
98
99impl Cartridge {
100    fn get_prg_rom(&self, address: u32) -> u8 {
101        self.prg_rom[(address as usize) & (self.prg_rom.len() - 1)]
102    }
103
104    fn get_prg_ram(&self, address: u32) -> u8 {
105        if self.prg_ram.is_empty() {
106            return 0xFF;
107        }
108
109        self.prg_ram[(address as usize) & (self.prg_ram.len() - 1)]
110    }
111
112    fn set_prg_ram(&mut self, address: u32, value: u8) {
113        if self.prg_ram.is_empty() {
114            return;
115        }
116
117        let prg_ram_len = self.prg_ram.len();
118        self.prg_ram[(address as usize) & (prg_ram_len - 1)] = value;
119        if self.has_ram_battery {
120            self.prg_ram_dirty_bit = true;
121        }
122    }
123
124    fn get_chr_rom(&self, address: u32) -> u8 {
125        if self.chr_rom.is_empty() {
126            return 0xFF;
127        }
128
129        self.chr_rom[(address as usize) & (self.chr_rom.len() - 1)]
130    }
131
132    fn get_chr_ram(&self, address: u32) -> u8 {
133        if self.chr_ram.is_empty() {
134            return 0xFF;
135        }
136
137        self.chr_ram[(address as usize) & (self.chr_ram.len() - 1)]
138    }
139
140    fn set_chr_ram(&mut self, address: u32, value: u8) {
141        if self.chr_ram.is_empty() {
142            return;
143        }
144
145        let chr_ram_len = self.chr_ram.len();
146        self.chr_ram[(address as usize) & (chr_ram_len - 1)] = value;
147    }
148
149    fn move_rom_from(&mut self, other: &mut Self) {
150        self.prg_rom = mem::take(&mut other.prg_rom);
151        self.chr_rom = mem::take(&mut other.chr_rom);
152    }
153}
154
155#[derive(Debug, Clone, Encode, Decode)]
156pub(crate) struct MapperImpl<MapperData> {
157    cartridge: Cartridge,
158    data: MapperData,
159}

Not using derive macro because it only impls the trait when MapperData implements PartialClone, which is not a necessary bound here

163impl<MapperData: Clone> PartialClone for MapperImpl<MapperData> {
164    fn partial_clone(&self) -> Self {
165        Self { cartridge: self.cartridge.partial_clone(), data: self.data.clone() }
166    }
167}
169pub(crate) trait HasBasicPpuMapping {
170    fn map_ppu_address(&self, address: u16) -> PpuMapResult;
171}
172
173impl<MapperData> MapperImpl<MapperData>
174where
175    MapperImpl<MapperData>: HasBasicPpuMapping,
176{
177    fn read_ppu_address(&self, address: u16, vram: &[u8; 2048]) -> u8 {
178        self.map_ppu_address(address).read(&self.cartridge, vram)
179    }
180
181    fn write_ppu_address(&mut self, address: u16, value: u8, vram: &mut [u8; 2048]) {
182        self.map_ppu_address(address).write(value, &mut self.cartridge, vram);
183    }
184}
185
186#[allow(clippy::large_enum_variant)]
187#[derive(Debug, Clone, Encode, Decode, PartialClone, MatchEachVariantMacro)]
188pub(crate) enum Mapper {
189    Action52(#[partial_clone(partial)] MapperImpl<Action52>),
190    Axrom(#[partial_clone(partial)] MapperImpl<Axrom>),
191    BandaiFcg(#[partial_clone(partial)] MapperImpl<BandaiFcg>),
192    Bnrom(#[partial_clone(partial)] MapperImpl<Bnrom>),
193    Cnrom(#[partial_clone(partial)] MapperImpl<Cnrom>),
194    Gxrom(#[partial_clone(partial)] MapperImpl<Gxrom>),
195    Mmc1(#[partial_clone(partial)] MapperImpl<Mmc1>),
196    Mmc2(#[partial_clone(partial)] MapperImpl<Mmc2>),
197    Mmc3(#[partial_clone(partial)] MapperImpl<Mmc3>),
198    Mmc5(#[partial_clone(partial)] MapperImpl<Mmc5>),
199    Namco163(#[partial_clone(partial)] MapperImpl<Namco163>),
200    Namco175(#[partial_clone(partial)] MapperImpl<Namco175>),
201    Nrom(#[partial_clone(partial)] MapperImpl<Nrom>),
202    Sunsoft(#[partial_clone(partial)] MapperImpl<Sunsoft>),
203    Unrom512(#[partial_clone(partial)] MapperImpl<Unrom512>),
204    Uxrom(#[partial_clone(partial)] MapperImpl<Uxrom>),
205    Vrc4(#[partial_clone(partial)] MapperImpl<Vrc4>),
206    Vrc6(#[partial_clone(partial)] MapperImpl<Vrc6>),
207    Vrc7(#[partial_clone(partial)] MapperImpl<Vrc7>),
208}
209
210impl Mapper {

Retrieve the mapper's user-readable name. Only used for logging output.

212    pub(crate) fn name(&self) -> &'static str {
213        match self {
214            Self::Action52(..) => "Action 52",
215            Self::Axrom(..) => "AxROM",
216            Self::BandaiFcg(bandai_fcg) => bandai_fcg.name(),
217            Self::Bnrom(..) => "BNROM / NINA-001",
218            Self::Cnrom(..) => "CNROM",
219            Self::Gxrom(gxrom) => gxrom.name(),
220            Self::Mmc1(..) => "MMC1",
221            Self::Mmc2(mmc2) => mmc2.name(),
222            Self::Mmc3(mmc3) => mmc3.name(),
223            Self::Mmc5(..) => "MMC5",
224            Self::Namco163(..) => "Namco 163",
225            Self::Namco175(..) => "Namco 175",
226            Self::Nrom(..) => "NROM",
227            Self::Sunsoft(..) => "Sunsoft",
228            Self::Unrom512(..) => "UNROM 512",
229            Self::Uxrom(uxrom) => uxrom.name(),
230            Self::Vrc4(vrc4) => vrc4.name(),
231            Self::Vrc6(..) => "VRC6",
232            Self::Vrc7(..) => "VRC7",
233        }
234    }

Read a value from the given address in the CPU address space.

237    pub(crate) fn read_cpu_address(&mut self, address: u16, cpu_open_bus: u8) -> u8 {
238        match_each_variant!(self, mapper => mapper.read_cpu_address(address, cpu_open_bus))
239    }

Write a value to the given address in the CPU address space.

242    pub(crate) fn write_cpu_address(&mut self, address: u16, value: u8) {
243        match_each_variant!(self, mapper => mapper.write_cpu_address(address, value));
244    }

Read a value from the given address in the PPU address space.

247    pub(crate) fn read_ppu_address(&mut self, address: u16, vram: &[u8; 2048]) -> u8 {
248        match_each_variant!(self, mapper => mapper.read_ppu_address(address, vram))
249    }

Write a value to the given address in the PPU address space.

252    pub(crate) fn write_ppu_address(&mut self, address: u16, value: u8, vram: &mut [u8; 2048]) {
253        match_each_variant!(self, mapper => mapper.write_ppu_address(address, value, vram));
254    }

Perform any processing that should be performed after every PPU cycle.

257    pub(crate) fn tick(&mut self, ppu_bus_address: u16) {
258        if let Self::Mmc3(mmc3) = self {
259            mmc3.tick(ppu_bus_address);
260        }
261    }

Perform any processing that should be performed after every CPU cycle. Commonly used for interrupt counters and expansion audio.

265    pub(crate) fn tick_cpu(&mut self) {
266        match self {
267            Self::BandaiFcg(bandai_fcg) => {
268                bandai_fcg.tick_cpu();
269            }
270            Self::Mmc1(mmc1) => {
271                mmc1.tick_cpu();
272            }
273            Self::Mmc5(mmc5) => {
274                mmc5.tick_cpu();
275            }
276            Self::Namco163(namco163) => {
277                namco163.tick_cpu();
278            }
279            Self::Sunsoft(sunsoft) => {
280                sunsoft.tick_cpu();
281            }
282            Self::Vrc4(vrc4) => {
283                vrc4.tick_cpu();
284            }
285            Self::Vrc6(vrc6) => {
286                vrc6.tick_cpu();
287            }
288            Self::Vrc7(vrc7) => {
289                vrc7.tick_cpu();
290            }
291            _ => {}
292        }
293    }

Return whether this board is currently generating an IRQ.

296    pub(crate) fn interrupt_flag(&self) -> bool {
297        match self {
298            Self::BandaiFcg(bandai_fcg) => bandai_fcg.interrupt_flag(),
299            Self::Mmc3(mmc3) => mmc3.interrupt_flag(),
300            Self::Mmc5(mmc5) => mmc5.interrupt_flag(),
301            Self::Namco163(namco163) => namco163.interrupt_flag(),
302            Self::Sunsoft(sunsoft) => sunsoft.interrupt_flag(),
303            Self::Vrc4(vrc4) => vrc4.interrupt_flag(),
304            Self::Vrc6(vrc6) => vrc6.interrupt_flag(),
305            Self::Vrc7(vrc7) => vrc7.interrupt_flag(),
306            _ => false,
307        }
308    }

Process a PPUCTRL write. Only needed by the MMC5 mapper in order to know whether double height sprites are enabled.

312    pub(crate) fn process_ppu_ctrl_update(&mut self, value: u8) {
313        if let Self::Mmc5(mmc5) = self {
314            mmc5.process_ppu_ctrl_update(value);
315        }
316    }

Process a PPUMASK write. Used by MMC5 to know whether rendering is currently enabled

319    pub(crate) fn process_ppu_mask_update(&mut self, value: u8) {
320        if let Self::Mmc5(mmc5) = self {
321            mmc5.process_ppu_mask_update(value);
322        }
323    }

Notify the mapper that the CPU will imminently access the PPUDATA register. This is required by MMC5 to map PPUDATA reads/writes to the correct CHR banks.

This should be called before the actual memory access.

329    pub(crate) fn about_to_access_ppu_data(&mut self) {
330        if let Self::Mmc5(mmc5) = self {
331            mmc5.about_to_access_ppu_data();
332        }
333    }

Return whether the board's writable memory (if any) has been written to since the last time this method was called.

337    pub(crate) fn get_and_clear_ram_dirty_bit(&mut self) -> bool {
338        match self {
339            Mapper::BandaiFcg(mapper) => {
340                if mapper.get_and_clear_eeprom_dirty_bit() {
341                    return true;
342                }
343            }
344            Mapper::Namco163(mapper) if mapper.has_battery_backed_internal_ram() => {
345                if mapper.get_and_clear_internal_ram_dirty_bit() {
346                    return true;
347                }
348            }
349            Mapper::Unrom512(mapper) => {
350                if mapper.get_and_clear_dirty_bit() {
351                    return true;
352                }
353            }
354            _ => {}
355        }
356
357        match_each_variant!(self, mapper => {
358            let dirty_bit = mapper.cartridge.prg_ram_dirty_bit;
359            mapper.cartridge.prg_ram_dirty_bit = false;
360            dirty_bit
361        })
362    }

Return the board's writable memory as a slice. This will be an empty slice if the board has no PRG RAM or EEPROM.

366    pub(crate) fn get_prg_ram(&self) -> &[u8] {
367        match self {
368            Mapper::BandaiFcg(mapper) if let Some(eeprom) = mapper.eeprom() => {
369                return eeprom;
370            }
371            Mapper::Namco163(mapper) if mapper.has_battery_backed_internal_ram() => {
372                return mapper.get_internal_ram();
373            }
374            Mapper::Unrom512(mapper) if mapper.is_flashable() => {
375                // Some UNROM 512 cartridges have flashable PRG ROM that is used to store save data
376                return &mapper.cartridge.prg_rom;
377            }
378            _ => {}
379        }
380
381        match_each_variant!(self, mapper => &mapper.cartridge.prg_ram)
382    }

Retrieve the timing mode of the cartridge (NTSC/PAL).

385    pub(crate) fn timing_mode(&self) -> TimingMode {
386        match_each_variant!(self, mapper => mapper.cartridge.timing_mode)
387    }

If the board has expansion audio, generate an audio sample and mix it with the mixed APU sample.

If the board does not have expansion audio or it is not enabled then this method will simply return the mixed APU sample as-is.

394    pub(crate) fn sample_audio(&self, mixed_apu_sample: f64) -> f64 {
395        match self {
396            Self::Mmc5(mmc5) => mmc5.sample_audio(mixed_apu_sample),
397            Self::Namco163(namco163) => namco163.sample_audio(mixed_apu_sample),
398            Self::Sunsoft(sunsoft) => sunsoft.sample_audio(mixed_apu_sample),
399            Self::Vrc6(vrc6) => vrc6.sample_audio(mixed_apu_sample),
400            Self::Vrc7(vrc7) => vrc7.sample_audio(mixed_apu_sample),
401            _ => mixed_apu_sample,
402        }
403    }

Move cartridge ROM fields from another Mapper instance. Used when loading save states.

406    pub(crate) fn move_rom_from(&mut self, other: &mut Self) {
407        let other_cartridge = match_each_variant!(other, mapper => &mut mapper.cartridge);
408        match_each_variant!(self, mapper => mapper.cartridge.move_rom_from(other_cartridge));
409    }
411    pub(crate) fn reset(&mut self) {
412        if let Self::Action52(action52) = self {
413            action52.reset();
414        }
415    }
416}
417
418#[derive(Debug, Error)]
419pub enum CartridgeFileError {
420    #[error("I/O error: {source}")]
421    Io {
422        #[from]
423        source: io::Error,
424    },
425    #[error("invalid or unsupported file format")]
426    Format,
427    #[error("unsupported mapper: {mapper_number}")]
428    UnsupportedMapper { mapper_number: u16 },
429    #[error("cartridge header specifies both volatile and non-volatile PRG RAM")]
430    MultiplePrgRamTypes,
431    #[error(
432        "ROM is not large enough to hold an iNES header; expected at least 16 bytes, was {file_size} bytes"
433    )]
434    TooSmallForHeader { file_size: u32 },
435    #[error(
436        "Invalid PRG/CHR ROM size in ROM header: file size is {file_size} bytes, PRG ROM size is {prg_rom_size} bytes, CHR ROM size is {chr_rom_size} bytes"
437    )]
438    InvalidRomSize { file_size: u32, prg_rom_size: u32, chr_rom_size: u32 },
439    #[error("unsupported timing mode byte: {byte}")]
440    UnsupportedTimingMode { byte: u8 },
441}
442
443#[derive(Debug, Clone, Copy, PartialEq, Eq)]
444enum FileFormat {
445    INes,
446    Nes2Point0,
447}
448
449impl Display for FileFormat {
450    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
451        match self {
452            Self::INes => write!(f, "iNES"),
453            Self::Nes2Point0 => write!(f, "NES 2.0"),
454        }
455    }
456}
457
458#[derive(Debug, Clone)]
459pub struct INesHeader {
460    mapper_number: u16,
461    sub_mapper_number: u8,
462    timing_mode: TimingMode,
463    prg_rom_size: u32,
464    prg_ram_size: u32,
465    chr_rom_size: u32,
466    chr_ram_size: u32,
467    chr_type: ChrType,
468    nametable_mirroring: NametableMirroring,
469    has_trainer: bool,
470    has_battery: bool,
471    has_four_screen_vram: bool,
472}
473
474impl INesHeader {
475    fn parse_from_file(file_bytes: &[u8]) -> Result<INesHeader, CartridgeFileError> {
476        if file_bytes.len() < 16 {
477            return Err(CartridgeFileError::TooSmallForHeader {
478                file_size: file_bytes.len() as u32,
479            });
480        }
481
482        let header = &file_bytes[..16];
483
484        // All iNES headers should begin with this 4-byte sequence, which is "NES" followed by the
485        // character that MS-DOS used for EOF
486        if header[..4] != [b'N', b'E', b'S', 0x1A] {
487            return Err(CartridgeFileError::Format);
488        }
489
490        let format =
491            if header[7] & 0x0C == 0x08 { FileFormat::Nes2Point0 } else { FileFormat::INes };
492        let has_trainer = header[6].bit(2);
493        let mapper_number = u16::from((header[7] & 0xF0) | ((header[6] & 0xF0) >> 4));
494
495        let chr_rom_size = {
496            let mut chr_rom_size_8kb = u32::from(header[5]);
497            if format == FileFormat::Nes2Point0 {
498                chr_rom_size_8kb |= u32::from(header[9] & 0xF0) << 4;
499            }
500            8 * 1024 * chr_rom_size_8kb
501        };
502
503        let prg_rom_size = {
504            let mut prg_rom_size_16kb = u32::from(header[4]);
505            if format == FileFormat::Nes2Point0 {
506                prg_rom_size_16kb |= u32::from(header[9] & 0x0F) << 8;
507            }
508            let prg_rom_size = 16 * 1024 * prg_rom_size_16kb;
509
510            // Hack: Galaxian (J) only has 8KB of PRG ROM, and the iNES header usually contains a
511            // nonsensical PRG ROM size because it can't represent this
512            if should_apply_8kb_prg_rom_hack(file_bytes, mapper_number, has_trainer, chr_rom_size) {
513                log::info!("Ignoring PRG ROM size in header of {prg_rom_size} bytes");
514                8 * 1024
515            } else {
516                prg_rom_size
517            }
518        };
519
520        if header.len() + (prg_rom_size + chr_rom_size) as usize > file_bytes.len() {
521            return Err(CartridgeFileError::InvalidRomSize {
522                file_size: file_bytes.len() as u32,
523                prg_rom_size,
524                chr_rom_size,
525            });
526        }
527
528        let chr_type = if chr_rom_size == 0 { ChrType::RAM } else { ChrType::ROM };
529
530        let nametable_mirroring = if header[6].bit(0) {
531            NametableMirroring::Vertical
532        } else {
533            NametableMirroring::Horizontal
534        };
535
536        let has_four_screen_vram = header[6].bit(3);
537
538        let has_battery = header[6].bit(1);
539
540        log::info!("ROM header format: {format}");
541
542        let sub_mapper_number = match format {
543            FileFormat::Nes2Point0 => header[8] >> 4,
544            FileFormat::INes => 0,
545        };
546
547        let timing_mode = match format {
548            FileFormat::Nes2Point0 => {
549                let timing_mode_byte = header[12] & 0x03;
550                match timing_mode_byte {
551                    0x00 | 0x02 => TimingMode::Ntsc,
552                    0x01 => TimingMode::Pal,
553                    0x03 => {
554                        return Err(CartridgeFileError::UnsupportedTimingMode {
555                            byte: timing_mode_byte,
556                        });
557                    }
558                    _ => unreachable!("value & 0x03 should always be 0x00/0x01/0x02/0x03"),
559                }
560            }
561            FileFormat::INes => {
562                if header[9].bit(0) {
563                    TimingMode::Pal
564                } else {
565                    TimingMode::Ntsc
566                }
567            }
568        };
569
570        let prg_ram_size = determine_prg_ram_size(header, mapper_number, format);
571
572        let chr_ram_size = match (chr_type, format) {
573            (ChrType::RAM, FileFormat::Nes2Point0) => {
574                let chr_ram_shift = header[11] & 0x0F;
575                if chr_ram_shift > 0 { 64 << chr_ram_shift } else { 0 }
576            }
577            (ChrType::RAM, FileFormat::INes) => {
578                if mapper_number == unrom512::MAPPER_NUMBER {
579                    unrom512::INES_CHR_RAM_LEN
580                } else {
581                    8 * 1024
582                }
583            }
584            (ChrType::ROM, _) => 0,
585        };
586
587        Ok(Self {
588            mapper_number,
589            sub_mapper_number,
590            timing_mode,
591            prg_rom_size,
592            prg_ram_size,
593            chr_rom_size,
594            chr_ram_size,
595            chr_type,
596            nametable_mirroring,
597            has_trainer,
598            has_battery,
599            has_four_screen_vram,
600        })
601    }
602}

Check whether to assume the cartridge has 8KB of PRG ROM, which the iNES format cannot represent

605fn should_apply_8kb_prg_rom_hack(
606    file_bytes: &[u8],
607    mapper_number: u16,
608    has_trainer: bool,
609    chr_rom_size: u32,
610) -> bool {
611    if mapper_number != 0 {
612        // Only apply this hack for mapper 0
613        return false;
614    }
615
616    let trainer_len = if has_trainer { 512 } else { 0 };
617    let header_len = 16 + trainer_len;
618
619    file_bytes.len().saturating_sub(header_len).saturating_sub(chr_rom_size as usize) == 8 * 1024
620}
622fn determine_prg_ram_size(header: &[u8], mapper_number: u16, format: FileFormat) -> u32 {
623    let prg_ram_size = match format {
624        FileFormat::Nes2Point0 => {
625            let volatile_shift = header[10] & 0x0F;
626            let non_volatile_shift = header[10] >> 4;
627            // TODO separate these? very very few games have both volatile and non-volatile RAM
628            let volatile_ram = if volatile_shift > 0 { 64 << volatile_shift } else { 0 };
629            let non_volatile_ram =
630                if non_volatile_shift > 0 { 64 << non_volatile_shift } else { 0 };
631            let total_ram = volatile_ram + non_volatile_ram;
632
633            // Hack to handle MMC5 headers that don't specify PRG RAM size but expect 32KB/64KB of
634            // PRG RAM
635            if mapper_number == 5 && total_ram == 0 {
636                log::info!(
637                    "Ignoring PRG RAM size of 0 in MMC5 NES 2.0 header; setting to 64KB instead"
638                );
639                None
640            } else {
641                Some(total_ram)
642            }
643        }
644        FileFormat::INes => None,
645    };
646
647    // Default to 64KB for MMC5, 8KB for all other mappers
648    let default_ram_size = match mapper_number {
649        5 => 64 * 1024,
650        _ => 8 * 1024,
651    };
652    prg_ram_size.unwrap_or(default_ram_size)
653}

Parse cartridge data out of an iNES file.

Errors

This function will return an error if the given bytes do not appear to represent an iNES file.

660pub(crate) fn from_ines_file(
661    file_bytes: &[u8],
662    sav_bytes: Option<Vec<u8>>,
663    forced_timing_mode: Option<TimingMode>,
664) -> Result<Mapper, CartridgeFileError> {
665    let header = INesHeader::parse_from_file(file_bytes)?;
666
667    // Header is 16 bytes, trainer is 512 bytes if present
668    let prg_rom_start_address = if header.has_trainer { 16 + 512 } else { 16 } as usize;
669    let prg_rom_end_address = prg_rom_start_address + header.prg_rom_size as usize;
670    let chr_rom_end_address = prg_rom_end_address + header.chr_rom_size as usize;
671
672    let mut prg_rom = Vec::from(&file_bytes[prg_rom_start_address..prg_rom_end_address]);
673    let mut chr_rom = Vec::from(&file_bytes[prg_rom_end_address..chr_rom_end_address]);
674    if !chr_rom.is_empty() {
675        jgenesis_common::rom::mirror_to_next_power_of_two(&mut chr_rom);
676    }
677
678    // UNROM 512 stores save data in flashable PRG ROM instead of PRG RAM; replace PRG ROM with
679    // save contents if save is present and size matches
680    let original_prg_rom = prg_rom.clone();
681    if header.mapper_number == unrom512::MAPPER_NUMBER {
682        match &sav_bytes {
683            Some(sav_bytes) if sav_bytes.len() == prg_rom.len() => {
684                log::info!("Replacing UNROM 512 PRG ROM with contents of save file");
685                prg_rom.clone_from(sav_bytes);
686            }
687            _ => {}
688        }
689    }
690
691    let prg_ram = match &sav_bytes {
692        Some(sav_bytes) if sav_bytes.len() == header.prg_ram_size as usize => sav_bytes.clone(),
693        _ => vec![0xFF; header.prg_ram_size as usize],
694    };
695
696    let timing_mode = forced_timing_mode.unwrap_or(header.timing_mode);
697    if timing_mode != header.timing_mode {
698        log::info!(
699            "Forcing timing mode to {timing_mode}; ignoring timing mode {} from cartridge header",
700            header.timing_mode
701        );
702    }
703
704    let cartridge = Cartridge {
705        timing_mode,
706        prg_rom,
707        prg_ram,
708        has_ram_battery: header.has_battery,
709        prg_ram_dirty_bit: header.has_battery,
710        chr_rom,
711        chr_ram: vec![0; header.chr_ram_size as usize],
712    };
713
714    let chr_size = match header.chr_type {
715        ChrType::ROM => cartridge.chr_rom.len() as u32,
716        ChrType::RAM => header.chr_ram_size,
717    };
718
719    let mapper = match header.mapper_number {
720        0 => Mapper::Nrom(MapperImpl {
721            cartridge,
722            data: Nrom::new(header.chr_type, header.nametable_mirroring),
723        }),
724        1 => Mapper::Mmc1(MapperImpl { cartridge, data: Mmc1::new(header.chr_type) }),
725        2 | 71 => Mapper::Uxrom(MapperImpl {
726            cartridge,
727            data: Uxrom::new(
728                header.mapper_number,
729                header.sub_mapper_number,
730                header.chr_type,
731                header.nametable_mirroring,
732            ),
733        }),
734        3 => Mapper::Cnrom(MapperImpl {
735            cartridge,
736            data: Cnrom::new(header.chr_type, header.nametable_mirroring, header.sub_mapper_number),
737        }),
738        4 | 76 | 88 | 95 | 154 | 206 => Mapper::Mmc3(MapperImpl {
739            cartridge,
740            data: Mmc3::new(
741                header.chr_type,
742                header.prg_rom_size,
743                chr_size,
744                header.mapper_number,
745                header.sub_mapper_number,
746                header.nametable_mirroring,
747                header.has_four_screen_vram,
748            ),
749        }),
750        5 => Mapper::Mmc5(MapperImpl { cartridge, data: Mmc5::new() }),
751        7 => Mapper::Axrom(MapperImpl { cartridge, data: Axrom::new(header.chr_type) }),
752        9 => Mapper::Mmc2(MapperImpl { cartridge, data: Mmc2::new_mmc2() }),
753        10 => Mapper::Mmc2(MapperImpl { cartridge, data: Mmc2::new_mmc4() }),
754        11 | 66 | 140 => Mapper::Gxrom(MapperImpl {
755            cartridge,
756            data: Gxrom::new(header.mapper_number, header.nametable_mirroring),
757        }),
758        16 | 153 | 159 => Mapper::BandaiFcg(MapperImpl {
759            cartridge,
760            data: BandaiFcg::new(
761                header.mapper_number,
762                header.sub_mapper_number,
763                header.chr_type,
764                header.prg_ram_size,
765                sav_bytes.as_ref(),
766            ),
767        }),
768        19 => Mapper::Namco163(MapperImpl {
769            cartridge,
770            data: Namco163::new(
771                header.sub_mapper_number,
772                header.chr_type,
773                header.has_battery,
774                header.prg_ram_size,
775                sav_bytes,
776            ),
777        }),
778        21 | 22 | 23 | 25 => Mapper::Vrc4(MapperImpl {
779            cartridge,
780            data: Vrc4::new(header.mapper_number, header.sub_mapper_number, header.chr_type),
781        }),
782        24 | 26 => Mapper::Vrc6(MapperImpl {
783            cartridge,
784            data: Vrc6::new(header.mapper_number, header.chr_type),
785        }),
786        30 => {
787            let mapper = Unrom512::new(&original_prg_rom, &header);
788            Mapper::Unrom512(MapperImpl { cartridge, data: mapper })
789        }
790        34 => Mapper::Bnrom(MapperImpl {
791            cartridge,
792            data: Bnrom::new(header.chr_type, header.nametable_mirroring),
793        }),
794        69 => Mapper::Sunsoft(MapperImpl { cartridge, data: Sunsoft::new(header.chr_type) }),
795        85 => Mapper::Vrc7(MapperImpl {
796            cartridge,
797            data: Vrc7::new(header.sub_mapper_number, header.chr_type),
798        }),
799        210 => Mapper::Namco175(MapperImpl {
800            cartridge,
801            data: Namco175::new(
802                header.sub_mapper_number,
803                header.chr_type,
804                header.nametable_mirroring,
805            ),
806        }),
807        228 => Mapper::Action52(MapperImpl { cartridge, data: Action52::new(header.prg_rom_size) }),
808        _ => {
809            return Err(CartridgeFileError::UnsupportedMapper {
810                mapper_number: header.mapper_number,
811            });
812        }
813    };
814
815    log::info!("Timing mode: {timing_mode}");
816    log::info!("Mapper number: {} ({})", header.mapper_number, mapper.name());
817    log::info!("PRG ROM size: {}", header.prg_rom_size);
818    log::info!("PRG RAM size: {}", header.prg_ram_size);
819    log::info!("Cartridge has battery-backed PRG RAM: {}", header.has_battery);
820    log::info!("CHR ROM size: {}", header.chr_rom_size);
821    log::info!("CHR RAM size: {}", header.chr_ram_size);
822    log::info!("CHR memory type: {:?}", header.chr_type);
823    log::info!(
824        "Hardwired nametable mirroring: {:?} (not applicable to all mappers)",
825        header.nametable_mirroring
826    );
827    log::info!("Has 4-screen nametable VRAM: {}", header.has_four_screen_vram);
828
829    Ok(mapper)
830}