platform/android: Big re-write of native interface
Mainly convert mainloop and audio thread into native code for performance increase. (Calling into JNI every frame was costy) The code was cleaned up quite a bit, but I may have introduced new bugs in this process :< Former-commit-id: fdbc21b5ab39f3d2e36647fd1177dc9a84a16980 Former-commit-id: ac765dbee8c994e1b69cc694846511837c2685b9
This commit is contained in:
parent
08a7cd966a
commit
ba2eff82ac
952
Cargo.lock
generated
952
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -10,8 +10,8 @@ publish = false
|
|||
crate-type = ["staticlib", "cdylib"]
|
||||
|
||||
[dependencies]
|
||||
rustboyadvance-core = {path = "../../core/"}
|
||||
jni = { version = "0.16", default-features = false }
|
||||
rustboyadvance-core = {path = "../../core/", features = ["arm7tdmi_dispatch_table", "no_video_interface"]}
|
||||
jni = "0.17.0"
|
||||
log = {version = "0.4.8", features = ["release_max_level_info", "max_level_debug"]}
|
||||
|
||||
[target.'cfg(target_os="android")'.dependencies]
|
||||
|
|
129
bindings/rustboyadvance-jni/src/audio/connector.rs
Normal file
129
bindings/rustboyadvance-jni/src/audio/connector.rs
Normal file
|
@ -0,0 +1,129 @@
|
|||
use jni::objects::{GlobalRef, JMethodID, JObject, JValue};
|
||||
use jni::signature::{JavaType, Primitive};
|
||||
use jni::sys::{jlong, jmethodID};
|
||||
use jni::JNIEnv;
|
||||
|
||||
pub struct AudioJNIConnector {
|
||||
pub audio_player_ref: GlobalRef,
|
||||
pub audio_buffer_ref: GlobalRef,
|
||||
|
||||
/// jmethodID is safe to pass between threads but the jni-sys crate marked them as !Send
|
||||
/// TODO send patch to jni-sys
|
||||
mid_audio_write: jlong,
|
||||
mid_audio_play: jlong,
|
||||
mid_audio_pause: jlong,
|
||||
|
||||
pub sample_rate: i32,
|
||||
pub sample_count: usize,
|
||||
}
|
||||
|
||||
impl AudioJNIConnector {
|
||||
pub fn new(env: &JNIEnv, audio_player: JObject) -> AudioJNIConnector {
|
||||
let audio_player_ref = env.new_global_ref(audio_player).unwrap();
|
||||
let audio_player_klass = env.get_object_class(audio_player_ref.as_obj()).unwrap();
|
||||
|
||||
let mid_audio_write = env
|
||||
.get_method_id(audio_player_klass, "audioWrite", "([SII)I")
|
||||
.expect("failed to get methodID for audioWrite")
|
||||
.into_inner() as jlong;
|
||||
let mid_audio_play = env
|
||||
.get_method_id(audio_player_klass, "play", "()V")
|
||||
.expect("failed to get methodID for audioPlay")
|
||||
.into_inner() as jlong;
|
||||
let mid_audio_pause = env
|
||||
.get_method_id(audio_player_klass, "pause", "()V")
|
||||
.expect("failed to get methodID for audioPause")
|
||||
.into_inner() as jlong;
|
||||
|
||||
let mid_get_sample_rate = env
|
||||
.get_method_id(audio_player_klass, "getSampleRate", "()I")
|
||||
.expect("failed to get methodID for getSampleRate");
|
||||
let mid_get_sample_count = env
|
||||
.get_method_id(audio_player_klass, "getSampleCount", "()I")
|
||||
.expect("failed to get methodID for getSampleCount");
|
||||
|
||||
let result = env
|
||||
.call_method_unchecked(
|
||||
audio_player_ref.as_obj(),
|
||||
mid_get_sample_count,
|
||||
JavaType::Primitive(Primitive::Int),
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sample_count = match result {
|
||||
JValue::Int(sample_count) => sample_count as usize,
|
||||
_ => panic!("bad return value"),
|
||||
};
|
||||
|
||||
let result = env
|
||||
.call_method_unchecked(
|
||||
audio_player_ref.as_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"),
|
||||
};
|
||||
|
||||
let audio_buffer = env
|
||||
.new_short_array(sample_count as i32)
|
||||
.expect("failed to create sound buffer");
|
||||
let audio_buffer_ref = env.new_global_ref(audio_buffer).unwrap();
|
||||
|
||||
// Don't need this ref anymore
|
||||
drop(audio_player_klass);
|
||||
|
||||
AudioJNIConnector {
|
||||
audio_player_ref,
|
||||
audio_buffer_ref,
|
||||
mid_audio_pause,
|
||||
mid_audio_play,
|
||||
mid_audio_write,
|
||||
sample_rate,
|
||||
sample_count,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn pause(&self, env: &JNIEnv) {
|
||||
// TODO handle errors
|
||||
let _ = env.call_method_unchecked(
|
||||
self.audio_player_ref.as_obj(),
|
||||
JMethodID::from(self.mid_audio_pause as jmethodID),
|
||||
JavaType::Primitive(Primitive::Void),
|
||||
&[],
|
||||
);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn play(&self, env: &JNIEnv) {
|
||||
// TODO handle errors
|
||||
let _ = env.call_method_unchecked(
|
||||
self.audio_player_ref.as_obj(),
|
||||
JMethodID::from(self.mid_audio_play as jmethodID),
|
||||
JavaType::Primitive(Primitive::Void),
|
||||
&[],
|
||||
);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn write_audio_samples(&self, env: &JNIEnv, samples: &[i16]) {
|
||||
// TODO handle errors
|
||||
env.set_short_array_region(self.audio_buffer_ref.as_obj().into_inner(), 0, &samples)
|
||||
.unwrap();
|
||||
let _ = env.call_method_unchecked(
|
||||
self.audio_player_ref.as_obj(),
|
||||
JMethodID::from(self.mid_audio_write as jmethodID),
|
||||
JavaType::Primitive(Primitive::Int),
|
||||
&[
|
||||
JValue::from(self.audio_buffer_ref.as_obj()),
|
||||
JValue::Int(0), // offset_in_shorts
|
||||
JValue::Int(samples.len() as i32), // size_in_shorts
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
29
bindings/rustboyadvance-jni/src/audio/mod.rs
Normal file
29
bindings/rustboyadvance-jni/src/audio/mod.rs
Normal file
|
@ -0,0 +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;
|
||||
}
|
||||
}
|
66
bindings/rustboyadvance-jni/src/audio/thread.rs
Normal file
66
bindings/rustboyadvance-jni/src/audio/thread.rs
Normal file
|
@ -0,0 +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<i16>,
|
||||
) -> (JoinHandle<AudioJNIConnector>, Sender<AudioThreadCommand>) {
|
||||
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)
|
||||
}
|
403
bindings/rustboyadvance-jni/src/emulator.rs
Normal file
403
bindings/rustboyadvance-jni/src/emulator.rs
Normal file
|
@ -0,0 +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<Producer<i16>>,
|
||||
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<Renderer, String> {
|
||||
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<RefCell<Hardware>>,
|
||||
renderer: Renderer,
|
||||
audio_player_ref: GlobalRef,
|
||||
keypad: Keypad,
|
||||
pub emustate: Mutex<EmulationState>,
|
||||
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<EmulatorContext, String> {
|
||||
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<EmulatorContext, String> {
|
||||
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<EmulationState>, &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
|
||||
}
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
mod audio;
|
||||
mod emulator;
|
||||
/// JNI Bindings for rustboyadvance
|
||||
///
|
||||
mod rom_helper;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use emulator::EmulatorContext;
|
||||
|
||||
use std::os::raw::c_void;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
use jni::objects::*;
|
||||
use jni::sys::*;
|
||||
|
@ -21,112 +21,12 @@ use android_log;
|
|||
use env_logger;
|
||||
|
||||
use rustboyadvance_core::prelude::*;
|
||||
use rustboyadvance_core::util::audio::AudioRingBuffer;
|
||||
use rustboyadvance_core::StereoSample;
|
||||
|
||||
struct Hardware {
|
||||
jvm: JavaVM,
|
||||
frame_buffer_global_ref: GlobalRef,
|
||||
audio_buffer: AudioRingBuffer,
|
||||
key_state: u16,
|
||||
}
|
||||
|
||||
impl VideoInterface for Hardware {
|
||||
fn render(&mut self, buffer: &[u32]) {
|
||||
let env = self.jvm.get_env().unwrap();
|
||||
unsafe {
|
||||
env.set_int_array_region(
|
||||
self.frame_buffer_global_ref.as_obj().into_inner(),
|
||||
0,
|
||||
std::mem::transmute::<&[u32], &[i32]>(buffer),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
impl AudioInterface for Hardware {
|
||||
fn push_sample(&mut self, sample: StereoSample<i16>) {
|
||||
if self.audio_buffer.prod.push(sample.0).is_err() {
|
||||
warn!("failed to push audio sample");
|
||||
}
|
||||
if self.audio_buffer.prod.push(sample.1).is_err() {
|
||||
warn!("failed to push audio sample");
|
||||
}
|
||||
}
|
||||
}
|
||||
impl InputInterface for Hardware {
|
||||
fn poll(&mut self) -> u16 {
|
||||
self.key_state
|
||||
}
|
||||
}
|
||||
|
||||
struct Context {
|
||||
hwif: Rc<RefCell<Hardware>>,
|
||||
gba: GameBoyAdvance,
|
||||
}
|
||||
|
||||
static mut DID_LOAD: bool = false;
|
||||
|
||||
const NATIVE_EXCEPTION_CLASS: &'static str =
|
||||
"com/mrmichel/rustboyadvance/EmulatorBindings/NativeBindingException";
|
||||
|
||||
unsafe fn internal_open_context(
|
||||
env: &JNIEnv,
|
||||
bios: jbyteArray,
|
||||
rom: jbyteArray,
|
||||
frame_buffer: jintArray,
|
||||
save_file: JString,
|
||||
skip_bios: jboolean,
|
||||
) -> Result<Context, String> {
|
||||
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);
|
||||
|
||||
let frame_buffer_global_ref = env
|
||||
.new_global_ref(JObject::from(frame_buffer))
|
||||
.map_err(|e| format!("failed to add new global ref, error: {:?}", e))?;
|
||||
|
||||
let hw = Hardware {
|
||||
jvm: env.get_java_vm().unwrap(),
|
||||
frame_buffer_global_ref: frame_buffer_global_ref,
|
||||
audio_buffer: AudioRingBuffer::new(),
|
||||
key_state: 0xffff,
|
||||
};
|
||||
let hw = Rc::new(RefCell::new(hw));
|
||||
|
||||
let mut gba = GameBoyAdvance::new(bios, gamepak, hw.clone(), hw.clone(), hw.clone());
|
||||
|
||||
if skip_bios != 0 {
|
||||
debug!("skipping bios");
|
||||
gba.skip_bios();
|
||||
}
|
||||
|
||||
debug!("creating context");
|
||||
let context = Context {
|
||||
gba: gba,
|
||||
hwif: hw.clone(),
|
||||
};
|
||||
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
fn save_state(env: &JNIEnv, gba: &mut GameBoyAdvance) -> Result<jbyteArray, String> {
|
||||
let saved_state = gba
|
||||
.save_state()
|
||||
|
@ -149,8 +49,9 @@ fn load_state(env: &JNIEnv, gba: &mut GameBoyAdvance, state: jbyteArray) -> Resu
|
|||
pub mod bindings {
|
||||
use super::*;
|
||||
|
||||
unsafe fn lock_ctx<'a>(ctx: jlong) -> MutexGuard<'a, Context> {
|
||||
(*(ctx as *mut Mutex<Context>)).lock().unwrap()
|
||||
#[inline(always)]
|
||||
unsafe fn cast_ctx<'a>(ctx: jlong) -> &'a mut EmulatorContext {
|
||||
&mut (*(ctx as *mut EmulatorContext))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
@ -177,12 +78,23 @@ pub mod bindings {
|
|||
_obj: JClass,
|
||||
bios: jbyteArray,
|
||||
rom: jbyteArray,
|
||||
frame_buffer: jintArray,
|
||||
renderer_obj: JObject,
|
||||
audio_player_obj: JObject,
|
||||
keypad_obj: JObject,
|
||||
save_file: JString,
|
||||
skip_bios: jboolean,
|
||||
) -> jlong {
|
||||
match internal_open_context(&env, bios, rom, frame_buffer, save_file, skip_bios) {
|
||||
Ok(ctx) => Box::into_raw(Box::new(Mutex::new(ctx))) as jlong,
|
||||
match EmulatorContext::native_open_context(
|
||||
&env,
|
||||
bios,
|
||||
rom,
|
||||
renderer_obj,
|
||||
audio_player_obj,
|
||||
keypad_obj,
|
||||
save_file,
|
||||
skip_bios,
|
||||
) {
|
||||
Ok(ctx) => Box::into_raw(Box::new(ctx)) as jlong,
|
||||
Err(msg) => {
|
||||
env.throw_new(NATIVE_EXCEPTION_CLASS, msg).unwrap();
|
||||
-1
|
||||
|
@ -190,50 +102,23 @@ pub mod bindings {
|
|||
}
|
||||
}
|
||||
|
||||
fn internal_open_saved_state(
|
||||
env: &JNIEnv,
|
||||
state: jbyteArray,
|
||||
frame_buffer: jintArray,
|
||||
) -> Result<Context, String> {
|
||||
let state = env
|
||||
.convert_byte_array(state)
|
||||
.map_err(|e| format!("could not get state buffer, error {}", e))?;
|
||||
|
||||
let frame_buffer_global_ref = env
|
||||
.new_global_ref(JObject::from(frame_buffer))
|
||||
.map_err(|e| format!("failed to add new global ref, error: {:?}", e))?;
|
||||
|
||||
let hw = Hardware {
|
||||
jvm: env.get_java_vm().unwrap(),
|
||||
frame_buffer_global_ref: frame_buffer_global_ref,
|
||||
audio_buffer: AudioRingBuffer::new(),
|
||||
key_state: 0xffff,
|
||||
};
|
||||
let hw = Rc::new(RefCell::new(hw));
|
||||
|
||||
let gba = GameBoyAdvance::from_saved_state(&state, hw.clone(), hw.clone(), hw.clone())
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"failed to create GameBoyAdvance from saved state, error {:?}",
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Context {
|
||||
gba: gba,
|
||||
hwif: hw.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_EmulatorBindings_openSavedState(
|
||||
env: JNIEnv,
|
||||
_obj: JClass,
|
||||
state: jbyteArray,
|
||||
frame_buffer: jintArray,
|
||||
renderer_obj: JObject,
|
||||
audio_player_obj: JObject,
|
||||
keypad_obj: JObject,
|
||||
) -> jlong {
|
||||
match internal_open_saved_state(&env, state, frame_buffer) {
|
||||
Ok(ctx) => Box::into_raw(Box::new(Mutex::new(ctx))) as jlong,
|
||||
match EmulatorContext::native_open_saved_state(
|
||||
&env,
|
||||
state,
|
||||
renderer_obj,
|
||||
audio_player_obj,
|
||||
keypad_obj,
|
||||
) {
|
||||
Ok(ctx) => Box::into_raw(Box::new(ctx)) as jlong,
|
||||
Err(msg) => {
|
||||
env.throw_new(NATIVE_EXCEPTION_CLASS, msg).unwrap();
|
||||
-1
|
||||
|
@ -243,58 +128,90 @@ pub mod bindings {
|
|||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_EmulatorBindings_closeEmulator(
|
||||
env: JNIEnv,
|
||||
_obj: JClass,
|
||||
ctx: jlong,
|
||||
) {
|
||||
info!("destroying context {:#x}", ctx);
|
||||
// consume the wrapped content
|
||||
let _ = Box::from_raw(ctx as *mut Mutex<Context>);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_EmulatorBindings_runFrame(
|
||||
env: JNIEnv,
|
||||
_obj: JClass,
|
||||
ctx: jlong,
|
||||
frame_buffer: jintArray,
|
||||
) {
|
||||
let mut ctx = lock_ctx(ctx);
|
||||
|
||||
ctx.gba.frame();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_EmulatorBindings_collectAudioSamples(
|
||||
env: JNIEnv,
|
||||
_obj: JClass,
|
||||
ctx: jlong,
|
||||
) -> jshortArray {
|
||||
let ctx = lock_ctx(ctx);
|
||||
|
||||
let mut hw = ctx.hwif.borrow_mut();
|
||||
|
||||
let mut samples = Vec::with_capacity(1024);
|
||||
|
||||
while let Some(sample) = hw.audio_buffer.cons.pop() {
|
||||
samples.push(sample);
|
||||
}
|
||||
|
||||
let arr = env.new_short_array(samples.len() as jsize).unwrap();
|
||||
env.set_short_array_region(arr, 0, &samples).unwrap();
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_EmulatorBindings_setKeyState(
|
||||
_env: JNIEnv,
|
||||
_obj: JClass,
|
||||
ctx: jlong,
|
||||
key_state: jint,
|
||||
) {
|
||||
let mut ctx = lock_ctx(ctx);
|
||||
ctx.hwif.borrow_mut().key_state = key_state as u16;
|
||||
info!("waiting for emulation thread to stop");
|
||||
|
||||
{
|
||||
let ctx = cast_ctx(ctx);
|
||||
ctx.request_stop();
|
||||
while !ctx.is_stopped() {}
|
||||
}
|
||||
|
||||
info!("destroying context {:#x}", ctx);
|
||||
// consume the wrapped content
|
||||
let _ = Box::from_raw(ctx as *mut EmulatorContext);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_EmulatorBindings_runMainLoop(
|
||||
env: JNIEnv,
|
||||
_obj: JClass,
|
||||
ctx: jlong,
|
||||
) {
|
||||
let ctx = cast_ctx(ctx);
|
||||
match ctx.native_run(&env) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
env.throw_new(NATIVE_EXCEPTION_CLASS, format!("Error: {:?}", err))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_EmulatorBindings_pause(
|
||||
_env: JNIEnv,
|
||||
_obj: JClass,
|
||||
ctx: jlong,
|
||||
) {
|
||||
let ctx = cast_ctx(ctx);
|
||||
ctx.pause();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_EmulatorBindings_resume(
|
||||
_env: JNIEnv,
|
||||
_obj: JClass,
|
||||
ctx: jlong,
|
||||
) {
|
||||
let ctx = cast_ctx(ctx);
|
||||
ctx.resume();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_EmulatorBindings_setTurbo(
|
||||
_env: JNIEnv,
|
||||
_obj: JClass,
|
||||
ctx: jlong,
|
||||
turbo: jboolean,
|
||||
) {
|
||||
info!("setTurbo called!");
|
||||
let ctx = cast_ctx(ctx);
|
||||
ctx.set_turbo(turbo != 0);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_EmulatorBindings_stop(
|
||||
_env: JNIEnv,
|
||||
_obj: JClass,
|
||||
ctx: jlong,
|
||||
) {
|
||||
let ctx = cast_ctx(ctx);
|
||||
ctx.request_stop();
|
||||
while !ctx.is_stopped() {}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_EmulatorBindings_getFrameBuffer(
|
||||
env: JNIEnv,
|
||||
_obj: JClass,
|
||||
ctx: jlong,
|
||||
) -> jintArray {
|
||||
let ctx = cast_ctx(ctx);
|
||||
ctx.native_get_framebuffer(&env)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
@ -303,9 +220,14 @@ pub mod bindings {
|
|||
_obj: JClass,
|
||||
ctx: jlong,
|
||||
) -> jbyteArray {
|
||||
let mut ctx = lock_ctx(ctx);
|
||||
match save_state(&env, &mut ctx.gba) {
|
||||
let ctx = cast_ctx(ctx);
|
||||
ctx.pause();
|
||||
let (_lock, gba) = ctx.lock_and_get_gba();
|
||||
match save_state(&env, gba) {
|
||||
Ok(result) => {
|
||||
drop(_lock);
|
||||
drop(gba);
|
||||
ctx.resume();
|
||||
return result;
|
||||
}
|
||||
Err(msg) => {
|
||||
|
@ -322,9 +244,15 @@ pub mod bindings {
|
|||
ctx: jlong,
|
||||
state: jbyteArray,
|
||||
) {
|
||||
let mut ctx = lock_ctx(ctx);
|
||||
match load_state(&env, &mut ctx.gba, state) {
|
||||
Ok(_) => {}
|
||||
let ctx = cast_ctx(ctx);
|
||||
ctx.pause();
|
||||
let (_lock, gba) = ctx.lock_and_get_gba();
|
||||
match load_state(&env, gba, state) {
|
||||
Ok(_) => {
|
||||
drop(_lock);
|
||||
drop(gba);
|
||||
ctx.resume();
|
||||
}
|
||||
Err(msg) => env.throw_new(NATIVE_EXCEPTION_CLASS, msg).unwrap(),
|
||||
}
|
||||
}
|
||||
|
@ -335,7 +263,7 @@ pub mod bindings {
|
|||
_obj: JClass,
|
||||
ctx: jlong,
|
||||
) -> jstring {
|
||||
let ctx = lock_ctx(ctx);
|
||||
let ctx = cast_ctx(ctx);
|
||||
env.new_string(ctx.gba.get_game_title())
|
||||
.unwrap()
|
||||
.into_inner()
|
||||
|
@ -347,7 +275,7 @@ pub mod bindings {
|
|||
_obj: JClass,
|
||||
ctx: jlong,
|
||||
) -> jstring {
|
||||
let ctx = lock_ctx(ctx);
|
||||
let ctx = cast_ctx(ctx);
|
||||
env.new_string(ctx.gba.get_game_code())
|
||||
.unwrap()
|
||||
.into_inner()
|
||||
|
@ -359,7 +287,7 @@ pub mod bindings {
|
|||
_obj: JClass,
|
||||
ctx: jlong,
|
||||
) {
|
||||
let ctx = lock_ctx(ctx);
|
||||
let ctx = cast_ctx(ctx);
|
||||
info!("CPU LOG: {:#x?}", ctx.gba.cpu);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,9 @@ fn parse_rom_header(env: &JNIEnv, barr: jbyteArray) -> cartridge::header::Cartri
|
|||
cartridge::header::parse(&rom_data).unwrap()
|
||||
}
|
||||
|
||||
mod bindings {
|
||||
use super::*;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_RomHelper_getGameCode(
|
||||
env: JNIEnv,
|
||||
|
@ -28,3 +31,4 @@ pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_RomHelper_getGameTitle
|
|||
let header = parse_rom_header(&env, rom_data);
|
||||
env.new_string(header.game_title).unwrap().into_inner()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,7 +76,7 @@ pub trait AudioInterface {
|
|||
/// 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: StereoSample<i16>) {}
|
||||
fn push_sample(&mut self, samples: &[i16]) {}
|
||||
}
|
||||
|
||||
pub trait InputInterface {
|
||||
|
|
|
@ -349,10 +349,10 @@ impl SoundController {
|
|||
|
||||
let mut audio = audio_device.borrow_mut();
|
||||
self.output_buffer.drain(..).for_each(|(left, right)| {
|
||||
audio.push_sample((
|
||||
audio.push_sample(&[
|
||||
(left.round() as i16) * (std::i16::MAX / 512),
|
||||
(right.round() as i16) * (std::i16::MAX / 512),
|
||||
));
|
||||
]);
|
||||
});
|
||||
}
|
||||
if self.cycles_per_sample < *cycles_to_next_event {
|
||||
|
|
|
@ -132,16 +132,20 @@ macro_rules! host_breakpoint {
|
|||
}
|
||||
|
||||
pub mod audio {
|
||||
use ringbuf::{Consumer, Producer, RingBuffer};
|
||||
pub use ringbuf::{Consumer, Producer, RingBuffer};
|
||||
|
||||
pub struct AudioRingBuffer {
|
||||
pub prod: Producer<i16>,
|
||||
pub cons: Consumer<i16>,
|
||||
prod: Producer<i16>,
|
||||
cons: Consumer<i16>,
|
||||
}
|
||||
|
||||
impl AudioRingBuffer {
|
||||
pub fn new() -> AudioRingBuffer {
|
||||
let rb = RingBuffer::new(4096 * 2);
|
||||
AudioRingBuffer::new_with_capacity(2 * 4096)
|
||||
}
|
||||
|
||||
pub fn new_with_capacity(capacity: usize) -> AudioRingBuffer {
|
||||
let rb = RingBuffer::new(capacity);
|
||||
let (prod, cons) = rb.split();
|
||||
|
||||
AudioRingBuffer { prod, cons }
|
||||
|
@ -154,6 +158,10 @@ pub mod audio {
|
|||
pub fn consumer(&mut self) -> &mut Consumer<i16> {
|
||||
&mut self.cons
|
||||
}
|
||||
|
||||
pub fn split(self) -> (Producer<i16>, Consumer<i16>) {
|
||||
(self.prod, self.cons)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,62 +9,65 @@ public class EmulatorBindings {
|
|||
System.loadLibrary("rustboyadvance_jni");
|
||||
}
|
||||
|
||||
public class NativeBindingException extends Exception {
|
||||
public NativeBindingException(String errorMessage) {
|
||||
super(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a new emulator context
|
||||
*
|
||||
* @param bios bytearray of the GBA bios
|
||||
* @param rom bytearray of the rom to run
|
||||
* @param frameBuffer frameBuffer render target
|
||||
* @param renderer renderer instance
|
||||
* @param audioPlayer audio player instance
|
||||
* @param keypad Keypad instance
|
||||
* @param save_name name of the save file TODO remove this
|
||||
* @param skipBios skip bios
|
||||
* @return the emulator context to use pass to other methods in this class
|
||||
* @throws NativeBindingException
|
||||
*/
|
||||
public static native long openEmulator(byte[] bios, byte[] rom, int[] frameBuffer, String save_name, boolean skipBios) throws NativeBindingException;
|
||||
public static native long openEmulator(byte[] bios, byte[] rom, IFrameRenderer renderer, IAudioPlayer audioPlayer, Keypad keypad, String save_name, boolean skipBios) throws NativeBindingException;
|
||||
|
||||
/**
|
||||
* Open a new emulator context from a saved state buffer
|
||||
* @param savedState
|
||||
* @param frameBuffer
|
||||
*
|
||||
* @param savedState saved state buffer
|
||||
* @param renderer renderer instance
|
||||
* @param audioPlayer audio player instance
|
||||
* @param keypad Keypad instance
|
||||
* @return
|
||||
* @throws NativeBindingException
|
||||
*/
|
||||
public static native long openSavedState(byte[] savedState, int[] frameBuffer) throws NativeBindingException;
|
||||
|
||||
/**
|
||||
* Make the emulator boot directly into the cartridge
|
||||
* @param ctx
|
||||
* @throws NativeBindingException
|
||||
*/
|
||||
public static native void skipBios(long ctx) throws NativeBindingException;
|
||||
|
||||
public static native long openSavedState(byte[] savedState, IFrameRenderer renderer, IAudioPlayer audioPlayer, Keypad keypad) throws NativeBindingException;
|
||||
|
||||
/**
|
||||
* Destroys the emulator instance
|
||||
* should be put in a finalizer or else the emulator context may leak.
|
||||
*
|
||||
* @param ctx
|
||||
*/
|
||||
public static native void closeEmulator(long ctx);
|
||||
|
||||
|
||||
/**
|
||||
* Runs the emulation for a single frame.
|
||||
* Run the emulation thread
|
||||
*
|
||||
* @param ctx
|
||||
* @param frame_buffer will be filled with the frame buffer to render
|
||||
*/
|
||||
public static native void runFrame(long ctx, int[] frame_buffer);
|
||||
public static native void runMainLoop(long ctx);
|
||||
|
||||
/**
|
||||
* Collect pending audio samples
|
||||
* @param ctx
|
||||
* @return sample buffer
|
||||
*/
|
||||
public static native short[] collectAudioSamples(long ctx);
|
||||
public static native void pause(long ctx);
|
||||
|
||||
public static native void resume(long ctx);
|
||||
|
||||
public static native void setTurbo(long ctx, boolean turbo);
|
||||
|
||||
public static native void stop(long ctx);
|
||||
|
||||
|
||||
public static native int[] getFrameBuffer(long ctx);
|
||||
|
||||
// /**
|
||||
// * Runs the emulation for a single frame.
|
||||
// * @param ctx
|
||||
// * @param frame_buffer will be filled with the frame buffer to render
|
||||
// */
|
||||
// public static native void runFrame(long ctx, int[] frame_buffer);
|
||||
|
||||
/**
|
||||
* @param ctx
|
||||
|
@ -78,9 +81,9 @@ public class EmulatorBindings {
|
|||
*/
|
||||
public static native String getGameCode(long ctx);
|
||||
|
||||
|
||||
/**
|
||||
* Sets the keystate
|
||||
*
|
||||
* @param keyState
|
||||
*/
|
||||
public static native void setKeyState(long ctx, int keyState);
|
||||
|
@ -105,7 +108,14 @@ public class EmulatorBindings {
|
|||
|
||||
/**
|
||||
* Logs the emulator state
|
||||
*
|
||||
* @return non-zero value on failure
|
||||
*/
|
||||
public static native void log(long ctx);
|
||||
|
||||
public class NativeBindingException extends Exception {
|
||||
public NativeBindingException(String errorMessage) {
|
||||
super(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package com.mrmichel.rustboyadvance;
|
||||
|
||||
public interface IAudioPlayer {
|
||||
int audioWrite(short[] buffer, int offsetInShorts, int sizeInShorts);
|
||||
|
||||
void pause();
|
||||
|
||||
void play();
|
||||
|
||||
int getSampleCount();
|
||||
|
||||
int getSampleRate();
|
||||
|
||||
int availableBufferSize();
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.mrmichel.rustboyadvance;
|
||||
|
||||
public interface IFrameRenderer {
|
||||
void renderFrame(int[] framebuffer);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.mrmichel.rustdroid_emu.core;
|
||||
package com.mrmichel.rustboyadvance;
|
||||
|
||||
public class Keypad {
|
||||
private int keyState;
|
||||
|
@ -11,6 +11,18 @@ public class Keypad {
|
|||
this.keyState = 0xffff;
|
||||
}
|
||||
|
||||
public void onKeyDown(Key key) {
|
||||
this.keyState = this.keyState & ~(1 << key.keyBit);
|
||||
}
|
||||
|
||||
public void onKeyUp(Key key) {
|
||||
this.keyState = this.keyState | (1 << key.keyBit);
|
||||
}
|
||||
|
||||
public int getKeyState() {
|
||||
return keyState;
|
||||
}
|
||||
|
||||
public enum Key {
|
||||
ButtonA(0),
|
||||
ButtonB(1),
|
||||
|
@ -25,20 +37,8 @@ public class Keypad {
|
|||
|
||||
private final int keyBit;
|
||||
|
||||
private Key(int keyBit) {
|
||||
Key(int keyBit) {
|
||||
this.keyBit = keyBit;
|
||||
}
|
||||
}
|
||||
|
||||
public void onKeyDown(Key key) {
|
||||
this.keyState = this.keyState & ~(1 << key.keyBit);
|
||||
}
|
||||
|
||||
public void onKeyUp(Key key) {
|
||||
this.keyState = this.keyState | (1 << key.keyBit);
|
||||
}
|
||||
|
||||
public int getKeyState() {
|
||||
return keyState;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package com.mrmichel.rustdroid_emu.core;
|
||||
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioTrack;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import com.mrmichel.rustboyadvance.IAudioPlayer;
|
||||
|
||||
|
||||
/**
|
||||
* Simple wrapper around the android AudioTrack class that implements IAudioPlayer
|
||||
*/
|
||||
public class AndroidAudioPlayer implements IAudioPlayer {
|
||||
private static final String TAG = "AndroidAudioPlayer";
|
||||
|
||||
private static final int BUFFER_SIZE_IN_BYTES = 8192;
|
||||
private static int SAMPLE_RATE_HZ = 44100;
|
||||
|
||||
private AudioTrack audioTrack;
|
||||
|
||||
public AndroidAudioPlayer() {
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
AudioTrack.Builder audioTrackBuilder = new AudioTrack.Builder()
|
||||
.setAudioFormat(new AudioFormat.Builder()
|
||||
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
||||
.setSampleRate(SAMPLE_RATE_HZ)
|
||||
.setChannelMask(AudioFormat.CHANNEL_IN_STEREO | AudioFormat.CHANNEL_OUT_STEREO)
|
||||
.build()
|
||||
)
|
||||
.setBufferSizeInBytes(AndroidAudioPlayer.BUFFER_SIZE_IN_BYTES)
|
||||
.setTransferMode(AudioTrack.MODE_STREAM);
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
audioTrackBuilder.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY);
|
||||
}
|
||||
this.audioTrack = audioTrackBuilder.build();
|
||||
} else {
|
||||
this.audioTrack = new AudioTrack(
|
||||
AudioManager.STREAM_MUSIC,
|
||||
SAMPLE_RATE_HZ,
|
||||
AudioFormat.CHANNEL_IN_STEREO | AudioFormat.CHANNEL_OUT_STEREO,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
AndroidAudioPlayer.BUFFER_SIZE_IN_BYTES,
|
||||
AudioTrack.MODE_STREAM);
|
||||
}
|
||||
Log.d(TAG, "sampleCount = " + this.getSampleCount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int audioWrite(short[] buffer, int offsetInShorts, int sizeInShorts) {
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
return this.audioTrack.write(buffer, offsetInShorts, sizeInShorts, AudioTrack.WRITE_NON_BLOCKING);
|
||||
} else {
|
||||
// Native bindings will do its best to make sure this doesn't block anyway
|
||||
return this.audioTrack.write(buffer, offsetInShorts, sizeInShorts);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause() {
|
||||
this.audioTrack.pause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void play() {
|
||||
this.audioTrack.play();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSampleCount() {
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
return this.audioTrack.getBufferSizeInFrames();
|
||||
} else {
|
||||
return BUFFER_SIZE_IN_BYTES / 2;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSampleRate() {
|
||||
return this.audioTrack.getSampleRate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int availableBufferSize() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package com.mrmichel.rustdroid_emu.core;
|
||||
|
||||
import android.media.AudioTrack;
|
||||
|
||||
public class AudioThread extends Thread {
|
||||
|
||||
AudioTrack audioTrack;
|
||||
Emulator emulator;
|
||||
boolean enabled;
|
||||
boolean stopping;
|
||||
|
||||
public AudioThread(AudioTrack audioTrack, Emulator emulator) {
|
||||
super();
|
||||
this.audioTrack = audioTrack;
|
||||
this.emulator = emulator;
|
||||
this.enabled = true;
|
||||
this.stopping = false;
|
||||
}
|
||||
|
||||
public void setStopping(boolean stopping) {
|
||||
this.stopping = stopping;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isStopping() {
|
||||
return stopping;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
super.run();
|
||||
|
||||
while (!stopping) {
|
||||
if (enabled) {
|
||||
short[] samples = emulator.collectAudioSamples();
|
||||
audioTrack.write(samples, 0, samples.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +1,27 @@
|
|||
package com.mrmichel.rustdroid_emu.core;
|
||||
|
||||
import com.mrmichel.rustboyadvance.EmulatorBindings;
|
||||
import com.mrmichel.rustboyadvance.IFrameRenderer;
|
||||
import com.mrmichel.rustboyadvance.Keypad;
|
||||
|
||||
public class Emulator {
|
||||
|
||||
public class EmulatorException extends Exception {
|
||||
public EmulatorException(String errorMessage) {
|
||||
super(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public Keypad keypad;
|
||||
/// context received by the native binding
|
||||
private long ctx = -1;
|
||||
|
||||
private int[] frameBuffer;
|
||||
public Keypad keypad;
|
||||
|
||||
public Emulator() {
|
||||
this.frameBuffer = new int[240 * 160];
|
||||
private AndroidAudioPlayer audioPlayer;
|
||||
private IFrameRenderer frameRenderer;
|
||||
public Emulator(IFrameRenderer frameRenderer, AndroidAudioPlayer audioPlayer) {
|
||||
this.keypad = new Keypad();
|
||||
this.frameRenderer = frameRenderer;
|
||||
this.audioPlayer = audioPlayer;
|
||||
}
|
||||
|
||||
public Emulator(long ctx) {
|
||||
public Emulator(long ctx, IFrameRenderer frameRenderer, AndroidAudioPlayer audioPlayer) {
|
||||
this.ctx = ctx;
|
||||
this.frameBuffer = new int[240 * 160];
|
||||
this.frameRenderer = frameRenderer;
|
||||
this.audioPlayer = audioPlayer;
|
||||
this.keypad = new Keypad();
|
||||
|
||||
}
|
||||
|
@ -35,29 +33,38 @@ public class Emulator {
|
|||
return ctx;
|
||||
}
|
||||
|
||||
public void runMainLoop() {
|
||||
EmulatorBindings.runMainLoop(this.ctx);
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
EmulatorBindings.pause(this.ctx);
|
||||
this.audioPlayer.pause();
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
EmulatorBindings.resume(this.ctx);
|
||||
this.audioPlayer.play();
|
||||
}
|
||||
|
||||
public void setTurbo(boolean turbo) {
|
||||
EmulatorBindings.setTurbo(ctx, turbo);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
EmulatorBindings.stop(this.ctx);
|
||||
this.audioPlayer.pause();
|
||||
|
||||
}
|
||||
|
||||
public int[] getFrameBuffer() {
|
||||
return frameBuffer;
|
||||
return EmulatorBindings.getFrameBuffer(this.ctx);
|
||||
}
|
||||
|
||||
public synchronized void runFrame() {
|
||||
EmulatorBindings.setKeyState(ctx, keypad.getKeyState());
|
||||
EmulatorBindings.runFrame(ctx, frameBuffer);
|
||||
}
|
||||
|
||||
public synchronized short[] collectAudioSamples() {
|
||||
return EmulatorBindings.collectAudioSamples(ctx);
|
||||
}
|
||||
|
||||
public synchronized void setKeyState(int keyState) {
|
||||
EmulatorBindings.setKeyState(this.ctx, keyState);
|
||||
}
|
||||
|
||||
|
||||
public synchronized byte[] saveState() throws EmulatorBindings.NativeBindingException {
|
||||
return EmulatorBindings.saveState(this.ctx);
|
||||
}
|
||||
|
||||
|
||||
public synchronized void loadState(byte[] state) throws EmulatorBindings.NativeBindingException {
|
||||
if (ctx != -1) {
|
||||
EmulatorBindings.loadState(this.ctx, state);
|
||||
|
@ -66,13 +73,12 @@ public class Emulator {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public synchronized void open(byte[] bios, byte[] rom, String saveName, boolean skipBios) throws EmulatorBindings.NativeBindingException {
|
||||
this.ctx = EmulatorBindings.openEmulator(bios, rom, this.frameBuffer, saveName, skipBios);
|
||||
this.ctx = EmulatorBindings.openEmulator(bios, rom, this.frameRenderer, this.audioPlayer, this.keypad, saveName, skipBios);
|
||||
}
|
||||
|
||||
public synchronized void openSavedState(byte[] savedState) throws EmulatorBindings.NativeBindingException {
|
||||
this.ctx = EmulatorBindings.openSavedState(savedState, this.frameBuffer);
|
||||
this.ctx = EmulatorBindings.openSavedState(savedState, this.frameRenderer, this.audioPlayer, this.keypad);
|
||||
}
|
||||
|
||||
public synchronized void close() {
|
||||
|
@ -112,4 +118,10 @@ public class Emulator {
|
|||
public synchronized void log() {
|
||||
EmulatorBindings.log(this.ctx);
|
||||
}
|
||||
|
||||
public class EmulatorException extends Exception {
|
||||
public EmulatorException(String errorMessage) {
|
||||
super(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import android.graphics.BitmapFactory;
|
|||
|
||||
import com.mrmichel.rustdroid_emu.Util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.ArrayList;
|
||||
|
@ -74,7 +73,7 @@ public class SnapshotManager {
|
|||
SQLiteDatabase db = dbHelper.getWritableDatabase();
|
||||
|
||||
File file = snapshot.getFile();
|
||||
db.delete(dbHelper.TABLE_NAME, "dataFile = '" + file.toString() + "'", null);
|
||||
db.delete(SnapshotDatabaseHelper.TABLE_NAME, "dataFile = '" + file.toString() + "'", null);
|
||||
file.delete();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,43 +1,35 @@
|
|||
package com.mrmichel.rustdroid_emu.ui;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.mrmichel.rustdroid_emu.core.Emulator;
|
||||
|
||||
public class EmulationThread extends Thread {
|
||||
|
||||
private static final String TAG = "EmulationThread";
|
||||
|
||||
public static final long NANOSECONDS_PER_MILLISECOND = 1000000;
|
||||
public static final long FRAME_TIME = 1000000000 / 60;
|
||||
|
||||
private Emulator emulator;
|
||||
private ScreenView screenView;
|
||||
|
||||
private boolean turbo;
|
||||
private boolean running;
|
||||
private boolean stopping;
|
||||
|
||||
public EmulationThread(Emulator emulator, ScreenView screenView) {
|
||||
this.emulator = emulator;
|
||||
this.screenView = screenView;
|
||||
this.running = true;
|
||||
}
|
||||
|
||||
public void setStopping(boolean stopping) {
|
||||
this.stopping = stopping;
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
public void pauseEmulation() {
|
||||
running = false;
|
||||
this.emulator.pause();
|
||||
}
|
||||
|
||||
public void resumeEmulation() {
|
||||
running = true;
|
||||
this.emulator.resume();
|
||||
}
|
||||
|
||||
public void setTurbo(boolean turbo) {
|
||||
this.turbo = turbo;
|
||||
}
|
||||
|
||||
public boolean isTurbo() { return turbo; }
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
super.run();
|
||||
|
@ -45,27 +37,12 @@ public class EmulationThread extends Thread {
|
|||
// wait until renderer is ready
|
||||
while (!screenView.getRenderer().isReady());
|
||||
|
||||
while (!stopping) {
|
||||
if (running) {
|
||||
long startTimer = System.nanoTime();
|
||||
emulator.runFrame();
|
||||
if (!turbo) {
|
||||
long currentTime = System.nanoTime();
|
||||
long timePassed = currentTime - startTimer;
|
||||
while (!emulator.isOpen());
|
||||
|
||||
long delay = FRAME_TIME - timePassed;
|
||||
if (delay > 0) {
|
||||
try {
|
||||
Thread.sleep(delay / NANOSECONDS_PER_MILLISECOND);
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screenView.updateFrame(emulator.getFrameBuffer());
|
||||
}
|
||||
}
|
||||
running = true;
|
||||
emulator.runMainLoop();
|
||||
Log.d(TAG, "Native runMainLoop returned!");
|
||||
running = false;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.mrmichel.rustdroid_emu.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
@ -8,7 +7,6 @@ import android.graphics.Bitmap;
|
|||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioTrack;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
@ -28,11 +26,11 @@ import androidx.appcompat.app.AppCompatActivity;
|
|||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.mrmichel.rustboyadvance.EmulatorBindings;
|
||||
import com.mrmichel.rustboyadvance.Keypad;
|
||||
import com.mrmichel.rustdroid_emu.R;
|
||||
import com.mrmichel.rustdroid_emu.Util;
|
||||
import com.mrmichel.rustdroid_emu.core.AudioThread;
|
||||
import com.mrmichel.rustdroid_emu.core.AndroidAudioPlayer;
|
||||
import com.mrmichel.rustdroid_emu.core.Emulator;
|
||||
import com.mrmichel.rustdroid_emu.core.Keypad;
|
||||
import com.mrmichel.rustdroid_emu.core.RomManager;
|
||||
import com.mrmichel.rustdroid_emu.core.Snapshot;
|
||||
import com.mrmichel.rustdroid_emu.core.SnapshotManager;
|
||||
|
@ -42,7 +40,6 @@ import java.io.ByteArrayOutputStream;
|
|||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class EmulatorActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener {
|
||||
|
||||
|
@ -53,15 +50,12 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
|
|||
private static final int LOAD_ROM_REQUESTCODE = 123;
|
||||
private static final int LOAD_SNAPSHOT_REQUESTCODE = 124;
|
||||
|
||||
private static int SAMPLE_RATE_HZ = 44100;
|
||||
|
||||
private Menu menu;
|
||||
|
||||
private RomManager.RomMetadataEntry romMetadata;
|
||||
private byte[] bios;
|
||||
private EmulationThread emulationThread;
|
||||
private AudioThread audioThread;
|
||||
private AudioTrack audioTrack;
|
||||
private AndroidAudioPlayer audioPlayer;
|
||||
private byte[] on_resume_saved_state = null;
|
||||
|
||||
private Emulator emulator;
|
||||
|
@ -78,7 +72,7 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
|
|||
if (!isEmulatorRunning()) {
|
||||
return;
|
||||
}
|
||||
emulationThread.setTurbo(((CompoundButton) findViewById(R.id.tbTurbo)).isChecked());
|
||||
emulator.setTurbo(((CompoundButton) findViewById(R.id.tbTurbo)).isChecked());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -244,18 +238,9 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
|
|||
}
|
||||
|
||||
private void killThreads() {
|
||||
if (audioThread != null) {
|
||||
audioThread.setStopping(true);
|
||||
try {
|
||||
audioThread.join();
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "audio thread join interrupted");
|
||||
}
|
||||
audioThread = null;
|
||||
}
|
||||
if (emulationThread != null) {
|
||||
try {
|
||||
emulationThread.setStopping(true);
|
||||
emulator.stop();
|
||||
emulationThread.join();
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "emulation thread join interrupted");
|
||||
|
@ -266,12 +251,8 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
|
|||
|
||||
private void createThreads() {
|
||||
emulationThread = new EmulationThread(emulator, screenView);
|
||||
audioThread = new AudioThread(audioTrack, emulator);
|
||||
|
||||
emulationThread.setTurbo(turboButton.isChecked());
|
||||
|
||||
emulator.setTurbo(turboButton.isChecked());
|
||||
emulationThread.start();
|
||||
audioThread.start();
|
||||
}
|
||||
|
||||
public void onRomLoaded(byte[] rom, String savePath) {
|
||||
|
@ -313,7 +294,7 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
|
|||
|
||||
outState.putString("saveFile", saveFile.getPath());
|
||||
|
||||
outState.putBoolean("turbo", emulationThread.isTurbo());
|
||||
outState.putBoolean("turbo", false);
|
||||
|
||||
} catch (Exception e) {
|
||||
Util.showAlertDiaglogAndExit(this, e);
|
||||
|
@ -328,30 +309,7 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
|
|||
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
AudioTrack.Builder audioTrackBuilder = new AudioTrack.Builder()
|
||||
.setAudioFormat(new AudioFormat.Builder()
|
||||
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
||||
.setSampleRate(SAMPLE_RATE_HZ)
|
||||
.setChannelMask(AudioFormat.CHANNEL_IN_STEREO | AudioFormat.CHANNEL_OUT_STEREO)
|
||||
.build()
|
||||
)
|
||||
.setBufferSizeInBytes(4096)
|
||||
.setTransferMode(AudioTrack.MODE_STREAM);
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
audioTrackBuilder.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY);
|
||||
}
|
||||
this.audioTrack = audioTrackBuilder.build();
|
||||
} else {
|
||||
this.audioTrack = new AudioTrack(
|
||||
AudioManager.STREAM_MUSIC,
|
||||
SAMPLE_RATE_HZ,
|
||||
AudioFormat.CHANNEL_IN_STEREO | AudioFormat.CHANNEL_OUT_STEREO,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
4096,
|
||||
AudioTrack.MODE_STREAM);
|
||||
}
|
||||
this.audioTrack.play();
|
||||
this.audioPlayer = new AndroidAudioPlayer();
|
||||
|
||||
findViewById(R.id.bStart).setOnTouchListener(this);
|
||||
findViewById(R.id.bSelect).setOnTouchListener(this);
|
||||
|
@ -370,7 +328,7 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
|
|||
this.bios = getIntent().getByteArrayExtra("bios");
|
||||
|
||||
this.screenView = findViewById(R.id.gba_view);
|
||||
this.emulator = new Emulator();
|
||||
this.emulator = new Emulator(this.screenView, this.audioPlayer);
|
||||
|
||||
final String saveFilePath;
|
||||
|
||||
|
@ -406,7 +364,7 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
|
|||
boolean turbo = savedInstanceState.getBoolean("turbo");
|
||||
|
||||
turboButton.setPressed(turbo);
|
||||
emulationThread.setTurbo(turbo);
|
||||
emulator.setTurbo(turbo);
|
||||
|
||||
} catch (Exception e) {
|
||||
Util.showAlertDiaglogAndExit(thisActivity, e);
|
||||
|
@ -481,7 +439,6 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
|
|||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
audioTrack.stop();
|
||||
pauseEmulation();
|
||||
killThreads();
|
||||
}
|
||||
|
@ -489,7 +446,6 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
|
|||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
audioTrack.stop();
|
||||
pauseEmulation();
|
||||
screenView.onPause();
|
||||
}
|
||||
|
@ -499,7 +455,7 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
|
|||
super.onResume();
|
||||
screenView.onResume();
|
||||
resumeEmulation();
|
||||
audioTrack.play();
|
||||
audioPlayer.play();
|
||||
}
|
||||
|
||||
public void doSaveSnapshot() {
|
||||
|
|
|
@ -7,7 +7,9 @@ import android.util.AttributeSet;
|
|||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
public class ScreenView extends GLSurfaceView implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
import com.mrmichel.rustboyadvance.IFrameRenderer;
|
||||
|
||||
public class ScreenView extends GLSurfaceView implements SharedPreferences.OnSharedPreferenceChangeListener, IFrameRenderer {
|
||||
private ScreenRenderer mRenderer;
|
||||
|
||||
public ScreenView(Context context) {
|
||||
|
@ -33,11 +35,6 @@ public class ScreenView extends GLSurfaceView implements SharedPreferences.OnSha
|
|||
this.setRenderMode(RENDERMODE_WHEN_DIRTY);
|
||||
}
|
||||
|
||||
public void updateFrame(int[] frameBuffer) {
|
||||
mRenderer.updateTexture(frameBuffer);
|
||||
requestRender();
|
||||
}
|
||||
|
||||
public ScreenRenderer getRenderer() {
|
||||
return mRenderer;
|
||||
}
|
||||
|
@ -49,4 +46,10 @@ public class ScreenView extends GLSurfaceView implements SharedPreferences.OnSha
|
|||
mRenderer.setColorCorrection(colorCorrection);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderFrame(int[] frameBuffer) {
|
||||
mRenderer.updateTexture(frameBuffer);
|
||||
requestRender();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ edition = "2018"
|
|||
[dependencies]
|
||||
rustboyadvance-core = { path = "../../core/", features = ["elf_support"] }
|
||||
sdl2 = { version = "0.33.0", features = ["image"] }
|
||||
ringbuf = "0.2.1"
|
||||
ringbuf = "0.2.2"
|
||||
bytesize = "1.0.0"
|
||||
clap = { version = "2.33", features = ["color", "yaml"] }
|
||||
log = "0.4.8"
|
||||
|
|
|
@ -40,9 +40,9 @@ impl AudioInterface for Sdl2AudioPlayer {
|
|||
self.freq
|
||||
}
|
||||
|
||||
fn push_sample(&mut self, sample: StereoSample<i16>) {
|
||||
fn push_sample(&mut self, sample: &[i16]) {
|
||||
#![allow(unused_must_use)]
|
||||
self.producer.push(sample);
|
||||
self.producer.push((sample[0], sample[1]));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -70,8 +70,8 @@ impl AudioInterface for Interface {
|
|||
}
|
||||
|
||||
fn push_sample(&mut self, samples: StereoSample<i16>) {
|
||||
self.audio_ring_buffer.prod.push(samples.0).unwrap();
|
||||
self.audio_ring_buffer.prod.push(samples.1).unwrap();
|
||||
self.audio_ring_buffer.producer().push(samples.0).unwrap();
|
||||
self.audio_ring_buffer.producer().push(samples.1).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -170,7 +170,7 @@ impl Emulator {
|
|||
pub fn collect_audio_samples(&self) -> Result<Float32Array, JsValue> {
|
||||
let mut interface = self.interface.borrow_mut();
|
||||
|
||||
let consumer = &mut interface.audio_ring_buffer.cons;
|
||||
let consumer = interface.audio_ring_buffer.consumer();
|
||||
let mut samples = Vec::with_capacity(consumer.len());
|
||||
while let Some(sample) = consumer.pop() {
|
||||
samples.push(convert_sample(sample));
|
||||
|
|
Reference in a new issue