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"]
|
crate-type = ["staticlib", "cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rustboyadvance-core = {path = "../../core/"}
|
rustboyadvance-core = {path = "../../core/", features = ["arm7tdmi_dispatch_table", "no_video_interface"]}
|
||||||
jni = { version = "0.16", default-features = false }
|
jni = "0.17.0"
|
||||||
log = {version = "0.4.8", features = ["release_max_level_info", "max_level_debug"]}
|
log = {version = "0.4.8", features = ["release_max_level_info", "max_level_debug"]}
|
||||||
|
|
||||||
[target.'cfg(target_os="android")'.dependencies]
|
[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
|
/// JNI Bindings for rustboyadvance
|
||||||
///
|
///
|
||||||
mod rom_helper;
|
mod rom_helper;
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use emulator::EmulatorContext;
|
||||||
|
|
||||||
use std::os::raw::c_void;
|
use std::os::raw::c_void;
|
||||||
use std::path::Path;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::sync::{Mutex, MutexGuard};
|
|
||||||
|
|
||||||
use jni::objects::*;
|
use jni::objects::*;
|
||||||
use jni::sys::*;
|
use jni::sys::*;
|
||||||
|
@ -21,112 +21,12 @@ use android_log;
|
||||||
use env_logger;
|
use env_logger;
|
||||||
|
|
||||||
use rustboyadvance_core::prelude::*;
|
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;
|
static mut DID_LOAD: bool = false;
|
||||||
|
|
||||||
const NATIVE_EXCEPTION_CLASS: &'static str =
|
const NATIVE_EXCEPTION_CLASS: &'static str =
|
||||||
"com/mrmichel/rustboyadvance/EmulatorBindings/NativeBindingException";
|
"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> {
|
fn save_state(env: &JNIEnv, gba: &mut GameBoyAdvance) -> Result<jbyteArray, String> {
|
||||||
let saved_state = gba
|
let saved_state = gba
|
||||||
.save_state()
|
.save_state()
|
||||||
|
@ -149,8 +49,9 @@ fn load_state(env: &JNIEnv, gba: &mut GameBoyAdvance, state: jbyteArray) -> Resu
|
||||||
pub mod bindings {
|
pub mod bindings {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
unsafe fn lock_ctx<'a>(ctx: jlong) -> MutexGuard<'a, Context> {
|
#[inline(always)]
|
||||||
(*(ctx as *mut Mutex<Context>)).lock().unwrap()
|
unsafe fn cast_ctx<'a>(ctx: jlong) -> &'a mut EmulatorContext {
|
||||||
|
&mut (*(ctx as *mut EmulatorContext))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
@ -177,12 +78,23 @@ pub mod bindings {
|
||||||
_obj: JClass,
|
_obj: JClass,
|
||||||
bios: jbyteArray,
|
bios: jbyteArray,
|
||||||
rom: jbyteArray,
|
rom: jbyteArray,
|
||||||
frame_buffer: jintArray,
|
renderer_obj: JObject,
|
||||||
|
audio_player_obj: JObject,
|
||||||
|
keypad_obj: JObject,
|
||||||
save_file: JString,
|
save_file: JString,
|
||||||
skip_bios: jboolean,
|
skip_bios: jboolean,
|
||||||
) -> jlong {
|
) -> jlong {
|
||||||
match internal_open_context(&env, bios, rom, frame_buffer, save_file, skip_bios) {
|
match EmulatorContext::native_open_context(
|
||||||
Ok(ctx) => Box::into_raw(Box::new(Mutex::new(ctx))) as jlong,
|
&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) => {
|
Err(msg) => {
|
||||||
env.throw_new(NATIVE_EXCEPTION_CLASS, msg).unwrap();
|
env.throw_new(NATIVE_EXCEPTION_CLASS, msg).unwrap();
|
||||||
-1
|
-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]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_EmulatorBindings_openSavedState(
|
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_EmulatorBindings_openSavedState(
|
||||||
env: JNIEnv,
|
env: JNIEnv,
|
||||||
_obj: JClass,
|
_obj: JClass,
|
||||||
state: jbyteArray,
|
state: jbyteArray,
|
||||||
frame_buffer: jintArray,
|
renderer_obj: JObject,
|
||||||
|
audio_player_obj: JObject,
|
||||||
|
keypad_obj: JObject,
|
||||||
) -> jlong {
|
) -> jlong {
|
||||||
match internal_open_saved_state(&env, state, frame_buffer) {
|
match EmulatorContext::native_open_saved_state(
|
||||||
Ok(ctx) => Box::into_raw(Box::new(Mutex::new(ctx))) as jlong,
|
&env,
|
||||||
|
state,
|
||||||
|
renderer_obj,
|
||||||
|
audio_player_obj,
|
||||||
|
keypad_obj,
|
||||||
|
) {
|
||||||
|
Ok(ctx) => Box::into_raw(Box::new(ctx)) as jlong,
|
||||||
Err(msg) => {
|
Err(msg) => {
|
||||||
env.throw_new(NATIVE_EXCEPTION_CLASS, msg).unwrap();
|
env.throw_new(NATIVE_EXCEPTION_CLASS, msg).unwrap();
|
||||||
-1
|
-1
|
||||||
|
@ -243,58 +128,90 @@ pub mod bindings {
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_EmulatorBindings_closeEmulator(
|
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,
|
_env: JNIEnv,
|
||||||
_obj: JClass,
|
_obj: JClass,
|
||||||
ctx: jlong,
|
ctx: jlong,
|
||||||
key_state: jint,
|
|
||||||
) {
|
) {
|
||||||
let mut ctx = lock_ctx(ctx);
|
info!("waiting for emulation thread to stop");
|
||||||
ctx.hwif.borrow_mut().key_state = key_state as u16;
|
|
||||||
|
{
|
||||||
|
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]
|
#[no_mangle]
|
||||||
|
@ -303,9 +220,14 @@ pub mod bindings {
|
||||||
_obj: JClass,
|
_obj: JClass,
|
||||||
ctx: jlong,
|
ctx: jlong,
|
||||||
) -> jbyteArray {
|
) -> jbyteArray {
|
||||||
let mut ctx = lock_ctx(ctx);
|
let ctx = cast_ctx(ctx);
|
||||||
match save_state(&env, &mut ctx.gba) {
|
ctx.pause();
|
||||||
|
let (_lock, gba) = ctx.lock_and_get_gba();
|
||||||
|
match save_state(&env, gba) {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
|
drop(_lock);
|
||||||
|
drop(gba);
|
||||||
|
ctx.resume();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
Err(msg) => {
|
Err(msg) => {
|
||||||
|
@ -322,9 +244,15 @@ pub mod bindings {
|
||||||
ctx: jlong,
|
ctx: jlong,
|
||||||
state: jbyteArray,
|
state: jbyteArray,
|
||||||
) {
|
) {
|
||||||
let mut ctx = lock_ctx(ctx);
|
let ctx = cast_ctx(ctx);
|
||||||
match load_state(&env, &mut ctx.gba, state) {
|
ctx.pause();
|
||||||
Ok(_) => {}
|
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(),
|
Err(msg) => env.throw_new(NATIVE_EXCEPTION_CLASS, msg).unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -335,7 +263,7 @@ pub mod bindings {
|
||||||
_obj: JClass,
|
_obj: JClass,
|
||||||
ctx: jlong,
|
ctx: jlong,
|
||||||
) -> jstring {
|
) -> jstring {
|
||||||
let ctx = lock_ctx(ctx);
|
let ctx = cast_ctx(ctx);
|
||||||
env.new_string(ctx.gba.get_game_title())
|
env.new_string(ctx.gba.get_game_title())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into_inner()
|
.into_inner()
|
||||||
|
@ -347,7 +275,7 @@ pub mod bindings {
|
||||||
_obj: JClass,
|
_obj: JClass,
|
||||||
ctx: jlong,
|
ctx: jlong,
|
||||||
) -> jstring {
|
) -> jstring {
|
||||||
let ctx = lock_ctx(ctx);
|
let ctx = cast_ctx(ctx);
|
||||||
env.new_string(ctx.gba.get_game_code())
|
env.new_string(ctx.gba.get_game_code())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into_inner()
|
.into_inner()
|
||||||
|
@ -359,7 +287,7 @@ pub mod bindings {
|
||||||
_obj: JClass,
|
_obj: JClass,
|
||||||
ctx: jlong,
|
ctx: jlong,
|
||||||
) {
|
) {
|
||||||
let ctx = lock_ctx(ctx);
|
let ctx = cast_ctx(ctx);
|
||||||
info!("CPU LOG: {:#x?}", ctx.gba.cpu);
|
info!("CPU LOG: {:#x?}", ctx.gba.cpu);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,22 +9,26 @@ fn parse_rom_header(env: &JNIEnv, barr: jbyteArray) -> cartridge::header::Cartri
|
||||||
cartridge::header::parse(&rom_data).unwrap()
|
cartridge::header::parse(&rom_data).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
mod bindings {
|
||||||
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_RomHelper_getGameCode(
|
use super::*;
|
||||||
env: JNIEnv,
|
|
||||||
_obj: JClass,
|
|
||||||
rom_data: jbyteArray,
|
|
||||||
) -> jstring {
|
|
||||||
let header = parse_rom_header(&env, rom_data);
|
|
||||||
env.new_string(header.game_code).unwrap().into_inner()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_RomHelper_getGameTitle(
|
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_RomHelper_getGameCode(
|
||||||
env: JNIEnv,
|
env: JNIEnv,
|
||||||
_obj: JClass,
|
_obj: JClass,
|
||||||
rom_data: jbyteArray,
|
rom_data: jbyteArray,
|
||||||
) -> jstring {
|
) -> jstring {
|
||||||
let header = parse_rom_header(&env, rom_data);
|
let header = parse_rom_header(&env, rom_data);
|
||||||
env.new_string(header.game_title).unwrap().into_inner()
|
env.new_string(header.game_code).unwrap().into_inner()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_RomHelper_getGameTitle(
|
||||||
|
env: JNIEnv,
|
||||||
|
_obj: JClass,
|
||||||
|
rom_data: jbyteArray,
|
||||||
|
) -> jstring {
|
||||||
|
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
|
/// Sample should be normilized to siged 16bit values
|
||||||
/// Note: It is not guarentied that the sample will be played
|
/// Note: It is not guarentied that the sample will be played
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn push_sample(&mut self, samples: StereoSample<i16>) {}
|
fn push_sample(&mut self, samples: &[i16]) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait InputInterface {
|
pub trait InputInterface {
|
||||||
|
|
|
@ -349,10 +349,10 @@ impl SoundController {
|
||||||
|
|
||||||
let mut audio = audio_device.borrow_mut();
|
let mut audio = audio_device.borrow_mut();
|
||||||
self.output_buffer.drain(..).for_each(|(left, right)| {
|
self.output_buffer.drain(..).for_each(|(left, right)| {
|
||||||
audio.push_sample((
|
audio.push_sample(&[
|
||||||
(left.round() as i16) * (std::i16::MAX / 512),
|
(left.round() as i16) * (std::i16::MAX / 512),
|
||||||
(right.round() as i16) * (std::i16::MAX / 512),
|
(right.round() as i16) * (std::i16::MAX / 512),
|
||||||
));
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if self.cycles_per_sample < *cycles_to_next_event {
|
if self.cycles_per_sample < *cycles_to_next_event {
|
||||||
|
|
|
@ -132,16 +132,20 @@ macro_rules! host_breakpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod audio {
|
pub mod audio {
|
||||||
use ringbuf::{Consumer, Producer, RingBuffer};
|
pub use ringbuf::{Consumer, Producer, RingBuffer};
|
||||||
|
|
||||||
pub struct AudioRingBuffer {
|
pub struct AudioRingBuffer {
|
||||||
pub prod: Producer<i16>,
|
prod: Producer<i16>,
|
||||||
pub cons: Consumer<i16>,
|
cons: Consumer<i16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioRingBuffer {
|
impl AudioRingBuffer {
|
||||||
pub fn new() -> 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();
|
let (prod, cons) = rb.split();
|
||||||
|
|
||||||
AudioRingBuffer { prod, cons }
|
AudioRingBuffer { prod, cons }
|
||||||
|
@ -154,6 +158,10 @@ pub mod audio {
|
||||||
pub fn consumer(&mut self) -> &mut Consumer<i16> {
|
pub fn consumer(&mut self) -> &mut Consumer<i16> {
|
||||||
&mut self.cons
|
&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");
|
System.loadLibrary("rustboyadvance_jni");
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NativeBindingException extends Exception {
|
|
||||||
public NativeBindingException(String errorMessage) {
|
|
||||||
super(errorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a new emulator context
|
* Open a new emulator context
|
||||||
* @param bios bytearray of the GBA bios
|
*
|
||||||
* @param rom bytearray of the rom to run
|
* @param bios bytearray of the GBA bios
|
||||||
* @param frameBuffer frameBuffer render target
|
* @param rom bytearray of the rom to run
|
||||||
* @param save_name name of the save file TODO remove this
|
* @param renderer renderer instance
|
||||||
* @param skipBios skip bios
|
* @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
|
* @return the emulator context to use pass to other methods in this class
|
||||||
* @throws NativeBindingException
|
* @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
|
* 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
|
* @return
|
||||||
* @throws NativeBindingException
|
* @throws NativeBindingException
|
||||||
*/
|
*/
|
||||||
public static native long openSavedState(byte[] savedState, int[] frameBuffer) throws NativeBindingException;
|
public static native long openSavedState(byte[] savedState, IFrameRenderer renderer, IAudioPlayer audioPlayer, Keypad keypad) throws NativeBindingException;
|
||||||
|
|
||||||
/**
|
|
||||||
* Make the emulator boot directly into the cartridge
|
|
||||||
* @param ctx
|
|
||||||
* @throws NativeBindingException
|
|
||||||
*/
|
|
||||||
public static native void skipBios(long ctx) throws NativeBindingException;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroys the emulator instance
|
* Destroys the emulator instance
|
||||||
* should be put in a finalizer or else the emulator context may leak.
|
* should be put in a finalizer or else the emulator context may leak.
|
||||||
|
*
|
||||||
* @param ctx
|
* @param ctx
|
||||||
*/
|
*/
|
||||||
public static native void closeEmulator(long ctx);
|
public static native void closeEmulator(long ctx);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the emulation for a single frame.
|
* Run the emulation thread
|
||||||
|
*
|
||||||
* @param ctx
|
* @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);
|
||||||
|
|
||||||
/**
|
public static native void pause(long ctx);
|
||||||
* Collect pending audio samples
|
|
||||||
* @param ctx
|
public static native void resume(long ctx);
|
||||||
* @return sample buffer
|
|
||||||
*/
|
public static native void setTurbo(long ctx, boolean turbo);
|
||||||
public static native short[] collectAudioSamples(long ctx);
|
|
||||||
|
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
|
* @param ctx
|
||||||
|
@ -78,9 +81,9 @@ public class EmulatorBindings {
|
||||||
*/
|
*/
|
||||||
public static native String getGameCode(long ctx);
|
public static native String getGameCode(long ctx);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the keystate
|
* Sets the keystate
|
||||||
|
*
|
||||||
* @param keyState
|
* @param keyState
|
||||||
*/
|
*/
|
||||||
public static native void setKeyState(long ctx, int keyState);
|
public static native void setKeyState(long ctx, int keyState);
|
||||||
|
@ -105,7 +108,14 @@ public class EmulatorBindings {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs the emulator state
|
* Logs the emulator state
|
||||||
|
*
|
||||||
* @return non-zero value on failure
|
* @return non-zero value on failure
|
||||||
*/
|
*/
|
||||||
public static native void log(long ctx);
|
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 {
|
public class Keypad {
|
||||||
private int keyState;
|
private int keyState;
|
||||||
|
@ -11,6 +11,18 @@ public class Keypad {
|
||||||
this.keyState = 0xffff;
|
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 {
|
public enum Key {
|
||||||
ButtonA(0),
|
ButtonA(0),
|
||||||
ButtonB(1),
|
ButtonB(1),
|
||||||
|
@ -25,20 +37,8 @@ public class Keypad {
|
||||||
|
|
||||||
private final int keyBit;
|
private final int keyBit;
|
||||||
|
|
||||||
private Key(int keyBit) {
|
Key(int keyBit) {
|
||||||
this.keyBit = 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;
|
package com.mrmichel.rustdroid_emu.core;
|
||||||
|
|
||||||
import com.mrmichel.rustboyadvance.EmulatorBindings;
|
import com.mrmichel.rustboyadvance.EmulatorBindings;
|
||||||
|
import com.mrmichel.rustboyadvance.IFrameRenderer;
|
||||||
|
import com.mrmichel.rustboyadvance.Keypad;
|
||||||
|
|
||||||
public class Emulator {
|
public class Emulator {
|
||||||
|
|
||||||
public class EmulatorException extends Exception {
|
public Keypad keypad;
|
||||||
public EmulatorException(String errorMessage) {
|
|
||||||
super(errorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// context received by the native binding
|
/// context received by the native binding
|
||||||
private long ctx = -1;
|
private long ctx = -1;
|
||||||
|
|
||||||
private int[] frameBuffer;
|
private AndroidAudioPlayer audioPlayer;
|
||||||
public Keypad keypad;
|
private IFrameRenderer frameRenderer;
|
||||||
|
public Emulator(IFrameRenderer frameRenderer, AndroidAudioPlayer audioPlayer) {
|
||||||
public Emulator() {
|
|
||||||
this.frameBuffer = new int[240 * 160];
|
|
||||||
this.keypad = new Keypad();
|
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.ctx = ctx;
|
||||||
this.frameBuffer = new int[240 * 160];
|
this.frameRenderer = frameRenderer;
|
||||||
|
this.audioPlayer = audioPlayer;
|
||||||
this.keypad = new Keypad();
|
this.keypad = new Keypad();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -35,29 +33,38 @@ public class Emulator {
|
||||||
return ctx;
|
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() {
|
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 {
|
public synchronized byte[] saveState() throws EmulatorBindings.NativeBindingException {
|
||||||
return EmulatorBindings.saveState(this.ctx);
|
return EmulatorBindings.saveState(this.ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public synchronized void loadState(byte[] state) throws EmulatorBindings.NativeBindingException {
|
public synchronized void loadState(byte[] state) throws EmulatorBindings.NativeBindingException {
|
||||||
if (ctx != -1) {
|
if (ctx != -1) {
|
||||||
EmulatorBindings.loadState(this.ctx, state);
|
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 {
|
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 {
|
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() {
|
public synchronized void close() {
|
||||||
|
@ -112,4 +118,10 @@ public class Emulator {
|
||||||
public synchronized void log() {
|
public synchronized void log() {
|
||||||
EmulatorBindings.log(this.ctx);
|
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 com.mrmichel.rustdroid_emu.Util;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.sql.Timestamp;
|
import java.sql.Timestamp;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -74,7 +73,7 @@ public class SnapshotManager {
|
||||||
SQLiteDatabase db = dbHelper.getWritableDatabase();
|
SQLiteDatabase db = dbHelper.getWritableDatabase();
|
||||||
|
|
||||||
File file = snapshot.getFile();
|
File file = snapshot.getFile();
|
||||||
db.delete(dbHelper.TABLE_NAME, "dataFile = '" + file.toString() + "'", null);
|
db.delete(SnapshotDatabaseHelper.TABLE_NAME, "dataFile = '" + file.toString() + "'", null);
|
||||||
file.delete();
|
file.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,43 +1,35 @@
|
||||||
package com.mrmichel.rustdroid_emu.ui;
|
package com.mrmichel.rustdroid_emu.ui;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import com.mrmichel.rustdroid_emu.core.Emulator;
|
import com.mrmichel.rustdroid_emu.core.Emulator;
|
||||||
|
|
||||||
public class EmulationThread extends Thread {
|
public class EmulationThread extends Thread {
|
||||||
|
|
||||||
|
private static final String TAG = "EmulationThread";
|
||||||
|
|
||||||
public static final long NANOSECONDS_PER_MILLISECOND = 1000000;
|
public static final long NANOSECONDS_PER_MILLISECOND = 1000000;
|
||||||
public static final long FRAME_TIME = 1000000000 / 60;
|
public static final long FRAME_TIME = 1000000000 / 60;
|
||||||
|
|
||||||
private Emulator emulator;
|
private Emulator emulator;
|
||||||
private ScreenView screenView;
|
private ScreenView screenView;
|
||||||
|
|
||||||
private boolean turbo;
|
|
||||||
private boolean running;
|
private boolean running;
|
||||||
private boolean stopping;
|
|
||||||
|
|
||||||
public EmulationThread(Emulator emulator, ScreenView screenView) {
|
public EmulationThread(Emulator emulator, ScreenView screenView) {
|
||||||
this.emulator = emulator;
|
this.emulator = emulator;
|
||||||
this.screenView = screenView;
|
this.screenView = screenView;
|
||||||
this.running = true;
|
this.running = false;
|
||||||
}
|
|
||||||
|
|
||||||
public void setStopping(boolean stopping) {
|
|
||||||
this.stopping = stopping;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void pauseEmulation() {
|
public void pauseEmulation() {
|
||||||
running = false;
|
this.emulator.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resumeEmulation() {
|
public void resumeEmulation() {
|
||||||
running = true;
|
this.emulator.resume();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTurbo(boolean turbo) {
|
|
||||||
this.turbo = turbo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isTurbo() { return turbo; }
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
super.run();
|
super.run();
|
||||||
|
@ -45,27 +37,12 @@ public class EmulationThread extends Thread {
|
||||||
// wait until renderer is ready
|
// wait until renderer is ready
|
||||||
while (!screenView.getRenderer().isReady());
|
while (!screenView.getRenderer().isReady());
|
||||||
|
|
||||||
while (!stopping) {
|
while (!emulator.isOpen());
|
||||||
if (running) {
|
|
||||||
long startTimer = System.nanoTime();
|
|
||||||
emulator.runFrame();
|
|
||||||
if (!turbo) {
|
|
||||||
long currentTime = System.nanoTime();
|
|
||||||
long timePassed = currentTime - startTimer;
|
|
||||||
|
|
||||||
long delay = FRAME_TIME - timePassed;
|
running = true;
|
||||||
if (delay > 0) {
|
emulator.runMainLoop();
|
||||||
try {
|
Log.d(TAG, "Native runMainLoop returned!");
|
||||||
Thread.sleep(delay / NANOSECONDS_PER_MILLISECOND);
|
running = false;
|
||||||
} catch (Exception e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
screenView.updateFrame(emulator.getFrameBuffer());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.mrmichel.rustdroid_emu.ui;
|
package com.mrmichel.rustdroid_emu.ui;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
@ -8,7 +7,6 @@ import android.graphics.Bitmap;
|
||||||
import android.media.AudioFormat;
|
import android.media.AudioFormat;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.media.AudioTrack;
|
import android.media.AudioTrack;
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
@ -28,11 +26,11 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.mrmichel.rustboyadvance.EmulatorBindings;
|
import com.mrmichel.rustboyadvance.EmulatorBindings;
|
||||||
|
import com.mrmichel.rustboyadvance.Keypad;
|
||||||
import com.mrmichel.rustdroid_emu.R;
|
import com.mrmichel.rustdroid_emu.R;
|
||||||
import com.mrmichel.rustdroid_emu.Util;
|
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.Emulator;
|
||||||
import com.mrmichel.rustdroid_emu.core.Keypad;
|
|
||||||
import com.mrmichel.rustdroid_emu.core.RomManager;
|
import com.mrmichel.rustdroid_emu.core.RomManager;
|
||||||
import com.mrmichel.rustdroid_emu.core.Snapshot;
|
import com.mrmichel.rustdroid_emu.core.Snapshot;
|
||||||
import com.mrmichel.rustdroid_emu.core.SnapshotManager;
|
import com.mrmichel.rustdroid_emu.core.SnapshotManager;
|
||||||
|
@ -42,7 +40,6 @@ import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
public class EmulatorActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener {
|
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_ROM_REQUESTCODE = 123;
|
||||||
private static final int LOAD_SNAPSHOT_REQUESTCODE = 124;
|
private static final int LOAD_SNAPSHOT_REQUESTCODE = 124;
|
||||||
|
|
||||||
private static int SAMPLE_RATE_HZ = 44100;
|
|
||||||
|
|
||||||
private Menu menu;
|
private Menu menu;
|
||||||
|
|
||||||
private RomManager.RomMetadataEntry romMetadata;
|
private RomManager.RomMetadataEntry romMetadata;
|
||||||
private byte[] bios;
|
private byte[] bios;
|
||||||
private EmulationThread emulationThread;
|
private EmulationThread emulationThread;
|
||||||
private AudioThread audioThread;
|
private AndroidAudioPlayer audioPlayer;
|
||||||
private AudioTrack audioTrack;
|
|
||||||
private byte[] on_resume_saved_state = null;
|
private byte[] on_resume_saved_state = null;
|
||||||
|
|
||||||
private Emulator emulator;
|
private Emulator emulator;
|
||||||
|
@ -78,7 +72,7 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
|
||||||
if (!isEmulatorRunning()) {
|
if (!isEmulatorRunning()) {
|
||||||
return;
|
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() {
|
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) {
|
if (emulationThread != null) {
|
||||||
try {
|
try {
|
||||||
emulationThread.setStopping(true);
|
emulator.stop();
|
||||||
emulationThread.join();
|
emulationThread.join();
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Log.e(TAG, "emulation thread join interrupted");
|
Log.e(TAG, "emulation thread join interrupted");
|
||||||
|
@ -266,12 +251,8 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
|
||||||
|
|
||||||
private void createThreads() {
|
private void createThreads() {
|
||||||
emulationThread = new EmulationThread(emulator, screenView);
|
emulationThread = new EmulationThread(emulator, screenView);
|
||||||
audioThread = new AudioThread(audioTrack, emulator);
|
emulator.setTurbo(turboButton.isChecked());
|
||||||
|
|
||||||
emulationThread.setTurbo(turboButton.isChecked());
|
|
||||||
|
|
||||||
emulationThread.start();
|
emulationThread.start();
|
||||||
audioThread.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onRomLoaded(byte[] rom, String savePath) {
|
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.putString("saveFile", saveFile.getPath());
|
||||||
|
|
||||||
outState.putBoolean("turbo", emulationThread.isTurbo());
|
outState.putBoolean("turbo", false);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Util.showAlertDiaglogAndExit(this, 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);
|
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||||
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
|
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 23) {
|
this.audioPlayer = new AndroidAudioPlayer();
|
||||||
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();
|
|
||||||
|
|
||||||
findViewById(R.id.bStart).setOnTouchListener(this);
|
findViewById(R.id.bStart).setOnTouchListener(this);
|
||||||
findViewById(R.id.bSelect).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.bios = getIntent().getByteArrayExtra("bios");
|
||||||
|
|
||||||
this.screenView = findViewById(R.id.gba_view);
|
this.screenView = findViewById(R.id.gba_view);
|
||||||
this.emulator = new Emulator();
|
this.emulator = new Emulator(this.screenView, this.audioPlayer);
|
||||||
|
|
||||||
final String saveFilePath;
|
final String saveFilePath;
|
||||||
|
|
||||||
|
@ -406,7 +364,7 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
|
||||||
boolean turbo = savedInstanceState.getBoolean("turbo");
|
boolean turbo = savedInstanceState.getBoolean("turbo");
|
||||||
|
|
||||||
turboButton.setPressed(turbo);
|
turboButton.setPressed(turbo);
|
||||||
emulationThread.setTurbo(turbo);
|
emulator.setTurbo(turbo);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Util.showAlertDiaglogAndExit(thisActivity, e);
|
Util.showAlertDiaglogAndExit(thisActivity, e);
|
||||||
|
@ -481,7 +439,6 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
audioTrack.stop();
|
|
||||||
pauseEmulation();
|
pauseEmulation();
|
||||||
killThreads();
|
killThreads();
|
||||||
}
|
}
|
||||||
|
@ -489,7 +446,6 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
|
||||||
@Override
|
@Override
|
||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
audioTrack.stop();
|
|
||||||
pauseEmulation();
|
pauseEmulation();
|
||||||
screenView.onPause();
|
screenView.onPause();
|
||||||
}
|
}
|
||||||
|
@ -499,7 +455,7 @@ public class EmulatorActivity extends AppCompatActivity implements View.OnClickL
|
||||||
super.onResume();
|
super.onResume();
|
||||||
screenView.onResume();
|
screenView.onResume();
|
||||||
resumeEmulation();
|
resumeEmulation();
|
||||||
audioTrack.play();
|
audioPlayer.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void doSaveSnapshot() {
|
public void doSaveSnapshot() {
|
||||||
|
|
|
@ -7,7 +7,9 @@ import android.util.AttributeSet;
|
||||||
|
|
||||||
import androidx.preference.PreferenceManager;
|
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;
|
private ScreenRenderer mRenderer;
|
||||||
|
|
||||||
public ScreenView(Context context) {
|
public ScreenView(Context context) {
|
||||||
|
@ -33,11 +35,6 @@ public class ScreenView extends GLSurfaceView implements SharedPreferences.OnSha
|
||||||
this.setRenderMode(RENDERMODE_WHEN_DIRTY);
|
this.setRenderMode(RENDERMODE_WHEN_DIRTY);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateFrame(int[] frameBuffer) {
|
|
||||||
mRenderer.updateTexture(frameBuffer);
|
|
||||||
requestRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ScreenRenderer getRenderer() {
|
public ScreenRenderer getRenderer() {
|
||||||
return mRenderer;
|
return mRenderer;
|
||||||
}
|
}
|
||||||
|
@ -49,4 +46,10 @@ public class ScreenView extends GLSurfaceView implements SharedPreferences.OnSha
|
||||||
mRenderer.setColorCorrection(colorCorrection);
|
mRenderer.setColorCorrection(colorCorrection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void renderFrame(int[] frameBuffer) {
|
||||||
|
mRenderer.updateTexture(frameBuffer);
|
||||||
|
requestRender();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ edition = "2018"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rustboyadvance-core = { path = "../../core/", features = ["elf_support"] }
|
rustboyadvance-core = { path = "../../core/", features = ["elf_support"] }
|
||||||
sdl2 = { version = "0.33.0", features = ["image"] }
|
sdl2 = { version = "0.33.0", features = ["image"] }
|
||||||
ringbuf = "0.2.1"
|
ringbuf = "0.2.2"
|
||||||
bytesize = "1.0.0"
|
bytesize = "1.0.0"
|
||||||
clap = { version = "2.33", features = ["color", "yaml"] }
|
clap = { version = "2.33", features = ["color", "yaml"] }
|
||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
|
|
|
@ -40,9 +40,9 @@ impl AudioInterface for Sdl2AudioPlayer {
|
||||||
self.freq
|
self.freq
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_sample(&mut self, sample: StereoSample<i16>) {
|
fn push_sample(&mut self, sample: &[i16]) {
|
||||||
#![allow(unused_must_use)]
|
#![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>) {
|
fn push_sample(&mut self, samples: StereoSample<i16>) {
|
||||||
self.audio_ring_buffer.prod.push(samples.0).unwrap();
|
self.audio_ring_buffer.producer().push(samples.0).unwrap();
|
||||||
self.audio_ring_buffer.prod.push(samples.1).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> {
|
pub fn collect_audio_samples(&self) -> Result<Float32Array, JsValue> {
|
||||||
let mut interface = self.interface.borrow_mut();
|
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());
|
let mut samples = Vec::with_capacity(consumer.len());
|
||||||
while let Some(sample) = consumer.pop() {
|
while let Some(sample) = consumer.pop() {
|
||||||
samples.push(convert_sample(sample));
|
samples.push(convert_sample(sample));
|
||||||
|
|
Reference in a new issue