1use bincode::{Decode, Encode};
2use jgenesis_common::num::GetBit;
3use jgenesis_proc_macros::MatchEachVariantMacro;
4use std::cmp;
5use std::ops::RangeInclusive;
6
7const CODEMASTERS_CHECKSUM_ADDR: usize = 0x7FE6;
8const SEGA_HEADER_ADDR_RANGE: RangeInclusive<usize> = 0x7FF0..=0x7FFF;
9
10macro_rules! invalid_cartridge_address {
11    ($address:expr) => {
12        panic!("Invalid cartridge address {:04X}, must be $0000-$BFFF", $address)
13    };
14}
15
16fn read_16kb_banked(bytes: &[u8], address: u16, bank: u32) -> u8 {
17    let rom_addr = (bank << 14) | u32::from(address & 0x3FFF);
18    read_wrapped(bytes, rom_addr)
19}
20
21fn read_wrapped(bytes: &[u8], address: u32) -> u8 {
22    let wrapped_addr = (address as usize) & (bytes.len() - 1);
23    bytes[wrapped_addr]
24}
25
26fn write_16kb_banked(bytes: &mut [u8], address: u16, bank: u32, value: u8) {
27    let rom_addr = (bank << 14) | u32::from(address & 0x3FFF);
28    write_wrapped(bytes, rom_addr, value);
29}
30
31fn write_wrapped(bytes: &mut [u8], address: u32, value: u8) {
32    let wrapped_addr = (address as usize) & (bytes.len() - 1);
33    bytes[wrapped_addr] = value;
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
37pub struct SegaMapper {
38    rom_banks: [u32; 3],
39    ram_bank: u32,
40    ram_enabled: bool,
41}
42
43impl SegaMapper {
44    pub fn new() -> Self {
45        Self { rom_banks: [0, 1, 2], ram_bank: 0, ram_enabled: false }
46    }
47
48    pub fn read(&self, address: u16, rom: &[u8], ram: &[u8]) -> u8 {
49        match address {
50            0x0000..=0x03FF => {
51                // First 1KB of ROM
52                read_wrapped(rom, address.into())
53            }
54            0x0400..=0x7FFF => {
55                // Mappable ROM banks 0/1
56                let bank = self.rom_banks[(address / 0x4000) as usize];
57                read_16kb_banked(rom, address, bank)
58            }
59            0x8000..=0xBFFF => {
60                // Mappable ROM bank 2 or RAM bank
61                if self.ram_enabled {
62                    read_16kb_banked(ram, address, self.ram_bank)
63                } else {
64                    read_16kb_banked(rom, address, self.rom_banks[2])
65                }
66            }
67            0xC000..=0xFFFF => invalid_cartridge_address!(address),
68        }
69    }
70
71    pub fn write(&mut self, address: u16, value: u8, ram: &mut [u8], ram_dirty: &mut bool) {
72        match address {
73            0x8000..=0xBFFF if self.ram_enabled => {
74                // RAM bank (if mapped)
75                write_16kb_banked(ram, address, self.ram_bank, value);
76                *ram_dirty = true;
77            }
78            0xFFFC => {
79                // Control / RAM banking
80                self.ram_bank = value.bit(2).into();
81                self.ram_enabled = value.bit(3);
82                // TODO Bank shifting and RAM enabled for $C000-$FFFF; not used by any known software
83                log::debug!(
84                    "$FFFC write: {value:02X} (RAM bank = {}, RAM enabled = {})",
85                    self.ram_bank,
86                    self.ram_enabled
87                );
88            }
89            0xFFFD..=0xFFFF => {
90                // ROM banking
91                let bank_idx = address - 0xFFFD;
92                self.rom_banks[bank_idx as usize] = value.into();
93                log::debug!("${address:02X} write: {value:02X} (ROM bank {bank_idx})");
94            }
95            _ => {}
96        }
97    }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
101pub struct CodemastersMapper {
102    rom_banks: [u32; 3],
103    ram_enabled: bool,
104}
105
106impl CodemastersMapper {
107    pub fn new() -> Self {
108        Self { rom_banks: [0, 1, 2], ram_enabled: false }
109    }
110
111    pub fn read(&self, address: u16, rom: &[u8], ram: &[u8]) -> u8 {
112        match address {
113            0x0000..=0x9FFF => {
114                // ROM banks (16KB)
115                let bank = self.rom_banks[(address / 0x4000) as usize];
116                read_16kb_banked(rom, address, bank)
117            }
118            0xA000..=0xBFFF => {
119                // RAM or upper half of ROM bank 2
120                if self.ram_enabled {
121                    // No Codemasters game has more than 8KB of RAM
122                    read_wrapped(ram, (address & 0x1FFF).into())
123                } else {
124                    read_16kb_banked(rom, address, self.rom_banks[2])
125                }
126            }
127            0xC000..=0xFFFF => invalid_cartridge_address!(address),
128        }
129    }
130
131    pub fn write(&mut self, address: u16, value: u8, ram: &mut [u8], ram_dirty: &mut bool) {
132        match address {
133            0x0000..=0x3FFF => {
134                // ROM bank 0
135                self.rom_banks[0] = value.into();
136                log::trace!("${address:04X} write: {value:02X} (ROM bank 0)");
137            }
138            0x4000..=0x7FFF => {
139                // ROM bank 1 and RAM enabled
140                self.rom_banks[1] = value.into();
141                self.ram_enabled = value.bit(7);
142                log::trace!(
143                    "${address:04X} write: {value:02X} (ROM bank 1, RAM enabled = {})",
144                    self.ram_enabled
145                );
146            }
147            0x8000..=0xBFFF => {
148                // ROM bank 2 and RAM (in upper half if enabled)
149                if self.ram_enabled && address >= 0xA000 {
150                    write_wrapped(ram, (address & 0x1FFF).into(), value);
151                    *ram_dirty = true;
152                } else {
153                    self.rom_banks[2] = value.into();
154                    log::trace!("${address:04X} write: {value:02X} (ROM bank 2)");
155                }
156            }
157            0xC000..=0xFFFF => {}
158        }
159    }
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
163pub struct Sg1000Mapper {
164    ram_start_addr: usize,
165}
166
167impl Sg1000Mapper {
168    pub fn new(unmirrored_rom: &[u8]) -> Self {
169        // Some documentation says RAM starts at $A000, but some games have more than 8KB of RAM
170        // and expect it to start at $8000 (e.g. The Castle)
171        Self { ram_start_addr: cmp::max(0x8000, unmirrored_rom.len()) }
172    }
173}
174
175#[allow(clippy::unused_self)]
176impl Sg1000Mapper {
177    pub fn read(&self, address: u16, rom: &[u8], ram: &[u8]) -> u8 {
178        match address {
179            0x0000..=0x7FFF => rom[(address as usize) & (rom.len() - 1)],
180            0x8000..=0xFFFF => {
181                let address = address as usize;
182                if address < self.ram_start_addr {
183                    rom[address & (rom.len() - 1)]
184                } else {
185                    ram[(address - self.ram_start_addr) & (ram.len() - 1)]
186                }
187            }
188        }
189    }
190
191    pub fn write(&mut self, address: u16, value: u8, ram: &mut [u8], ram_dirty: &mut bool) {
192        let address = address as usize;
193        if address >= self.ram_start_addr {
194            ram[(address - self.ram_start_addr) & (ram.len() - 1)] = value;
195            *ram_dirty = true;
196        }
197    }
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MatchEachVariantMacro)]
201pub enum Mapper {
202    Sega(SegaMapper),
203    Codemasters(CodemastersMapper),
204    Sg1000(Sg1000Mapper),
205}
206
207impl Mapper {
208    // Codemasters ROMs have a 16-bit checksum at $7FE6 which is the sum of all 16-bit words in the ROM
209    // except for the words in the Sega header. If summing all of the words matches the word at
210    // $7FE6, assume this is a Codemasters ROM
211    pub fn detect_from_rom(rom: &[u8]) -> Self {
212        if rom.len() < 32 * 1024 {
213            // No real ROMs should be less than 32KB, but regardless this isn't a Codemasters ROM
214            return Self::Sega(SegaMapper::new());
215        }
216
217        let expected_checksum = u16::from_le_bytes([
218            rom[CODEMASTERS_CHECKSUM_ADDR],
219            rom[CODEMASTERS_CHECKSUM_ADDR + 1],
220        ]);
221        let mut checksum = 0_u16;
222        for address in (0..rom.len() & !1).step_by(2) {
223            if !SEGA_HEADER_ADDR_RANGE.contains(&address) {
224                let word = u16::from_le_bytes([rom[address], rom[address + 1]]);
225                checksum = checksum.wrapping_add(word);
226            }
227        }
228
229        if checksum == expected_checksum {
230            log::info!(
231                "Codemasters-style ROM checksum is {checksum:04X}, matches word at {CODEMASTERS_CHECKSUM_ADDR:04X}; assuming this is a Codemasters ROM"
232            );
233            Self::Codemasters(CodemastersMapper::new())
234        } else {
235            Self::Sega(SegaMapper::new())
236        }
237    }
238
239    pub fn read(&self, address: u16, rom: &[u8], ram: &[u8]) -> u8 {
240        match_each_variant!(self, mapper => mapper.read(address, rom, ram))
241    }
242
243    pub fn write(&mut self, address: u16, value: u8, ram: &mut [u8], ram_dirty: &mut bool) {
244        match_each_variant!(self, mapper => mapper.write(address, value, ram, ram_dirty));
245    }
246
247    pub fn name(&self) -> &'static str {
248        match self {
249            Self::Sega(_) => "Sega",
250            Self::Codemasters(_) => "Codemasters",
251            Self::Sg1000(_) => "SG-1000",
252        }
253    }
254}