feat(audio): implement nominal Direct Sound playback and FIFO DMA
This commit is contained in:
parent
894f524cda
commit
5888c4d292
21 changed files with 933 additions and 129 deletions
|
|
@ -43,6 +43,7 @@ Window smoke tests (`--*-smoke-test`) need a logged-in desktop session. Details
|
|||
| BIOS services (SWI) | [docs/hardware/bios.md](docs/hardware/bios.md) |
|
||||
| Timers, IE/IF/IME, HALT/STOP | [docs/hardware/timers-irq.md](docs/hardware/timers-irq.md) |
|
||||
| DMA | [docs/hardware/dma.md](docs/hardware/dma.md) |
|
||||
| Direct Sound, FIFOs, PSG limits, and mixing | [docs/hardware/audio.md](docs/hardware/audio.md) |
|
||||
| VCOUNT/DISPSTAT, HBlank/VBlank, row capture | [docs/hardware/display-timing.md](docs/hardware/display-timing.md) |
|
||||
| Backgrounds, sprites, mosaic, windows, blending, input | [docs/hardware/video.md](docs/hardware/video.md) |
|
||||
| Demos and key mapping | [docs/demos.md](docs/demos.md) |
|
||||
|
|
|
|||
219
crates/gba-core/src/audio.rs
Normal file
219
crates/gba-core/src/audio.rs
Normal 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],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
104
crates/gba-core/src/memory/audio_step_tests.rs
Normal file
104
crates/gba-core/src/memory/audio_step_tests.rs
Normal 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);
|
||||
}
|
||||
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
|||
318
crates/gba-core/tests/direct_sound.rs
Normal file
318
crates/gba-core/tests/direct_sound.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -53,9 +53,12 @@ The repository is a Cargo workspace with three crates:
|
|||
| `crates/gba-core/src/cpu/fetch_history_tests.rs` | Shared sample consumption, no execution-entry rereads, and failed-step retry |
|
||||
| `crates/gba-core/src/cpu/fetch_boundary_tests.rs` | Actual fetch-region snapshots, split refills, IWRAM lane requirements, ROM-window boundaries, and nominal timing |
|
||||
| `crates/gba-core/src/io.rs` | I/O registers, timers, HALT/STOP wake-up, next-event bounds, and IRQ latches |
|
||||
| `crates/gba-core/src/io/inactive.rs` | Disabled sound and disconnected serial initialization with explicit unsupported-activity validation |
|
||||
| `crates/gba-core/src/audio.rs` | Direct Sound FIFO/playback state, idle PSG/wave banks, and instantaneous stereo mixing |
|
||||
| `crates/gba-core/tests/direct_sound.rs` | FIFO lanes/reset, signed mixing, timer selection, DMA refill, and HALT/STOP |
|
||||
| `crates/gba-core/src/memory/audio_step_tests.rs` | Audio access phases, shadow validation, and CPU/DMA failure rollback |
|
||||
| `crates/gba-core/src/io/inactive.rs` | Disconnected serial initialization with explicit unsupported-activity validation |
|
||||
| `crates/gba-core/tests/inactive_devices.rs` | Device masks, wave RAM, GPIO pull-ups, serial data, and failed CPU/DMA write isolation |
|
||||
| `crates/gba-core/src/io/timer_step.rs` | Staged timer bank, shared divider phase, and timer IF bits at ordered CPU/DMA bus phases |
|
||||
| `crates/gba-core/src/io/timer_step.rs` | Staged timer/audio state, divider phase, timer IF bits, and sound refill requests at ordered CPU/DMA bus phases |
|
||||
| `crates/gba-core/src/memory/timer_step_tests.rs` | Timer access phases, coherent lanes, cascades, IF ordering, rollback, DMA, idle, and clock ownership |
|
||||
| `crates/gba-core/src/display.rs` | Display clock, scanline status, comparison edges, and display IRQ events |
|
||||
| `crates/gba-core/src/dma.rs` | DMA registers, internal pointers, retained channel data, trigger state, priority, and completion IRQs |
|
||||
|
|
|
|||
|
|
@ -1,49 +1,112 @@
|
|||
# Sound initialization subset
|
||||
# Direct Sound subset
|
||||
|
||||
Sound synthesis and host audio output are not implemented.
|
||||
The core now models disabled sound-register state so firmware can perform a functional sound reset.
|
||||
This is not a silent-audio compatibility mode. Enabling sound returns an explicit diagnostic.
|
||||
The core models timer-driven Direct Sound A/B playback and a bounded sound-DMA configuration.
|
||||
It does not produce desktop audio. Programmable sound generator (PSG) synthesis remains unsupported.
|
||||
`Memory::audio_level()` exposes the current digital stereo level for deterministic inspection, not a continuous sample stream.
|
||||
|
||||
## Supported state
|
||||
## Registers and inactive PSG state
|
||||
|
||||
| Address | Behavior |
|
||||
| --- | --- |
|
||||
| `0x04000060..0x04000081` | Programmable sound generator (PSG) registers remain zero; writes have no effect while master sound is disabled |
|
||||
| `0x04000082` (`SOUNDCNT_H`) | Retains mixing/routing/timer configuration with readback mask `0x770f` |
|
||||
| `0x04000084` (`SOUNDCNT_X`) | Reads zero; status flags are read-only; a write setting bit 7 fails |
|
||||
| `0x04000088` (`SOUNDBIAS`) | Retains bias/resolution configuration with mask `0xc3fe`; no analog output is generated |
|
||||
| `0x04000090..0x0400009f` | Read/write wave RAM in the CPU-accessible inactive bank |
|
||||
| `0x04000060..0x04000081` | Idle PSG configuration and documented read masks; writes are ignored while master sound is disabled |
|
||||
| `0x04000082` (`SOUNDCNT_H`) | Mixing, routing, timer selection, and FIFO reset strobes; readback mask `0x770f` |
|
||||
| `0x04000084` (`SOUNDCNT_X`) | Master enable in bit 7; read-only PSG activity flags remain zero |
|
||||
| `0x04000088` (`SOUNDBIAS`) | Bias/resolution configuration with mask `0xc3fe` |
|
||||
| `0x04000090..0x0400009f` | CPU wave RAM window, opposite the bank selected by `SOUND3CNT_L` bit 6 |
|
||||
| `0x040000a0..0x040000a7` | FIFO A/B writes with byte, halfword, and word access boundaries preserved |
|
||||
|
||||
Unused mapped bytes through `0x0400008b` read zero and ignore writes.
|
||||
The FIFO reset strobes in `SOUNDCNT_H` do not latch. No FIFO data can be queued in this subset.
|
||||
FIFO addresses and the remaining sound-region holes stay unmapped.
|
||||
All sound configuration starts at zero in raw memory. RegisterRamReset bit 6 sets the functional bias default to `0x0200`.
|
||||
Unused mapped register bytes read zero and ignore writes.
|
||||
FIFO reads and the `0x0400008c..0x0400008f` gap remain explicit unmapped-access errors; no FIFO open-bus value is invented.
|
||||
Raw memory starts with zero sound state. RegisterRamReset bit 6 sets the functional bias default to `0x0200`.
|
||||
|
||||
With master sound disabled, `SOUND3CNT_L` stays zero. The CPU accesses wave bank 1.
|
||||
Bank 0 cannot be selected or played and remains zero. Disabling master sound does not erase wave RAM.
|
||||
The BIOS reset explicitly clears the accessible wave RAM through ordinary stores.
|
||||
Future bank switching and channel playback need a complete sound engine, not a larger register array.
|
||||
Master disable clears PSG configuration and the FIFO queues, but retains wave RAM and Direct Sound routing/bias.
|
||||
The CPU then accesses wave bank 1. Idle bank selection can expose bank 0 after master enable.
|
||||
The original BIOS reset clears bank 1 and both FIFO queues. It retains bank 0 and any separate in-flight playback word.
|
||||
This is a functional reset subset, not a claim of complete firmware ordering or sound-reset equivalence.
|
||||
|
||||
## Diagnostics and atomicity
|
||||
An enabled PSG channel trigger returns `MemoryError::UnsupportedIo` with the exact address and value.
|
||||
This applies even when the configured channel would be inaudible. Disabled PSG trigger writes are ignored.
|
||||
Length, envelope, sweep, frequency, and wave-bank playback clocks are not implemented.
|
||||
Do not interpret successful idle configuration writes as PSG synthesis.
|
||||
|
||||
A write setting `SOUNDCNT_X` bit 7 returns `MemoryError::UnsupportedIo`, including the address, byte value, and operation.
|
||||
Status bits and unused bits alone do not enable sound.
|
||||
Validation occurs before any bytes in the access are written.
|
||||
Block stores validate every register value before committing earlier writes. Failed CPU and DMA steps remain retryable.
|
||||
## FIFO and timer behavior
|
||||
|
||||
Original tests cover disabled-register writes, masks, byte lanes, wave retention, and BIOS reset selection.
|
||||
They also check rejected sound activation through byte/word accesses, ARM block stores, and DMA without partial state changes.
|
||||
Each channel has a seven-word queue, a separate four-byte playback word, and a held signed sample.
|
||||
Timer 0 or timer 1 overflow clocks the selected channel while master sound is enabled.
|
||||
Timer 1 cascade overflows work. Timer IRQ enable and stereo routing do not gate sample consumption.
|
||||
Timers 2 and 3 cannot clock Direct Sound.
|
||||
|
||||
## Remaining work
|
||||
On each selected overflow:
|
||||
|
||||
Implement channel clocks, length/envelope/sweep state, wave banking, FIFO consumption, timer-triggered sound DMA, and mixing.
|
||||
Add a host-independent sample interface before connecting a desktop audio backend.
|
||||
Do not acknowledge active sound requests without implementing their device behavior.
|
||||
1. Request sound DMA if the queue contains at most three words, before loading a playback word.
|
||||
2. Load a queued word if the playback word is empty.
|
||||
3. Latch the next signed eight-bit sample in little-endian order.
|
||||
|
||||
The held sample remains unchanged between overflows. An empty playback word supplies zero on the next overflow.
|
||||
This underflow policy follows both consulted emulator implementations; the cited hardware report leaves it uncertain.
|
||||
A bulk advance consumes all available samples and then reaches zero without looping over every empty overflow.
|
||||
|
||||
A partial FIFO write updates the addressed lanes of the next queue slot and enqueues that whole word.
|
||||
Other lanes retain that slot's previous bytes. A write to a full queue clears the queue.
|
||||
Reset strobes clear queues without changing the held sample or remaining playback bytes.
|
||||
Queue clearing also zeros stored queue slots in this nominal model.
|
||||
Exact stale-lane behavior across overflow/reset remains unverified on hardware.
|
||||
|
||||
HALT continues timer/audio progress. STOP freezes it.
|
||||
Master disable stops sample consumption; route disable only mutes the mixer path.
|
||||
|
||||
## Sound DMA
|
||||
|
||||
Special timing supports DMA1 to FIFO A and DMA2 to FIFO B.
|
||||
Each request transfers four 32-bit words with a fixed destination, regardless of programmed count, width, or destination mode.
|
||||
Source address control, channel priority, repeat enable, completion IRQ, and retained DMA data still apply.
|
||||
Without repeat, the channel disables after the fourth word. With repeat, it waits for another timer request.
|
||||
|
||||
Other channel/FIFO pairings and non-FIFO destinations remain explicit `DmaError::UnsupportedControl` cases.
|
||||
The references disagree about request routing outside the supported pairing; hardware reports permit non-FIFO destinations.
|
||||
The core does not silently guess those configurations. Video capture and Game Pak DRQ remain unsupported.
|
||||
|
||||
Requests become visible at successful machine-step boundaries. Active DMA blocks ignore further requests.
|
||||
Device-only bulk advances coalesce unserviced requests; they do not execute or replay DMA blocks.
|
||||
Sub-instruction DMA arbitration and exact refill latency are not modeled.
|
||||
|
||||
## Mixing and timing ownership
|
||||
|
||||
Each routed sample contributes `sample * 2` at 50% volume or `sample * 4` at 100% volume.
|
||||
The mixer adds the ten-bit bias, clips each side to `0..1023`, and subtracts 512.
|
||||
`StereoLevel` therefore contains signed levels in `-512..511`. Master disable returns zero.
|
||||
Bias resolution bits retain their values but do not yet drive pulse-width modulation (PWM) sampling or quantization.
|
||||
There is no resampling, analog filtering, sample queue, or host output backend.
|
||||
Polling once per video frame will not reconstruct audio.
|
||||
|
||||
Timer and sound state share the staged CPU/DMA clock.
|
||||
Reads and writes observe their nominal bus-completion phase, after earlier timer overflows.
|
||||
Successful steps commit once. Failed steps discard FIFO consumption, held samples, register writes, and refill requests.
|
||||
Block-store validation simulates sound enable/disable in a temporary state before committing any RAM or I/O writes.
|
||||
Other devices retain their existing scheduling limits.
|
||||
|
||||
## Validation and remaining work
|
||||
|
||||
Original tests cover signed samples, byte order, queue capacity, partial writes, resets, underflow, stereo routing, and clipping.
|
||||
They also cover cascaded timers, DMA block/repeat behavior, source progress, completion IRQs, HALT/STOP, and clock batching.
|
||||
CPU/DMA failure tests verify transactional rollback and sound writes at bus completion.
|
||||
BIOS tests cover disabled FIFO reset and the supported wave-bank policy.
|
||||
|
||||
These tests validate the documented nominal model, not hardware audio fidelity.
|
||||
Next work: PSG clocks and synthesis, a timestamped or fixed-rate output stream, PWM/mixer sampling, and a Darwin host backend.
|
||||
The local Emerald startup now passes master enable and stops at a PSG channel 1 trigger.
|
||||
See [the local runtime result](../research/emerald-reset.md).
|
||||
|
||||
## References
|
||||
|
||||
- [GBATEK sound control registers](https://problemkaputt.de/gbatek-gba-sound-control-registers.htm): disabled PSG behavior, master enable, masks, FIFO reset strobes, and bias.
|
||||
- [GBATEK](https://mgba-emu.github.io/gbatek/): channel 3 wave-bank access and register layout.
|
||||
- [mGBA BIOS service implementation at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/bios.c): functional reset defaults. No upstream implementation was copied.
|
||||
- [GBATEK Direct Sound](https://problemkaputt.de/gbatek-gba-sound-channel-a-and-b-dma-sound.htm): registers, timer selection, signed samples, and DMA blocks.
|
||||
- [GBATEK sound control](https://problemkaputt.de/gbatek-gba-sound-control-registers.htm): master enable, read masks, reset strobes, routing, and bias.
|
||||
- [GBATEK mirror](https://mgba-emu.github.io/gbatek/): wave-bank access and register layout.
|
||||
- [gbadev Direct Sound overview](https://gbadev.net/gbadoc/audio/directsound.html): introductory FIFO/timer/DMA behavior; not authority for queue edge cases.
|
||||
- [Gericom's hardware findings, mGBA issue 1847](https://github.com/mgba-emu/mgba/issues/1847): seven-word queue, separate playback buffer, request order, partial writes, and overflow observations.
|
||||
- [mGBA audio at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/audio.c): playback, reset, mixing, and destination-based request routing comparison.
|
||||
- [NanoBoyAdvance APU at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/hw/apu/apu.cc): master gating, playback word, underflow, and mixing.
|
||||
- [NanoBoyAdvance FIFO](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/hw/apu/channel/fifo.hh), [registers](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/hw/apu/registers.cc), and [DMA](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/hw/dma/dma.cc): queue/reset policy and channel-based request routing comparison.
|
||||
- [mGBA BIOS at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/bios.c): functional reset defaults.
|
||||
|
||||
Reset values are source-backed. Exact Nintendo firmware ordering, timing, and analog behavior are not established.
|
||||
The Rust implementation and regression programs are original. No upstream implementation or external test ROM was copied into the repository.
|
||||
|
|
|
|||
|
|
@ -216,11 +216,12 @@ Supported flags can be combined:
|
|||
| 3 | Clear all 96 KiB of video RAM: `0x06000000..0x06017fff` |
|
||||
| 4 | Clear sprite attribute memory (OAM): `0x07000000..0x070003ff` |
|
||||
| 5 | Reset supported idle serial state and select general-purpose inputs |
|
||||
| 6 | Reset disabled sound state, mixing control, bias, and accessible wave RAM |
|
||||
| 6 | Disable sound, reset FIFO queues and mixing control, set bias, and clear accessible wave bank 1 |
|
||||
| 7 | Reset supported display, DMA, timer, and interrupt registers as described below |
|
||||
|
||||
All low-byte flag combinations, including `r0=0xff`, are accepted within the supported device subset.
|
||||
This is not full audio or serial support. [Sound activation](audio.md) and [serial transfers](serial.md) still fail explicitly.
|
||||
This is not full audio or serial support. [PSG playback](audio.md) and [serial transfers](serial.md) still fail explicitly.
|
||||
Sound reset retains wave bank 0 and the separate in-flight playback word; exact firmware sound-reset equivalence is not established.
|
||||
The firmware always clears the low halfword of SIODATA32, even when bit 5 is clear; its upper halfword is preserved.
|
||||
|
||||
Every supported request first writes `DISPCNT=0x0080`, including requests with no selected flags.
|
||||
|
|
@ -509,5 +510,5 @@ Unsupported SWIs, unsupported exception vectors, and null/misaligned IRQ callbac
|
|||
The CPU reports `CpuError::UnsupportedInstruction` with `bios::UNSUPPORTED_TRAP`, rather than silently treating a service as a no-op.
|
||||
Invalid arithmetic/decompression arguments use the same CPU error type with the distinct `bios::INVALID_ARGUMENT_TRAP` instruction.
|
||||
Prior boot/service steps remain committed on failure.
|
||||
Active sound, serial transfers, HardReset, and serial/Game Pak STOP wake-up remain unimplemented.
|
||||
PSG playback, host audio output, serial transfers, HardReset, and serial/Game Pak STOP wake-up remain unimplemented.
|
||||
This subset is not sufficient for Pokémon Emerald compatibility.
|
||||
|
|
|
|||
|
|
@ -59,7 +59,9 @@ Exact startup/completion placement remains nominal, not independently verified p
|
|||
See [CPU resume timing](cpu.md#nominal-cpu-resume-after-dma) for failure, idle, and refill rules.
|
||||
Startup scheduling delays, channel-resumption costs, a full CPU fetch pipeline, and display-bus contention remain unmodeled.
|
||||
|
||||
Special timing modes (sound FIFO/video capture), Game Pak DRQ, and prohibited source mode 3 return `DmaError::UnsupportedControl`.
|
||||
Special timing supports DMA1/FIFO A and DMA2/FIFO B with four-word blocks, forced word width, and fixed destinations.
|
||||
Timer-driven requests, repeat, and completion IRQs are described in [Direct Sound](audio.md).
|
||||
Other special-mode pairings, video capture, Game Pak DRQ, and prohibited source mode 3 return `DmaError::UnsupportedControl`.
|
||||
DMA reads below work RAM reuse known channel data as described below. They never expose BIOS bytes.
|
||||
DMA writes to DMA registers return `DmaError::RegisterDestination`; self-modifying transfers are not supported.
|
||||
Other accesses use the existing memory map, alignment checks, mirrors, and I/O write rules.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
Otherwise it services one ready DMA data unit, keeping the CPU paused.
|
||||
If HALT is waiting and no DMA is ready, it advances device clocks to the next event without executing CPU code.
|
||||
Otherwise, it samples the IRQ line, including the CPU's interrupt mask, then enters IRQ mode or executes one instruction.
|
||||
Each successful step advances display and timer clocks by its nominal cost.
|
||||
Each successful step advances display, timer, and Direct Sound clocks by its nominal cost.
|
||||
`Machine::last_timing()` reports separate code, data, internal, and idle cycle totals for the last successful step.
|
||||
`StepTiming::idle_cycles` counts device-clock cycles during HALT; it does not count CPU work.
|
||||
IRQ entry is a separate step; the vector instruction runs on the following step.
|
||||
|
|
@ -15,7 +15,7 @@ Entry charges the discarded incoming-state PC+8/PC+4 fetch and the ARM vector pa
|
|||
DMA resume can make the discarded fetch non-sequential. Missing discarded bytes do not prevent IRQ entry.
|
||||
See [refill timing](../research/refill-fetch-timing.md) for history effects and remaining limits.
|
||||
A device event during a step can trigger IRQ entry once no DMA is ready and CPU masks allow delivery.
|
||||
Timer accesses use ordered bus-completion phases. Other device writes still take effect before their bulk clock update.
|
||||
Timer and sound accesses use ordered bus-completion phases. Other device writes still take effect before their bulk clock update.
|
||||
`StepKind::Dma { channel }` identifies a DMA unit. `StepKind::HaltIdle` identifies one bounded HALT clock advance.
|
||||
`StepKind::StopIdle` identifies a stopped system with no clock progress.
|
||||
The other step kinds remain `Instruction` and `IrqEntry`.
|
||||
|
|
@ -34,10 +34,11 @@ The transaction also owns timer IF bits. Reads see overflows through the current
|
|||
A later internal cycle can raise an acknowledged timer request again. Non-timer IF bits retain their existing behavior.
|
||||
IRQ delivery still waits until the next machine step. A committed timer request can wake HALT, but not STOP.
|
||||
|
||||
Successful steps commit timers, the shared divider phase, and timer IF bits.
|
||||
Failed steps discard staged counters, divider progress, reload/control writes, and requests.
|
||||
Successful steps commit timers, the shared divider phase, timer IF bits, and Direct Sound state.
|
||||
Failed steps discard staged counters, divider progress, timer/audio writes, FIFO consumption, and requests.
|
||||
Display and other device clocks then advance once, without advancing timers again.
|
||||
Those devices still use the bulk scheduler. Mid-instruction display events, general bus arbitration, and IRQ synchronization delays remain unmodeled.
|
||||
Display and remaining devices still use the bulk scheduler. Mid-instruction display events, general bus arbitration, and IRQ synchronization delays remain unmodeled.
|
||||
[Direct Sound](audio.md) consumes selected timer overflows in the timer transaction; sound DMA requests commit at step boundaries.
|
||||
Host clock advances and HALT idle batches retain ordinary bulk timer advancement; STOP idle does not advance timers.
|
||||
|
||||
The unchanged [prefetch cancellation probe](../research/prefetch-cancellation.md) now matches both published interval totals and actual timer samples.
|
||||
|
|
@ -92,7 +93,8 @@ See [prescaler evidence and original tests](../research/timer-prescaler.md).
|
|||
|
||||
Hardware startup and register-write delays are still not modeled. Writes retain the immediate bus-completion policy.
|
||||
The shared-phase tests validate this bounded model, not absolute enable/reload edges on hardware.
|
||||
Audio events and interrupt sources other than timers, display events, DMA completion, and keypad input are not implemented.
|
||||
Timers 0 and 1 can clock Direct Sound playback and request FIFO DMA independently of timer IRQ enable.
|
||||
Interrupt sources other than timers, display events, DMA completion, and keypad input are not implemented.
|
||||
|
||||
`--timer-demo` executes original ARM code that configures Timer 0, IE, and IME.
|
||||
The timer overflows after 16 supplied cycles and enters the original handler through vector `0x18`.
|
||||
|
|
@ -228,7 +230,7 @@ The current desktop demos do not enter STOP; the frontend does not yet provide a
|
|||
|
||||
Only keypad wake-up is implemented. General-purpose serial and Game Pak wake sources require their missing external devices.
|
||||
Oscillator restart delays, exact entry/wake edges, and hardware-specific IF behavior remain unverified.
|
||||
There is no host-thread sleep, wall-clock accounting, or audio clock implementation.
|
||||
There is no host-thread sleep or wall-clock accounting. STOP freezes the emulated timer-driven audio clock.
|
||||
|
||||
The graphics demo now uses the optional original BIOS replacement's VBlankIntrWait service.
|
||||
Its IRQ callback acknowledges VBlank and updates the BIOS RAM flag before the service returns.
|
||||
|
|
|
|||
|
|
@ -9,10 +9,11 @@ No ROM bytes, extracted code, or assets were added to the repository.
|
|||
|
||||
## General correction
|
||||
|
||||
The core now provides a bounded [disabled sound](../hardware/audio.md) and [disconnected serial](../hardware/serial.md) initialization model.
|
||||
The reset correction introduced bounded sound and [disconnected serial](../hardware/serial.md) initialization models.
|
||||
The [sound model](../hardware/audio.md) now also supports timer-driven Direct Sound and bounded FIFO DMA.
|
||||
The BIOS performs reset through original ARM stores against that model.
|
||||
Bit 5 selects general-purpose input pins and clears supported serial reset registers.
|
||||
Bit 6 disables sound, clears mixing configuration and accessible wave RAM, and sets the functional bias default.
|
||||
Bit 6 disables sound, resets FIFO queues, clears mixing configuration and accessible wave RAM, and sets the functional bias default.
|
||||
The low halfword of SIODATA32 is cleared unconditionally, matching the documented BIOS side effect.
|
||||
|
||||
This is not a game-specific bypass. Original ARM and Thumb callers exercise selected flags and the all-device request.
|
||||
|
|
@ -28,13 +29,18 @@ direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --s
|
|||
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --window --frames 120
|
||||
```
|
||||
|
||||
Both runs now pass the BIOS reset and stop at the game's sound activation write:
|
||||
The earlier reset-only implementation stopped at master sound enable (`0x04000084`, PC `0x082e0518`).
|
||||
After the Direct Sound increment, both runs pass that write and stop at an enabled PSG channel 1 trigger:
|
||||
|
||||
```text
|
||||
PC=0x082e0518
|
||||
unsupported sound master enable (audio engine not implemented): write 0x8f at 0x04000084
|
||||
Steps: 321159; instructions: 320647; IRQ entries: 0; DMA units: 512; HALT idle: 0
|
||||
Nominal cycles: 1038738
|
||||
PC=0x082e03bc
|
||||
unsupported PSG channel trigger (synthesis not implemented): write 0x80 at 0x04000065
|
||||
```
|
||||
|
||||
The native run captures three startup frames. Direct Sound behavior is validated by original tests, not by audible game output.
|
||||
|
||||
The native run captures startup frames before this diagnostic. These are not evidence of a working title screen or gameplay.
|
||||
The window run does not reach its requested frame limit and correctly exits with failure.
|
||||
Terminal execution is also diagnostic, not a compatibility pass.
|
||||
|
|
@ -46,9 +52,10 @@ Workspace debug/release tests, strict Clippy, public ARM/Thumb/memory/BIOS check
|
|||
Native ROM-window tests and graphics smoke modes pass on Darwin arm64.
|
||||
The pinned Pong debug/release reports match.
|
||||
|
||||
The next observed startup requirement is active audio-device behavior, starting at SOUNDCNT_X master enable.
|
||||
Implement channel/FIFO clocks, state, DMA requests, and an output interface before claiming sound support.
|
||||
Do not make enable succeed solely to suppress this diagnostic.
|
||||
The next observed startup requirement is PSG channel playback, beginning with pulse channel 1.
|
||||
Direct Sound now has FIFO clocks, nominal DMA requests, and an instantaneous digital-level inspection interface.
|
||||
A continuous sample stream and desktop output are still missing. Do not treat digital-level tests as audible-game validation.
|
||||
Do not suppress the PSG trigger diagnostic without implementing the requested device behavior.
|
||||
Cartridge saves remain a separate required feature. SRAM alone will not support Emerald's Flash save protocol.
|
||||
|
||||
Hardware and emulator references are recorded in the linked subsystem documents.
|
||||
|
|
|
|||
|
|
@ -78,7 +78,8 @@ Update this file when a feature lands or a limit is removed.
|
|||
- Raw ROM-file loading with original BIOS boot, terminal diagnostics, and final CPU/step reports.
|
||||
- Firmware boot initializes BG2/BG3 identity scales for bitmap and affine programs that do not program their own matrices.
|
||||
- RegisterRamReset supports disabled sound and disconnected serial initialization, including the common `r0=0xff` request.
|
||||
- Sound activation and unsupported serial operations produce explicit address/value diagnostics with whole-access and block-store rollback.
|
||||
- Direct Sound A/B support timer-driven FIFO playback, stereo digital levels, and DMA1/A or DMA2/B refill blocks.
|
||||
- PSG triggers and unsupported serial operations produce explicit address/value diagnostics with whole-access and block-store rollback.
|
||||
- ROM windows with scanline capture, keyboard input, focus-loss release, bounded execution slices, and interactive keypad wake from STOP.
|
||||
- Optional ROM-window frame limits and an original input-test ROM generator.
|
||||
- Headless JSON ROM suites with PC or VBlank completion, register/CPSR/memory/pixel assertions, scheduled buttons, bounded budgets, and structured failure reports.
|
||||
|
|
@ -134,8 +135,9 @@ Internal affine origins, vertical window flags, and vertical mosaic counters tra
|
|||
Horizontal window comparators retain four-cycle event history.
|
||||
Sprite rows prepare one line ahead with a whole-row cycle40 sample and nominal per-row work limits.
|
||||
Individual fetch timing, hardware-accurate work cutoffs, video-bus contention, and per-pixel color composition remain unimplemented.
|
||||
There is no sound synthesis, host audio output, or cartridge save support.
|
||||
The [sound initialization subset](hardware/audio.md) supports disabled register state and wave RAM; master sound enable remains diagnostic.
|
||||
There is no PSG synthesis, host audio output, or cartridge save support.
|
||||
The [Direct Sound subset](hardware/audio.md) supports FIFO/timer/DMA behavior, idle PSG state, wave banking, and instantaneous digital mixing.
|
||||
It does not yet provide a continuous sample stream. PSG playback and unverified sound-DMA pairings remain diagnostic.
|
||||
The [serial initialization subset](hardware/serial.md) supports idle normal data and disconnected general-purpose pins, not active transfers.
|
||||
Mapped I/O covers DMA, timers, interrupts, WAITCNT, display control/status, background control/scroll/affine registers, window/effect registers, KEYINPUT/KEYCNT, POSTFLG, and HALTCNT.
|
||||
Keypad IRQs use a documented polling model; hardware retrigger edges and asynchronous input timing remain unverified.
|
||||
|
|
@ -145,7 +147,8 @@ All named graphics demos connect desktop input and rendering to the emulated CPU
|
|||
ROM files can run in bounded terminal mode or a window with the original BIOS replacement.
|
||||
ROM windows have no demo-specific startup checks. Input is sampled between bounded execution slices.
|
||||
Audio, saves, and external BIOS loading remain unavailable. ROM loading does not establish commercial-game compatibility.
|
||||
A local Emerald run now completes its all-device reset and reaches sound activation at `0x04000084`, where it stops explicitly.
|
||||
A local Emerald run now completes its all-device reset and master sound enable.
|
||||
It stops explicitly at a PSG channel 1 trigger at `0x04000065`; no title-screen or gameplay compatibility is established.
|
||||
Its captured startup frames do not establish title-screen or gameplay compatibility. See [the startup result](research/emerald-reset.md).
|
||||
See [cartridge loading](hardware/cartridge.md) for file limits, step budgets, and exit status.
|
||||
The [ROM suite runner](rom-tests.md) has original regressions and a [pinned public ARM baseline](public-arm-tests.md).
|
||||
|
|
|
|||
|
|
@ -39,8 +39,9 @@ The [pinned Pong homebrew](public-pong.md) passes scripted menus, ball movement,
|
|||
The native Darwin window also starts and presents frames without a diagnostic.
|
||||
This exposed and corrected missing firmware affine-scale initialization, without a game-specific patch.
|
||||
Frame-based scenarios and the preparation adapter now make the result repeatable.
|
||||
A local Emerald run now passes its initial all-device reset and stops at sound master enable.
|
||||
See [the runtime result](research/emerald-reset.md). Active audio-device behavior is the next demonstrated startup requirement.
|
||||
A local Emerald run now passes its initial all-device reset and master sound enable, then stops at a PSG channel 1 trigger.
|
||||
The core has a tested Direct Sound subset, but no continuous sample stream or desktop audio output.
|
||||
See [the runtime result](research/emerald-reset.md). PSG playback is the next demonstrated startup requirement.
|
||||
The separate save requirement remains cartridge SRAM, followed by safe host save persistence.
|
||||
SRAM is byte-addressable battery-backed save memory. Flash and EEPROM remain separate protocols.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue