Code for the Sunsoft 5A / 5B / FME-7 boards (iNES mapper 69).

3use crate::bus::cartridge::mappers::{
4    BankSizeKb, ChrType, CpuMapResult, NametableMirroring, PpuMapResult,
5};
6use crate::bus::cartridge::{HasBasicPpuMapping, MapperImpl};
7use bincode::{Decode, Encode};
8use jgenesis_common::num::GetBit;
9use std::sync::LazyLock;
11#[allow(clippy::upper_case_acronyms)]
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
13enum PrgType {
14    ROM,
15    RAM,
16}
17
18#[derive(Debug, Clone, Encode, Decode)]
19struct Sunsoft5bChannel {
20    wave_step: bool,
21    divider: u8,
22    timer: u16,
23    period: u16,
24    tone_enabled: bool,
25    volume: u8,
26}
27
28const AUDIO_DIVIDER: u8 = 16;
29
30impl Sunsoft5bChannel {
31    fn new() -> Self {
32        Self {
33            wave_step: false,
34            divider: AUDIO_DIVIDER,
35            timer: 0,
36            period: 0,
37            tone_enabled: false,
38            volume: 0,
39        }
40    }
41
42    fn handle_period_low_update(&mut self, value: u8) {
43        self.period = (self.period & 0xFF00) | u16::from(value);
44    }
45
46    fn handle_period_high_update(&mut self, value: u8) {
47        self.period = (self.period & 0x00FF) | (u16::from(value & 0x0F) << 8);
48    }
49
50    fn handle_volume_update(&mut self, value: u8) {
51        self.volume = value & 0x0F;
52    }
53
54    fn sample(&self) -> u8 {
55        if !self.tone_enabled { self.volume } else { u8::from(self.wave_step) * self.volume }
56    }
57
58    fn sample_analog(&self) -> f64 {
59        static DAC_LOOKUP_TABLE: LazyLock<[f64; 16]> = LazyLock::new(|| {
60            let mut lookup_table = [0.0; 16];
61
62            // Arbitrary value, will get normalized later
63            lookup_table[1] = 1.0;
64
65            // https://en.wikipedia.org/wiki/Decibel
66            //
67            // Each step produces an output difference of 3dB which gives this equation, where L0 is the
68            // previous output value on a linear scale and L1 is the current value:
69            //   3dB = 20 * log10(L1/L0)
70            // Simple algebra brings this to:
71            //   L1 = L0 * 10^(3/20)
72            for i in 2..16 {
73                lookup_table[i] = lookup_table[i - 1] * 10.0_f64.powf(3.0 / 20.0);
74            }
75
76            // Hack: the 5B has some sort of compressor that appears to make the difference between
77            // volume 14 and volume 15 much smaller than it should be.
78            //
79            // Without doing anything, volume 14 will end up around (0.707 * volume 15), so modify
80            // volume 15 to be a bit higher than that.
81            //
82            // This also has the effect of making all of the lower volumes a little louder, which is
83            // desired because otherwise they will sound too soft.
84            lookup_table[15] *= 0.72;
85
86            // Normalize the values so that the max is 1.0
87            let max = lookup_table[15];
88            for value in &mut lookup_table[1..] {
89                *value /= max;
90            }
91
92            lookup_table
93        });
94
95        DAC_LOOKUP_TABLE[self.sample() as usize]
96    }
97
98    fn clock(&mut self) {
99        self.timer += 1;
100        if self.timer >= self.period {
101            self.timer = 0;
102            self.wave_step = !self.wave_step;
103        }
104    }
105
106    fn tick_cpu(&mut self) {
107        self.divider -= 1;
108        if self.divider == 0 {
109            self.divider = AUDIO_DIVIDER;
110            self.clock();
111        }
112    }
113}
114
115#[derive(Debug, Clone, Encode, Decode)]
116struct Sunsoft5bAudioUnit {
117    register_select: u8,
118    register_writes_enabled: bool,
119    channel_1: Sunsoft5bChannel,
120    channel_2: Sunsoft5bChannel,
121    channel_3: Sunsoft5bChannel,
122}
123
124impl Sunsoft5bAudioUnit {
125    fn new() -> Self {
126        Self {
127            register_select: 0,
128            register_writes_enabled: false,
129            channel_1: Sunsoft5bChannel::new(),
130            channel_2: Sunsoft5bChannel::new(),
131            channel_3: Sunsoft5bChannel::new(),
132        }
133    }
134
135    fn handle_select_update(&mut self, value: u8) {
136        self.register_select = value & 0x0F;
137        self.register_writes_enabled = value & 0xF0 == 0;
138    }
139
140    fn handle_write(&mut self, value: u8) {
141        if !self.register_writes_enabled {
142            return;
143        }
144
145        match self.register_select {
146            0x00 => {
147                self.channel_1.handle_period_low_update(value);
148            }
149            0x01 => {
150                self.channel_1.handle_period_high_update(value);
151            }
152            0x02 => {
153                self.channel_2.handle_period_low_update(value);
154            }
155            0x03 => {
156                self.channel_2.handle_period_high_update(value);
157            }
158            0x04 => {
159                self.channel_3.handle_period_low_update(value);
160            }
161            0x05 => {
162                self.channel_3.handle_period_high_update(value);
163            }
164            0x07 => {
165                self.channel_3.tone_enabled = !value.bit(2);
166                self.channel_2.tone_enabled = !value.bit(1);
167                self.channel_1.tone_enabled = !value.bit(0);
168            }
169            0x08 => {
170                self.channel_1.handle_volume_update(value);
171            }
172            0x09 => {
173                self.channel_2.handle_volume_update(value);
174            }
175            0x0A => {
176                self.channel_3.handle_volume_update(value);
177            }
178            _ => {}
179        }
180    }
181
182    fn tick_cpu(&mut self) {
183        self.channel_1.tick_cpu();
184        self.channel_2.tick_cpu();
185        self.channel_3.tick_cpu();
186    }
187
188    fn sample(&self) -> f64 {
189        (self.channel_1.sample_analog()
190            + self.channel_2.sample_analog()
191            + self.channel_3.sample_analog())
192            / 3.0
193    }
194
195    fn enabled(&self) -> bool {
196        self.channel_1.tone_enabled
197            || self.channel_2.tone_enabled
198            || self.channel_3.tone_enabled
199            || self.channel_1.volume != 0
200            || self.channel_2.volume != 0
201            || self.channel_3.volume != 0
202    }
203}
204
205#[derive(Debug, Clone, Encode, Decode)]
206pub(crate) struct Sunsoft {
207    prg_banks: [u8; 4],
208    prg_bank_0_type: PrgType,
209    prg_ram_enabled: bool,
210    chr_type: ChrType,
211    chr_banks: [u8; 8],
212    nametable_mirroring: NametableMirroring,
213    command_register: u8,
214    irq_enabled: bool,
215    irq_counter_enabled: bool,
216    irq_counter: u16,
217    irq_triggered: bool,
218    audio: Sunsoft5bAudioUnit,
219}
220
221impl Sunsoft {
222    pub(crate) fn new(chr_type: ChrType) -> Self {
223        Self {
224            prg_banks: [0; 4],
225            prg_bank_0_type: PrgType::ROM,
226            prg_ram_enabled: false,
227            chr_type,
228            chr_banks: [0; 8],
229            nametable_mirroring: NametableMirroring::Vertical,
230            command_register: 0,
231            irq_enabled: false,
232            irq_counter_enabled: false,
233            irq_counter: 0,
234            irq_triggered: false,
235            audio: Sunsoft5bAudioUnit::new(),
236        }
237    }
238}
239
240impl MapperImpl<Sunsoft> {
241    fn map_cpu_address(&self, address: u16) -> CpuMapResult {
242        match address {
243            0x0000..=0x401F => panic!("invalid CPU map address: {address:04X}"),
244            0x4020..=0x5FFF => CpuMapResult::None,
245            0x6000..=0x7FFF => match self.data.prg_bank_0_type {
246                PrgType::ROM => {
247                    let prg_rom_addr =
248                        BankSizeKb::Eight.to_absolute_address(self.data.prg_banks[0], address);
249                    CpuMapResult::PrgROM(prg_rom_addr)
250                }
251                PrgType::RAM => {
252                    if self.data.prg_ram_enabled && !self.cartridge.prg_ram.is_empty() {
253                        let prg_ram_addr =
254                            BankSizeKb::Eight.to_absolute_address(self.data.prg_banks[0], address);
255                        CpuMapResult::PrgRAM(prg_ram_addr)
256                    } else {
257                        CpuMapResult::None
258                    }
259                }
260            },
261            0x8000..=0xDFFF => {
262                // 0x8000..=0x9FFF to bank index 1
263                // 0xA000..=0xBFFF to bank index 2
264                // 0xC000..=0xDFFF to bank index 3
265                let bank_index = (address - 0x6000) / 0x2000;
266                let prg_rom_addr = BankSizeKb::Eight
267                    .to_absolute_address(self.data.prg_banks[bank_index as usize], address);
268                CpuMapResult::PrgROM(prg_rom_addr)
269            }
270            0xE000..=0xFFFF => {
271                // Fixed to last bank
272                let prg_rom_addr = BankSizeKb::Eight
273                    .to_absolute_address_last_bank(self.cartridge.prg_rom.len() as u32, address);
274                CpuMapResult::PrgROM(prg_rom_addr)
275            }
276        }
277    }
278
279    pub(crate) fn read_cpu_address(&self, address: u16, cpu_open_bus: u8) -> u8 {
280        self.map_cpu_address(address).read(&self.cartridge).unwrap_or(cpu_open_bus)
281    }
282
283    pub(crate) fn write_cpu_address(&mut self, address: u16, value: u8) {
284        match address {
285            0x0000..=0x401F => panic!("invalid CPU map address: {address:04X}"),
286            0x4020..=0x5FFF => {}
287            0x6000..=0x7FFF => {
288                self.map_cpu_address(address).write(value, &mut self.cartridge);
289            }
290            0x8000..=0x9FFF => {
291                self.data.command_register = value & 0x0F;
292            }
293            0xA000..=0xBFFF => match self.data.command_register {
294                0x00..=0x07 => {
295                    self.data.chr_banks[self.data.command_register as usize] = value;
296                }
297                0x08 => {
298                    self.data.prg_banks[0] = value & 0x3F;
299                    self.data.prg_bank_0_type =
300                        if value.bit(6) { PrgType::RAM } else { PrgType::ROM };
301                    self.data.prg_ram_enabled = value.bit(7);
302                }
303                0x09..=0x0B => {
304                    let prg_bank_index = self.data.command_register - 0x08;
305                    self.data.prg_banks[prg_bank_index as usize] = value & 0x3F;
306                }
307                0x0C => {
308                    self.data.nametable_mirroring = match value & 0x03 {
309                        0x00 => NametableMirroring::Vertical,
310                        0x01 => NametableMirroring::Horizontal,
311                        0x02 => NametableMirroring::SingleScreenBank0,
312                        0x03 => NametableMirroring::SingleScreenBank1,
313                        _ => unreachable!("value & 0x03 should always be 0x00/0x01/0x02/0x03"),
314                    };
315                }
316                0x0D => {
317                    self.data.irq_enabled = value.bit(0);
318                    self.data.irq_counter_enabled = value.bit(7);
319                    self.data.irq_triggered = false;
320                }
321                0x0E => {
322                    self.data.irq_counter = (self.data.irq_counter & 0xFF00) | u16::from(value);
323                }
324                0x0F => {
325                    self.data.irq_counter =
326                        (self.data.irq_counter & 0x00FF) | (u16::from(value) << 8);
327                }
328                _ => panic!("command register should always contain 0-15"),
329            },
330            0xC000..=0xDFFF => {
331                self.data.audio.handle_select_update(value);
332            }
333            0xE000..=0xFFFF => {
334                self.data.audio.handle_write(value);
335            }
336        }
337    }
338
339    pub(crate) fn interrupt_flag(&self) -> bool {
340        self.data.irq_triggered
341    }
342
343    pub(crate) fn tick_cpu(&mut self) {
344        self.data.audio.tick_cpu();
345
346        if !self.data.irq_counter_enabled {
347            return;
348        }
349
350        if self.data.irq_enabled && self.data.irq_counter == 0 {
351            self.data.irq_triggered = true;
352        }
353        self.data.irq_counter = self.data.irq_counter.wrapping_sub(1);
354    }
355
356    pub(crate) fn sample_audio(&self, mixed_apu_sample: f64) -> f64 {
357        if !self.data.audio.enabled() {
358            return mixed_apu_sample;
359        }
360
361        let sunsoft_5b_sample = self.data.audio.sample();
362
363        // This audio chip appears to slightly decrease APU channel volume
364        0.7 * mixed_apu_sample - sunsoft_5b_sample
365    }
366}
367
368impl HasBasicPpuMapping for MapperImpl<Sunsoft> {
369    fn map_ppu_address(&self, address: u16) -> PpuMapResult {
370        match address {
371            0x0000..=0x1FFF => {
372                let chr_bank_index = address / 0x0400;
373                let chr_addr = BankSizeKb::One
374                    .to_absolute_address(self.data.chr_banks[chr_bank_index as usize], address);
375                self.data.chr_type.to_map_result(chr_addr)
376            }
377            0x2000..=0x3EFF => {
378                PpuMapResult::Vram(self.data.nametable_mirroring.map_to_vram(address))
379            }
380            0x3F00..=0xFFFF => panic!("invalid PPU map address: {address:04X}"),
381        }
382    }
383}