feat(audio): implement nominal Direct Sound playback and FIFO DMA

This commit is contained in:
0xrsydn 2026-10-07 01:35:55 +07:00
commit 5888c4d292
21 changed files with 933 additions and 129 deletions

View file

@ -0,0 +1,219 @@
//! Nominal Direct Sound device state. No host audio or PSG synthesis.
pub const SOUND_START: u32 = 0x0400_0060;
pub const SOUNDCNT_H: u32 = 0x0400_0082;
pub const SOUNDCNT_X: u32 = 0x0400_0084;
pub const SOUNDBIAS: u32 = 0x0400_0088;
pub const WAVE_RAM: u32 = 0x0400_0090;
pub const FIFO_A: u32 = 0x0400_00a0;
pub const FIFO_B: u32 = 0x0400_00a4;
/// Instantaneous digital mixer levels, not a sampled audio stream.
/// Bias is applied, clipped to ten bits, then centered at 512: -512..=511.
/// Master disable returns zero. PWM quantization and analog filtering are absent.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct StereoLevel {
pub left: i16,
pub right: i16,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
struct Fifo {
words: [u32; 7],
read: usize,
len: usize,
playback: u32,
remaining: u8,
sample: i8,
}
impl Fifo {
fn reset_queue(&mut self) {
self.words = [0; 7];
self.read = 0;
self.len = 0;
}
fn write(&mut self, address: u32, bytes: &[u8]) {
if self.len == 7 {
self.reset_queue();
return;
}
let slot = (self.read + self.len) % 7;
for (offset, &byte) in bytes.iter().enumerate() {
let shift = ((address & 3) as usize + offset) * 8;
self.words[slot] = (self.words[slot] & !(255 << shift)) | (u32::from(byte) << shift);
}
self.len += 1;
}
fn clock(&mut self, overflows: u64) -> bool {
let mut request = false;
// At most 32 queued/in-flight bytes, then one empty sample. This bounds
// device-only advances even with a timer overflow on every clock.
for _ in 0..overflows.min(33) {
request |= self.len <= 3;
if self.remaining == 0 && self.len != 0 {
self.playback = self.words[self.read];
self.read = (self.read + 1) % 7;
self.len -= 1;
self.remaining = 4;
}
self.sample = self.playback as i8;
if self.remaining != 0 {
self.playback >>= 8;
self.remaining -= 1;
}
}
request
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Audio {
enabled: bool,
psg: [u16; 17],
control: u16,
bias: u16,
wave: [[u8; 16]; 2],
fifo: [Fifo; 2],
requests: [bool; 2],
}
impl Audio {
pub(crate) fn mapped(address: u32) -> bool {
matches!(address, SOUND_START..=0x0400_008b | WAVE_RAM..=0x0400_00a7)
}
pub(crate) fn unsupported(&self, address: u32, value: u8) -> Option<&'static str> {
if self.enabled
&& matches!(
address,
0x0400_0065 | 0x0400_006d | 0x0400_0075 | 0x0400_007d
)
&& value & 0x80 != 0
{
Some("PSG channel trigger (synthesis not implemented)")
} else {
None
}
}
fn wave_bank(&self) -> usize {
1 ^ usize::from((self.psg[8] >> 6) & 1)
}
pub(crate) fn read8(&self, address: u32) -> Option<u8> {
if (WAVE_RAM..FIFO_A).contains(&address) {
return Some(self.wave[self.wave_bank()][(address - WAVE_RAM) as usize]);
}
let value = match address & !1 {
SOUND_START..=0x0400_0080 => {
let mask = match address & !1 {
0x0400_0060 => 0x007f,
0x0400_0062 | 0x0400_0068 => 0xffc0,
0x0400_0064 | 0x0400_006c | 0x0400_0074 => 0x4000,
0x0400_0070 => 0x00e0,
0x0400_0072 => 0xe000,
0x0400_0078 => 0xff00,
0x0400_007c => 0x40ff,
0x0400_0080 => 0xff77,
_ => 0,
};
self.psg[((address - SOUND_START) / 2) as usize] & mask
}
SOUNDCNT_H => self.control,
SOUNDCNT_X => u16::from(self.enabled) << 7,
SOUNDBIAS => self.bias,
0x0400_0086 | 0x0400_008a => 0,
_ => return None, // FIFO is write-only; open-bus readback is not modeled.
};
Some(value.to_le_bytes()[(address & 1) as usize])
}
/// One bus access. Partial FIFO writes advance a whole word, not one byte.
pub(crate) fn write(&mut self, address: u32, bytes: &[u8]) {
if (FIFO_A..=FIFO_B + 3).contains(&address) {
self.fifo[((address - FIFO_A) / 4) as usize].write(address, bytes);
return;
}
for (offset, &value) in bytes.iter().enumerate() {
let address = address + offset as u32;
if (WAVE_RAM..FIFO_A).contains(&address) {
self.wave[self.wave_bank()][(address - WAVE_RAM) as usize] = value;
continue;
}
let shift = (address & 1) * 8;
let merge = |old: u16| (old & !(255 << shift)) | (u16::from(value) << shift);
match address & !1 {
SOUND_START..=0x0400_0080 if self.enabled => {
let register = &mut self.psg[((address - SOUND_START) / 2) as usize];
*register = merge(*register);
}
SOUNDCNT_H => {
self.control = merge(self.control) & 0x770f;
if address & 1 != 0 {
for (index, mask) in [8, 128].into_iter().enumerate() {
if value & mask != 0 {
self.fifo[index].reset_queue();
}
}
}
}
SOUNDCNT_X if address & 1 == 0 => {
if self.enabled && value & 0x80 == 0 {
self.psg = [0; 17];
for fifo in &mut self.fifo {
fifo.reset_queue();
}
}
self.enabled = value & 0x80 != 0;
}
SOUNDBIAS => self.bias = merge(self.bias) & 0xc3fe,
_ => {}
}
}
}
pub(crate) fn timer_overflows(&mut self, timer: usize, count: u64) {
if !self.enabled || timer > 1 {
return;
}
for index in 0..2 {
let selected = usize::from((self.control >> (10 + index * 4)) & 1);
if selected == timer {
self.requests[index] |= self.fifo[index].clock(count);
}
}
}
pub(crate) fn take_requests(&mut self) -> [bool; 2] {
std::mem::take(&mut self.requests)
}
pub(crate) fn level(&self) -> StereoLevel {
if !self.enabled {
return StereoLevel::default();
}
let mut sides = [0i16; 2]; // right, left
for index in 0..2 {
let volume = if self.control & (4 << index) != 0 {
4
} else {
2
};
for (side, level) in sides.iter_mut().enumerate() {
if self.control & (1 << (8 + index * 4 + side)) != 0 {
*level += i16::from(self.fifo[index].sample) * volume;
}
}
}
for level in &mut sides {
*level = (*level + (self.bias & 0x3ff) as i16).clamp(0, 1023) - 512;
}
StereoLevel {
left: sides[1],
right: sides[0],
}
}
}

View file

@ -92,12 +92,15 @@ fn emit_ram_reset(a: &mut ArmImage) {
a.literal(1, SOUNDCNT_X);
store_halfword(a, 0); // Disable sound; PSG register state is held in reset.
a.literal(1, SOUNDCNT_H);
a.emit(0xe3a0_3c88); // MOV r3,#0x8800 (clear both FIFO queues)
store_halfword(a, 0);
a.emit(0xe3a0_3000); // MOV r3,#0
store_halfword(a, 0);
a.literal(1, SOUNDBIAS);
a.emit(0xe3a0_3c02); // MOV r3,#0x200 (bias default)
store_halfword(a, 0);
a.emit(0xe3a0_3000); // MOV r3,#0
clear_words(a, WAVE_RAM, 16); // Accessible inactive bank; other bank cannot be enabled yet.
clear_words(a, WAVE_RAM, 16); // CPU-accessible inactive bank 1; bank 0 is retained.
a.label("ram_reset_other");
a.emit(0xe310_0080); // TST r0,#other registers

View file

@ -1,6 +1,7 @@
//! Four-channel GBA direct memory access (DMA) controller.
//! Transfers are scheduled one data unit at a time. Startup/resumption delays,
//! CPU bus handoff, sound FIFO, video capture, and Game Pak DRQ are not modeled.
//! CPU bus handoff, video capture, and Game Pak DRQ are not modeled.
//! Sound requests support DMA1/FIFO A and DMA2/FIFO B; other pairings are diagnostic.
//! Each channel retains known source data across blocks, including blocked reads.
use std::{error::Error, fmt};
@ -79,12 +80,16 @@ impl Channel {
(self.control >> 12) & 3
}
fn unsupported(&self) -> bool {
self.start_timing() == 3 || self.control & 0x800 != 0 || self.control & 0x180 == 0x180
fn unsupported(&self, index: usize) -> bool {
(self.start_timing() == 3
&& (!(1..=2).contains(&index)
|| self.destination != crate::audio::FIFO_A + (index as u32 - 1) * 4))
|| self.control & 0x800 != 0
|| self.control & 0x180 == 0x180
}
fn width(&self) -> AccessWidth {
if self.control & 0x400 != 0 {
if self.start_timing() == 3 || self.control & 0x400 != 0 {
AccessWidth::Word
} else {
AccessWidth::Halfword
@ -92,7 +97,9 @@ impl Channel {
}
fn initial_count(&self, index: usize) -> u32 {
if self.count != 0 {
if self.start_timing() == 3 {
4
} else if self.count != 0 {
u32::from(self.count)
} else if index == 3 {
0x1_0000
@ -183,6 +190,15 @@ impl Dma {
}
}
pub(crate) fn trigger_sound(&mut self, requests: [bool; 2]) {
for (index, requested) in requests.into_iter().enumerate() {
let channel = &mut self.channels[index + 1];
if requested && channel.enabled() && channel.start_timing() == 3 && !channel.active {
channel.active = true;
}
}
}
/// Select the highest-priority ready channel without changing its state.
/// Unsupported enabled modes report a diagnostic rather than silently waiting.
pub(crate) fn next(&self) -> Result<Option<Transfer>, DmaError> {
@ -190,7 +206,7 @@ impl Dma {
if !channel.enabled() {
continue;
}
if channel.unsupported() {
if channel.unsupported(index) {
return Err(DmaError::UnsupportedControl {
channel: index,
control: channel.control,
@ -225,7 +241,11 @@ impl Dma {
channel.current_destination = advance_address(
channel.current_destination,
width,
(channel.control >> 5) & 3,
if channel.start_timing() == 3 {
2
} else {
(channel.control >> 5) & 3
},
);
channel.remaining -= 1;
channel.first = false;
@ -236,7 +256,7 @@ impl Dma {
if channel.control & 0x200 != 0 && channel.start_timing() != 0 {
channel.remaining = channel.initial_count(index);
channel.first = true;
if channel.control & 0x60 == 0x60 {
if channel.start_timing() != 3 && channel.control & 0x60 == 0x60 {
channel.current_destination = channel.destination & !(width - 1);
}
} else {

View file

@ -1,13 +1,12 @@
//! GBA I/O subset: DMA, timers, interrupts, WAITCNT, display, keypad input,
//! disabled sound state, and disconnected serial initialization.
//! Timers share a free-running prescaler phase. Hardware startup/write delays,
//! audio events, and interrupt delivery delays are not modeled.
//! Direct Sound, and disconnected serial initialization.
//! Timers share a free-running prescaler phase. Hardware startup/write delays
//! and interrupt delivery delays are not modeled.
mod inactive;
pub use inactive::{
JOYCNT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT, SIODATA32, SIODATA8, SOUNDBIAS, SOUNDCNT_H,
SOUNDCNT_X, SOUND_START, WAVE_RAM,
};
use crate::audio::Audio;
pub use crate::audio::{FIFO_A, FIFO_B, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X, SOUND_START, WAVE_RAM};
pub use inactive::{JOYCNT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT, SIODATA32, SIODATA8};
mod timer_step;
pub(crate) use timer_step::TimerStep;
use timer_step::TIMER_IRQ_MASK;
@ -157,6 +156,7 @@ enum PowerState {
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub(crate) struct Io {
inactive: inactive::Inactive,
pub(crate) audio: Audio,
timers: [Timer; 4],
// Low ten system-clock bits cover every supported divider. STOP freezes this phase.
timer_phase: u16,
@ -181,7 +181,8 @@ pub(crate) struct Io {
impl Io {
pub(crate) fn mapped(address: u32) -> bool {
inactive::Inactive::mapped(address)
Audio::mapped(address)
|| inactive::Inactive::mapped(address)
|| matches!(address, 0x0400_0000..=0x0400_0057 | DMA_BASE..=DMA_END | 0x0400_0100..=0x0400_010f | 0x0400_0130..=0x0400_0133 | 0x0400_0200..=0x0400_020b | POSTFLG..=0x0400_0303)
}
@ -190,6 +191,9 @@ impl Io {
}
pub(crate) fn read8(&self, address: u32) -> Option<u8> {
if Audio::mapped(address) {
return self.audio.read8(address);
}
if inactive::Inactive::mapped(address) {
return self.inactive.read8(address);
}
@ -420,17 +424,25 @@ impl Io {
}
pub(crate) fn commit_timer_step(&mut self, step: TimerStep) {
self.audio = step.audio;
self.dma.trigger_sound(self.audio.take_requests());
self.timers = step.timers;
self.timer_phase = step.phase;
self.pending = (self.pending & !TIMER_IRQ_MASK) | step.pending;
self.wake_if_requested();
}
fn advance_timer_bank(timers: &mut [Timer; 4], phase: &mut u16, cycles: u32) -> u16 {
fn advance_timer_bank(
timers: &mut [Timer; 4],
phase: &mut u16,
audio: &mut Audio,
cycles: u32,
) -> u16 {
let mut pending = 0;
let mut overflows = 0;
for (index, timer) in timers.iter_mut().enumerate() {
overflows = timer.advance(cycles, overflows, *phase);
audio.timer_overflows(index, overflows);
if overflows != 0 && timer.control & 0x40 != 0 {
pending |= 1 << (3 + index);
}
@ -473,8 +485,13 @@ impl Io {
self.dma.trigger(vblank, hblank);
self.pending |= self.display.advance(cycles);
if advance_timers {
self.pending |=
Self::advance_timer_bank(&mut self.timers, &mut self.timer_phase, cycles);
self.pending |= Self::advance_timer_bank(
&mut self.timers,
&mut self.timer_phase,
&mut self.audio,
cycles,
);
self.dma.trigger_sound(self.audio.take_requests());
}
self.wake_if_requested();
}

View file

@ -1,14 +1,7 @@
//! Disabled sound and disconnected serial initialization state.
//! Sound enable, FIFO writes, serial transfers, UART/multiplayer/Joybus modes,
//! and GPIO interrupts remain explicit diagnostics, not silent device stubs.
//! Disconnected serial initialization state. Active transfers remain diagnostic.
use super::replace_byte;
pub const SOUND_START: u32 = 0x0400_0060;
pub const SOUNDCNT_H: u32 = 0x0400_0082;
pub const SOUNDCNT_X: u32 = 0x0400_0084;
pub const SOUNDBIAS: u32 = 0x0400_0088;
pub const WAVE_RAM: u32 = 0x0400_0090;
pub const SIODATA32: u32 = 0x0400_0120;
pub const SIOCNT: u32 = 0x0400_0128;
pub const SIODATA8: u32 = 0x0400_012a;
@ -19,11 +12,6 @@ pub const JOY_TRANS: u32 = 0x0400_0154;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub(super) struct Inactive {
sound_control: u16,
bias: u16,
// With sound disabled, SOUND3CNT_L is zero. The CPU accesses bank 1;
// bank 0 stays zero because enabling sound/bank selection is unsupported.
wave: [u8; 16],
serial_data: [u8; 4],
serial_send: u8,
serial_control: u16,
@ -33,7 +21,6 @@ pub(super) struct Inactive {
impl Inactive {
pub(super) fn mapped(address: u32) -> bool {
matches!(address,
SOUND_START..=0x0400_008b | WAVE_RAM..=0x0400_009f |
SIODATA32..=0x0400_0123 | SIOCNT..=0x0400_012b |
RCNT..=0x0400_0135 | JOYCNT..=0x0400_0141 |
JOY_RECV..=0x0400_0157)
@ -43,9 +30,6 @@ impl Inactive {
/// stores can validate all values before committing any RAM/I/O writes.
pub(super) fn unsupported(address: u32, value: u8) -> Option<&'static str> {
match address {
SOUNDCNT_X if value & 0x80 != 0 => {
Some("sound master enable (audio engine not implemented)")
}
SIOCNT if value & 0x80 != 0 => Some("serial transfer start"),
0x0400_0129 if value & 0x20 != 0 => Some("multiplayer/UART serial mode"),
0x0400_0135 if value & 0x40 != 0 => Some("Joybus serial mode"),
@ -62,16 +46,10 @@ impl Inactive {
}
pub(super) fn read8(&self, address: u32) -> Option<u8> {
if (WAVE_RAM..=WAVE_RAM + 15).contains(&address) {
return Some(self.wave[(address - WAVE_RAM) as usize]);
}
if (SIODATA32..=SIODATA32 + 3).contains(&address) {
return Some(self.serial_data[(address - SIODATA32) as usize]);
}
let value = match address & !1 {
SOUNDCNT_H => self.sound_control,
SOUNDBIAS => self.bias,
SOUND_START..=0x0400_008a => 0, // PSG stays reset while master enable is clear.
SIOCNT => {
let si = if self.rcnt & 0x8000 != 0 {
self.gpio_pins() & 4
@ -96,25 +74,17 @@ impl Inactive {
pub(super) fn write8(&mut self, address: u32, value: u8) {
debug_assert!(Self::unsupported(address, value).is_none());
if (WAVE_RAM..=WAVE_RAM + 15).contains(&address) {
self.wave[(address - WAVE_RAM) as usize] = value;
return;
}
if (SIODATA32..=SIODATA32 + 3).contains(&address) {
self.serial_data[(address - SIODATA32) as usize] = value;
return;
}
match address & !1 {
SOUNDCNT_H => {
self.sound_control = replace_byte(self.sound_control, address, value) & 0x770f
}
SOUNDBIAS => self.bias = replace_byte(self.bias, address, value) & 0xc3fe,
SIOCNT => {
self.serial_control = replace_byte(self.serial_control, address, value) & 0x500b
}
SIODATA8 if address & 1 == 0 => self.serial_send = value,
RCNT => self.rcnt = replace_byte(self.rcnt, address, value) & 0x80ff,
_ => {} // Disabled PSG writes and unused/read-only bits have no effect.
_ => {} // Unused/read-only bits have no effect.
}
}
}

View file

@ -1,4 +1,4 @@
//! Staged timer clocks for one machine instruction, IRQ entry, or DMA unit.
//! Staged timer and Direct Sound clocks for one instruction, IRQ entry, or DMA unit.
//! Other devices remain on the instruction-boundary scheduler.
use super::{Io, Timer, IF, TIMER_BASE};
@ -6,6 +6,7 @@ pub(super) const TIMER_IRQ_MASK: u16 = 0x78;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct TimerStep {
pub(crate) audio: crate::audio::Audio,
pub(super) timers: [Timer; 4],
pub(super) phase: u16,
pub(super) pending: u16,
@ -15,6 +16,7 @@ pub(crate) struct TimerStep {
impl TimerStep {
pub(super) fn new(io: &Io) -> Self {
Self {
audio: io.audio,
timers: io.timers,
phase: io.timer_phase,
pending: io.pending & TIMER_IRQ_MASK,
@ -24,13 +26,20 @@ impl TimerStep {
pub(crate) fn advance_to(&mut self, elapsed: u32) {
assert!(elapsed >= self.elapsed, "timer step time must be monotonic");
self.pending |=
Io::advance_timer_bank(&mut self.timers, &mut self.phase, elapsed - self.elapsed);
self.pending |= Io::advance_timer_bank(
&mut self.timers,
&mut self.phase,
&mut self.audio,
elapsed - self.elapsed,
);
self.elapsed = elapsed;
}
/// All lanes of a bus access observe the same staged time.
pub(crate) fn read8(&self, address: u32, committed: u8) -> Option<u8> {
if crate::audio::Audio::mapped(address) {
return self.audio.read8(address);
}
match address {
TIMER_BASE..=0x0400_010f => {
Some(self.timers[((address - TIMER_BASE) / 4) as usize].read8(address))

View file

@ -1,5 +1,6 @@
//! An educational GBA starting point, not a game-compatible emulator.
pub mod audio;
pub mod bios;
pub mod cpu;
pub mod display;

View file

@ -1,5 +1,7 @@
use std::{cell::Cell, error::Error, fmt};
#[cfg(test)]
mod audio_step_tests;
mod fetch;
#[cfg(test)]
mod fetch_tests;
@ -18,6 +20,7 @@ pub(crate) use fetch::InstructionFetch;
use iwram_bus::IwramBus;
use crate::{
audio::{Audio, StereoLevel},
cpu::InstructionSet,
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
dma::{DmaError, DMA_BASE, DMA_END},
@ -97,7 +100,7 @@ struct CpuAccess {
/// Successful DMA accesses drive local lanes even before the first CPU instruction.
/// Channel data remains separate; general and sub-instruction CPU bus handoff is unmodeled.
/// Instruction buffering and supported bus history consume shared fetch samples.
/// Timer data accesses use staged machine-step time; other per-access device timing remains unmodeled.
/// Timer and sound accesses use staged machine-step time; other per-access device timing remains unmodeled.
pub struct Memory {
external_ram: Vec<u8>,
internal_ram: Vec<u8>,
@ -162,6 +165,17 @@ impl Memory {
Ok(memory)
}
/// Current committed Direct Sound mixer level. This is not a host sample stream.
pub fn audio_level(&self) -> StereoLevel {
self.io.audio.level()
}
fn audio_state(&self) -> Audio {
self.timer_step
.get()
.map_or(self.io.audio, |step| step.audio)
}
/// Total elapsed emulated clock cycles, wrapping at u64::MAX. Reads/writes
/// alone consume no cycles. STOP discards externally supplied clock advances.
pub fn cycles(&self) -> u64 {
@ -727,7 +741,7 @@ impl Memory {
bytes: [u8; N],
) -> Result<(), MemoryError> {
let index = self.write_index::<N>(address)?;
Self::validate_io_values(address, &bytes)?;
Self::validate_io_values(&mut self.audio_state(), address, &bytes)?;
self.record_access(
address,
match N {
@ -753,6 +767,15 @@ impl Memory {
self.iwram_bus.access(address, width, value);
}
if address >> 24 == 0x04 {
if Audio::mapped(address) {
if let Some(mut step) = self.timer_step.get() {
step.audio.write(address, &bytes);
self.timer_step.set(Some(step));
} else {
self.io.audio.write(address, &bytes);
}
return Ok(());
}
if address & !3 == KEYINPUT {
self.io.write_keypad(address, &bytes);
return Ok(());
@ -843,11 +866,17 @@ impl Memory {
}
}
fn validate_io_values(address: u32, bytes: &[u8]) -> Result<(), MemoryError> {
fn validate_io_values(
audio: &mut Audio,
address: u32,
bytes: &[u8],
) -> Result<(), MemoryError> {
if address >> 24 == 0x04 {
for (offset, &value) in bytes.iter().enumerate() {
let address = address + offset as u32;
if let Some(operation) = Io::unsupported_write(address, value) {
if let Some(operation) = Io::unsupported_write(address, value)
.or_else(|| audio.unsupported(address, value))
{
return Err(MemoryError::UnsupportedIo {
address,
value,
@ -855,6 +884,9 @@ impl Memory {
});
}
}
if Audio::mapped(address) {
audio.write(address, bytes);
}
}
Ok(())
}
@ -863,9 +895,10 @@ impl Memory {
/// leave a partial block store. This is not a model of hardware data aborts.
/// Mapped I/O reads currently have no side effects; writes cannot fail after validation.
pub(crate) fn write_words(&mut self, writes: &[(u32, u32)]) -> Result<(), MemoryError> {
let mut audio = self.audio_state();
for &(address, value) in writes {
self.write_index::<4>(address)?;
Self::validate_io_values(address, &value.to_le_bytes())?;
Self::validate_io_values(&mut audio, address, &value.to_le_bytes())?;
}
for &(address, value) in writes {
// The map cannot change between validation and these writes.

View file

@ -0,0 +1,104 @@
//! Original checks of audio bus phases and transactional timer/audio ownership.
use super::{timing_event_tests::prepared, *};
use crate::{
io::{FIFO_A, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X, TIMER_BASE},
machine::Machine,
};
fn sound(memory: &mut Memory, period: u16) {
memory.write16(SOUNDCNT_X, 0x80).unwrap();
memory.write16(SOUNDBIAS, 0x200).unwrap();
memory.write16(SOUNDCNT_H, 0x0304).unwrap();
memory
.write32(
TIMER_BASE,
0x00800000 | u32::from(0u16.wrapping_sub(period)),
)
.unwrap();
}
#[test]
fn fifo_store_becomes_visible_after_its_bus_completion_not_at_instruction_start() {
let (cpu, mut memory) = prepared(
false,
&[0xe5810000, 0xe1a00000],
&[(0, 0x04030201), (1, FIFO_A)],
);
sound(&mut memory, 1);
let mut machine = Machine::new(cpu, memory);
machine.step().unwrap();
assert_eq!(machine.cycles(), 7);
assert_eq!(machine.memory().audio_level().left, 0); // Earlier seven empty overflows.
machine.memory_mut().advance_cycles(1);
assert_eq!(machine.memory().audio_level().left, 4);
}
#[test]
fn master_enable_store_does_not_replay_earlier_timer_edges() {
let (cpu, mut memory) = prepared(false, &[0xe5810000], &[(0, 0x80), (1, SOUNDCNT_X)]);
sound(&mut memory, 1);
memory.write16(SOUNDCNT_X, 0).unwrap();
memory.write32(FIFO_A, 0x04030201).unwrap();
let mut machine = Machine::new(cpu, memory);
machine.step().unwrap();
assert_eq!(machine.memory().audio_level().left, 0);
machine.memory_mut().advance_cycles(1);
assert_eq!(machine.memory().audio_level().left, 4);
}
#[test]
fn failed_cpu_load_rolls_back_timer_audio_and_pending_refill_requests() {
let (cpu, mut memory) = prepared(false, &[0xe8910005], &[(1, SOUNDBIAS)]); // LDM: bias then unmapped 8c.
sound(&mut memory, 1);
memory.write32(FIFO_A, 0x04030201).unwrap();
let audio = memory.io.audio;
let mut machine = Machine::new(cpu, memory);
let cpu = machine.cpu().clone();
for _ in 0..2 {
assert!(machine.step().is_err());
assert_eq!(machine.memory().io.audio, audio);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.cpu(), &cpu);
assert!(machine.memory().timer_step.get().is_none());
}
}
#[test]
fn failed_dma_source_and_destination_restore_audio_after_speculative_timer_edges() {
for (source, destination, word) in [(FIFO_A, 0x02001000, 0), (0x02000000, 0x04000064, 0x8000)] {
let (cpu, mut memory) = prepared(false, &[0xe1a00000], &[]);
sound(&mut memory, 1);
memory.write32(FIFO_A, 0x04030201).unwrap();
memory.write32(0x02000000, word).unwrap();
memory.write32(DMA_BASE, source).unwrap();
memory.write32(DMA_BASE + 4, destination).unwrap();
memory.write32(DMA_BASE + 8, 0x84000001).unwrap();
let audio = memory.io.audio;
let mut machine = Machine::new(cpu, memory);
for _ in 0..2 {
assert!(machine.step().is_err());
assert_eq!(machine.memory().io.audio, audio);
assert_eq!(machine.cycles(), 0);
}
}
}
#[test]
fn batch_validation_simulates_sound_enable_and_disable_before_committing_any_write() {
let mut memory = Memory::new(vec![]).unwrap();
let original = memory.io.audio;
assert!(memory
.write_words(&[
(0x02000000, 0x11223344),
(SOUNDCNT_X, 0x80),
(0x04000064, 0x8000),
])
.is_err());
assert_eq!(memory.read32(0x02000000).unwrap(), 0);
assert_eq!(memory.io.audio, original);
memory
.write_words(&[(SOUNDCNT_X, 0x80), (SOUNDCNT_X, 0), (0x04000064, 0x8000)])
.unwrap();
assert_eq!(memory.read16(0x04000064).unwrap(), 0);
assert_eq!(memory.read16(SOUNDCNT_X).unwrap(), 0);
}

View file

@ -19,6 +19,32 @@ const REGIONS: [(u32, u32, u32); 5] = [
];
const MARKER: u32 = 0xa55a_c33c;
#[test]
fn sound_reset_clears_disabled_fifo_queues_and_only_accessible_wave_bank() {
use gba_core::io::{FIFO_A, SOUNDCNT_H, SOUNDCNT_X, TIMER_BASE, WAVE_RAM};
for thumb in [false, true] {
let (mut machine, return_pc) = prepare(0x40, thumb, 0x1f);
let bus = machine.memory_mut();
bus.write16(SOUNDCNT_X, 0x80).unwrap();
bus.write16(0x04000070, 0x40).unwrap();
bus.write32(WAVE_RAM, MARKER).unwrap(); // CPU bank 0.
bus.write16(SOUNDCNT_X, 0).unwrap();
bus.write32(WAVE_RAM, MARKER).unwrap(); // CPU bank 1.
bus.write32(FIFO_A, 0x7f7f7f7f).unwrap();
complete(&mut machine, return_pc);
let bus = machine.memory_mut();
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0);
assert_eq!(bus.read32(WAVE_RAM).unwrap(), 0);
bus.write16(SOUNDCNT_X, 0x80).unwrap();
bus.write16(0x04000070, 0x40).unwrap();
assert_eq!(bus.read32(WAVE_RAM).unwrap(), MARKER);
bus.write16(SOUNDCNT_H, 0x0304).unwrap();
bus.write32(TIMER_BASE, 0x0080ffff).unwrap();
bus.advance_cycles(1);
assert_eq!(bus.audio_level().left, 0);
}
}
fn literal(code: &mut Vec<u32>, register: u32, value: u32) {
code.extend([0xe59f_0000 | register << 12, 0xea00_0000, value]);
}

View file

@ -0,0 +1,318 @@
//! 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_psg_masks_work_without_claiming_psg_playback() {
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();
for address in [0x04000064, 0x0400006c, 0x04000074, 0x0400007c] {
let previous = bus.read16(address).unwrap();
assert!(bus
.write16(address, 0xffff)
.unwrap_err()
.to_string()
.contains("PSG channel trigger"));
assert_eq!(bus.read16(address).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));
}
}

View file

@ -38,7 +38,7 @@ fn disabled_sound_registers_masks_lanes_and_wave_ram() {
assert_eq!(bus.read16(SOUNDCNT_H).unwrap(), 0x7700);
assert_eq!(bus.read16(SOUNDBIAS).unwrap(), 0xc3fe);
// FIFOs are not ordinary register latches.
assert!(bus.write32(0x040000a0, 0).is_err());
bus.write32(0x040000a0, 0).unwrap();
assert!(bus.read32(0x040000a0).is_err());
}
@ -80,7 +80,6 @@ fn idle_normal_serial_data_and_control_have_distinct_widths() {
fn unsupported_operations_have_specific_retryable_diagnostics() {
let mut bus = memory();
for (address, value, description) in [
(SOUNDCNT_X, 0x80, "sound master enable"),
(SIOCNT, 0x80, "serial transfer start"),
(SIOCNT + 1, 0x20, "multiplayer/UART"),
(RCNT + 1, 0x40, "Joybus serial mode"),
@ -105,9 +104,10 @@ fn unsupported_operations_have_specific_retryable_diagnostics() {
#[test]
fn block_store_rejects_later_activation_before_earlier_control_writes() {
let code: [u32; 7] = [
0xe3a00301, 0xe2800080, 0xe59f1008, 0xe3a02080, 0xe8a00006, 0xeafffffe, 0x770f0000,
0xe3a00301, 0xe2800060, 0xe59f1008, 0xe3a02902, 0xe8a00006, 0xeafffffe, 0x12345678,
];
let bus = Memory::new(code.into_iter().flat_map(u32::to_le_bytes).collect()).unwrap();
let mut bus = Memory::new(code.into_iter().flat_map(u32::to_le_bytes).collect()).unwrap();
bus.write16(SOUNDCNT_X, 0x80).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
for _ in 0..4 {
machine.step().unwrap();
@ -115,24 +115,25 @@ fn block_store_rejects_later_activation_before_earlier_control_writes() {
let before = machine.cpu().clone();
let cycles = machine.cycles();
let error = machine.step().unwrap_err();
assert!(error.to_string().contains("sound master enable"));
assert!(error.to_string().contains("PSG channel trigger"));
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.memory().read16(SOUNDCNT_H).unwrap(), 0);
assert_eq!(machine.memory().read16(0x04000060).unwrap(), 0);
assert_eq!(machine.step(), Err(error));
}
#[test]
fn dma_activation_error_preserves_channel_progress_and_clocks() {
let mut bus = memory();
bus.write32(0x02000000, 0x80).unwrap();
bus.write16(SOUNDCNT_X, 0x80).unwrap();
bus.write32(0x02000000, 0x8000).unwrap();
bus.write32(DMA_BASE, 0x02000000).unwrap();
bus.write32(DMA_BASE + 4, SOUNDCNT_X).unwrap();
bus.write32(DMA_BASE + 4, 0x04000064).unwrap();
bus.write32(DMA_BASE + 8, 0x84000001).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
let before = machine.cpu().clone();
let error = machine.step().unwrap_err();
assert!(error.to_string().contains("sound master enable"));
assert!(error.to_string().contains("PSG channel trigger"));
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.step(), Err(error));