//! Original Direct Sound tests. No game data or upstream test programs. use gba_core::{ audio::StereoLevel, cpu::Cpu, dma::{DMA_BASE, DMA_STRIDE}, io::{FIFO_A, FIFO_B, HALTCNT, IF, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X, TIMER_BASE, WAVE_RAM}, machine::{Machine, StepKind}, memory::{Memory, ROM_START}, }; fn bus() -> Memory { let mut bus = Memory::new(0xeaff_fffeu32.to_le_bytes().to_vec()).unwrap(); bus.write16(SOUNDCNT_X, 0x80).unwrap(); bus.write16(SOUNDBIAS, 0x200).unwrap(); bus.write16(SOUNDCNT_H, 0x0304).unwrap(); // A, both sides, 100%, timer 0. bus } fn timer(bus: &mut Memory, index: u32, period: u16, control: u16) { bus.write32( TIMER_BASE + index * 4, u32::from(0u16.wrapping_sub(period)) | u32::from(control) << 16, ) .unwrap(); } fn stereo(value: i16) -> StereoLevel { StereoLevel { left: value, right: value, } } fn sound_dma(bus: &mut Memory, channel: u32, control: u16) { let base = DMA_BASE + channel * DMA_STRIDE; bus.write32(base, 0x0200_0000).unwrap(); bus.write32(base + 4, FIFO_A + (channel - 1) * 4).unwrap(); bus.write32(base + 8, u32::from(control | 0xb000) << 16 | 1) .unwrap(); } #[test] fn signed_little_endian_samples_hold_between_edges_then_underflow_to_zero() { let mut bus = bus(); bus.write32(FIFO_A, 0x7f80ff01).unwrap(); timer(&mut bus, 0, 10, 0x80); assert_eq!(bus.audio_level(), stereo(0)); for sample in [4, -4, -512, 508, 0, 0] { bus.advance_cycles(9); let previous = bus.audio_level(); bus.advance_cycles(0); assert_eq!(bus.audio_level(), previous); bus.advance_cycles(1); assert_eq!(bus.audio_level(), stereo(sample)); } assert!(bus.read32(FIFO_A).is_err()); } #[test] fn independent_timer_selection_cascade_routing_volume_and_clipping() { let mut bus = bus(); bus.write16(SOUNDCNT_H, 0x610c).unwrap(); // A right/timer0, B left/timer1, both 100%. bus.write32(FIFO_A, 0x7f7f7f7f).unwrap(); bus.write32(FIFO_B, 0x80808080).unwrap(); timer(&mut bus, 0, 2, 0x80); timer(&mut bus, 1, 2, 0x84); // Cascade: one B sample per two A samples. bus.advance_cycles(2); assert_eq!( bus.audio_level(), StereoLevel { left: 0, right: 508 } ); bus.advance_cycles(2); assert_eq!( bus.audio_level(), StereoLevel { left: -512, right: 508 } ); bus.write16(SOUNDCNT_H, 0x3300).unwrap(); // Both sides, both 50%, retain held samples. assert_eq!(bus.audio_level(), stereo(-2)); bus.write16(SOUNDBIAS, 0).unwrap(); assert_eq!(bus.audio_level(), stereo(-512)); bus.write16(SOUNDBIAS, 0x3fe).unwrap(); bus.write16(SOUNDCNT_H, 0x0304).unwrap(); assert_eq!(bus.audio_level(), stereo(511)); } #[test] fn muted_routes_still_consume_but_master_disable_freezes_playback() { let mut bus = bus(); bus.write16(SOUNDCNT_H, 4).unwrap(); bus.write32(FIFO_A, 0x04030201).unwrap(); timer(&mut bus, 0, 1, 0x80); bus.advance_cycles(1); assert_eq!(bus.audio_level(), stereo(0)); bus.write16(SOUNDCNT_H, 0x0304).unwrap(); assert_eq!(bus.audio_level(), stereo(4)); bus.write16(SOUNDCNT_X, 0).unwrap(); bus.advance_cycles(100); assert_eq!(bus.audio_level(), stereo(0)); bus.write16(SOUNDCNT_X, 0x80).unwrap(); bus.advance_cycles(1); assert_eq!(bus.audio_level(), stereo(8)); // In-flight word is separate from the reset queue. } #[test] fn reset_strobes_clear_queues_not_in_flight_word_or_held_level() { let mut bus = bus(); bus.write32(FIFO_A, 0x04030201).unwrap(); bus.write32(FIFO_A, 0x7f7f7f7f).unwrap(); timer(&mut bus, 0, 1, 0x80); bus.advance_cycles(1); bus.write16(SOUNDCNT_H, 0x8b04).unwrap(); assert_eq!(bus.read16(SOUNDCNT_H).unwrap(), 0x0304); assert_eq!(bus.audio_level(), stereo(4)); for sample in [8, 12, 16, 0] { bus.advance_cycles(1); assert_eq!(bus.audio_level(), stereo(sample)); } } #[test] fn partial_writes_enqueue_words_and_preserve_other_lanes_on_reused_slots() { let mut bus = bus(); timer(&mut bus, 0, 1, 0x80); // Visit each of the seven queue slots. Reuse the first with a byte write. for _ in 0..7 { bus.write32(FIFO_A, 0x04030201).unwrap(); bus.advance_cycles(4); } bus.write8(FIFO_A + 1, 9).unwrap(); for sample in [4, 36, 12, 16] { bus.advance_cycles(1); assert_eq!(bus.audio_level(), stereo(sample)); } bus.write16(FIFO_A + 2, 0x0807).unwrap(); for sample in [4, 8, 28, 32] { bus.advance_cycles(1); assert_eq!(bus.audio_level(), stereo(sample)); } } #[test] fn full_queue_overflow_clears_queued_words() { let mut bus = bus(); for _ in 0..8 { bus.write32(FIFO_A, 0x7f7f7f7f).unwrap(); } timer(&mut bus, 0, 1, 0x80); bus.advance_cycles(1); assert_eq!(bus.audio_level(), stereo(0)); bus.write32(FIFO_A, 1).unwrap(); bus.advance_cycles(1); assert_eq!(bus.audio_level(), stereo(4)); } #[test] fn wave_banks_and_idle_channel_masks_preserve_explicit_limits() { let mut bus = bus(); bus.write16(0x04000062, 0xffff).unwrap(); assert_eq!(bus.read16(0x04000062).unwrap(), 0xffc0); bus.write32(WAVE_RAM, 0x11223344).unwrap(); // CPU bank 1. bus.write16(0x04000070, 0x40).unwrap(); assert_eq!(bus.read32(WAVE_RAM).unwrap(), 0); bus.write32(WAVE_RAM, 0xaabbccdd).unwrap(); let previous = bus.read16(0x04000074).unwrap(); assert!(bus .write16(0x04000074, 0xffff) .unwrap_err() .to_string() .contains("PSG channel trigger")); assert_eq!(bus.read16(0x04000074).unwrap(), previous); bus.write16(SOUNDCNT_X, 0).unwrap(); assert_eq!(bus.read32(WAVE_RAM).unwrap(), 0x11223344); assert_eq!(bus.read16(0x04000062).unwrap(), 0); bus.write16(0x04000064, 0xffff).unwrap(); // Disabled PSG writes are ignored. bus.write16(SOUNDCNT_X, 0xff).unwrap(); assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80); bus.write16(0x04000070, 0x40).unwrap(); assert_eq!(bus.read32(WAVE_RAM).unwrap(), 0xaabbccdd); } #[test] fn sound_dma_forces_four_words_and_fixed_destination_then_repeats_and_interrupts() { for channel in [1, 2] { let mut bus = bus(); let fifo = FIFO_A + (channel - 1) * 4; let route = if channel == 1 { 0x0304 } else { 0x3008 }; bus.write16(SOUNDCNT_H, route).unwrap(); for index in 0..8 { bus.write32(0x02000000 + index * 4, 0x01010101 * (index + 1)) .unwrap(); } sound_dma(&mut bus, channel, 0x4200); // IRQ/repeat, programmed count1, halfword/increment destination. timer(&mut bus, 0, 100, 0x80); bus.advance_cycles(100); // Empty playback, request. let mut machine = Machine::new(Cpu::new(ROM_START), bus); for _ in 0..4 { assert_eq!( machine.step().unwrap(), StepKind::Dma { channel: channel as usize } ); } assert_ne!( machine.memory().read16(IF).unwrap() & (1 << (8 + channel)), 0 ); assert_ne!( machine .memory() .read16(DMA_BASE + channel * DMA_STRIDE + 10) .unwrap() & 0x8000, 0 ); // No fifth unit. Four queued words suppress the next request BEFORE the playback word loads. assert!(matches!(machine.step().unwrap(), StepKind::Instruction)); let remaining = 100 - (machine.cycles() as u32 % 100); machine.memory_mut().advance_cycles(remaining); assert_eq!(machine.memory().audio_level(), stereo(4)); assert!(matches!(machine.step().unwrap(), StepKind::Instruction)); let remaining = 100 - (machine.cycles() as u32 % 100); machine.memory_mut().advance_cycles(remaining); for _ in 0..4 { assert_eq!( machine.step().unwrap(), StepKind::Dma { channel: channel as usize } ); } // Drain without servicing further requests. The repeated block must continue the source, // use four-byte strides, and never advance the FIFO destination into the other channel. for offset in 0..30 { let remaining = 100 - (machine.cycles() as u32 % 100); machine.memory_mut().advance_cycles(remaining); assert_eq!( machine.memory().audio_level(), stereo(((offset + 2) / 4 + 1) * 4) ); } assert!(machine.memory().read32(fifo).is_err()); } } #[test] fn nonrepeat_sound_dma_disables_after_four_words() { let mut bus = bus(); sound_dma(&mut bus, 1, 0); timer(&mut bus, 0, 1000, 0x80); bus.advance_cycles(1000); let mut machine = Machine::new(Cpu::new(ROM_START), bus); for _ in 0..4 { assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 1 }); } assert_eq!( machine.memory().read16(DMA_BASE + DMA_STRIDE + 10).unwrap() & 0x8000, 0 ); } #[test] fn halt_clocks_samples_and_stop_freezes_them() { for stop in [false, true] { let mut bus = bus(); bus.write32(FIFO_A, 0x04030201).unwrap(); timer(&mut bus, 0, 10, 0x80); bus.write8(HALTCNT, if stop { 0x80 } else { 0 }).unwrap(); let mut machine = Machine::new(Cpu::new(ROM_START), bus); machine.step().unwrap(); assert_eq!( machine.memory().audio_level(), stereo(if stop { 0 } else { 4 }) ); if stop { machine.memory_mut().advance_cycles(u32::MAX); assert_eq!(machine.cycles(), 0); } } } #[test] fn large_device_batches_match_single_clocks_with_and_without_row_capture() { for capture in [false, true] { let mut bulk = bus(); let mut small = bus(); for bus in [&mut bulk, &mut small] { if capture { bus.set_scanline_rendering(true); } bus.write16(SOUNDCNT_H, 0x730c).unwrap(); for index in 1..=7 { bus.write32(FIFO_A, index * 0x01010101).unwrap(); bus.write32(FIFO_B, (index + 8) * 0x01010101).unwrap(); } timer(bus, 0, 2, 0x80); timer(bus, 1, 3, 0x80); } for cycles in [1, 2, 3, 17, 39, 1000] { bulk.advance_cycles(cycles); for _ in 0..cycles { small.advance_cycles(1); } assert_eq!(bulk.audio_level(), small.audio_level()); } bulk.set_scanline_rendering(false); bulk.advance_cycles(u32::MAX); assert_eq!(bulk.audio_level(), stereo(0)); } }