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:
Michel Heily 2020-09-30 00:27:00 +03:00
parent 08a7cd966a
commit ba2eff82ac
25 changed files with 1566 additions and 892 deletions

952
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -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]

View 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
],
);
}
}

View 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;
}
}

View 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)
}

View 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
}
}

View file

@ -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);
}
}

View file

@ -9,22 +9,26 @@ fn parse_rom_header(env: &JNIEnv, barr: jbyteArray) -> cartridge::header::Cartri
cartridge::header::parse(&rom_data).unwrap()
}
#[no_mangle]
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_RomHelper_getGameCode(
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()
}
mod bindings {
use super::*;
#[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()
#[no_mangle]
pub unsafe extern "C" fn Java_com_mrmichel_rustboyadvance_RomHelper_getGameCode(
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]
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()
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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)
}
}
}

View file

@ -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 save_name name of the save file TODO remove this
* @param skipBios skip bios
*
* @param bios bytearray of the GBA bios
* @param rom bytearray of the rom to run
* @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);
}
}
}

View file

@ -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();
}

View file

@ -0,0 +1,5 @@
package com.mrmichel.rustboyadvance;
public interface IFrameRenderer {
void renderFrame(int[] framebuffer);
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}
}

View file

@ -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);
}
}
}

View file

@ -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();
}

View file

@ -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;
}

View file

@ -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() {

View file

@ -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();
}
}

View file

@ -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"

View file

@ -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]));
}
}

View file

@ -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));