From 0de8a60006568e9cc01bb445b8363f6d715390d4 Mon Sep 17 00:00:00 2001 From: Michel Heily Date: Sat, 3 Oct 2020 22:09:38 +0300 Subject: [PATCH] core: Start working on a scheduler A more robust cycle aware event scheduling, to easily implement serial-io, dmg audio channels and improve accuracy. This brings a slight performance hit :/ I also ran dos2unix on some of the files :D Former-commit-id: 62f4ba33e3a083b7976d6512ba6f5720ec493aa0 Former-commit-id: a4b3a92cd1eb156bbe9fd0ef77fbb0e7a45660cb --- bindings/rustboyadvance-jni/src/audio/mod.rs | 58 +- .../rustboyadvance-jni/src/audio/thread.rs | 132 +-- bindings/rustboyadvance-jni/src/emulator.rs | 806 +++++++++--------- core/src/gba.rs | 96 ++- core/src/gpu/mod.rs | 141 ++- core/src/lib.rs | 1 + core/src/overrides.rs | 116 +-- core/src/sched.rs | 305 +++++++ core/src/sound/mod.rs | 92 +- 9 files changed, 1113 insertions(+), 634 deletions(-) create mode 100644 core/src/sched.rs diff --git a/bindings/rustboyadvance-jni/src/audio/mod.rs b/bindings/rustboyadvance-jni/src/audio/mod.rs index 45b2c89..e12abcb 100644 --- a/bindings/rustboyadvance-jni/src/audio/mod.rs +++ b/bindings/rustboyadvance-jni/src/audio/mod.rs @@ -1,29 +1,29 @@ -pub mod connector; -pub mod thread; - -pub mod util { - - use jni::objects::{JObject, JValue}; - 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; - } -} +pub mod connector; +pub mod thread; + +pub mod util { + + use jni::objects::{JObject, JValue}; + 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; + } +} diff --git a/bindings/rustboyadvance-jni/src/audio/thread.rs b/bindings/rustboyadvance-jni/src/audio/thread.rs index a0d78be..d2e1efa 100644 --- a/bindings/rustboyadvance-jni/src/audio/thread.rs +++ b/bindings/rustboyadvance-jni/src/audio/thread.rs @@ -1,66 +1,66 @@ -use super::connector::AudioJNIConnector; - -use std::sync::mpsc::{channel, Sender}; -use std::thread; -use std::thread::JoinHandle; - -use rustboyadvance_core::util::audio::Consumer; - -use jni::JavaVM; - -#[derive(Debug)] -#[allow(dead_code)] -pub enum AudioThreadCommand { - RenderAudio, - Pause, - Play, - Terminate, -} - -pub(crate) fn spawn_audio_worker_thread( - audio_connector: AudioJNIConnector, - jvm: JavaVM, - mut consumer: Consumer, -) -> (JoinHandle, Sender) { - let (tx, rx) = channel(); - - let handle = thread::spawn(move || { - info!("[AudioWorker] spawned!"); - - info!("[AudioWorker] Attaching JVM"); - let env = jvm.attach_current_thread().unwrap(); - - loop { - let command = rx.recv().unwrap(); - match command { - AudioThreadCommand::Pause => { - info!("[AudioWorker] - got {:?} command", command); - audio_connector.pause(&env); - } - - AudioThreadCommand::Play => { - info!("[AudioWorker] - got {:?} command", command); - audio_connector.play(&env); - } - - AudioThreadCommand::RenderAudio => { - let mut samples = [0; 4096 * 2]; // TODO is this memset expansive ? - let count = consumer.pop_slice(&mut samples); - - audio_connector.write_audio_samples(&env, &samples[0..count]); - } - AudioThreadCommand::Terminate => { - info!("[AudioWorker] - got terminate command!"); - break; - } - } - } - - info!("[AudioWorker] terminating"); - - // return the audio connector back - audio_connector - }); - - (handle, tx) -} +use super::connector::AudioJNIConnector; + +use std::sync::mpsc::{channel, Sender}; +use std::thread; +use std::thread::JoinHandle; + +use rustboyadvance_core::util::audio::Consumer; + +use jni::JavaVM; + +#[derive(Debug)] +#[allow(dead_code)] +pub enum AudioThreadCommand { + RenderAudio, + Pause, + Play, + Terminate, +} + +pub(crate) fn spawn_audio_worker_thread( + audio_connector: AudioJNIConnector, + jvm: JavaVM, + mut consumer: Consumer, +) -> (JoinHandle, Sender) { + let (tx, rx) = channel(); + + let handle = thread::spawn(move || { + info!("[AudioWorker] spawned!"); + + info!("[AudioWorker] Attaching JVM"); + let env = jvm.attach_current_thread().unwrap(); + + loop { + let command = rx.recv().unwrap(); + match command { + AudioThreadCommand::Pause => { + info!("[AudioWorker] - got {:?} command", command); + audio_connector.pause(&env); + } + + AudioThreadCommand::Play => { + info!("[AudioWorker] - got {:?} command", command); + audio_connector.play(&env); + } + + AudioThreadCommand::RenderAudio => { + let mut samples = [0; 4096 * 2]; // TODO is this memset expansive ? + let count = consumer.pop_slice(&mut samples); + + audio_connector.write_audio_samples(&env, &samples[0..count]); + } + AudioThreadCommand::Terminate => { + info!("[AudioWorker] - got terminate command!"); + break; + } + } + } + + info!("[AudioWorker] terminating"); + + // return the audio connector back + audio_connector + }); + + (handle, tx) +} diff --git a/bindings/rustboyadvance-jni/src/emulator.rs b/bindings/rustboyadvance-jni/src/emulator.rs index c199bb1..5a231e4 100644 --- a/bindings/rustboyadvance-jni/src/emulator.rs +++ b/bindings/rustboyadvance-jni/src/emulator.rs @@ -1,403 +1,403 @@ -use rustboyadvance_core::prelude::*; -use rustboyadvance_core::util::audio::{AudioRingBuffer, Producer}; -// 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}; - -use jni::objects::{GlobalRef, JMethodID, JObject, JString, JValue}; -use jni::signature; -use jni::sys::{jboolean, jbyteArray, jintArray, jmethodID}; -use jni::JNIEnv; - -use crate::audio::{self, connector::AudioJNIConnector, thread::AudioThreadCommand}; - -struct Hardware { - sample_rate: i32, - audio_producer: Option>, - key_state: u16, -} - -impl AudioInterface for Hardware { - 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 - } -} - -impl InputInterface for Hardware { - fn poll(&mut self) -> u16 { - self.key_state - } -} - -struct Renderer { - renderer_ref: GlobalRef, - frame_buffer_ref: GlobalRef, - mid_render_frame: jmethodID, -} - -impl Renderer { - fn new(env: &JNIEnv, renderer_obj: JObject) -> Result { - let renderer_ref = env - .new_global_ref(renderer_obj) - .map_err(|e| format!("failed to add new global ref, error: {:?}", e))?; - - let frame_buffer = env - .new_int_array(240 * 160) - .map_err(|e| format!("failed to create framebuffer, error: {:?}", e))?; - let frame_buffer_ref = env - .new_global_ref(frame_buffer) - .map_err(|e| format!("failed to add new global ref, error: {:?}", e))?; - let renderer_klass = env - .get_object_class(renderer_ref.as_obj()) - .expect("failed to get renderer class"); - let mid_render_frame = env - .get_method_id(renderer_klass, "renderFrame", "([I)V") - .expect("failed to get methodID for renderFrame") - .into_inner(); - - Ok(Renderer { - renderer_ref, - frame_buffer_ref, - mid_render_frame, - }) - } - - #[inline] - fn render_frame(&self, env: &JNIEnv, buffer: &[u32]) { - unsafe { - env.set_int_array_region( - self.frame_buffer_ref.as_obj().into_inner(), - 0, - std::mem::transmute::<&[u32], &[i32]>(buffer), - ) - .unwrap(); - } - - env.call_method_unchecked( - self.renderer_ref.as_obj(), - JMethodID::from(self.mid_render_frame), - signature::JavaType::Primitive(signature::Primitive::Void), - &[JValue::from(self.frame_buffer_ref.as_obj())], - ) - .expect("failed to call renderFrame"); - } -} - -struct Keypad { - keypad_ref: GlobalRef, - mid_get_key_state: jmethodID, -} - -impl Keypad { - fn new(env: &JNIEnv, keypad_obj: JObject) -> Keypad { - let keypad_ref = env - .new_global_ref(keypad_obj) - .expect("failed to create keypad_ref"); - let keypad_klass = env - .get_object_class(keypad_ref.as_obj()) - .expect("failed to create keypad class"); - let mid_get_key_state = env - .get_method_id(keypad_klass, "getKeyState", "()I") - .expect("failed to get methodID for getKeyState") - .into_inner(); - - Keypad { - keypad_ref, - mid_get_key_state, - } - } - - #[inline] - fn get_key_state(&self, env: &JNIEnv) -> u16 { - match env.call_method_unchecked( - self.keypad_ref.as_obj(), - JMethodID::from(self.mid_get_key_state), - signature::JavaType::Primitive(signature::Primitive::Int), - &[], - ) { - Ok(JValue::Int(key_state)) => key_state as u16, - _ => panic!("failed to call getKeyState"), - } - } -} - -#[derive(Debug, PartialEq, Copy, Clone)] -pub enum EmulationState { - Initial, - Pausing, - Paused, - Running(bool), - Stopping, - Stopped, -} - -impl Default for EmulationState { - fn default() -> EmulationState { - EmulationState::Initial - } -} - -pub struct EmulatorContext { - hwif: Rc>, - renderer: Renderer, - audio_player_ref: GlobalRef, - keypad: Keypad, - pub emustate: Mutex, - pub gba: GameBoyAdvance, -} - -impl EmulatorContext { - pub fn native_open_context( - env: &JNIEnv, - bios: jbyteArray, - rom: jbyteArray, - renderer_obj: JObject, - audio_player: JObject, - keypad_obj: JObject, - save_file: JString, - skip_bios: jboolean, - ) -> Result { - let bios = env - .convert_byte_array(bios) - .map_err(|e| format!("could not get bios buffer, error {}", e))? - .into_boxed_slice(); - let rom = env - .convert_byte_array(rom) - .map_err(|e| format!("could not get rom buffer, error {}", e))? - .into_boxed_slice(); - let save_file: String = env - .get_string(save_file) - .map_err(|_| String::from("could not get save path"))? - .into(); - let gamepak = GamepakBuilder::new() - .take_buffer(rom) - .save_path(&Path::new(&save_file)) - .build() - .map_err(|e| format!("failed to load rom, gba result: {:?}", e))?; - info!("Loaded ROM file {:?}", gamepak.header); - - info!("Creating renderer"); - let renderer = Renderer::new(env, renderer_obj)?; - - info!("Creating GBA Instance"); - let hw = Rc::new(RefCell::new(Hardware { - sample_rate: audio::util::get_sample_rate(env, audio_player), - audio_producer: None, - key_state: 0xffff, - })); - let mut gba = GameBoyAdvance::new(bios, gamepak, hw.clone(), hw.clone()); - if skip_bios != 0 { - info!("skipping bios"); - gba.skip_bios(); - } - - info!("creating keypad"); - 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()), - hwif: hw.clone(), - }; - Ok(context) - } - - pub fn native_open_saved_state( - env: &JNIEnv, - state: jbyteArray, - renderer_obj: JObject, - audio_player: JObject, - keypad_obj: JObject, - ) -> Result { - let state = env - .convert_byte_array(state) - .map_err(|e| format!("could not get state buffer, error {}", e))?; - - let renderer = Renderer::new(env, renderer_obj)?; - - let hw = Rc::new(RefCell::new(Hardware { - sample_rate: audio::util::get_sample_rate(env, audio_player), - audio_producer: None, - key_state: 0xffff, - })); - let gba = - GameBoyAdvance::from_saved_state(&state, hw.clone(), hw.clone()).map_err(|e| { - format!( - "failed to create GameBoyAdvance from saved state, 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()), - hwif: hw.clone(), - }) - } - - fn render_video(&mut self, env: &JNIEnv) { - self.renderer.render_frame(env, self.gba.get_frame_buffer()); - } - - /// Lock the emulation loop in order to perform updates to the struct - pub fn lock_and_get_gba(&mut self) -> (MutexGuard, &mut GameBoyAdvance) { - (self.emustate.lock().unwrap(), &mut self.gba) - } - - /// Run the emulation main loop - pub fn native_run(&mut self, env: &JNIEnv) -> Result<(), jni::errors::Error> { - const FRAME_TIME: Duration = Duration::from_nanos(1_000_000_000u64 / 60); - - // Set the state to running - *self.emustate.lock().unwrap() = EmulationState::Running(false); - - // Extract current JVM - let jvm = env.get_java_vm().unwrap(); - - // 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.hwif.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); - - info!("starting main emulation loop"); - - // let mut fps_counter = FpsCounter::default(); - - 'running: loop { - let emustate = *self.emustate.lock().unwrap(); - - let limiter = match emustate { - EmulationState::Initial => unsafe { std::hint::unreachable_unchecked() }, - EmulationState::Stopped => unsafe { std::hint::unreachable_unchecked() }, - EmulationState::Pausing => { - info!("emulation pause requested"); - *self.emustate.lock().unwrap() = EmulationState::Paused; - continue; - } - EmulationState::Paused => continue, - EmulationState::Stopping => break 'running, - EmulationState::Running(turbo) => !turbo, - }; - - let start_time = Instant::now(); - // check key state - self.hwif.borrow_mut().key_state = self.keypad.get_key_state(env); - - // run frame - self.gba.frame(); - - // render video - self.render_video(env); - - // request audio worker to render the audio now - audio_thread_tx - .send(AudioThreadCommand::RenderAudio) - .unwrap(); - - // if let Some(fps) = fps_counter.tick() { - // info!("FPS {}", fps); - // } - - if limiter { - let time_passed = start_time.elapsed(); - let delay = FRAME_TIME.checked_sub(time_passed); - match delay { - None => {} - Some(delay) => { - std::thread::sleep(delay); - } - } - } - } - - info!("stopping, terminating audio worker"); - 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(); - info!("audio worker terminated"); - - audio_connector.pause(env); - - self.hwif.borrow_mut().audio_producer = None; - - *self.emustate.lock().unwrap() = EmulationState::Stopped; - - Ok(()) - } - - pub fn native_get_framebuffer(&mut self, env: &JNIEnv) -> jintArray { - let fb = env.new_int_array(240 * 160).unwrap(); - self.pause(); - unsafe { - env.set_int_array_region( - fb, - 0, - std::mem::transmute::<&[u32], &[i32]>(self.gba.get_frame_buffer()), - ) - .unwrap(); - } - self.resume(); - - fb - } - - pub fn pause(&mut self) { - *self.emustate.lock().unwrap() = EmulationState::Pausing; - while *self.emustate.lock().unwrap() != EmulationState::Paused { - info!("awaiting pause...") - } - } - - pub fn resume(&mut self) { - *self.emustate.lock().unwrap() = EmulationState::Running(false); - } - - pub fn set_turbo(&mut self, turbo: bool) { - *self.emustate.lock().unwrap() = EmulationState::Running(turbo); - } - - pub fn request_stop(&mut self) { - if EmulationState::Stopped != *self.emustate.lock().unwrap() { - *self.emustate.lock().unwrap() = EmulationState::Stopping; - } - } - - pub fn is_stopped(&self) -> bool { - *self.emustate.lock().unwrap() == EmulationState::Stopped - } -} +use rustboyadvance_core::prelude::*; +use rustboyadvance_core::util::audio::{AudioRingBuffer, Producer}; +// 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}; + +use jni::objects::{GlobalRef, JMethodID, JObject, JString, JValue}; +use jni::signature; +use jni::sys::{jboolean, jbyteArray, jintArray, jmethodID}; +use jni::JNIEnv; + +use crate::audio::{self, connector::AudioJNIConnector, thread::AudioThreadCommand}; + +struct Hardware { + sample_rate: i32, + audio_producer: Option>, + key_state: u16, +} + +impl AudioInterface for Hardware { + 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 + } +} + +impl InputInterface for Hardware { + fn poll(&mut self) -> u16 { + self.key_state + } +} + +struct Renderer { + renderer_ref: GlobalRef, + frame_buffer_ref: GlobalRef, + mid_render_frame: jmethodID, +} + +impl Renderer { + fn new(env: &JNIEnv, renderer_obj: JObject) -> Result { + let renderer_ref = env + .new_global_ref(renderer_obj) + .map_err(|e| format!("failed to add new global ref, error: {:?}", e))?; + + let frame_buffer = env + .new_int_array(240 * 160) + .map_err(|e| format!("failed to create framebuffer, error: {:?}", e))?; + let frame_buffer_ref = env + .new_global_ref(frame_buffer) + .map_err(|e| format!("failed to add new global ref, error: {:?}", e))?; + let renderer_klass = env + .get_object_class(renderer_ref.as_obj()) + .expect("failed to get renderer class"); + let mid_render_frame = env + .get_method_id(renderer_klass, "renderFrame", "([I)V") + .expect("failed to get methodID for renderFrame") + .into_inner(); + + Ok(Renderer { + renderer_ref, + frame_buffer_ref, + mid_render_frame, + }) + } + + #[inline] + fn render_frame(&self, env: &JNIEnv, buffer: &[u32]) { + unsafe { + env.set_int_array_region( + self.frame_buffer_ref.as_obj().into_inner(), + 0, + std::mem::transmute::<&[u32], &[i32]>(buffer), + ) + .unwrap(); + } + + env.call_method_unchecked( + self.renderer_ref.as_obj(), + JMethodID::from(self.mid_render_frame), + signature::JavaType::Primitive(signature::Primitive::Void), + &[JValue::from(self.frame_buffer_ref.as_obj())], + ) + .expect("failed to call renderFrame"); + } +} + +struct Keypad { + keypad_ref: GlobalRef, + mid_get_key_state: jmethodID, +} + +impl Keypad { + fn new(env: &JNIEnv, keypad_obj: JObject) -> Keypad { + let keypad_ref = env + .new_global_ref(keypad_obj) + .expect("failed to create keypad_ref"); + let keypad_klass = env + .get_object_class(keypad_ref.as_obj()) + .expect("failed to create keypad class"); + let mid_get_key_state = env + .get_method_id(keypad_klass, "getKeyState", "()I") + .expect("failed to get methodID for getKeyState") + .into_inner(); + + Keypad { + keypad_ref, + mid_get_key_state, + } + } + + #[inline] + fn get_key_state(&self, env: &JNIEnv) -> u16 { + match env.call_method_unchecked( + self.keypad_ref.as_obj(), + JMethodID::from(self.mid_get_key_state), + signature::JavaType::Primitive(signature::Primitive::Int), + &[], + ) { + Ok(JValue::Int(key_state)) => key_state as u16, + _ => panic!("failed to call getKeyState"), + } + } +} + +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum EmulationState { + Initial, + Pausing, + Paused, + Running(bool), + Stopping, + Stopped, +} + +impl Default for EmulationState { + fn default() -> EmulationState { + EmulationState::Initial + } +} + +pub struct EmulatorContext { + hwif: Rc>, + renderer: Renderer, + audio_player_ref: GlobalRef, + keypad: Keypad, + pub emustate: Mutex, + pub gba: GameBoyAdvance, +} + +impl EmulatorContext { + pub fn native_open_context( + env: &JNIEnv, + bios: jbyteArray, + rom: jbyteArray, + renderer_obj: JObject, + audio_player: JObject, + keypad_obj: JObject, + save_file: JString, + skip_bios: jboolean, + ) -> Result { + let bios = env + .convert_byte_array(bios) + .map_err(|e| format!("could not get bios buffer, error {}", e))? + .into_boxed_slice(); + let rom = env + .convert_byte_array(rom) + .map_err(|e| format!("could not get rom buffer, error {}", e))? + .into_boxed_slice(); + let save_file: String = env + .get_string(save_file) + .map_err(|_| String::from("could not get save path"))? + .into(); + let gamepak = GamepakBuilder::new() + .take_buffer(rom) + .save_path(&Path::new(&save_file)) + .build() + .map_err(|e| format!("failed to load rom, gba result: {:?}", e))?; + info!("Loaded ROM file {:?}", gamepak.header); + + info!("Creating renderer"); + let renderer = Renderer::new(env, renderer_obj)?; + + info!("Creating GBA Instance"); + let hw = Rc::new(RefCell::new(Hardware { + sample_rate: audio::util::get_sample_rate(env, audio_player), + audio_producer: None, + key_state: 0xffff, + })); + let mut gba = GameBoyAdvance::new(bios, gamepak, hw.clone(), hw.clone()); + if skip_bios != 0 { + info!("skipping bios"); + gba.skip_bios(); + } + + info!("creating keypad"); + 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()), + hwif: hw.clone(), + }; + Ok(context) + } + + pub fn native_open_saved_state( + env: &JNIEnv, + state: jbyteArray, + renderer_obj: JObject, + audio_player: JObject, + keypad_obj: JObject, + ) -> Result { + let state = env + .convert_byte_array(state) + .map_err(|e| format!("could not get state buffer, error {}", e))?; + + let renderer = Renderer::new(env, renderer_obj)?; + + let hw = Rc::new(RefCell::new(Hardware { + sample_rate: audio::util::get_sample_rate(env, audio_player), + audio_producer: None, + key_state: 0xffff, + })); + let gba = + GameBoyAdvance::from_saved_state(&state, hw.clone(), hw.clone()).map_err(|e| { + format!( + "failed to create GameBoyAdvance from saved state, 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()), + hwif: hw.clone(), + }) + } + + fn render_video(&mut self, env: &JNIEnv) { + self.renderer.render_frame(env, self.gba.get_frame_buffer()); + } + + /// Lock the emulation loop in order to perform updates to the struct + pub fn lock_and_get_gba(&mut self) -> (MutexGuard, &mut GameBoyAdvance) { + (self.emustate.lock().unwrap(), &mut self.gba) + } + + /// Run the emulation main loop + pub fn native_run(&mut self, env: &JNIEnv) -> Result<(), jni::errors::Error> { + const FRAME_TIME: Duration = Duration::from_nanos(1_000_000_000u64 / 60); + + // Set the state to running + *self.emustate.lock().unwrap() = EmulationState::Running(false); + + // Extract current JVM + let jvm = env.get_java_vm().unwrap(); + + // 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.hwif.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); + + info!("starting main emulation loop"); + + // let mut fps_counter = FpsCounter::default(); + + 'running: loop { + let emustate = *self.emustate.lock().unwrap(); + + let limiter = match emustate { + EmulationState::Initial => unsafe { std::hint::unreachable_unchecked() }, + EmulationState::Stopped => unsafe { std::hint::unreachable_unchecked() }, + EmulationState::Pausing => { + info!("emulation pause requested"); + *self.emustate.lock().unwrap() = EmulationState::Paused; + continue; + } + EmulationState::Paused => continue, + EmulationState::Stopping => break 'running, + EmulationState::Running(turbo) => !turbo, + }; + + let start_time = Instant::now(); + // check key state + self.hwif.borrow_mut().key_state = self.keypad.get_key_state(env); + + // run frame + self.gba.frame(); + + // render video + self.render_video(env); + + // request audio worker to render the audio now + audio_thread_tx + .send(AudioThreadCommand::RenderAudio) + .unwrap(); + + // if let Some(fps) = fps_counter.tick() { + // info!("FPS {}", fps); + // } + + if limiter { + let time_passed = start_time.elapsed(); + let delay = FRAME_TIME.checked_sub(time_passed); + match delay { + None => {} + Some(delay) => { + std::thread::sleep(delay); + } + } + } + } + + info!("stopping, terminating audio worker"); + 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(); + info!("audio worker terminated"); + + audio_connector.pause(env); + + self.hwif.borrow_mut().audio_producer = None; + + *self.emustate.lock().unwrap() = EmulationState::Stopped; + + Ok(()) + } + + pub fn native_get_framebuffer(&mut self, env: &JNIEnv) -> jintArray { + let fb = env.new_int_array(240 * 160).unwrap(); + self.pause(); + unsafe { + env.set_int_array_region( + fb, + 0, + std::mem::transmute::<&[u32], &[i32]>(self.gba.get_frame_buffer()), + ) + .unwrap(); + } + self.resume(); + + fb + } + + pub fn pause(&mut self) { + *self.emustate.lock().unwrap() = EmulationState::Pausing; + while *self.emustate.lock().unwrap() != EmulationState::Paused { + info!("awaiting pause...") + } + } + + pub fn resume(&mut self) { + *self.emustate.lock().unwrap() = EmulationState::Running(false); + } + + pub fn set_turbo(&mut self, turbo: bool) { + *self.emustate.lock().unwrap() = EmulationState::Running(turbo); + } + + pub fn request_stop(&mut self) { + if EmulationState::Stopped != *self.emustate.lock().unwrap() { + *self.emustate.lock().unwrap() = EmulationState::Stopping; + } + } + + pub fn is_stopped(&self) -> bool { + *self.emustate.lock().unwrap() == EmulationState::Stopped + } +} diff --git a/core/src/gba.rs b/core/src/gba.rs index e9caee7..735498b 100644 --- a/core/src/gba.rs +++ b/core/src/gba.rs @@ -11,13 +11,14 @@ use super::dma::DmaController; use super::gpu::*; use super::interrupt::*; use super::iodev::*; +use super::sched::{EventHandler, EventType, Scheduler, SharedScheduler}; use super::sound::SoundController; use super::sysbus::SysBus; use super::timer::Timers; -use super::{AudioInterface, InputInterface}; #[cfg(not(feature = "no_video_interface"))] use super::VideoInterface; +use super::{AudioInterface, InputInterface}; pub struct GameBoyAdvance { pub sysbus: Box, @@ -30,6 +31,8 @@ pub struct GameBoyAdvance { pub cycles_to_next_event: usize, + scheduler: SharedScheduler, + overshoot_cycles: usize, interrupt_flags: SharedInterruptFlags, } @@ -37,6 +40,7 @@ pub struct GameBoyAdvance { #[derive(Serialize, Deserialize)] struct SaveState { sysbus: Box, + scheduler: Scheduler, interrupt_flags: u16, cpu: arm7tdmi::Core, } @@ -58,8 +62,7 @@ impl GameBoyAdvance { pub fn new( bios_rom: Box<[u8]>, gamepak: Cartridge, - #[cfg(not(feature = "no_video_interface"))] - video_device: Rc>, + #[cfg(not(feature = "no_video_interface"))] video_device: Rc>, audio_device: Rc>, input_device: Rc>, ) -> GameBoyAdvance { @@ -70,12 +73,14 @@ impl GameBoyAdvance { }; let interrupt_flags = Rc::new(Cell::new(IrqBitmask(0))); + let scheduler = Scheduler::new_shared(); let intc = InterruptController::new(interrupt_flags.clone()); - let gpu = Box::new(Gpu::new(interrupt_flags.clone())); + let gpu = Box::new(Gpu::new(scheduler.clone(), interrupt_flags.clone())); let dmac = DmaController::new(interrupt_flags.clone()); let timers = Timers::new(interrupt_flags.clone()); let sound_controller = Box::new(SoundController::new( + scheduler.clone(), audio_device.borrow().get_sample_rate() as f32, )); let io = IoDevices::new(intc, gpu, dmac, timers, sound_controller); @@ -92,6 +97,8 @@ impl GameBoyAdvance { audio_device: audio_device, input_device: input_device, + scheduler: scheduler, + cycles_to_next_event: 1, overshoot_cycles: 0, interrupt_flags: interrupt_flags, @@ -104,8 +111,7 @@ impl GameBoyAdvance { pub fn from_saved_state( savestate: &[u8], - #[cfg(not(feature = "no_video_interface"))] - video_device: Rc>, + #[cfg(not(feature = "no_video_interface"))] video_device: Rc>, audio_device: Rc>, input_device: Rc>, ) -> bincode::Result { @@ -114,7 +120,10 @@ impl GameBoyAdvance { let arm7tdmi = decoded.cpu; let mut sysbus = decoded.sysbus; let interrupts = Rc::new(Cell::new(IrqBitmask(decoded.interrupt_flags))); + let scheduler = decoded.scheduler.make_shared(); + sysbus.io.gpu.set_scheduler(scheduler.clone()); + sysbus.io.sound.set_scheduler(scheduler.clone()); sysbus.io.connect_irq(interrupts.clone()); Ok(GameBoyAdvance { @@ -131,6 +140,8 @@ impl GameBoyAdvance { cycles_to_next_event: 1, overshoot_cycles: 0, + + scheduler: scheduler, }) } @@ -139,6 +150,7 @@ impl GameBoyAdvance { cpu: self.cpu.clone(), sysbus: self.sysbus.clone(), interrupt_flags: self.interrupt_flags.get().value(), + scheduler: (*self.scheduler).clone(), }; bincode::serialize(&s) @@ -149,10 +161,13 @@ impl GameBoyAdvance { self.cpu = decoded.cpu; self.sysbus = decoded.sysbus; + self.scheduler = Scheduler::make_shared(decoded.scheduler); self.interrupt_flags = Rc::new(Cell::new(IrqBitmask(decoded.interrupt_flags))); - // Redistribute shared pointer for interrupts + // Redistribute shared pointers self.sysbus.io.connect_irq(self.interrupt_flags.clone()); + self.sysbus.io.gpu.set_scheduler(self.scheduler.clone()); + self.sysbus.io.sound.set_scheduler(self.scheduler.clone()); self.cycles_to_next_event = 1; @@ -177,10 +192,12 @@ impl GameBoyAdvance { pub fn frame(&mut self) { self.key_poll(); + let mut scheduler = self.scheduler.clone(); + let mut remaining_cycles = CYCLES_FULL_REFRESH - self.overshoot_cycles; while remaining_cycles > 0 { - let cycles = self.step(); + let cycles = self.step(&mut scheduler); if remaining_cycles >= cycles { remaining_cycles -= cycles; } else { @@ -228,15 +245,15 @@ impl GameBoyAdvance { self.cpu.cycles - previous_cycles } - pub fn step(&mut self) -> usize { + pub fn step(&mut self, scheduler: &mut Scheduler) -> usize { // I hate myself for doing this, but rust left me no choice. let io = unsafe { let ptr = &mut *self.sysbus as *mut SysBus; &mut (*ptr).io as &mut IoDevices }; - let mut cycles_left = self.cycles_to_next_event; - let mut cycles_to_next_event = std::usize::MAX; + let available_cycles = self.scheduler.get_cycles_to_next_event(); + let mut cycles_left = available_cycles; let mut cycles = 0; while cycles_left > 0 { @@ -259,18 +276,9 @@ impl GameBoyAdvance { cycles_left -= _cycles; } - // update gpu & sound io.timers.update(cycles, &mut self.sysbus); - io.gpu.update( - cycles, - &mut cycles_to_next_event, - self.sysbus.as_mut(), - #[cfg(not(feature = "no_video_interface"))] - &self.video_device, - ); - io.sound - .update(cycles, &mut cycles_to_next_event, &self.audio_device); - self.cycles_to_next_event = cycles_to_next_event; + + scheduler.run(cycles, self); cycles } @@ -293,17 +301,11 @@ impl GameBoyAdvance { let cycles = self.step_cpu(io); let breakpoint = self.check_breakpoint(); - let mut _ignored = 0; - // update gpu & sound io.timers.update(cycles, &mut self.sysbus); - io.gpu.update( - cycles, - &mut _ignored, - self.sysbus.as_mut(), - #[cfg(not(feature = "no_video_interface"))] - &self.video_device, - ); - io.sound.update(cycles, &mut _ignored, &self.audio_device); + + // update gpu & sound + let mut scheduler = self.scheduler.clone(); + scheduler.run(cycles, self); breakpoint } @@ -320,6 +322,25 @@ impl GameBoyAdvance { } } +impl EventHandler for GameBoyAdvance { + fn handle_event(&mut self, event: EventType, extra_cycles: usize) { + let io = unsafe { + let ptr = &mut *self.sysbus as *mut SysBus; + &mut (*ptr).io as &mut IoDevices + }; + match event { + EventType::Gpu(event) => io.gpu.on_event( + event, + extra_cycles, + self.sysbus.as_mut(), + #[cfg(not(feature = "no_video_interface"))] + &self.video_device, + ), + EventType::Apu(event) => io.sound.on_event(event, extra_cycles, &self.audio_device), + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -337,6 +358,7 @@ mod tests { } } + #[cfg(not(feature = "no_video_interface"))] impl VideoInterface for DummyInterface {} impl AudioInterface for DummyInterface {} impl InputInterface for DummyInterface {} @@ -350,8 +372,14 @@ mod tests { .build() .unwrap(); let dummy = Rc::new(RefCell::new(DummyInterface::new())); - let mut gba = - GameBoyAdvance::new(bios, cartridge, dummy.clone(), dummy.clone(), dummy.clone()); + let mut gba = GameBoyAdvance::new( + bios, + cartridge, + #[cfg(not(feature = "no_video_interface"))] + dummy.clone(), + dummy.clone(), + dummy.clone(), + ); gba.skip_bios(); gba diff --git a/core/src/gpu/mod.rs b/core/src/gpu/mod.rs index 228b14f..09f627b 100644 --- a/core/src/gpu/mod.rs +++ b/core/src/gpu/mod.rs @@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize}; use super::bus::*; use super::dma::{DmaNotifer, TIMING_HBLANK, TIMING_VBLANK}; use super::interrupt::{self, Interrupt, InterruptConnect, SharedInterruptFlags}; +use super::sched::*; pub use super::sysbus::consts::*; use super::util::BoxedMemory; #[cfg(not(feature = "no_video_interface"))] @@ -183,6 +184,11 @@ pub struct Gpu { pub state: GpuState, interrupt_flags: SharedInterruptFlags, + /// When deserializing this struct using serde, make sure to call Gpu:::set_scheduler + #[serde(skip)] + #[serde(default = "Scheduler::new_shared")] + scheduler: SharedScheduler, + /// how many cycles left until next gpu state ? cycles_left_for_current_state: usize, @@ -224,9 +230,11 @@ impl InterruptConnect for Gpu { } impl Gpu { - pub fn new(interrupt_flags: SharedInterruptFlags) -> Gpu { + pub fn new(mut scheduler: SharedScheduler, interrupt_flags: SharedInterruptFlags) -> Gpu { + scheduler.add_gpu_event(GpuEvent::HDraw, CYCLES_HDRAW); Gpu { interrupt_flags, + scheduler, dispcnt: DisplayControl(0x80), dispstat: DisplayStatus(0), backgrounds: [ @@ -261,6 +269,10 @@ impl Gpu { } } + pub fn set_scheduler(&mut self, scheduler: SharedScheduler) { + self.scheduler = scheduler; + } + pub fn write_dispcnt(&mut self, value: u16) { let new_dispcnt = DisplayControl(value); let old_mode = self.dispcnt.mode(); @@ -414,6 +426,93 @@ impl Gpu { &self.frame_buffer } + #[inline] + fn update_vcount(&mut self, value: usize) { + self.vcount = value; + let vcount_setting = self.dispstat.vcount_setting(); + self.dispstat + .set_vcount_flag(vcount_setting == self.vcount as u16); + + if self.dispstat.vcount_irq_enable() && self.dispstat.get_vcount_flag() { + interrupt::signal_irq(&self.interrupt_flags, Interrupt::LCD_VCounterMatch); + } + } + + #[inline] + fn handle_hdraw(&mut self, dma_notifier: &mut D) -> (GpuEvent, usize) { + self.dispstat.set_hblank_flag(true); + if self.dispstat.hblank_irq_enable() { + interrupt::signal_irq(&self.interrupt_flags, Interrupt::LCD_HBlank); + }; + dma_notifier.notify(TIMING_HBLANK); + + // Next event + (GpuEvent::HBlank, CYCLES_HBLANK) + } + + fn handle_hblank( + &mut self, + dma_notifier: &mut D, + #[cfg(not(feature = "no_video_interface"))] video_device: &VideoDeviceRcRefCell, + ) -> (GpuEvent, usize) { + self.update_vcount(self.vcount + 1); + + if self.vcount < DISPLAY_HEIGHT { + self.dispstat.set_hblank_flag(false); + self.render_scanline(); + // update BG2/3 reference points on the end of a scanline + for i in 0..2 { + self.bg_aff[i].internal_x += self.bg_aff[i].pb as i16 as i32; + self.bg_aff[i].internal_y += self.bg_aff[i].pd as i16 as i32; + } + + (GpuEvent::HDraw, CYCLES_HDRAW) + } else { + // latch BG2/3 reference points on vblank + for i in 0..2 { + self.bg_aff[i].internal_x = self.bg_aff[i].x; + self.bg_aff[i].internal_y = self.bg_aff[i].y; + } + + self.dispstat.set_vblank_flag(true); + self.dispstat.set_hblank_flag(false); + if self.dispstat.vblank_irq_enable() { + interrupt::signal_irq(&self.interrupt_flags, Interrupt::LCD_VBlank); + }; + + dma_notifier.notify(TIMING_VBLANK); + + #[cfg(not(feature = "no_video_interface"))] + video_device.borrow_mut().render(&self.frame_buffer); + + self.obj_buffer_reset(); + + (GpuEvent::VBlankHDraw, CYCLES_HDRAW) + } + } + + fn handle_vblank_hdraw(&mut self) -> (GpuEvent, usize) { + self.dispstat.set_hblank_flag(true); + if self.dispstat.hblank_irq_enable() { + interrupt::signal_irq(&self.interrupt_flags, Interrupt::LCD_HBlank); + }; + (GpuEvent::VBlankHBlank, CYCLES_HBLANK) + } + + fn handle_vblank_hblank(&mut self) -> (GpuEvent, usize) { + if self.vcount < DISPLAY_HEIGHT + VBLANK_LINES - 1 { + self.update_vcount(self.vcount + 1); + self.dispstat.set_hblank_flag(false); + (GpuEvent::VBlankHDraw, CYCLES_HDRAW) + } else { + self.update_vcount(0); + self.dispstat.set_vblank_flag(false); + self.dispstat.set_hblank_flag(false); + self.render_scanline(); + (GpuEvent::HDraw, CYCLES_HDRAW) + } + } + pub fn on_state_completed( &mut self, completed: GpuState, @@ -440,12 +539,8 @@ impl Gpu { // Transition to HBlank self.state = HBlank; self.cycles_left_for_current_state = CYCLES_HBLANK; - self.dispstat.set_hblank_flag(true); - if self.dispstat.hblank_irq_enable() { - interrupt::signal_irq(&self.interrupt_flags, Interrupt::LCD_HBlank); - }; - dma_notifier.notify(TIMING_HBLANK); + self.handle_hdraw(dma_notifier); } HBlank => { update_vcount!(self.vcount + 1); @@ -538,6 +633,29 @@ impl Gpu { *cycles_to_next_event = self.cycles_left_for_current_state; } } + + pub fn on_event( + &mut self, + event: GpuEvent, + extra_cycles: usize, + dma_notifier: &mut D, + #[cfg(not(feature = "no_video_interface"))] video_device: &VideoDeviceRcRefCell, + ) where + D: DmaNotifer, + { + let (next_event, cycles) = match event { + GpuEvent::HDraw => self.handle_hdraw(dma_notifier), + GpuEvent::HBlank => self.handle_hblank( + dma_notifier, + #[cfg(not(feature = "no_video_interface"))] + video_device, + ), + GpuEvent::VBlankHDraw => self.handle_vblank_hdraw(), + GpuEvent::VBlankHBlank => self.handle_vblank_hblank(), + }; + self.scheduler + .schedule(EventType::Gpu(next_event), cycles - extra_cycles); + } } impl Bus for Gpu { @@ -625,6 +743,7 @@ mod tests { frame_counter: usize, } + #[cfg(not(feature = "no_video_interface"))] impl VideoInterface for TestVideoInterface { fn render(&mut self, _buffer: &[u32]) { self.frame_counter += 1; @@ -633,8 +752,13 @@ mod tests { #[test] fn test_gpu_state_machine() { - let mut gpu = Gpu::new(Rc::new(Cell::new(Default::default()))); + let mut gpu = Gpu::new( + Scheduler::new_shared(), + Rc::new(Cell::new(Default::default())), + ); + #[cfg(not(feature = "no_video_interface"))] let video = Rc::new(RefCell::new(TestVideoInterface::default())); + #[cfg(not(feature = "no_video_interface"))] let video_clone: VideoDeviceRcRefCell = video.clone(); let mut dma_notifier = NopDmaNotifer; let mut cycles_to_next_event = CYCLES_FULL_REFRESH; @@ -659,6 +783,7 @@ mod tests { for line in 0..160 { println!("line = {}", line); + #[cfg(not(feature = "no_video_interface"))] assert_eq!(video.borrow().frame_counter, 0); assert_eq!(gpu.vcount, line); assert_eq!(gpu.state, GpuState::HDraw); @@ -676,6 +801,7 @@ mod tests { assert_eq!(gpu.interrupt_flags.get().LCD_VCounterMatch(), false); } + #[cfg(not(feature = "no_video_interface"))] assert_eq!(video.borrow().frame_counter, 1); for line in 0..68 { @@ -694,6 +820,7 @@ mod tests { update!(CYCLES_HBLANK); } + #[cfg(not(feature = "no_video_interface"))] assert_eq!(video.borrow().frame_counter, 1); assert_eq!(total_cycles, CYCLES_FULL_REFRESH); diff --git a/core/src/lib.rs b/core/src/lib.rs index c35a956..b290e1f 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -40,6 +40,7 @@ pub mod arm7tdmi; pub mod cartridge; pub mod disass; pub mod gpu; +mod sched; pub mod sound; pub mod sysbus; pub use sysbus::SysBus; diff --git a/core/src/overrides.rs b/core/src/overrides.rs index b02a0fa..9a30bc2 100644 --- a/core/src/overrides.rs +++ b/core/src/overrides.rs @@ -1,58 +1,58 @@ -use std::collections::HashMap; -use std::convert::TryFrom; - -use yaml_rust::YamlLoader; - -use super::cartridge::BackupType; - -#[derive(Debug)] -pub struct GameOverride { - force_rtc: bool, - save_type: Option, -} - -impl GameOverride { - pub fn force_rtc(&self) -> bool { - self.force_rtc - } - pub fn save_type(&self) -> Option { - self.save_type - } -} - -lazy_static! { - static ref GAME_OVERRIDES: HashMap = { - let mut m = HashMap::new(); - - let docs = YamlLoader::load_from_str(include_str!("../overrides.yaml")) - .expect("failed to load overrides file"); - - let doc = &docs[0]; - let games = doc.as_vec().unwrap(); - - for game in games { - 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) { - Ok(x) => Some(x), - _ => panic!("{}: invalid save type {:#}", game_code, save_type), - } - } else { - None - }; - - let game_overrride = GameOverride { - force_rtc, - save_type, - }; - m.insert(game_code, game_overrride); - } - - m - }; -} - -pub fn get_game_overrides(game_code: &str) -> Option<&GameOverride> { - GAME_OVERRIDES.get(game_code) -} +use std::collections::HashMap; +use std::convert::TryFrom; + +use yaml_rust::YamlLoader; + +use super::cartridge::BackupType; + +#[derive(Debug)] +pub struct GameOverride { + force_rtc: bool, + save_type: Option, +} + +impl GameOverride { + pub fn force_rtc(&self) -> bool { + self.force_rtc + } + pub fn save_type(&self) -> Option { + self.save_type + } +} + +lazy_static! { + static ref GAME_OVERRIDES: HashMap = { + let mut m = HashMap::new(); + + let docs = YamlLoader::load_from_str(include_str!("../overrides.yaml")) + .expect("failed to load overrides file"); + + let doc = &docs[0]; + let games = doc.as_vec().unwrap(); + + for game in games { + 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) { + Ok(x) => Some(x), + _ => panic!("{}: invalid save type {:#}", game_code, save_type), + } + } else { + None + }; + + let game_overrride = GameOverride { + force_rtc, + save_type, + }; + m.insert(game_code, game_overrride); + } + + m + }; +} + +pub fn get_game_overrides(game_code: &str) -> Option<&GameOverride> { + GAME_OVERRIDES.get(game_code) +} diff --git a/core/src/sched.rs b/core/src/sched.rs new file mode 100644 index 0000000..4360bfe --- /dev/null +++ b/core/src/sched.rs @@ -0,0 +1,305 @@ +use std::cell::UnsafeCell; +use std::rc::Rc; + +use serde::{Deserialize, Serialize}; + +const NUM_EVENTS: usize = 32; + +#[repr(u32)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)] +pub enum GpuEvent { + HDraw, + HBlank, + VBlankHDraw, + VBlankHBlank, +} + +#[repr(u32)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)] +pub enum ApuEvent { + Psg1Generate, + Psg2Generate, + Psg3Generate, + Psg4Generate, + Sample, +} + +#[repr(u32)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)] +pub enum EventType { + Gpu(GpuEvent), + Apu(ApuEvent), +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct Event { + typ: EventType, + /// Timestamp in cycles + time: usize, +} + +impl Event { + fn new(typ: EventType, time: usize) -> Event { + Event { typ, time } + } + + fn get_type(&self) -> EventType { + self.typ + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Scheduler { + timestamp: usize, + events: Vec, +} + +// Opt-out of runtime borrow checking by using unsafe cell +// SAFETY: We need to make sure that the scheduler event queue is not modified while iterating it. +#[repr(transparent)] +#[derive(Debug)] +pub struct SharedScheduler(Rc>); + +impl std::ops::Deref for SharedScheduler { + type Target = Scheduler; + + fn deref(&self) -> &Self::Target { + unsafe { &(*self.0.get()) } + } +} + +impl std::ops::DerefMut for SharedScheduler { + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { &mut (*self.0.get()) } + } +} + +impl Clone for SharedScheduler { + fn clone(&self) -> SharedScheduler { + SharedScheduler(self.0.clone()) + } +} + +pub trait EventHandler { + /// Handle the scheduler event + fn handle_event(&mut self, e: EventType, extra_cycles: usize); +} + +impl Scheduler { + pub fn new_shared() -> SharedScheduler { + let sched = Scheduler { + timestamp: 0, + events: Vec::with_capacity(NUM_EVENTS), + }; + SharedScheduler(Rc::new(UnsafeCell::new(sched))) + } + + pub fn make_shared(self) -> SharedScheduler { + SharedScheduler(Rc::new(UnsafeCell::new(self))) + } + + pub fn schedule(&mut self, typ: EventType, cycles: usize) { + let event = Event::new(typ, self.timestamp + cycles); + let idx = self + .events + .binary_search_by(|e| e.time.cmp(&event.time)) + .unwrap_or_else(|x| x); + self.events.insert(idx, event); + } + + pub fn add_gpu_event(&mut self, e: GpuEvent, cycles: usize) { + self.schedule(EventType::Gpu(e), cycles); + } + + pub fn add_apu_event(&mut self, e: ApuEvent, cycles: usize) { + self.schedule(EventType::Apu(e), cycles); + } + + pub fn run(&mut self, cycles: usize, handler: &mut H) { + let run_to = self.timestamp + cycles; + self.timestamp = run_to; + + while self.events.len() > 0 { + if run_to >= self.events[0].time { + let event = self.events.remove(0); + handler.handle_event(event.get_type(), run_to - event.time); + } else { + return; + } + } + } + + pub fn get_cycles_to_next_event(&self) -> usize { + assert_ne!(self.events.len(), 0); + self.events[0].time - self.timestamp + } + + #[allow(unused)] + fn is_empty(&self) -> bool { + self.events.is_empty() + } +} + +#[cfg(test)] +mod test { + + use super::*; + + /// Some usecase example where a struct holds the scheduler + struct Holder { + sched: SharedScheduler, + event_bitmask: u32, + } + + const BIT_GPU_VBLANKHDRAW: u32 = 1 << 0; + const BIT_APU_PSG1GENERATE: u32 = 1 << 1; + const BIT_APU_PSG2GENERATE: u32 = 1 << 2; + const BIT_APU_PSG3GENERATE: u32 = 1 << 3; + const BIT_APU_PSG4GENERATE: u32 = 1 << 4; + const BIT_APU_SAMPLE: u32 = 1 << 5; + + #[inline] + fn get_event_bit(e: EventType) -> u32 { + match e { + EventType::Gpu(GpuEvent::VBlankHDraw) => BIT_GPU_VBLANKHDRAW, + EventType::Apu(ApuEvent::Psg1Generate) => BIT_APU_PSG1GENERATE, + EventType::Apu(ApuEvent::Psg2Generate) => BIT_APU_PSG2GENERATE, + EventType::Apu(ApuEvent::Psg3Generate) => BIT_APU_PSG3GENERATE, + EventType::Apu(ApuEvent::Psg4Generate) => BIT_APU_PSG4GENERATE, + EventType::Apu(ApuEvent::Sample) => BIT_APU_SAMPLE, + _ => unimplemented!("unsupported event for this test"), + } + } + + impl Holder { + fn new() -> Holder { + Holder { + sched: Scheduler::new_shared(), + event_bitmask: 0, + } + } + + fn is_event_done(&self, e: EventType) -> bool { + (self.event_bitmask & get_event_bit(e)) != 0 + } + } + + impl EventHandler for Holder { + fn handle_event(&mut self, e: EventType, extra_cycles: usize) { + println!("[holder] got event {:?} extra_cycles {}", e, extra_cycles); + self.event_bitmask |= get_event_bit(e); + } + } + + #[test] + fn test_scheduler() { + let mut holder = Holder::new(); + + // clone the sched so we get a reference that is not owned by the holder + // SAFETY: since the SharedScheduler is built upon an UnsafeCell instead of RefCell, we are sacrificing runtime safety checks for performance. + // It is safe since the events iteration allows the EventHandler to modify the queue. + + let mut sched = holder.sched.clone(); + holder + .sched + .schedule(EventType::Gpu(GpuEvent::VBlankHDraw), 240); + holder + .sched + .schedule(EventType::Apu(ApuEvent::Psg1Generate), 60); + holder.sched.schedule(EventType::Apu(ApuEvent::Sample), 512); + holder + .sched + .schedule(EventType::Apu(ApuEvent::Psg2Generate), 13); + holder + .sched + .schedule(EventType::Apu(ApuEvent::Psg4Generate), 72); + + println!("all events"); + for e in sched.events.iter() { + let typ = e.get_type(); + println!("{:?}", typ); + } + + macro_rules! run_for { + ($cycles:expr) => { + println!("running the scheduler for {} cycles", $cycles); + sched.run($cycles, &mut holder); + if (!sched.is_empty()) { + println!( + "cycles for next event: {}", + sched.get_cycles_to_next_event() + ); + } + }; + } + + run_for!(100); + + assert_eq!( + holder.is_event_done(EventType::Apu(ApuEvent::Psg1Generate)), + true + ); + assert_eq!( + holder.is_event_done(EventType::Apu(ApuEvent::Psg2Generate)), + true + ); + assert_eq!( + holder.is_event_done(EventType::Apu(ApuEvent::Psg4Generate)), + true + ); + assert_eq!( + holder.is_event_done(EventType::Apu(ApuEvent::Sample)), + false + ); + assert_eq!( + holder.is_event_done(EventType::Gpu(GpuEvent::VBlankHDraw)), + false + ); + + run_for!(100); + + assert_eq!( + holder.is_event_done(EventType::Gpu(GpuEvent::VBlankHDraw)), + false + ); + assert_eq!( + holder.is_event_done(EventType::Apu(ApuEvent::Sample)), + false + ); + + run_for!(100); + + assert_eq!( + holder.is_event_done(EventType::Gpu(GpuEvent::VBlankHDraw)), + true + ); + assert_eq!( + holder.is_event_done(EventType::Apu(ApuEvent::Sample)), + false + ); + + run_for!(211); + + assert_eq!( + holder.is_event_done(EventType::Apu(ApuEvent::Sample)), + false + ); + + run_for!(1); + + assert_eq!(holder.is_event_done(EventType::Apu(ApuEvent::Sample)), true); + + println!("all events (holder)"); + for e in holder.sched.events.iter() { + let typ = e.get_type(); + println!("{:?}", typ); + } + + println!("all events (cloned again)"); + let sched_cloned = holder.sched.clone(); + for e in sched_cloned.events.iter() { + let typ = e.get_type(); + println!("{:?}", typ); + } + } +} diff --git a/core/src/sound/mod.rs b/core/src/sound/mod.rs index 01176b1..0472b2e 100644 --- a/core/src/sound/mod.rs +++ b/core/src/sound/mod.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; use super::dma::DmaController; use super::iodev::consts::*; +use super::sched::*; use crate::{AudioInterface, StereoSample}; @@ -62,6 +63,10 @@ type AudioDeviceRcRefCell = Rc>; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct SoundController { + #[serde(skip)] + #[serde(default = "Scheduler::new_shared")] + scheduler: SharedScheduler, + cycles: usize, // cycles count when we last provided a new sample. mse: bool, @@ -101,9 +106,13 @@ pub struct SoundController { } impl SoundController { - pub fn new(audio_device_sample_rate: f32) -> SoundController { + pub fn new(mut scheduler: SharedScheduler, audio_device_sample_rate: f32) -> SoundController { let resampler = CosineResampler::new(32768_f32, audio_device_sample_rate); + let cycles_per_sample = 512; + scheduler.schedule(EventType::Apu(ApuEvent::Sample), cycles_per_sample); SoundController { + scheduler, + cycles_per_sample, cycles: 0, mse: false, left_volume: 0, @@ -127,7 +136,6 @@ impl SoundController { sqr1_cur_vol: 0, sound_bias: 0x200, sample_rate: 32_768f32, - cycles_per_sample: 512, dma_sound: [Default::default(), Default::default()], resampler: resampler, @@ -135,6 +143,10 @@ impl SoundController { } } + pub fn set_scheduler(&mut self, scheduler: SharedScheduler) { + self.scheduler = scheduler; + } + pub fn handle_read(&self, io_addr: u32) -> u16 { let value = match io_addr { REG_SOUNDCNT_X => cbit(7, self.mse), @@ -277,6 +289,11 @@ impl SoundController { self.resampler.in_freq = self.sample_rate; } self.cycles_per_sample = 512 >> resolution; + info!( + "bias - setting sample frequency to {}hz", + self.cycles_per_sample + ); + // TODO this will not affect the currently scheduled sample event } _ => { @@ -317,46 +334,47 @@ impl SoundController { } } - pub fn update( - &mut self, - cycles: usize, - cycles_to_next_event: &mut usize, - audio_device: &AudioDeviceRcRefCell, - ) { - self.cycles += cycles; - while self.cycles >= self.cycles_per_sample { - self.cycles -= self.cycles_per_sample; + #[inline] + fn on_sample(&mut self, extra_cycles: usize, audio_device: &AudioDeviceRcRefCell) { + let mut sample = [0f32; 2]; - // time to push a new sample! - - let mut sample = [0f32; 2]; - - for channel in 0..=1 { - let mut dma_sample = 0; - for dma in &mut self.dma_sound { - if dma.is_stereo_channel_enabled(channel) { - let value = dma.value as i16; - dma_sample += value * (2 << dma.volume_shift); - } + for channel in 0..=1 { + let mut dma_sample = 0; + for dma in &mut self.dma_sound { + if dma.is_stereo_channel_enabled(channel) { + let value = dma.value as i16; + dma_sample += value * (2 << dma.volume_shift); } - - apply_bias(&mut dma_sample, self.sound_bias.bit_range(0..10) as i16); - sample[channel] = dma_sample as i32 as f32; } - let stereo_sample = (sample[0], sample[1]); - self.resampler.feed(stereo_sample, &mut self.output_buffer); - - let mut audio = audio_device.borrow_mut(); - self.output_buffer.drain(..).for_each(|(left, right)| { - audio.push_sample(&[ - (left.round() as i16) * (std::i16::MAX / 512), - (right.round() as i16) * (std::i16::MAX / 512), - ]); - }); + apply_bias(&mut dma_sample, self.sound_bias.bit_range(0..10) as i16); + sample[channel] = dma_sample as i32 as f32; } - if self.cycles_per_sample < *cycles_to_next_event { - *cycles_to_next_event = self.cycles_per_sample; + + let stereo_sample = (sample[0], sample[1]); + self.resampler.feed(stereo_sample, &mut self.output_buffer); + + let mut audio = audio_device.borrow_mut(); + self.output_buffer.drain(..).for_each(|(left, right)| { + audio.push_sample(&[ + (left.round() as i16) * (std::i16::MAX / 512), + (right.round() as i16) * (std::i16::MAX / 512), + ]); + }); + + self.scheduler + .add_apu_event(ApuEvent::Sample, self.cycles_per_sample - extra_cycles); + } + + pub fn on_event( + &mut self, + event: ApuEvent, + extra_cycles: usize, + audio_device: &AudioDeviceRcRefCell, + ) { + match event { + ApuEvent::Sample => self.on_sample(extra_cycles, audio_device), + _ => debug!("got {:?} event", event), } } }