rustynes_cpu/
ines.rs

1//! iNES ROM format loader.
2//!
3//! This module provides basic iNES ROM loading functionality for testing purposes.
4//! For the full emulator, a more comprehensive ROM loader will be in rustynes-core.
5
6use std::fs::File;
7use std::io::{self, Read};
8use std::path::Path;
9
10/// iNES ROM header (16 bytes).
11#[derive(Debug, Clone)]
12pub struct INesHeader {
13    /// PRG-ROM size in 16 KB units
14    pub prg_rom_size: u8,
15    /// CHR-ROM size in 8 KB units (0 means CHR-RAM)
16    pub chr_rom_size: u8,
17    /// Mapper number
18    pub mapper: u8,
19    /// Mirroring type (0 = horizontal, 1 = vertical)
20    pub mirroring: u8,
21    /// Has battery-backed RAM
22    pub battery: bool,
23    /// Has trainer
24    pub trainer: bool,
25    /// Four-screen VRAM
26    pub four_screen: bool,
27}
28
29impl INesHeader {
30    /// Parse iNES header from bytes.
31    ///
32    /// # Errors
33    ///
34    /// Returns an error if the header does not contain the iNES magic number ("NES\x1A").
35    pub fn parse(header: &[u8; 16]) -> Result<Self, String> {
36        // Check magic number "NES\x1A"
37        if header[0] != b'N' || header[1] != b'E' || header[2] != b'S' || header[3] != 0x1A {
38            return Err("Invalid iNES header magic number".to_string());
39        }
40
41        let prg_rom_size = header[4];
42        let chr_rom_size = header[5];
43        let flags6 = header[6];
44        let flags7 = header[7];
45
46        // Extract mapper number
47        let mapper_lo = (flags6 & 0xF0) >> 4;
48        let mapper_hi = flags7 & 0xF0;
49        let mapper = mapper_hi | mapper_lo;
50
51        // Extract flags
52        let mirroring = flags6 & 0x01;
53        let battery = (flags6 & 0x02) != 0;
54        let trainer = (flags6 & 0x04) != 0;
55        let four_screen = (flags6 & 0x08) != 0;
56
57        Ok(Self {
58            prg_rom_size,
59            chr_rom_size,
60            mapper,
61            mirroring,
62            battery,
63            trainer,
64            four_screen,
65        })
66    }
67}
68
69/// iNES ROM file.
70#[derive(Debug, Clone)]
71pub struct INesRom {
72    /// ROM header
73    pub header: INesHeader,
74    /// PRG-ROM data (program code)
75    pub prg_rom: Vec<u8>,
76    /// CHR-ROM data (graphics)
77    pub chr_rom: Vec<u8>,
78}
79
80impl INesRom {
81    /// Load an iNES ROM file from disk.
82    ///
83    /// # Errors
84    ///
85    /// Returns an IO error if the file cannot be opened or read, or if the ROM data is invalid.
86    pub fn load<P: AsRef<Path>>(path: P) -> io::Result<Self> {
87        let mut file = File::open(path)?;
88        let mut buffer = Vec::new();
89        file.read_to_end(&mut buffer)?;
90
91        Self::from_bytes(&buffer).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
92    }
93
94    /// Parse iNES ROM from bytes.
95    ///
96    /// # Errors
97    ///
98    /// Returns an error if the data is too small, contains an invalid iNES header,
99    /// or has invalid PRG-ROM or CHR-ROM sizes.
100    pub fn from_bytes(data: &[u8]) -> Result<Self, String> {
101        if data.len() < 16 {
102            return Err("ROM file too small".to_string());
103        }
104
105        // Parse header
106        let mut header_bytes = [0u8; 16];
107        header_bytes.copy_from_slice(&data[0..16]);
108        let header = INesHeader::parse(&header_bytes)?;
109
110        let mut offset = 16;
111
112        // Skip trainer if present (512 bytes)
113        if header.trainer {
114            offset += 512;
115        }
116
117        // Read PRG-ROM
118        let prg_rom_bytes = (header.prg_rom_size as usize) * 16384;
119        if data.len() < offset + prg_rom_bytes {
120            return Err("Invalid PRG-ROM size".to_string());
121        }
122        let prg_rom = data[offset..offset + prg_rom_bytes].to_vec();
123        offset += prg_rom_bytes;
124
125        // Read CHR-ROM
126        let chr_rom_bytes = (header.chr_rom_size as usize) * 8192;
127        let chr_rom = if chr_rom_bytes > 0 {
128            if data.len() < offset + chr_rom_bytes {
129                return Err("Invalid CHR-ROM size".to_string());
130            }
131            data[offset..offset + chr_rom_bytes].to_vec()
132        } else {
133            // CHR-RAM
134            vec![0; 8192]
135        };
136
137        Ok(Self {
138            header,
139            prg_rom,
140            chr_rom,
141        })
142    }
143
144    /// Get PRG-ROM size in bytes.
145    pub fn prg_rom_size(&self) -> usize {
146        self.prg_rom.len()
147    }
148
149    /// Get CHR-ROM size in bytes.
150    pub fn chr_rom_size(&self) -> usize {
151        self.chr_rom.len()
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_ines_header_parse() {
161        let header_bytes = [
162            b'N', b'E', b'S', 0x1A, // Magic
163            0x02, // 2 * 16KB PRG-ROM
164            0x01, // 1 * 8KB CHR-ROM
165            0x00, // Flags 6: Mapper 0, horizontal mirroring
166            0x00, // Flags 7
167            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
168        ];
169
170        let header = INesHeader::parse(&header_bytes).unwrap();
171        assert_eq!(header.prg_rom_size, 2);
172        assert_eq!(header.chr_rom_size, 1);
173        assert_eq!(header.mapper, 0);
174        assert_eq!(header.mirroring, 0);
175        assert!(!header.battery);
176        assert!(!header.trainer);
177    }
178
179    #[test]
180    fn test_ines_header_invalid_magic() {
181        let header_bytes = [
182            b'N', b'E', b'X', 0x1A, // Invalid magic
183            0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
184        ];
185
186        assert!(INesHeader::parse(&header_bytes).is_err());
187    }
188}