huc3.rsannotatedhuc3.rssource294 lines · 9.6 KB · raw

HuC-3 mapper, used by Robopon and a few Japan-only games

References: https://gbdev.io/pandocs/HuC3.html https://gbdev.gg8.se/forums/viewtopic.php?id=744

7use crate::cartridge::mappers::{basic_map_ram_address, basic_map_rom_address};
8use bincode::{Decode, Encode};
9use jgenesis_common::frontend::SaveWriter;
10use jgenesis_common::num::GetBit;
11use jgenesis_common::timeutils;
12use std::iter;
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
15enum RamMapping {
16    // $0
17    #[default]
18    RamReadOnly,
19    // $A
20    RamReadWrite,
21    // $B
22    RtcSendCommand,
23    // $C
24    RtcReceiveResponse,
25    // $D
26    RtcSemaphore,
27    // $E
28    Ir,
29    // Other values
30    OpenBus,
31}
32
33impl RamMapping {
34    fn from_byte(byte: u8) -> Self {
35        match byte & 0xF {
36            0x0 => Self::RamReadOnly,
37            0xA => Self::RamReadWrite,
38            0xB => Self::RtcSendCommand,
39            0xC => Self::RtcReceiveResponse,
40            0xD => Self::RtcSemaphore,
41            0xE => Self::Ir,
42            _ => Self::OpenBus,
43        }
44    }
45}
46
47#[derive(Debug, Clone, Encode, Decode)]
48pub struct Huc3 {
49    rom_bank: u8,
50    rom_addr_mask: u32,
51    ram_bank: u8,
52    ram_addr_mask: u32,
53    ram_mapping: RamMapping,
54    rtc: Huc3Rtc,
55}
56
57impl Huc3 {
58    pub fn new(rom_len: u32, ram_len: u32, rtc: Option<Huc3Rtc>) -> Self {
59        Self {
60            rom_bank: 0,
61            rom_addr_mask: rom_len - 1,
62            ram_bank: 0,
63            ram_addr_mask: ram_len - 1,
64            ram_mapping: RamMapping::default(),
65            rtc: rtc.unwrap_or_else(Huc3Rtc::new),
66        }
67    }
68
69    pub fn map_rom_address(&self, address: u16) -> u32 {
70        basic_map_rom_address(address, self.rom_bank.into(), true, self.rom_addr_mask)
71    }
72
73    pub fn write_rom_address(&mut self, address: u16, value: u8) {
74        match address {
75            0x0000..=0x1FFF => {
76                // RAM mode select
77                self.ram_mapping = RamMapping::from_byte(value);
78                log::trace!("HuC-3 $A000-$BFFF mapping: {:?}", self.ram_mapping);
79            }
80            0x2000..=0x3FFF => {
81                // ROM bank
82                self.rom_bank = value;
83            }
84            0x4000..=0x5FFF => {
85                // RAM bank
86                self.ram_bank = value;
87            }
88            0x6000..=0x7FFF => {
89                // Unknown functionality; ignore
90            }
91            0x8000..=0xFFFF => panic!("Invalid ROM address: {address:04X}"),
92        }
93    }
94
95    pub fn read_ram(&self, address: u16, sram: &[u8]) -> u8 {
96        match self.ram_mapping {
97            RamMapping::RamReadOnly | RamMapping::RamReadWrite => {
98                basic_map_ram_address(true, address, self.ram_bank.into(), self.ram_addr_mask)
99                    .map_or(0xFF, |ram_addr| sram[ram_addr as usize])
100            }
101            RamMapping::RtcReceiveResponse => {
102                let response = 0x80 | (self.rtc.command << 4) | self.rtc.response;
103                log::trace!(
104                    "RTC response received ({response:02X}): command {:X} response {:X}",
105                    self.rtc.command,
106                    self.rtc.response
107                );
108                response
109            }
110            RamMapping::RtcSemaphore => 0xFE | u8::from(self.rtc.busy_cycles_remaining == 0),
111            RamMapping::Ir => {
112                // Normally $C1 means "saw light" and $C0 means "didn't see light"; always return $C0
113                0xC0
114            }
115            RamMapping::RtcSendCommand | RamMapping::OpenBus => 0xFF,
116        }
117    }
118
119    pub fn write_ram(&mut self, address: u16, value: u8, sram: &mut [u8]) {
120        match self.ram_mapping {
121            RamMapping::RamReadWrite => {
122                if let Some(ram_addr) =
123                    basic_map_ram_address(true, address, self.ram_bank.into(), self.ram_addr_mask)
124                {
125                    sram[ram_addr as usize] = value;
126                }
127            }
128            RamMapping::RtcSendCommand => {
129                self.rtc.command = (value >> 4) & 0x07;
130                self.rtc.argument = value & 0x0F;
131                log::trace!(
132                    "RTC command sent ({value:02X}): command {:X} argument {:X}",
133                    self.rtc.command,
134                    self.rtc.argument
135                );
136            }
137            RamMapping::RtcSemaphore => {
138                if !value.bit(0) {
139                    self.rtc.execute_command();
140                    log::trace!("Executing RTC command");
141                }
142            }
143            RamMapping::RamReadOnly
144            | RamMapping::RtcReceiveResponse
145            | RamMapping::Ir
146            | RamMapping::OpenBus => {}
147        }
148    }
149
150    pub fn tick_cpu(&mut self) {
151        self.rtc.busy_cycles_remaining = self.rtc.busy_cycles_remaining.saturating_sub(1);
152    }
153
154    pub fn update_rtc_time(&mut self) {
155        self.rtc.update_time();
156    }
157
158    pub fn save_rtc_state<S: SaveWriter>(&self, save_writer: &mut S) -> Result<(), S::Err> {
159        save_writer.persist_serialized("rtc", &self.rtc)
160    }
161}
162
163const NANOS_PER_MINUTE: u128 = 60 * 1_000_000_000;
164const MINUTES_PER_DAY: u64 = 1440;
165
166const RTC_RAM_LEN: usize = 256;

Arbitrary value, HuC-3 games supposedly expect a delay instead of immediate command execution

169const RTC_BUSY_CYCLES: u16 = 1000;
171#[derive(Debug, Clone, Encode, Decode)]
172pub struct Huc3Rtc {
173    memory: Box<[u8; RTC_RAM_LEN]>,
174    memory_address: u8,
175    last_update_time_nanos: u128,
176    nanos_of_minute: u128,
177    minutes_of_day: u16,
178    day: u16,
179    command: u8,
180    argument: u8,
181    response: u8,
182    busy_cycles_remaining: u16,
183}
184
185impl Huc3Rtc {
186    fn new() -> Self {
187        let memory =
188            iter::repeat_with(|| rand::random::<u8>() & 0x0F).take(RTC_RAM_LEN).collect::<Vec<_>>();
189
190        Self {
191            memory: memory.into_boxed_slice().try_into().unwrap(),
192            memory_address: 0,
193            last_update_time_nanos: timeutils::current_time_nanos(),
194            nanos_of_minute: 0,
195            minutes_of_day: rand::random(),
196            day: rand::random(),
197            command: 0,
198            argument: 0,
199            response: 0,
200            busy_cycles_remaining: 0,
201        }
202    }
203
204    fn update_time(&mut self) {
205        let now_nanos = timeutils::current_time_nanos();
206        let elapsed_nanos = now_nanos.saturating_sub(self.last_update_time_nanos);
207        self.last_update_time_nanos = now_nanos;
208
209        self.nanos_of_minute += elapsed_nanos;
210
211        let elapsed_minutes = self.nanos_of_minute / NANOS_PER_MINUTE;
212        self.nanos_of_minute %= NANOS_PER_MINUTE;
213
214        let new_minutes_of_day = u64::from(self.minutes_of_day) + elapsed_minutes as u64;
215        let elapsed_days = new_minutes_of_day / MINUTES_PER_DAY;
216        self.minutes_of_day = (new_minutes_of_day % MINUTES_PER_DAY) as u16;
217
218        self.day = self.day.wrapping_add(elapsed_days as u16);
219
220        // These memory addresses seem to hold the current time always?
221        self.memory[0x10..0x13].copy_from_slice(&to_nibbles(self.minutes_of_day));
222        self.memory[0x13..0x16].copy_from_slice(&to_nibbles(self.day));
223    }
224
225    fn execute_command(&mut self) {
226        self.busy_cycles_remaining = RTC_BUSY_CYCLES;
227
228        match self.command {
229            0x1 => {
230                // Read value and increment address
231                self.response = self.memory[self.memory_address as usize] & 0x0F;
232                self.memory_address = self.memory_address.wrapping_add(1);
233            }
234            0x3 => {
235                // Write value and increment address
236                self.memory[self.memory_address as usize] = self.argument;
237                self.memory_address = self.memory_address.wrapping_add(1);
238            }
239            0x4 => {
240                // Update address low nibble
241                self.memory_address = (self.memory_address & 0xF0) | self.argument;
242            }
243            0x5 => {
244                // Update address high nibble
245                self.memory_address = (self.memory_address & 0x0F) | (self.argument << 4);
246            }
247            0x6 => match self.argument {
248                0x0 => {
249                    // Copy current time to $00-$06 in RTC memory
250                    self.memory[0..3].copy_from_slice(&to_nibbles(self.minutes_of_day));
251                    self.memory[3..6].copy_from_slice(&to_nibbles(self.day));
252                }
253                0x1 => {
254                    // Copy $00-$06 in RTC memory to current time
255                    self.minutes_of_day = from_nibbles(&self.memory[0..3]);
256                    self.day = from_nibbles(&self.memory[3..6]);
257
258                    log::trace!(
259                        "Updated RTC time: minutes={}, days={}",
260                        self.minutes_of_day,
261                        self.day
262                    );
263
264                    // TODO event time?
265                }
266                0x2 => {
267                    // Some sort of status command? Games expect it to always respond with 1
268                    self.response = 0x01;
269                }
270                0xE => {
271                    log::warn!("HuC-3 tone generator is not emulated");
272                }
273                _ => {
274                    log::warn!("Unexpected HuC-3 extended command: {:X}", self.argument);
275                }
276            },
277            _ => {
278                log::warn!(
279                    "Unexpected HuC-3 command: command {:X} argument {:X}",
280                    self.command,
281                    self.argument
282                );
283            }
284        }
285    }
286}
287
288fn to_nibbles(value: u16) -> [u8; 3] {
289    [(value & 0x0F) as u8, ((value >> 4) & 0x0F) as u8, ((value >> 8) & 0x0F) as u8]
290}
291
292fn from_nibbles(nibbles: &[u8]) -> u16 {
293    u16::from(nibbles[0]) | (u16::from(nibbles[1]) << 4) | (u16::from(nibbles[2]) << 8)
294}