1#![allow(
19 clippy::cast_possible_truncation,
20 clippy::cast_lossless,
21 clippy::missing_const_for_fn,
22 clippy::needless_pass_by_ref_mut,
23 clippy::manual_range_patterns,
24 clippy::match_same_arms,
25 clippy::struct_excessive_bools,
26 clippy::doc_markdown,
27 clippy::range_plus_one,
28 clippy::single_match_else,
29 clippy::bool_to_int_with_if,
30 clippy::unnested_or_patterns,
31 clippy::single_match,
32 clippy::doc_lazy_continuation,
33 clippy::too_long_first_doc_paragraph
34)]
35
36use crate::cartridge::Mirroring;
37use crate::mapper::{Mapper, MapperCaps, MapperError};
38use alloc::{boxed::Box, vec::Vec};
39use alloc::{format, vec};
40
41const PRG_BANK_8K: usize = 0x2000;
42const CHR_BANK_1K: usize = 0x0400;
43const CHR_BANK_8K: usize = 0x2000;
44const NAMETABLE_SIZE: usize = 0x0400;
45const NAMETABLE_SIZE_U16: u16 = 0x0400;
46
47const VRC6_SECTION_VERSION: u8 = 3;
61
62const POWER_ON_CHR: [u8; 8] = [0, 1, 2, 3, 4, 5, 6, 7];
88
89const VRC6_AUDIO_TAIL_LEN: usize = 1 + 7 + 7 + 8;
92
93fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
94 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
95 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
96 let physical = mirroring.physical_bank(table);
97 physical * NAMETABLE_SIZE + local
98}
99
100pub(crate) const VRC6_MIX_SCALE: i16 = 650;
131
132#[derive(Clone, Default)]
138pub(crate) struct Vrc6Pulse {
139 pub(crate) ctrl: u8,
142 pub(crate) period: u16,
144 pub(crate) enabled: bool,
146 pub(crate) timer: u16,
148 pub(crate) step: u8,
150}
151
152impl Vrc6Pulse {
153 pub(crate) fn clock(&mut self) {
156 if !self.enabled {
157 return;
158 }
159 if self.timer == 0 {
160 self.timer = self.period;
161 self.step = (self.step + 1) & 0x0F;
162 } else {
163 self.timer -= 1;
164 }
165 }
166
167 pub(crate) fn output(&self) -> u8 {
169 if !self.enabled {
170 return 0;
171 }
172 let duty = (self.ctrl >> 4) & 0x07;
173 let ignore_duty = (self.ctrl & 0x80) != 0;
174 let volume = self.ctrl & 0x0F;
175 if ignore_duty || self.step <= duty {
176 volume
177 } else {
178 0
179 }
180 }
181}
182
183#[derive(Clone, Default)]
188pub(crate) struct Vrc6Saw {
189 pub(crate) rate: u8,
191 pub(crate) period: u16,
193 pub(crate) enabled: bool,
195 pub(crate) timer: u16,
197 pub(crate) step: u8,
200 pub(crate) acc: u8,
202}
203
204impl Vrc6Saw {
205 pub(crate) fn clock(&mut self) {
206 if !self.enabled {
207 return;
208 }
209 if self.timer == 0 {
210 self.timer = self.period;
211 self.step += 1;
214 if (self.step & 1) == 1 {
215 self.acc = self.acc.wrapping_add(self.rate);
216 }
217 if self.step >= 14 {
218 self.step = 0;
219 self.acc = 0;
220 }
221 } else {
222 self.timer -= 1;
223 }
224 }
225
226 pub(crate) fn output(&self) -> u8 {
228 if !self.enabled {
229 return 0;
230 }
231 self.acc >> 3
232 }
233}
234
235pub struct Vrc6 {
238 prg_rom: Box<[u8]>,
239 chr_rom: Box<[u8]>,
240 vram: Box<[u8]>,
241 chr_is_ram: bool,
242 prg_16: u8, prg_8: u8, chr: [u8; 8],
245 mirroring: Mirroring,
246 prg_ram: Box<[u8]>,
249 swap_a01: bool,
252
253 irq_latch: u8,
254 irq_counter: u8,
255 irq_enabled: bool,
256 irq_enable_after_ack: bool,
257 irq_mode_scanline: bool,
258 irq_prescaler: i32,
259 irq_pending: bool,
260
261 audio_ctrl: u8,
266 pulse1: Vrc6Pulse,
267 pulse2: Vrc6Pulse,
268 saw: Vrc6Saw,
269}
270
271#[cfg_attr(not(feature = "mapper-audio"), allow(dead_code))]
272impl Vrc6 {
273 pub fn new(
279 prg_rom: Box<[u8]>,
280 chr_rom: Box<[u8]>,
281 mapper_id: u16,
282 mirroring: Mirroring,
283 ) -> Result<Self, MapperError> {
284 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
285 return Err(MapperError::Invalid(format!(
286 "VRC6 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
287 prg_rom.len()
288 )));
289 }
290 let chr_is_ram = chr_rom.is_empty();
291 let chr: Box<[u8]> = if chr_is_ram {
292 vec![0u8; CHR_BANK_8K].into_boxed_slice()
293 } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
294 chr_rom
295 } else {
296 return Err(MapperError::Invalid(format!(
297 "VRC6 CHR-ROM size {} is not a multiple of 1 KiB",
298 chr_rom.len()
299 )));
300 };
301 Ok(Self {
302 prg_rom,
303 chr_rom: chr,
304 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
305 chr_is_ram,
306 prg_16: 0,
307 prg_8: 0,
308 chr: POWER_ON_CHR,
309 mirroring,
310 prg_ram: vec![0u8; 8 * 1024].into_boxed_slice(),
312 swap_a01: mapper_id == 26,
313 irq_latch: 0,
314 irq_counter: 0,
315 irq_enabled: false,
316 irq_enable_after_ack: false,
317 irq_mode_scanline: false,
318 irq_prescaler: 341,
319 irq_pending: false,
320 audio_ctrl: 0,
321 pulse1: Vrc6Pulse::default(),
322 pulse2: Vrc6Pulse::default(),
323 saw: Vrc6Saw::default(),
324 })
325 }
326
327 fn effective_period_p(&self, p: &Vrc6Pulse) -> u16 {
330 let shift = match (self.audio_ctrl >> 1) & 0x03 {
331 0 => 0,
332 1 => 4,
333 _ => 8,
334 };
335 p.period >> shift
336 }
337
338 fn effective_period_s(&self) -> u16 {
339 let shift = match (self.audio_ctrl >> 1) & 0x03 {
340 0 => 0,
341 1 => 4,
342 _ => 8,
343 };
344 self.saw.period >> shift
345 }
346
347 #[cfg(feature = "mapper-audio")]
350 fn clock_audio(&mut self) {
351 if (self.audio_ctrl & 0x01) != 0 {
353 return;
354 }
355 let p1_period = self.effective_period_p(&self.pulse1);
359 let p2_period = self.effective_period_p(&self.pulse2);
360 let saw_period = self.effective_period_s();
361 let saved_p1 = self.pulse1.period;
362 let saved_p2 = self.pulse2.period;
363 let saved_saw = self.saw.period;
364 self.pulse1.period = p1_period;
365 self.pulse2.period = p2_period;
366 self.saw.period = saw_period;
367 self.pulse1.clock();
368 self.pulse2.clock();
369 self.saw.clock();
370 self.pulse1.period = saved_p1;
371 self.pulse2.period = saved_p2;
372 self.saw.period = saved_saw;
373 }
374
375 fn prg_offset(&self, addr: u16) -> usize {
376 let total_8k = (self.prg_rom.len() / PRG_BANK_8K).max(1);
377 let last1 = total_8k - 1;
378 match addr {
379 0x8000..=0xBFFF => {
380 let bank16 = (self.prg_16 as usize) & 0x0F;
381 let bank8 = (bank16 << 1) | (((addr & 0x2000) >> 13) as usize);
382 (bank8 % total_8k) * PRG_BANK_8K + (addr as usize & 0x1FFF)
383 }
384 0xC000..=0xDFFF => {
385 let bank8 = (self.prg_8 as usize) & 0x1F;
386 (bank8 % total_8k) * PRG_BANK_8K + (addr as usize & 0x1FFF)
387 }
388 0xE000..=0xFFFF => last1 * PRG_BANK_8K + (addr as usize & 0x1FFF),
389 _ => 0,
390 }
391 }
392
393 fn chr_offset(&self, addr: u16) -> usize {
394 let addr = (addr & 0x1FFF) as usize;
395 let total_1k = (self.chr_rom.len() / CHR_BANK_1K).max(1);
396 let slot = addr / CHR_BANK_1K;
397 let bank = (self.chr[slot] as usize) % total_1k;
398 bank * CHR_BANK_1K + (addr & (CHR_BANK_1K - 1))
399 }
400
401 fn clock_irq_counter(&mut self) {
402 if self.irq_counter == 0xFF {
403 self.irq_counter = self.irq_latch;
404 self.irq_pending = true;
405 } else {
406 self.irq_counter = self.irq_counter.wrapping_add(1);
407 }
408 }
409
410 fn decode_a(&self, addr: u16) -> u8 {
411 let a0 = (addr & 1) != 0;
412 let a1 = (addr & 2) != 0;
413 let (a0, a1) = if self.swap_a01 { (a1, a0) } else { (a0, a1) };
414 u8::from(a0) | (u8::from(a1) << 1)
415 }
416}
417
418impl Mapper for Vrc6 {
419 fn sram(&self) -> &[u8] {
420 &self.prg_ram
421 }
422 fn sram_mut(&mut self) -> &mut [u8] {
423 &mut self.prg_ram
424 }
425 fn caps(&self) -> MapperCaps {
428 MapperCaps {
429 cpu_cycle_hook: true,
430 audio: cfg!(feature = "mapper-audio"),
431 frame_event_hook: false,
432 irq_source: true,
433 }
434 }
435
436 fn cpu_read(&mut self, addr: u16) -> u8 {
437 match addr {
438 0x6000..=0x7FFF => self.prg_ram[(addr - 0x6000) as usize % self.prg_ram.len()],
447 0x8000..=0xFFFF => {
448 let off = self.prg_offset(addr);
449 self.prg_rom[off % self.prg_rom.len()]
450 }
451 _ => 0,
452 }
453 }
454
455 fn cpu_write(&mut self, addr: u16, value: u8) {
456 if (0x6000..=0x7FFF).contains(&addr) {
459 let len = self.prg_ram.len();
460 self.prg_ram[(addr - 0x6000) as usize % len] = value;
461 return;
462 }
463 let a = self.decode_a(addr);
464 match addr & 0xF000 {
465 0x8000 => self.prg_16 = value & 0x0F,
466 0x9000 => match a {
467 0 => self.pulse1.ctrl = value,
469 1 => {
471 self.pulse1.period = (self.pulse1.period & 0x0F00) | u16::from(value);
472 }
473 2 => {
475 self.pulse1.period =
476 (self.pulse1.period & 0x00FF) | (u16::from(value & 0x0F) << 8);
477 self.pulse1.enabled = (value & 0x80) != 0;
478 if !self.pulse1.enabled {
479 self.pulse1.step = 0;
480 }
481 }
482 _ => self.audio_ctrl = value,
484 },
485 0xA000 => match a {
486 0 => self.pulse2.ctrl = value,
488 1 => {
490 self.pulse2.period = (self.pulse2.period & 0x0F00) | u16::from(value);
491 }
492 2 => {
494 self.pulse2.period =
495 (self.pulse2.period & 0x00FF) | (u16::from(value & 0x0F) << 8);
496 self.pulse2.enabled = (value & 0x80) != 0;
497 if !self.pulse2.enabled {
498 self.pulse2.step = 0;
499 }
500 }
501 _ => {}
502 },
503 0xB000 => match a {
504 0 => self.saw.rate = value & 0x3F,
506 1 => {
508 self.saw.period = (self.saw.period & 0x0F00) | u16::from(value);
509 }
510 2 => {
512 self.saw.period = (self.saw.period & 0x00FF) | (u16::from(value & 0x0F) << 8);
513 self.saw.enabled = (value & 0x80) != 0;
514 if !self.saw.enabled {
515 self.saw.step = 0;
516 self.saw.acc = 0;
517 }
518 }
519 _ => {
520 self.mirroring = match (value >> 2) & 0x03 {
522 0 => Mirroring::Vertical,
523 1 => Mirroring::Horizontal,
524 2 => Mirroring::SingleScreenA,
525 _ => Mirroring::SingleScreenB,
526 };
527 }
528 },
529 0xC000 => self.prg_8 = value & 0x1F,
530 0xD000 => self.chr[a as usize] = value,
531 0xE000 => self.chr[(a + 4) as usize] = value,
532 0xF000 => match a {
533 0 => self.irq_latch = value,
534 1 => {
535 self.irq_enable_after_ack = (value & 0x01) != 0;
536 self.irq_enabled = (value & 0x02) != 0;
537 self.irq_mode_scanline = (value & 0x04) == 0;
538 if self.irq_enabled {
539 self.irq_counter = self.irq_latch;
540 self.irq_prescaler = 341;
541 }
542 self.irq_pending = false;
543 }
544 2 => {
545 self.irq_pending = false;
546 self.irq_enabled = self.irq_enable_after_ack;
547 }
548 _ => {}
549 },
550 _ => {}
551 }
552 }
553
554 fn ppu_read(&mut self, addr: u16) -> u8 {
555 let addr = addr & 0x3FFF;
556 match addr {
557 0x0000..=0x1FFF => {
558 let off = self.chr_offset(addr);
559 self.chr_rom[off % self.chr_rom.len()]
560 }
561 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring) % self.vram.len()],
562 _ => 0,
563 }
564 }
565
566 fn ppu_write(&mut self, addr: u16, value: u8) {
567 let addr = addr & 0x3FFF;
568 match addr {
569 0x0000..=0x1FFF => {
570 if self.chr_is_ram {
571 let len = self.chr_rom.len();
572 self.chr_rom[addr as usize % len] = value;
573 }
574 }
575 0x2000..=0x3EFF => {
576 let off = nametable_offset(addr, self.mirroring) % self.vram.len();
577 self.vram[off] = value;
578 }
579 _ => {}
580 }
581 }
582
583 fn notify_cpu_cycle(&mut self) {
584 #[cfg(feature = "mapper-audio")]
586 self.clock_audio();
587
588 if !self.irq_enabled {
589 return;
590 }
591 if self.irq_mode_scanline {
592 self.irq_prescaler -= 3;
593 if self.irq_prescaler <= 0 {
594 self.irq_prescaler += 341;
595 self.clock_irq_counter();
596 }
597 } else {
598 self.clock_irq_counter();
599 }
600 }
601
602 #[cfg(feature = "mapper-audio")]
603 fn mix_audio(&mut self) -> i32 {
604 let p1 = i16::from(self.pulse1.output());
614 let p2 = i16::from(self.pulse2.output());
615 let saw = i16::from(self.saw.output());
616 i32::from(((p1 + p2 + saw) - 30) * VRC6_MIX_SCALE)
619 }
620
621 fn irq_pending(&self) -> bool {
622 self.irq_pending
623 }
624
625 fn current_mirroring(&self) -> Mirroring {
626 self.mirroring
627 }
628
629 fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
630 let mapper_id = if self.swap_a01 { 26 } else { 24 };
631 let mut info = crate::mapper::MapperDebugInfo {
632 mapper_id,
633 name: "VRC6".into(),
634 mirroring: crate::mapper::mirroring_name(self.current_mirroring()),
635 ..Default::default()
636 };
637 info.prg_banks
638 .push(("PRG16".into(), format!("{:#04x}", self.prg_16)));
639 info.prg_banks
640 .push(("PRG8".into(), format!("{:#04x}", self.prg_8)));
641 for (i, b) in self.chr.iter().enumerate() {
642 info.chr_banks
643 .push((format!("CHR{i}"), format!("{b:#04x}")));
644 }
645 info.irq_state
646 .push(("latch".into(), format!("{:#04x}", self.irq_latch)));
647 info.irq_state
648 .push(("counter".into(), format!("{:#04x}", self.irq_counter)));
649 info.irq_state
650 .push(("enabled".into(), format!("{}", self.irq_enabled)));
651 info.irq_state
652 .push(("pending".into(), format!("{}", self.irq_pending)));
653 info
654 }
655
656 fn save_state(&self) -> Vec<u8> {
657 let mut out =
669 Vec::with_capacity(48 + self.vram.len() + VRC6_AUDIO_TAIL_LEN + self.ram_block_len());
670 out.push(VRC6_SECTION_VERSION);
671 out.push(self.prg_16);
672 out.push(self.prg_8);
673 out.extend_from_slice(&self.chr);
674 out.push(self.mirroring as u8);
675 out.push(u8::from(self.swap_a01));
676 out.push(self.irq_latch);
677 out.push(self.irq_counter);
678 out.push(u8::from(self.irq_enabled));
679 out.push(u8::from(self.irq_enable_after_ack));
680 out.push(u8::from(self.irq_mode_scanline));
681 out.extend_from_slice(&self.irq_prescaler.to_le_bytes());
682 out.push(u8::from(self.irq_pending));
683 out.extend_from_slice(&self.vram);
684 out.push(self.audio_ctrl);
686 Self::write_pulse(&mut out, &self.pulse1);
687 Self::write_pulse(&mut out, &self.pulse2);
688 Self::write_saw(&mut out, &self.saw);
689 out.extend_from_slice(&self.prg_ram);
691 if self.chr_is_ram {
692 out.extend_from_slice(&self.chr_rom);
693 }
694 out
695 }
696
697 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
698 let scalar_len = 1 + 1 + 1 + 8 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 4 + 1;
699 let core_expected = scalar_len + self.vram.len();
700 if data.len() < core_expected {
701 return Err(MapperError::WrongLength {
702 expected: core_expected,
703 got: data.len(),
704 });
705 }
706 let version = data[0];
707 if version != VRC6_SECTION_VERSION {
711 return Err(MapperError::UnsupportedVersion(version));
712 }
713 let tail_off = core_expected;
716 let expected = tail_off + VRC6_AUDIO_TAIL_LEN + self.ram_block_len();
717 if data.len() != expected {
718 return Err(MapperError::WrongLength {
719 expected,
720 got: data.len(),
721 });
722 }
723 self.prg_16 = data[1];
724 self.prg_8 = data[2];
725 self.chr.copy_from_slice(&data[3..11]);
726 self.mirroring = match data[11] {
727 0 => Mirroring::Horizontal,
728 1 => Mirroring::Vertical,
729 2 => Mirroring::SingleScreenA,
730 3 => Mirroring::SingleScreenB,
731 4 => Mirroring::FourScreen,
732 5 => Mirroring::MapperControlled,
733 other => return Err(MapperError::Invalid(format!("mirroring {other}"))),
734 };
735 self.swap_a01 = data[12] != 0;
736 self.irq_latch = data[13];
737 self.irq_counter = data[14];
738 self.irq_enabled = data[15] != 0;
739 self.irq_enable_after_ack = data[16] != 0;
740 self.irq_mode_scanline = data[17] != 0;
741 self.irq_prescaler = i32::from_le_bytes(
742 data[18..22]
743 .try_into()
744 .map_err(|_| MapperError::Invalid("prescaler".into()))?,
745 );
746 self.irq_pending = data[22] != 0;
747 self.vram.copy_from_slice(&data[23..23 + self.vram.len()]);
748
749 self.audio_ctrl = data[tail_off];
751 Self::read_pulse(&data[tail_off + 1..tail_off + 8], &mut self.pulse1);
752 Self::read_pulse(&data[tail_off + 8..tail_off + 15], &mut self.pulse2);
753 Self::read_saw(&data[tail_off + 15..tail_off + 23], &mut self.saw);
754 let ram_off = tail_off + VRC6_AUDIO_TAIL_LEN;
756 let (prg, chr) = data[ram_off..].split_at(self.prg_ram.len());
757 self.prg_ram.copy_from_slice(prg);
758 if self.chr_is_ram {
759 self.chr_rom.copy_from_slice(chr);
760 }
761 Ok(())
762 }
763}
764
765impl Vrc6 {
766 fn ram_block_len(&self) -> usize {
770 self.prg_ram.len()
771 + if self.chr_is_ram {
772 self.chr_rom.len()
773 } else {
774 0
775 }
776 }
777}
778
779impl Vrc6 {
780 fn write_pulse(out: &mut Vec<u8>, p: &Vrc6Pulse) {
781 out.push(p.ctrl);
782 out.extend_from_slice(&p.period.to_le_bytes());
783 out.push(u8::from(p.enabled));
784 out.extend_from_slice(&p.timer.to_le_bytes());
785 out.push(p.step);
786 }
787
788 fn write_saw(out: &mut Vec<u8>, s: &Vrc6Saw) {
789 out.push(s.rate);
790 out.extend_from_slice(&s.period.to_le_bytes());
791 out.push(u8::from(s.enabled));
792 out.extend_from_slice(&s.timer.to_le_bytes());
793 out.push(s.step);
794 out.push(s.acc);
795 }
796
797 fn read_pulse(src: &[u8], p: &mut Vrc6Pulse) {
798 p.ctrl = src[0];
799 p.period = u16::from_le_bytes([src[1], src[2]]);
800 p.enabled = src[3] != 0;
801 p.timer = u16::from_le_bytes([src[4], src[5]]);
802 p.step = src[6] & 0x0F;
803 }
804
805 fn read_saw(src: &[u8], s: &mut Vrc6Saw) {
806 s.rate = src[0] & 0x3F;
807 s.period = u16::from_le_bytes([src[1], src[2]]);
808 s.enabled = src[3] != 0;
809 s.timer = u16::from_le_bytes([src[4], src[5]]);
810 s.step = src[6];
811 s.acc = src[7];
812 }
813}
814
815#[cfg(test)]
816mod tests {
817 use super::*;
818
819 fn synth(banks_8k: usize) -> Box<[u8]> {
820 let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
821 for b in 0..banks_8k {
822 v[b * PRG_BANK_8K] = b as u8;
823 }
824 v.into_boxed_slice()
825 }
826
827 fn synth_chr(banks_1k: usize) -> Box<[u8]> {
828 let mut v = vec![0u8; banks_1k * CHR_BANK_1K];
829 for b in 0..banks_1k {
830 v[b * CHR_BANK_1K] = b as u8;
831 }
832 v.into_boxed_slice()
833 }
834
835 #[test]
836 fn vrc6_audio_register_decoders_latch_state() {
837 let mut m = Vrc6::new(synth(8), synth_chr(8), 24, Mirroring::Vertical).unwrap();
838 m.cpu_write(0x9000, 0x8F);
840 m.cpu_write(0x9001, 0x23);
842 m.cpu_write(0x9002, 0x81); assert!(m.pulse1.enabled);
844 assert_eq!(m.pulse1.period, 0x123);
845 assert_eq!(m.pulse1.ctrl, 0x8F);
846
847 m.cpu_write(0xA000, 0x07); m.cpu_write(0xA001, 0x40);
850 m.cpu_write(0xA002, 0x80); assert!(m.pulse2.enabled);
852 assert_eq!(m.pulse2.period, 0x040);
853
854 m.cpu_write(0xB000, 0x05); m.cpu_write(0xB001, 0x20);
857 m.cpu_write(0xB002, 0x80); assert!(m.saw.enabled);
859 assert_eq!(m.saw.rate, 5);
860 assert_eq!(m.saw.period, 0x020);
861
862 m.cpu_write(0xB003, 0b0000_0100); assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
865 }
866
867 #[test]
868 fn vrc6_pulse_oscillator_steps_through_duty() {
869 let mut p = Vrc6Pulse {
870 ctrl: 0x4F, period: 4, enabled: true,
873 timer: 0,
874 step: 0,
875 };
876 let mut outputs = Vec::new();
878 for _ in 0..32 {
879 p.clock();
880 outputs.push(p.output());
881 }
882 assert!(outputs.contains(&0x0F));
885 assert!(outputs.contains(&0));
886 }
887
888 #[test]
889 fn vrc6_sawtooth_emits_ramp() {
890 let mut s = Vrc6Saw {
891 rate: 0x10,
892 period: 2,
893 enabled: true,
894 timer: 0,
895 step: 0,
896 acc: 0,
897 };
898 let mut sampled = Vec::new();
900 for _ in 0..60 {
901 s.clock();
902 sampled.push(s.output());
903 }
904 let peak = sampled.iter().copied().max().unwrap();
906 assert!(peak > 0, "saw must emit a non-zero peak");
907 assert!(sampled.contains(&0));
909 }
910
911 #[cfg(feature = "mapper-audio")]
912 #[test]
913 fn vrc6_mix_audio_is_nonzero_when_active() {
914 let mut m = Vrc6::new(synth(8), synth_chr(8), 24, Mirroring::Vertical).unwrap();
915 m.cpu_write(0x9000, 0x8F);
917 m.cpu_write(0x9001, 0x10);
918 m.cpu_write(0x9002, 0x81);
919 m.clock_audio();
921 let s = m.mix_audio();
922 assert!(s < 0, "mix_audio with only p1 must be below center");
925 }
926
927 #[cfg(feature = "mapper-audio")]
928 #[test]
929 fn vrc6_mix_audio_silent_when_disabled() {
930 let m = Vrc6::new(synth(8), synth_chr(8), 24, Mirroring::Vertical).unwrap();
931 let mut m = m;
934 let s = m.mix_audio();
935 assert_eq!(s, -19500);
936 }
937
938 #[test]
939 fn vrc6_save_state_round_trips_audio() {
940 let mut m = Vrc6::new(synth(8), synth_chr(8), 24, Mirroring::Vertical).unwrap();
941 m.cpu_write(0x9000, 0x8F);
942 m.cpu_write(0x9001, 0x12);
943 m.cpu_write(0x9002, 0x83);
944 m.cpu_write(0xB000, 0x07);
945 let blob = m.save_state();
946 assert_eq!(
947 blob[0], VRC6_SECTION_VERSION,
948 "save_state writes the current version"
949 );
950
951 let mut m2 = Vrc6::new(synth(8), synth_chr(8), 24, Mirroring::Vertical).unwrap();
952 m2.load_state(&blob).expect("round-trip");
953 assert_eq!(m2.pulse1.ctrl, 0x8F);
954 assert_eq!(m2.pulse1.period, 0x312);
955 assert!(m2.pulse1.enabled);
956 assert_eq!(m2.saw.rate, 0x07);
957 }
958
959 #[test]
963 fn vrc6_pre_v3_blobs_are_refused() {
964 let m = Vrc6::new(synth(8), synth_chr(8), 24, Mirroring::Vertical).unwrap();
965 let core_len = 23 + m.vram.len();
966 let mut v1 = m.save_state()[..core_len].to_vec();
967 v1[0] = 1;
968 let mut v2 = m.save_state()[..core_len + VRC6_AUDIO_TAIL_LEN].to_vec();
969 v2[0] = 2;
970 let mut m2 = Vrc6::new(synth(8), synth_chr(8), 24, Mirroring::Vertical).unwrap();
971 for (v, old) in [(1u8, &v1), (2, &v2)] {
972 assert!(matches!(
973 m2.load_state(old),
974 Err(MapperError::UnsupportedVersion(got)) if got == v
975 ));
976 }
977 }
978
979 #[test]
983 fn vrc6_save_state_carries_prg_ram_and_chr_ram() {
984 let mut m = Vrc6::new(synth(8), Box::new([]), 24, Mirroring::Vertical).unwrap();
985 m.cpu_write(0x6000, 0x5A);
986 m.cpu_write(0x7FFF, 0xA5);
987 m.ppu_write(0x0000, 0x11);
988 m.ppu_write(0x03FF, 0x22);
991 let blob = m.save_state();
992 let mut m2 = Vrc6::new(synth(8), Box::new([]), 24, Mirroring::Vertical).unwrap();
993 m2.load_state(&blob).expect("round-trip");
994 assert_eq!(m2.cpu_read(0x6000), 0x5A);
995 assert_eq!(m2.cpu_read(0x7FFF), 0xA5);
996 assert_eq!(m2.ppu_read(0x0000), 0x11);
997 assert_eq!(m2.ppu_read(0x03FF), 0x22);
998 }
999
1000 #[test]
1009 fn vrc6_chr_registers_power_on_as_identity() {
1010 for mapper_id in [24, 26] {
1011 let mut m = Vrc6::new(synth(8), synth_chr(32), mapper_id, Mirroring::Vertical).unwrap();
1012 for slot in 0..8u16 {
1013 assert_eq!(
1014 m.ppu_read(slot * 0x0400),
1015 slot as u8,
1016 "mapper {mapper_id}: CHR slot {slot} must read bank {slot} at power-on"
1017 );
1018 }
1019 }
1020 }
1021
1022 #[test]
1028 fn vrc6_soft_reset_keeps_chr_registers() {
1029 let mut m = Vrc6::new(synth(8), synth_chr(32), 24, Mirroring::Vertical).unwrap();
1030 m.cpu_write(0xD000, 0x1F);
1031 m.reset();
1032 assert_eq!(m.ppu_read(0x0000), 0x1F, "reset must not reload slot 0");
1033 assert_eq!(
1034 m.ppu_read(0x0400),
1035 1,
1036 "untouched slot keeps its power-on bank"
1037 );
1038 }
1039
1040 #[test]
1042 fn vrc6_truncated_ram_tail_is_rejected() {
1043 let m = Vrc6::new(synth(8), synth_chr(8), 24, Mirroring::Vertical).unwrap();
1044 let blob = m.save_state();
1045 let mut m2 = Vrc6::new(synth(8), synth_chr(8), 24, Mirroring::Vertical).unwrap();
1046 let err = m2
1047 .load_state(&blob[..blob.len() - 1])
1048 .expect_err("a truncated v3 blob must be rejected");
1049 assert!(matches!(err, MapperError::WrongLength { .. }), "{err:?}");
1050 }
1051}