diff --git a/Cargo.toml b/Cargo.toml index 0b1816c..cb60160 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "rustboyadvance-core/", "platform/rustboyadvance-sdl2", + "platform/rustboyadvance-libretro", "platform/rustboyadvance-minifb", "platform/rustboyadvance-wasm", "bindings/rustboyadvance-jni", diff --git a/platform/rustboyadvance-libretro/Cargo.toml b/platform/rustboyadvance-libretro/Cargo.toml new file mode 100644 index 0000000..b12100b --- /dev/null +++ b/platform/rustboyadvance-libretro/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "rustboyadvance-libretro" +version = "0.1.0" +authors = ["Michel Heily "] +edition = "2018" + + +[lib] +crate-type = ["cdylib"] + + +[dependencies] +rustboyadvance-core = { path = "../../rustboyadvance-core/" } +log = "0.4.8" +libc = "0.2" +libretro-sys = "0.1.1" +bit = "^0.1" +unsafe_unwrap = "0.1.0" + +[dependencies.libretro-backend] +git = "https://github.com/michelhe/libretro-backend.git" +branch = "rustboyadvance" +features = ["logging"] \ No newline at end of file diff --git a/platform/rustboyadvance-libretro/README.md b/platform/rustboyadvance-libretro/README.md new file mode 100644 index 0000000..43864b0 --- /dev/null +++ b/platform/rustboyadvance-libretro/README.md @@ -0,0 +1,22 @@ +# Experimental RustBoyAdvance-NG retroarch core + +## Desktop Build + +To build for your host system, run +```sh +cargo build --release +``` + +For Linux, the output is `repo/target/release/librustboyadvance_libretro.so` +For windows, the output is `repo/target/release/rustboyadvance_libretro.dll` + +## Android Build + +Assuming you have NDK toolchain installed and configured, this crate can be built for android targets. + +For example, for armv7-linux-androideabi +```sh +cargo build --release --target=armv7-linux-androideabi +``` + +The output will be in `/repo/target/armv7-linux-androideabi/release/librustboyadvance_libretro.so` \ No newline at end of file diff --git a/platform/rustboyadvance-libretro/src/lib.rs b/platform/rustboyadvance-libretro/src/lib.rs new file mode 100644 index 0000000..303e53e --- /dev/null +++ b/platform/rustboyadvance-libretro/src/lib.rs @@ -0,0 +1,192 @@ +#[macro_use] +extern crate libretro_backend; + +#[macro_use] +extern crate log; + +use libretro_backend::{ + AudioVideoInfo, CoreInfo, GameData, JoypadButton, LoadGameResult, PixelFormat, RuntimeHandle, +}; + +use bit::BitIndex; +use unsafe_unwrap::UnsafeUnwrap; + +use rustboyadvance_core::keypad::Keys as GbaButton; +use rustboyadvance_core::prelude::*; +use rustboyadvance_core::util::audio::AudioRingBuffer; + +use std::path::Path; + +use std::cell::RefCell; +use std::rc::Rc; + +struct HwInterface { + key_state: u16, + audio_ring_buffer: AudioRingBuffer, +} + +impl HwInterface { + fn set_button_state(&mut self, button: JoypadButton, is_pressed: bool) { + let mapped_button = match button { + JoypadButton::A => GbaButton::ButtonA, + JoypadButton::B => GbaButton::ButtonB, + JoypadButton::Start => GbaButton::Start, + JoypadButton::Select => GbaButton::Select, + JoypadButton::Left => GbaButton::Left, + JoypadButton::Up => GbaButton::Up, + JoypadButton::Right => GbaButton::Right, + JoypadButton::Down => GbaButton::Down, + JoypadButton::L1 => GbaButton::ButtonL, + JoypadButton::R1 => GbaButton::ButtonR, + _ => unreachable!(), + }; + self.key_state.set_bit(mapped_button as usize, !is_pressed); + } +} + +// do nothing here, everything is handled in the libretro_backend::Core impl +impl VideoInterface for HwInterface {} + +impl AudioInterface for HwInterface { + fn push_sample(&mut self, samples: StereoSample) { + let prod = self.audio_ring_buffer.producer(); + prod.push(samples.0).unwrap(); + prod.push(samples.1).unwrap(); + } +} + +impl InputInterface for HwInterface { + fn poll(&mut self) -> u16 { + self.key_state + } +} + +#[derive(Default)] +struct RustBoyAdvanceCore { + gba: Option, + game_data: Option, + hwif: Option>>, +} + +impl libretro_backend::Core for RustBoyAdvanceCore { + fn info() -> CoreInfo { + info!("Getting core info!"); + CoreInfo::new("RustBoyAdvance", env!("CARGO_PKG_VERSION")) + .supports_roms_with_extension("gba") + } + + fn on_load_game(&mut self, game_data: GameData) -> LoadGameResult { + debug!("on_load_game"); + + let system_directory = libretro_backend::environment::get_system_directory(); + if system_directory.is_none() { + error!("no system directory!"); + return LoadGameResult::Failed(game_data); + } + let system_directory = system_directory.unwrap(); + let system_directory_path = Path::new(&system_directory); + + let bios_path = system_directory_path.join("gba_bios.bin"); + if !bios_path.exists() { + error!("bios file missing, please place it in {:?}", bios_path); + return LoadGameResult::Failed(game_data); + } + let bios = read_bin_file(&bios_path); + + if game_data.is_empty() { + error!("game data is empty!"); + return LoadGameResult::Failed(game_data); + } + + let result = if let Some(data) = game_data.data() { + GamepakBuilder::new() + .buffer(data) + .without_backup_to_file() + .build() + } else if let Some(path) = game_data.path() { + GamepakBuilder::new().file(Path::new(&path)).build() + } else { + unreachable!() + }; + + match (result, bios) { + (Ok(gamepak), Ok(bios)) => { + let av_info = AudioVideoInfo::new() + .video(240, 160, 60.0, PixelFormat::ARGB8888) + .audio(44100.0); + + let hwif = Rc::new(RefCell::new(HwInterface { + key_state: rustboyadvance_core::keypad::KEYINPUT_ALL_RELEASED, + audio_ring_buffer: AudioRingBuffer::new(), + })); + let gba = GameBoyAdvance::new( + bios.into_boxed_slice(), + gamepak, + hwif.clone(), + hwif.clone(), + hwif.clone(), + ); + + self.hwif = Some(hwif); + self.gba = Some(gba); + self.game_data = Some(game_data); + LoadGameResult::Success(av_info) + } + _ => LoadGameResult::Failed(game_data), + } + } + + fn on_run(&mut self, handle: &mut RuntimeHandle) { + let joypad_port = 0; + + // gba and hwif are `Some` after the game is loaded, so avoiding overhead of unwrap + let gba = unsafe { self.gba.as_mut().unsafe_unwrap() }; + let hwif = unsafe { self.hwif.as_mut().unsafe_unwrap() }; + + macro_rules! update_controllers { + ( $( $button:ident ),+ ) => ( + $( + hwif.borrow_mut().set_button_state( JoypadButton::$button, handle.is_joypad_button_pressed( joypad_port, JoypadButton::$button ) ); + )+ + ) + } + + update_controllers!(A, B, Start, Select, Left, Up, Right, Down, L1, R1); + + gba.frame(); + + let framebuffer = gba.get_frame_buffer(); + let bytes_per_pixel = 4; + let framebuffer_size = 240 * 160; + let uploaded_frame = unsafe { + std::slice::from_raw_parts( + framebuffer.as_ptr() as *const u8, + framebuffer_size * bytes_per_pixel, + ) + }; + handle.upload_video_frame(uploaded_frame); + + // upload sound samples + { + let mut audio_samples = [0; 4096 * 2]; + let mut hwif = hwif.borrow_mut(); + let consumer = hwif.audio_ring_buffer.consumer(); + let count = consumer.pop_slice(&mut audio_samples); + + handle.upload_audio_frame(&audio_samples[..count]); + } + } + + fn on_reset(&mut self) { + debug!("on_reset"); + self.gba.as_mut().unwrap().soft_reset(); + } + + fn on_unload_game(&mut self) -> GameData { + debug!("on_unload_game"); + self.game_data.take().unwrap() + } + // ... +} + +libretro_core!(RustBoyAdvanceCore); diff --git a/rustboyadvance-core/src/gba.rs b/rustboyadvance-core/src/gba.rs index b510cde..d50acd8 100644 --- a/rustboyadvance-core/src/gba.rs +++ b/rustboyadvance-core/src/gba.rs @@ -34,18 +34,17 @@ struct SaveState { cpu: arm7tdmi::Core, } -/// Checks if the bios provided is the real one, -/// Otherwise output a log warning to the user -fn check_real_bios(bios: &[u8]) { +/// Checks if the bios provided is the real one +fn check_real_bios(bios: &[u8]) -> bool { use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); hasher.input(bios); let digest = hasher.result(); let expected_hash = hex!("fd2547724b505f487e6dcb29ec2ecff3af35a841a77ab2e85fd87350abd36570"); - if digest.as_slice() != &expected_hash[..] { - warn!("This is not the real bios, some games may not be compatible"); - } + + digest.as_slice() == &expected_hash[..] } impl GameBoyAdvance { @@ -57,7 +56,10 @@ impl GameBoyAdvance { input_device: Rc>, ) -> GameBoyAdvance { // Warn the user if the bios is not the real one - check_real_bios(&bios_rom); + match check_real_bios(&bios_rom) { + true => info!("Verified bios rom"), + false => warn!("This is not the real bios rom, some games may not be compatible"), + }; let gpu = Box::new(Gpu::new()); let sound_controller = Box::new(SoundController::new( audio_device.borrow().get_sample_rate() as f32, @@ -251,6 +253,11 @@ impl GameBoyAdvance { pub fn get_frame_buffer(&self) -> &[u32] { self.sysbus.io.gpu.get_frame_buffer() } + + /// Reset the emulator + pub fn soft_reset(&mut self) { + self.cpu.reset(&mut self.sysbus); + } } #[cfg(test)] diff --git a/rustboyadvance-core/src/util.rs b/rustboyadvance-core/src/util.rs index 99d14c4..b447bcc 100644 --- a/rustboyadvance-core/src/util.rs +++ b/rustboyadvance-core/src/util.rs @@ -143,5 +143,13 @@ pub mod audio { AudioRingBuffer { prod, cons } } + + pub fn producer(&mut self) -> &mut Producer { + &mut self.prod + } + + pub fn consumer(&mut self) -> &mut Consumer { + &mut self.cons + } } }