From c9811cc27220e900fa6aca27f3fb14293c02ae41 Mon Sep 17 00:00:00 2001 From: Michel Heily Date: Tue, 13 Sep 2022 01:50:50 +0300 Subject: [PATCH] all: refactoring audio stuff and using structopt in desktop app Former-commit-id: 8fb2e158eba5f81bc9fb953bfa6d0f4d9e505a61 Former-commit-id: c436751be80c9517401777ec5060061383d75929 --- core/benches/performance.rs | 12 +- core/src/cartridge/backup/mod.rs | 9 +- core/src/gba.rs | 36 ++--- core/src/lib.rs | 24 +--- core/src/overrides.rs | 4 +- core/src/sound/dsp.rs | 2 +- core/src/sound/interface.rs | 63 ++++++++ core/src/sound/mod.rs | 16 +-- fps_bench/src/main.rs | 16 +-- platform/rustboyadvance-jni/src/audio/mod.rs | 47 +++--- .../rustboyadvance-jni/src/audio/thread.rs | 13 +- platform/rustboyadvance-jni/src/emulator.rs | 91 +++++------- platform/rustboyadvance-libretro/src/lib.rs | 36 ++--- platform/rustboyadvance-sdl2/Cargo.toml | 2 +- platform/rustboyadvance-sdl2/src/audio.rs | 99 +++++-------- platform/rustboyadvance-sdl2/src/main.rs | 136 +++++------------- platform/rustboyadvance-sdl2/src/options.rs | 64 +++++++++ platform/rustboyadvance-wasm/src/emulator.rs | 59 ++------ utils/src/lib.rs | 12 +- 19 files changed, 321 insertions(+), 420 deletions(-) create mode 100644 core/src/sound/interface.rs create mode 100644 platform/rustboyadvance-sdl2/src/options.rs diff --git a/core/benches/performance.rs b/core/benches/performance.rs index fe7968d..402414b 100644 --- a/core/benches/performance.rs +++ b/core/benches/performance.rs @@ -7,9 +7,6 @@ use std::rc::Rc; use rustboyadvance_core::prelude::*; -struct BenchmarkHardware {} -impl AudioInterface for BenchmarkHardware {} - fn create_gba() -> GameBoyAdvance { // TODO: do I really want this file in my repository ? let bios = include_bytes!("roms/normatt_gba_bios.bin"); @@ -22,14 +19,7 @@ fn create_gba() -> GameBoyAdvance { .build() .unwrap(); - let dummy = Rc::new(RefCell::new(BenchmarkHardware {})); - - let mut gba = GameBoyAdvance::new( - bios.to_vec().into_boxed_slice(), - gpak, - dummy.clone(), - dummy.clone(), - ); + let mut gba = GameBoyAdvance::new(bios.to_vec().into_boxed_slice(), gpak, NullAudio::new()); gba.skip_bios(); // skip initialization of the ROM to get to a stabilized scene for _ in 0..60 { diff --git a/core/src/cartridge/backup/mod.rs b/core/src/cartridge/backup/mod.rs index cbab666..6df916a 100644 --- a/core/src/cartridge/backup/mod.rs +++ b/core/src/cartridge/backup/mod.rs @@ -1,5 +1,5 @@ -use std::convert::TryFrom; use std::fmt; +use std::str::FromStr; mod backup_file; pub use backup_file::BackupFile; @@ -16,10 +16,9 @@ pub enum BackupType { AutoDetect = 5, } -impl TryFrom<&str> for BackupType { - type Error = String; - - fn try_from(s: &str) -> Result { +impl FromStr for BackupType { + type Err = String; + fn from_str(s: &str) -> Result { use BackupType::*; match s { "autodetect" => Ok(AutoDetect), diff --git a/core/src/gba.rs b/core/src/gba.rs index 4bf0bcc..9fe6965 100644 --- a/core/src/gba.rs +++ b/core/src/gba.rs @@ -1,5 +1,5 @@ /// Struct containing everything -use std::cell::{Cell, RefCell}; +use std::cell::Cell; use std::rc::Rc; use bincode; @@ -15,7 +15,7 @@ use super::sound::SoundController; use super::sysbus::SysBus; use super::timer::Timers; -use super::AudioInterface; +use super::sound::interface::DynAudioInterface; use arm7tdmi::{self, Arm7tdmiCore}; use rustboyadvance_utils::Shared; @@ -26,7 +26,7 @@ pub struct GameBoyAdvance { pub io_devs: Shared, pub scheduler: SharedScheduler, interrupt_flags: SharedInterruptFlags, - pub audio_device: Rc>, + pub audio_interface: DynAudioInterface, } #[derive(Serialize, Deserialize)] @@ -63,7 +63,7 @@ impl GameBoyAdvance { pub fn new( bios_rom: Box<[u8]>, gamepak: Cartridge, - audio_device: Rc>, + audio_interface: DynAudioInterface, ) -> GameBoyAdvance { // Warn the user if the bios is not the real one match check_real_bios(&bios_rom) { @@ -80,7 +80,7 @@ impl GameBoyAdvance { let timers = Timers::new(interrupt_flags.clone()); let sound_controller = Box::new(SoundController::new( &mut scheduler, - audio_device.borrow().get_sample_rate() as f32, + audio_interface.get_sample_rate() as f32, )); let io_devs = Shared::new(IoDevices::new( intc, @@ -103,7 +103,7 @@ impl GameBoyAdvance { cpu, sysbus, io_devs, - audio_device, + audio_interface, scheduler, interrupt_flags, }; @@ -117,7 +117,7 @@ impl GameBoyAdvance { savestate: &[u8], bios: Box<[u8]>, rom: Box<[u8]>, - audio_device: Rc>, + audio_interface: DynAudioInterface, ) -> bincode::Result { let decoded: Box = bincode::deserialize_from(savestate)?; @@ -148,7 +148,7 @@ impl GameBoyAdvance { sysbus, io_devs, interrupt_flags: interrupts, - audio_device, + audio_interface, scheduler, }) } @@ -299,7 +299,7 @@ impl GameBoyAdvance { Some(timers.handle_overflow_event(channel_id, event_time, apu, dmac)) } EventType::Gpu(gpu_event) => Some(io.gpu.on_event(gpu_event, &mut *self.sysbus)), - EventType::Apu(event) => Some(io.sound.on_event(event, &self.audio_device)), + EventType::Apu(event) => Some(io.sound.on_event(event, &mut self.audio_interface)), }; if let Some((new_event, when)) = new_event { // We schedule events added by event handlers relative to the handled event time @@ -378,21 +378,8 @@ impl GameBoyAdvance { #[cfg(test)] mod tests { use super::*; - use std::cell::RefCell; - use std::rc::Rc; - use crate::cartridge::GamepakBuilder; - use arm7tdmi::memory::BusIO; - - struct DummyAudio {} - - impl DummyAudio { - fn new() -> DummyAudio { - DummyAudio {} - } - } - - impl AudioInterface for DummyAudio {} + use crate::prelude::*; fn make_mock_gba(rom: &[u8]) -> GameBoyAdvance { let bios = vec![0; 0x4000].into_boxed_slice(); @@ -402,8 +389,7 @@ mod tests { .without_backup_to_file() .build() .unwrap(); - let dummy = Rc::new(RefCell::new(DummyAudio::new())); - let mut gba = GameBoyAdvance::new(bios, cartridge, dummy.clone()); + let mut gba = GameBoyAdvance::new(bios, cartridge, NullAudio::new()); gba.skip_bios(); gba diff --git a/core/src/lib.rs b/core/src/lib.rs index fbe1f7f..f30f5ad 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -55,26 +55,6 @@ pub(crate) mod overrides; #[cfg(feature = "debugger")] pub mod debugger; -pub type StereoSample = [T; 2]; - -pub trait AudioInterface { - fn get_sample_rate(&self) -> i32 { - 44100 - } - - /// Pushes a stereo sample into the audio device - /// Sample should be normilized to siged 16bit values - /// Note: It is not guarentied that the sample will be played - #[allow(unused_variables)] - fn push_sample(&mut self, samples: &[i16]) {} -} - -pub trait InputInterface { - fn poll(&mut self) -> u16 { - keypad::KEYINPUT_ALL_RELEASED - } -} - #[derive(Debug)] pub enum GBAError { IO(::std::io::Error), @@ -122,8 +102,10 @@ pub mod prelude { #[cfg(feature = "debugger")] pub use super::debugger::Debugger; pub use super::gpu::{DISPLAY_HEIGHT, DISPLAY_WIDTH}; + pub use super::sound::interface::{ + AudioInterface, DynAudioInterface, NullAudio, SimpleAudioInterface, + }; pub use super::Bus; - pub use super::{AudioInterface, StereoSample}; pub use super::{GBAError, GBAResult, GameBoyAdvance}; pub use rustboyadvance_utils::{read_bin_file, write_bin_file}; } diff --git a/core/src/overrides.rs b/core/src/overrides.rs index 9a30bc2..6154a32 100644 --- a/core/src/overrides.rs +++ b/core/src/overrides.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use std::convert::TryFrom; +use std::str::FromStr; use yaml_rust::YamlLoader; @@ -34,7 +34,7 @@ lazy_static! { let game_code = String::from(game["code"].as_str().unwrap()); let force_rtc = game["rtc"].as_bool().unwrap_or(false); let save_type = if let Some(save_type) = game["save_type"].as_str() { - match BackupType::try_from(save_type) { + match BackupType::from_str(save_type) { Ok(x) => Some(x), _ => panic!("{}: invalid save type {:#}", game_code, save_type), } diff --git a/core/src/sound/dsp.rs b/core/src/sound/dsp.rs index bc4bd8f..b08f7fa 100644 --- a/core/src/sound/dsp.rs +++ b/core/src/sound/dsp.rs @@ -1,4 +1,4 @@ -use crate::StereoSample; +use super::StereoSample; use serde::{Deserialize, Serialize}; diff --git a/core/src/sound/interface.rs b/core/src/sound/interface.rs new file mode 100644 index 0000000..ad6bb0f --- /dev/null +++ b/core/src/sound/interface.rs @@ -0,0 +1,63 @@ +use rustboyadvance_utils::audio::{AudioRingBuffer, SampleConsumer, SampleProducer}; + +pub type StereoSample = [T; 2]; + +pub trait AudioInterface { + fn get_sample_rate(&self) -> i32 { + 44100 + } + + /// Pushes stereo sample buffer into the audio device + /// Sample should be normilized to siged 16bit values + /// Note: It is not guarentied that the sample will be played + #[allow(unused_variables)] + fn push_sample(&mut self, sample: &StereoSample) {} +} + +pub struct SimpleAudioInterface { + producer: SampleProducer, + sample_rate: i32, +} + +impl SimpleAudioInterface { + pub fn create_channel( + sample_rate: i32, + buffer_size: Option, + ) -> (Box, SampleConsumer) { + let (producer, consumer) = + AudioRingBuffer::new_with_capacity(buffer_size.unwrap_or(8192)).split(); + ( + Box::new(SimpleAudioInterface { + producer, + sample_rate, + }), + consumer, + ) + } +} + +impl AudioInterface for SimpleAudioInterface { + #[inline] + fn get_sample_rate(&self) -> i32 { + self.sample_rate + } + + #[inline] + fn push_sample(&mut self, sample: &StereoSample) { + let _ = self.producer.push(sample[0]); + let _ = self.producer.push(sample[1]); + } +} + +pub type DynAudioInterface = Box; + +#[derive(Debug, Default)] +pub struct NullAudio {} + +impl AudioInterface for NullAudio {} + +impl NullAudio { + pub fn new() -> Box { + Box::new(NullAudio::default()) + } +} diff --git a/core/src/sound/mod.rs b/core/src/sound/mod.rs index f0ee16a..76c0335 100644 --- a/core/src/sound/mod.rs +++ b/core/src/sound/mod.rs @@ -1,6 +1,3 @@ -use std::cell::RefCell; -use std::rc::Rc; - use bit::BitIndex; use serde::{Deserialize, Serialize}; @@ -8,10 +5,10 @@ use super::dma::DmaController; use super::iodev::consts::*; use super::sched::*; -use crate::{AudioInterface, StereoSample}; - mod fifo; use fifo::SoundFifo; +pub mod interface; +pub use interface::{AudioInterface, DynAudioInterface, StereoSample}; mod dsp; use dsp::{CosineResampler, Resampler}; @@ -59,8 +56,6 @@ const REG_FIFO_A_H: u32 = REG_FIFO_A + 2; const REG_FIFO_B_L: u32 = REG_FIFO_B; const REG_FIFO_B_H: u32 = REG_FIFO_B + 2; -type AudioDeviceRcRefCell = Rc>; - #[derive(Serialize, Deserialize, Clone, Debug)] pub struct SoundController { cycles: usize, // cycles count when we last provided a new sample. @@ -321,7 +316,7 @@ impl SoundController { } #[inline] - fn on_sample(&mut self, audio_device: &AudioDeviceRcRefCell) -> FutureEvent { + fn on_sample(&mut self, audio_device: &mut DynAudioInterface) -> FutureEvent { let mut sample = [0f32, 0f32]; for (channel, out_sample) in sample.iter_mut().enumerate() { @@ -339,9 +334,8 @@ impl SoundController { self.resampler.feed(&sample, &mut self.output_buffer); - let mut audio = audio_device.borrow_mut(); self.output_buffer.drain(..).for_each(|[left, right]| { - audio.push_sample(&[ + audio_device.push_sample(&[ (left.round() as i16) * (std::i16::MAX / 512), (right.round() as i16) * (std::i16::MAX / 512), ]); @@ -352,7 +346,7 @@ impl SoundController { pub fn on_event( &mut self, event: ApuEvent, - audio_device: &AudioDeviceRcRefCell, + audio_device: &mut DynAudioInterface, ) -> FutureEvent { match event { ApuEvent::Sample => self.on_sample(audio_device), diff --git a/fps_bench/src/main.rs b/fps_bench/src/main.rs index f8231ea..0b1bcc4 100644 --- a/fps_bench/src/main.rs +++ b/fps_bench/src/main.rs @@ -1,21 +1,9 @@ -use std::cell::RefCell; use std::env; use std::path::Path; -use std::rc::Rc; use rustboyadvance_core::prelude::*; use rustboyadvance_utils::FpsCounter; -struct DummyAudio {} - -impl DummyAudio { - fn new() -> DummyAudio { - DummyAudio {} - } -} - -impl AudioInterface for DummyAudio {} - fn main() { if env::args().count() < 3 { eprintln!("usage: {} ", env::args().nth(0).unwrap()); @@ -35,9 +23,7 @@ fn main() { .build() .unwrap(); - let dummy = Rc::new(RefCell::new(DummyAudio::new())); - - let mut gba = GameBoyAdvance::new(bios.into_boxed_slice(), gamepak, dummy.clone()); + let mut gba = GameBoyAdvance::new(bios.into_boxed_slice(), gamepak, NullAudio::new()); gba.skip_bios(); let mut fps_counter = FpsCounter::default(); diff --git a/platform/rustboyadvance-jni/src/audio/mod.rs b/platform/rustboyadvance-jni/src/audio/mod.rs index e12abcb..a6c2417 100644 --- a/platform/rustboyadvance-jni/src/audio/mod.rs +++ b/platform/rustboyadvance-jni/src/audio/mod.rs @@ -7,23 +7,34 @@ pub mod util { use jni::signature::{JavaType, Primitive}; use jni::JNIEnv; - pub fn get_sample_rate(env: &JNIEnv, audio_player_obj: JObject) -> i32 { - let audio_player_klass = env.get_object_class(audio_player_obj).unwrap(); - let mid_get_sample_rate = env - .get_method_id(audio_player_klass, "getSampleRate", "()I") - .expect("failed to get methodID for getSampleRate"); - let result = env - .call_method_unchecked( - audio_player_obj, - mid_get_sample_rate, - JavaType::Primitive(Primitive::Int), - &[], - ) - .unwrap(); - let sample_rate = match result { - JValue::Int(sample_rate) => sample_rate as i32, - _ => panic!("bad return value"), - }; - return sample_rate; + macro_rules! call_audio_player_method { + ($env:ident, $audio_player_obj:ident, $method_name:literal, "()I") => {{ + let audio_player_klass = $env + .get_object_class($audio_player_obj) + .map_err(|e| format!("failed to get class: {:?}", e))?; + let mid_get_sample_rate = $env + .get_method_id(audio_player_klass, $method_name, "()I") + .map_err(|e| format!("failed to get methodID for {}: {:?}", $method_name, e))?; + let result = $env + .call_method_unchecked( + $audio_player_obj, + mid_get_sample_rate, + JavaType::Primitive(Primitive::Int), + &[], + ) + .map_err(|e| format!("getSampleRate() failed: {:?}", e))?; + match result { + JValue::Int(sample_rate) => Ok(sample_rate), + value => panic!("bad return value {:?}", value), + } + }}; + } + + pub fn get_sample_rate(env: &JNIEnv, audio_player_obj: JObject) -> Result { + call_audio_player_method!(env, audio_player_obj, "getSampleRate", "()I") + } + + pub fn get_sample_count(env: &JNIEnv, audio_player_obj: JObject) -> Result { + call_audio_player_method!(env, audio_player_obj, "getSampleCount", "()I") } } diff --git a/platform/rustboyadvance-jni/src/audio/thread.rs b/platform/rustboyadvance-jni/src/audio/thread.rs index 6f042f0..66e7874 100644 --- a/platform/rustboyadvance-jni/src/audio/thread.rs +++ b/platform/rustboyadvance-jni/src/audio/thread.rs @@ -4,7 +4,7 @@ use std::sync::mpsc::{channel, Sender}; use std::thread; use std::thread::JoinHandle; -use rustboyadvance_utils::audio::Consumer; +use rustboyadvance_utils::audio::SampleConsumer; use jni::JavaVM; @@ -20,8 +20,11 @@ pub enum AudioThreadCommand { pub(crate) fn spawn_audio_worker_thread( audio_connector: AudioJNIConnector, jvm: JavaVM, - mut consumer: Consumer, -) -> (JoinHandle, Sender) { + mut consumer: SampleConsumer, +) -> ( + JoinHandle<(AudioJNIConnector, SampleConsumer)>, + Sender, +) { let (tx, rx) = channel(); let handle = thread::spawn(move || { @@ -58,8 +61,8 @@ pub(crate) fn spawn_audio_worker_thread( info!("[AudioWorker] terminating"); - // return the audio connector back - audio_connector + // return the audio stuff back to main thread + (audio_connector, consumer) }); (handle, tx) diff --git a/platform/rustboyadvance-jni/src/emulator.rs b/platform/rustboyadvance-jni/src/emulator.rs index 8e7646c..069e091 100644 --- a/platform/rustboyadvance-jni/src/emulator.rs +++ b/platform/rustboyadvance-jni/src/emulator.rs @@ -1,10 +1,8 @@ use rustboyadvance_core::prelude::*; -use rustboyadvance_utils::audio::{AudioRingBuffer, Producer}; +use rustboyadvance_utils::audio::SampleConsumer; // use rustboyadvance_core::util::FpsCounter; -use std::cell::RefCell; use std::path::Path; -use std::rc::Rc; use std::sync::{Mutex, MutexGuard}; use std::time::{Duration, Instant}; @@ -15,28 +13,6 @@ use jni::JNIEnv; use crate::audio::{self, connector::AudioJNIConnector, thread::AudioThreadCommand}; -struct AudioDevice { - sample_rate: i32, - audio_producer: Option>, -} - -impl AudioInterface for AudioDevice { - fn push_sample(&mut self, samples: &[i16]) { - if let Some(prod) = &mut self.audio_producer { - for s in samples.iter() { - let _ = prod.push(*s); - } - } else { - // The gba is never ran before audio_producer is initialized - unreachable!() - } - } - - fn get_sample_rate(&self) -> i32 { - self.sample_rate - } -} - struct Renderer { renderer_ref: GlobalRef, frame_buffer_ref: GlobalRef, @@ -145,8 +121,20 @@ impl Default for EmulationState { } } +fn create_audio( + env: &JNIEnv, + audio_player_obj: JObject, +) -> Result<(Box, SampleConsumer), String> { + let sample_rate = audio::util::get_sample_rate(env, audio_player_obj)?; + let sample_count = audio::util::get_sample_count(env, audio_player_obj)? as usize; + Ok(SimpleAudioInterface::create_channel( + sample_rate, + Some(sample_count * 2), + )) +} + pub struct EmulatorContext { - audio_device: Rc>, + audio_consumer: Option, renderer: Renderer, audio_player_ref: GlobalRef, keypad: Keypad, @@ -188,11 +176,9 @@ impl EmulatorContext { let renderer = Renderer::new(env, renderer_obj)?; info!("Creating GBA Instance"); - let audio = Rc::new(RefCell::new(AudioDevice { - sample_rate: audio::util::get_sample_rate(env, audio_player), - audio_producer: None, - })); - let mut gba = GameBoyAdvance::new(bios, gamepak, audio.clone()); + let audio_player_ref = env.new_global_ref(audio_player).unwrap(); + let (audio_device, audio_consumer) = create_audio(env, audio_player_ref.as_obj())?; + let mut gba = GameBoyAdvance::new(bios, gamepak, audio_device); if skip_bios != 0 { info!("skipping bios"); gba.skip_bios(); @@ -202,14 +188,13 @@ impl EmulatorContext { let keypad = Keypad::new(env, keypad_obj); info!("creating context"); - let audio_player_ref = env.new_global_ref(audio_player).unwrap(); let context = EmulatorContext { gba, keypad, renderer, audio_player_ref, emustate: Mutex::new(EmulationState::default()), - audio_device: audio.clone(), + audio_consumer: Some(audio_consumer), }; Ok(context) } @@ -236,30 +221,25 @@ impl EmulatorContext { .map_err(|e| format!("could not get savestate buffer, error {}", e))?; let renderer = Renderer::new(env, renderer_obj)?; - - let audio = Rc::new(RefCell::new(AudioDevice { - sample_rate: audio::util::get_sample_rate(env, audio_player), - audio_producer: None, - })); - let gba = GameBoyAdvance::from_saved_state(&savestate, bios, rom, audio.clone()).map_err( - |e| { + let audio_player_ref = env.new_global_ref(audio_player).unwrap(); + let (audio_device, audio_consumer) = create_audio(env, audio_player_ref.as_obj())?; + let gba = + GameBoyAdvance::from_saved_state(&savestate, bios, rom, audio_device).map_err(|e| { format!( "failed to create GameBoyAdvance from saved savestate, error {:?}", e ) - }, - )?; + })?; let keypad = Keypad::new(env, keypad_obj); - let audio_player_ref = env.new_global_ref(audio_player).unwrap(); Ok(EmulatorContext { gba, keypad, renderer, audio_player_ref, emustate: Mutex::new(EmulationState::default()), - audio_device: audio.clone(), + audio_consumer: Some(audio_consumer), }) } @@ -285,15 +265,13 @@ impl EmulatorContext { // Instanciate an audio player connector let audio_connector = AudioJNIConnector::new(env, self.audio_player_ref.as_obj()); - // Create a ringbuffer between the emulator and the audio thread - let (prod, cons) = AudioRingBuffer::new_with_capacity(audio_connector.sample_count).split(); - - // Store the ringbuffer producer in the emulator - self.audio_device.borrow_mut().audio_producer = Some(prod); - // Spawn the audio worker thread, give it the audio connector, jvm and ringbuffer consumer - let (audio_thread_handle, audio_thread_tx) = - audio::thread::spawn_audio_worker_thread(audio_connector, jvm, cons); + // Note - after this operation `self` no longer has `audio_consumer` + let (audio_thread_handle, audio_thread_tx) = audio::thread::spawn_audio_worker_thread( + audio_connector, + jvm, + self.audio_consumer.take().unwrap(), + ); info!("starting main emulation loop"); @@ -302,7 +280,7 @@ impl EmulatorContext { 'running: loop { let emustate = *self.emustate.lock().unwrap(); - let limiter = match emustate { + let vsync = match emustate { EmulationState::Initial => unsafe { std::hint::unreachable_unchecked() }, EmulationState::Stopped => unsafe { std::hint::unreachable_unchecked() }, EmulationState::Pausing => { @@ -334,7 +312,7 @@ impl EmulatorContext { // info!("FPS {}", fps); // } - if limiter { + if vsync { let time_passed = start_time.elapsed(); let delay = FRAME_TIME.checked_sub(time_passed); match delay { @@ -350,13 +328,12 @@ impl EmulatorContext { audio_thread_tx.send(AudioThreadCommand::Terminate).unwrap(); // we surely have an endpoint, so it will work info!("waiting for audio worker to complete"); - let audio_connector = audio_thread_handle.join().unwrap(); + let (audio_connector, audio_consumer) = audio_thread_handle.join().unwrap(); + self.audio_consumer.replace(audio_consumer); info!("audio worker terminated"); audio_connector.pause(env); - self.audio_device.borrow_mut().audio_producer = None; - *self.emustate.lock().unwrap() = EmulationState::Stopped; Ok(()) diff --git a/platform/rustboyadvance-libretro/src/lib.rs b/platform/rustboyadvance-libretro/src/lib.rs index d1fd5a3..491a9d6 100644 --- a/platform/rustboyadvance-libretro/src/lib.rs +++ b/platform/rustboyadvance-libretro/src/lib.rs @@ -13,34 +13,18 @@ use unsafe_unwrap::UnsafeUnwrap; use rustboyadvance_core::keypad::Keys as _GbaButton; use rustboyadvance_core::prelude::*; -use rustboyadvance_utils::audio::AudioRingBuffer; +use rustboyadvance_utils::audio::SampleConsumer; use std::ops::Deref; use std::path::Path; -use std::cell::RefCell; use std::default::Default; -use std::rc::Rc; - -#[derive(Default)] -struct AudioDevice { - audio_ring_buffer: AudioRingBuffer, -} - -impl AudioInterface for AudioDevice { - fn push_sample(&mut self, samples: &[i16]) { - let prod = self.audio_ring_buffer.producer(); - for s in samples.iter() { - let _ = prod.push(*s); - } - } -} #[derive(Default)] struct RustBoyAdvanceCore { gba: Option, game_data: Option, - audio: Option>>, + audio_consumer: Option, } #[repr(transparent)] @@ -119,10 +103,12 @@ impl libretro_backend::Core for RustBoyAdvanceCore { .video(240, 160, 60.0, PixelFormat::ARGB8888) .audio(44100.0); - let audio = Rc::new(RefCell::new(AudioDevice::default())); - let gba = GameBoyAdvance::new(bios.into_boxed_slice(), gamepak, audio.clone()); + let (audio_device, audio_consumer) = + SimpleAudioInterface::create_channel(44100, None); - self.audio = Some(audio); + let gba = GameBoyAdvance::new(bios.into_boxed_slice(), gamepak, audio_device); + + self.audio_consumer = Some(audio_consumer); self.gba = Some(gba); self.game_data = Some(game_data); LoadGameResult::Success(av_info) @@ -136,7 +122,6 @@ impl libretro_backend::Core for RustBoyAdvanceCore { // gba and audio are `Some` after the game is loaded, so avoiding overhead of unwrap let gba = unsafe { self.gba.as_mut().unsafe_unwrap() }; - let audio = unsafe { self.audio.as_mut().unsafe_unwrap() }; let key_state = gba.get_key_state_mut(); macro_rules! update_controllers { @@ -165,12 +150,11 @@ impl libretro_backend::Core for RustBoyAdvanceCore { // upload sound samples { + let mut audio_consumer = self.audio_consumer.take().unwrap(); let mut audio_samples = [0; 4096 * 2]; - let mut audio = audio.borrow_mut(); - let consumer = audio.audio_ring_buffer.consumer(); - let count = consumer.pop_slice(&mut audio_samples); - + let count = audio_consumer.pop_slice(&mut audio_samples); handle.upload_audio_frame(&audio_samples[..count]); + self.audio_consumer.replace(audio_consumer); } } diff --git a/platform/rustboyadvance-sdl2/Cargo.toml b/platform/rustboyadvance-sdl2/Cargo.toml index e4136f5..5b46185 100644 --- a/platform/rustboyadvance-sdl2/Cargo.toml +++ b/platform/rustboyadvance-sdl2/Cargo.toml @@ -10,7 +10,7 @@ rustboyadvance-utils = { path = "../../utils/" } sdl2 = { version = "0.33.0", features = ["image"] } ringbuf = "0.2.2" bytesize = "1.0.0" -clap = { version = "2.33", features = ["color", "yaml"] } +structopt = "0.3" log = "0.4.8" flexi_logger = { version = "0.14", features = ["colors"] } bit = "^0.1" diff --git a/platform/rustboyadvance-sdl2/src/audio.rs b/platform/rustboyadvance-sdl2/src/audio.rs index 8a48db6..f106c97 100644 --- a/platform/rustboyadvance-sdl2/src/audio.rs +++ b/platform/rustboyadvance-sdl2/src/audio.rs @@ -1,98 +1,63 @@ use sdl2; -use sdl2::audio::{AudioCallback, AudioDevice, AudioSpec, AudioSpecDesired}; +use sdl2::audio::{AudioCallback, AudioDevice, AudioFormat, AudioSpec, AudioSpecDesired}; -use rustboyadvance_core::{AudioInterface, StereoSample}; +use rustboyadvance_core::prelude::SimpleAudioInterface; +use rustboyadvance_utils::audio::SampleConsumer; -use ringbuf; -use ringbuf::{Consumer, Producer, RingBuffer}; - -struct GbaAudioCallback { - consumer: Consumer>, +pub struct GbaAudioCallback { + consumer: SampleConsumer, + #[allow(unused)] spec: AudioSpec, } -pub struct DummyAudioPlayer {} - -impl AudioInterface for DummyAudioPlayer {} - -pub struct Sdl2AudioPlayer { - _device: AudioDevice, - producer: Producer>, - freq: i32, -} - impl AudioCallback for GbaAudioCallback { type Channel = i16; fn callback(&mut self, out_samples: &mut [i16]) { - let sample_count = out_samples.len() / 2; - - for i in 0..sample_count { - if let Some([left, right]) = self.consumer.pop() { - out_samples[2 * i] = left; - out_samples[2 * i + 1] = right; - } else { - out_samples[2 * i] = self.spec.silence as i16; - out_samples[2 * i + 1] = self.spec.silence as i16; - } + let written = self.consumer.pop_slice(out_samples); + for s in out_samples.iter_mut().skip(written) { + *s = self.spec.silence as i16; } } } -impl AudioInterface for Sdl2AudioPlayer { - fn get_sample_rate(&self) -> i32 { - self.freq - } - - fn push_sample(&mut self, sample: &[i16]) { - #![allow(unused_must_use)] - self.producer.push([sample[0], sample[1]]); - } -} - -pub fn create_audio_player(sdl: &sdl2::Sdl) -> Sdl2AudioPlayer { +pub fn create_audio_player( + sdl: &sdl2::Sdl, +) -> Result<(Box, AudioDevice), String> { let desired_spec = AudioSpecDesired { freq: Some(44_100), channels: Some(2), // stereo samples: None, }; - let audio_subsystem = sdl.audio().unwrap(); + let audio_subsystem = sdl.audio()?; let mut freq = 0; - let mut producer: Option>> = None; + let mut gba_audio = None; - let device = audio_subsystem - .open_playback(None, &desired_spec, |spec| { - info!("Found audio device: {:?}", spec); - freq = spec.freq; + let device = audio_subsystem.open_playback(None, &desired_spec, |spec| { + info!("Found audio device: {:?}", spec); + freq = spec.freq; - // Create a thread-safe SPSC fifo - let ringbuf_size = (spec.samples as usize) * 2; - let rb = RingBuffer::>::new(ringbuf_size); - let (prod, cons) = rb.split(); + if spec.format != AudioFormat::S16LSB { + panic!("Unsupported audio format {:?}", spec.format); + } - // move producer to the outer scope - producer = Some(prod); + // Create a thread-safe SPSC fifo + let ringbuf_samples_per_channel = (spec.samples as usize) * 2; // we want the ringbuf to hold 2 frames worth of samples + let ringbuf_size = (spec.channels as usize) * ringbuf_samples_per_channel; + info!("ringbuffer size = {}", ringbuf_size); - GbaAudioCallback { - consumer: cons, - spec, - } - }) - .unwrap(); + let (audio_device, consumer) = + SimpleAudioInterface::create_channel(freq, Some(ringbuf_size)); + // Move the audio to outer scope + gba_audio = Some(audio_device); + + GbaAudioCallback { consumer, spec } + })?; device.resume(); - Sdl2AudioPlayer { - _device: device, - freq, - producer: producer.unwrap(), - } -} - -pub fn create_dummy_player() -> DummyAudioPlayer { - info!("Dummy audio device"); - DummyAudioPlayer {} + Ok((gba_audio.take().unwrap(), device)) } diff --git a/platform/rustboyadvance-sdl2/src/main.rs b/platform/rustboyadvance-sdl2/src/main.rs index 3664650..d082608 100644 --- a/platform/rustboyadvance-sdl2/src/main.rs +++ b/platform/rustboyadvance-sdl2/src/main.rs @@ -12,21 +12,14 @@ use sdl2::EventPump; use bytesize; use spin_sleep; - -use std::cell::RefCell; -use std::rc::Rc; +use structopt::StructOpt; use std::fs; use std::io::Cursor; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::process; use std::time; -use std::convert::TryFrom; - -#[macro_use] -extern crate clap; - #[macro_use] extern crate log; use flexi_logger; @@ -34,12 +27,11 @@ use flexi_logger::*; mod audio; mod input; +mod options; mod video; -use audio::{create_audio_player, create_dummy_player}; use video::{SCREEN_HEIGHT, SCREEN_WIDTH}; -use rustboyadvance_core::cartridge::BackupType; use rustboyadvance_core::prelude::*; use rustboyadvance_utils::FpsCounter; @@ -50,9 +42,6 @@ const DEFAULT_GDB_SERVER_ADDR: &'static str = "localhost:1337"; const CANVAS_WIDTH: u32 = SCREEN_WIDTH; const CANVAS_HEIGHT: u32 = SCREEN_HEIGHT; -fn get_savestate_path(rom_filename: &Path) -> PathBuf { - rom_filename.with_extension("savestate") -} /// Waits for the user to drag a rom file to window fn wait_for_rom(canvas: &mut WindowCanvas, event_pump: &mut EventPump) -> Result { @@ -101,6 +90,17 @@ fn ask_download_bios() { const OPEN_SOURCE_BIOS_URL: &'static str = "https://github.com/Nebuleon/ReGBA/raw/master/bios/gba_bios.bin"; println!("Missing BIOS file. If you don't have the original GBA BIOS, you can download an open-source bios from {}", OPEN_SOURCE_BIOS_URL); + std::process::exit(0); +} + +fn load_bios(bios_path: &Path) -> Box<[u8]> { + match read_bin_file(bios_path) { + Ok(bios) => bios.into_boxed_slice(), + _ => { + ask_download_bios(); + unreachable!() + } + } } fn main() -> Result<(), Box> { @@ -114,24 +114,7 @@ fn main() -> Result<(), Box> { .start() .unwrap(); - let mut frame_limiter = true; - let yaml = load_yaml!("cli.yml"); - let matches = clap::App::from_yaml(yaml).get_matches(); - - let bios_path = Path::new(matches.value_of("bios").unwrap_or_default()); - let bios_bin = match read_bin_file(bios_path) { - Ok(bios) => bios.into_boxed_slice(), - _ => { - ask_download_bios(); - std::process::exit(0); - } - }; - - let skip_bios = matches.occurrences_of("skip_bios") != 0; - - let debug = matches.occurrences_of("debug") != 0; - let silent = matches.occurrences_of("silent") != 0; - let with_gdbserver = matches.occurrences_of("with_gdbserver") != 0; + let opts = options::Options::from_args(); info!("Initializing SDL2 context"); let sdl_context = sdl2::init().expect("failed to initialize sdl2"); @@ -174,65 +157,27 @@ fn main() -> Result<(), Box> { } }; - let mut rom_path = match matches.value_of("game_rom") { - Some(path) => path.to_string(), - _ => { - info!("[!] Rom file missing, please drag a rom file into the emulator window..."); - wait_for_rom(&mut canvas, &mut event_pump)? - } - }; - let mut renderer = video::Renderer::from_canvas(canvas); - let audio: Rc> = if silent { - Rc::new(RefCell::new(create_dummy_player())) - } else { - Rc::new(RefCell::new(create_audio_player(&sdl_context))) - }; + let (audio_interface, mut _sdl_audio_device) = audio::create_audio_player(&sdl_context)?; + let mut rom_name = opts.rom_name(); - let mut savestate_path = get_savestate_path(&Path::new(&rom_path)); + let bios_bin = load_bios(&opts.bios); - let mut rom_name = Path::new(&rom_path).file_name().unwrap().to_str().unwrap(); + let mut gba = GameBoyAdvance::new( + bios_bin.clone(), + opts.cartridge_from_opts()?, + audio_interface, + ); - let mut builder = GamepakBuilder::new() - .save_type(BackupType::try_from( - matches.value_of("save_type").unwrap(), - )?) - .file(Path::new(&rom_path)); - - if matches.occurrences_of("rtc") != 0 { - builder = builder.with_rtc(); - } - - let gamepak = builder.build()?; - - let mut gba = GameBoyAdvance::new(bios_bin.clone(), gamepak, audio.clone()); - - if skip_bios { + if opts.skip_bios { gba.skip_bios(); } - if debug { - #[cfg(feature = "debugger")] - { - gba.cpu.set_verbose(true); - let mut debugger = Debugger::new(); - info!("starting debugger..."); - debugger - .repl(&mut gba, matches.value_of("script_file")) - .unwrap(); - info!("ending debugger..."); - return Ok(()); - } - #[cfg(not(feature = "debugger"))] - { - panic!("Please compile me with 'debugger' feature"); - } - } - - if with_gdbserver { + if opts.gdbserver { todo!("gdb") } + let mut vsync = true; let mut fps_counter = FpsCounter::default(); let frame_time = time::Duration::new(0, 1_000_000_000u32 / 60); 'running: loop { @@ -244,7 +189,7 @@ fn main() -> Result<(), Box> { scancode: Some(scancode), .. } => match scancode { - Scancode::Space => frame_limiter = false, + Scancode::Space => vsync = false, k => input::on_keyboard_key_down(&mut gba, k), }, Event::KeyUp { @@ -265,28 +210,28 @@ fn main() -> Result<(), Box> { Scancode::F5 => { info!("Saving state ..."); let save = gba.save_state()?; - write_bin_file(&savestate_path, &save)?; + write_bin_file(&opts.savestate_path(), &save)?; info!( "Saved to {:?} ({})", - savestate_path, + opts.savestate_path(), bytesize::ByteSize::b(save.len() as u64) ); } Scancode::F9 => { - if savestate_path.is_file() { - let save = read_bin_file(&savestate_path)?; - info!("Restoring state from {:?}...", savestate_path); + if opts.savestate_path().is_file() { + let save = read_bin_file(&opts.savestate_path())?; + info!("Restoring state from {:?}...", opts.savestate_path()); gba.restore_state(&save)?; info!("Restored!"); } else { info!("Savestate not created, please create one by pressing F5"); } } - Scancode::Space => frame_limiter = true, + Scancode::Space => vsync = true, k => input::on_keyboard_key_up(&mut gba, k), }, Event::ControllerButtonDown { button, .. } => match button { - Button::RightStick => frame_limiter = !frame_limiter, + Button::RightStick => vsync = !vsync, b => input::on_controller_button_down(&mut gba, b), }, Event::ControllerButtonUp { button, .. } => { @@ -318,16 +263,7 @@ fn main() -> Result<(), Box> { } Event::Quit { .. } => break 'running, Event::DropFile { filename, .. } => { - // load the new rom - rom_path = filename; - savestate_path = get_savestate_path(&Path::new(&rom_path)); - rom_name = Path::new(&rom_path).file_name().unwrap().to_str().unwrap(); - let gamepak = GamepakBuilder::new().file(Path::new(&rom_path)).build()?; - let bios_bin = read_bin_file(bios_path).unwrap(); - - // create a new emulator - TODO, export to a function - gba = GameBoyAdvance::new(bios_bin.into_boxed_slice(), gamepak, audio.clone()); - gba.skip_bios(); + todo!("impl DropFile again") } _ => {} } @@ -341,7 +277,7 @@ fn main() -> Result<(), Box> { renderer.set_window_title(&title); } - if frame_limiter { + if vsync { let time_passed = start_time.elapsed(); let delay = frame_time.checked_sub(time_passed); match delay { diff --git a/platform/rustboyadvance-sdl2/src/options.rs b/platform/rustboyadvance-sdl2/src/options.rs new file mode 100644 index 0000000..19057a2 --- /dev/null +++ b/platform/rustboyadvance-sdl2/src/options.rs @@ -0,0 +1,64 @@ +use std::path::PathBuf; + +use rustboyadvance_core::{ + cartridge::{BackupType, GamepakBuilder}, + prelude::Cartridge, +}; +use structopt::StructOpt; + +const SAVE_TYPE_POSSIBLE_VALUES: &[&str] = + &["sram", "flash128k", "flash64k", "eeprom", "autodetect"]; + +#[derive(StructOpt, Debug)] +#[structopt(name = "rustboyadvance-sdl2")] +pub struct Options { + /// Rom file to emulate, may be a raw dump from a cartridge or a compiled ELF file + #[structopt(name = "ROM", parse(from_os_str))] + pub rom: PathBuf, + + /// Bios file to use + #[structopt(long, parse(from_os_str), default_value = "gba_bios.bin")] + pub bios: PathBuf, + + /// Skip running the bios boot animation and jump straight to the ROM + #[structopt(long)] + pub skip_bios: bool, + + /// Do not output sound + #[structopt(long)] + pub silent: bool, + + /// Initalize gdbserver and wait for a connection from gdb + #[structopt(short = "d", long)] + pub gdbserver: bool, + + /// Force emulation of RTC, use for games that have RTC but the emulator fails to detect + #[structopt(long)] + pub rtc: bool, + + /// Override save type, useful for troublemaking games that fool the auto detection + #[structopt(long, default_value = "autodetect", possible_values = SAVE_TYPE_POSSIBLE_VALUES)] + pub save_type: BackupType, +} + +type DynError = Box; + +impl Options { + pub fn cartridge_from_opts(&self) -> Result { + let mut builder = GamepakBuilder::new() + .save_type(self.save_type) + .file(&self.rom); + if self.rtc { + builder = builder.with_rtc(); + } + Ok(builder.build()?) + } + + pub fn savestate_path(&self) -> PathBuf { + self.rom.with_extension("savestate") + } + + pub fn rom_name(&self) -> &str { + self.rom.file_name().unwrap().to_str().unwrap() + } +} diff --git a/platform/rustboyadvance-wasm/src/emulator.rs b/platform/rustboyadvance-wasm/src/emulator.rs index 70f3508..481209b 100644 --- a/platform/rustboyadvance-wasm/src/emulator.rs +++ b/platform/rustboyadvance-wasm/src/emulator.rs @@ -1,24 +1,20 @@ -use std::cell::RefCell; -use std::rc::Rc; - +use rustboyadvance_utils::audio::SampleConsumer; use wasm_bindgen::prelude::*; use wasm_bindgen::Clamped; use js_sys::Float32Array; -use web_sys::AudioContext; use web_sys::CanvasRenderingContext2d; use rustboyadvance_core::keypad as gba_keypad; use rustboyadvance_core::prelude::*; -use rustboyadvance_utils::audio::AudioRingBuffer; use bit::BitIndex; #[wasm_bindgen] pub struct Emulator { gba: GameBoyAdvance, - interface: Rc>, + audio_consumer: SampleConsumer, frame: Option>, } @@ -33,51 +29,17 @@ fn translate_frame_to_u8(input_fb: &[u32], out_fb: &mut [u8]) { } } -struct Interface { - sample_rate: i32, - audio_ctx: AudioContext, - audio_ring_buffer: AudioRingBuffer, -} - -impl Drop for Interface { - fn drop(&mut self) { - let _ = self.audio_ctx.clone(); - } -} - -impl Interface { - fn new(audio_ctx: AudioContext) -> Result { - Ok(Interface { - sample_rate: audio_ctx.sample_rate() as i32, - audio_ctx: audio_ctx, - audio_ring_buffer: Default::default(), - }) - } -} - fn convert_sample(s: i16) -> f32 { (s as f32) / 32767_f32 } -impl AudioInterface for Interface { - fn get_sample_rate(&self) -> i32 { - self.sample_rate - } - - fn push_sample(&mut self, samples: &[i16]) { - let prod = self.audio_ring_buffer.producer(); - for s in samples.iter() { - let _ = prod.push(*s); - } - } -} - #[wasm_bindgen] impl Emulator { #[wasm_bindgen(constructor)] pub fn new(bios: &[u8], rom: &[u8]) -> Result { let audio_ctx = web_sys::AudioContext::new()?; - let interface = Rc::new(RefCell::new(Interface::new(audio_ctx)?)); + let (audio_device, audio_consumer) = + SimpleAudioInterface::create_channel(audio_ctx.sample_rate() as i32, None); let gamepak = GamepakBuilder::new() .take_buffer(rom.to_vec().into_boxed_slice()) @@ -85,11 +47,11 @@ impl Emulator { .build() .unwrap(); - let gba = GameBoyAdvance::new(bios.to_vec().into_boxed_slice(), gamepak, interface.clone()); + let gba = GameBoyAdvance::new(bios.to_vec().into_boxed_slice(), gamepak, audio_device); Ok(Emulator { gba, - interface, + audio_consumer, frame: Some(vec![0; 240 * 160 * 4].into_boxed_slice()), }) } @@ -152,12 +114,9 @@ impl Emulator { } } - pub fn collect_audio_samples(&self) -> Result { - let mut interface = self.interface.borrow_mut(); - - let consumer = interface.audio_ring_buffer.consumer(); - let mut samples = Vec::with_capacity(consumer.len()); - while let Some(sample) = consumer.pop() { + pub fn collect_audio_samples(&mut self) -> Result { + let mut samples = Vec::with_capacity(self.audio_consumer.len()); + while let Some(sample) = self.audio_consumer.pop() { samples.push(convert_sample(sample)); } diff --git a/utils/src/lib.rs b/utils/src/lib.rs index 6c1dc35..fe4d3e3 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -90,10 +90,12 @@ macro_rules! host_breakpoint { pub mod audio { pub use ringbuf::{Consumer, Producer, RingBuffer}; + pub type SampleProducer = Producer; + pub type SampleConsumer = Consumer; pub struct AudioRingBuffer { - prod: Producer, - cons: Consumer, + prod: SampleProducer, + cons: SampleConsumer, } impl Default for AudioRingBuffer { @@ -110,15 +112,15 @@ pub mod audio { AudioRingBuffer { prod, cons } } - pub fn producer(&mut self) -> &mut Producer { + pub fn producer(&mut self) -> &mut SampleProducer { &mut self.prod } - pub fn consumer(&mut self) -> &mut Consumer { + pub fn consumer(&mut self) -> &mut SampleConsumer { &mut self.cons } - pub fn split(self) -> (Producer, Consumer) { + pub fn split(self) -> (SampleProducer, SampleConsumer) { (self.prod, self.cons) } }