diff --git a/Cargo.lock b/Cargo.lock index 777a9d0..a1423f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -397,7 +397,6 @@ dependencies = [ "libc", "libloading", "libretro-sys", - "once_cell", "rgb565", "thiserror", "tracing", diff --git a/crates/retro_frontend/Cargo.toml b/crates/retro_frontend/Cargo.toml index 3093777..918e978 100644 --- a/crates/retro_frontend/Cargo.toml +++ b/crates/retro_frontend/Cargo.toml @@ -9,7 +9,6 @@ edition = "2021" libc = "0.2.155" libloading = "0.8.3" libretro-sys = "0.1.1" -once_cell = "1.19.0" rgb565 = "0.1.3" thiserror = "1.0.61" tracing = "0.1.40" diff --git a/crates/retro_frontend/src/core.rs b/crates/retro_frontend/src/core.rs deleted file mode 100644 index 54ec90f..0000000 --- a/crates/retro_frontend/src/core.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::path::Path; - -use crate::frontend; -use crate::result::Result; - -/// A "RAII" wrapper over a core, useful for making cleanup a bit less ardous. -pub struct Core(); - -impl Core { - /// Same as [frontend::load_core], but returns a struct which will keep the core - /// alive until it is dropped. - pub fn load>(path: P) -> Result { - frontend::load_core(path.as_ref())?; - Ok(Self {}) - } - - /// Same as [frontend::load_game]. - pub fn load_game>(&mut self, rom_path: P) -> Result<()> { - frontend::load_game(rom_path)?; - Ok(()) - } - - /// Same as [frontend::unload_game]. - pub fn unload_game(&mut self) -> Result<()> { - frontend::unload_game()?; - Ok(()) - } -} - -impl Drop for Core { - fn drop(&mut self) { - let _ = frontend::unload_core(); - } -} diff --git a/crates/retro_frontend/src/frontend.rs b/crates/retro_frontend/src/frontend.rs index e784a99..d5ebfae 100644 --- a/crates/retro_frontend/src/frontend.rs +++ b/crates/retro_frontend/src/frontend.rs @@ -1,98 +1,359 @@ -//! The primary frontend API. -//! This is a singleton API, not by choice, but due to Libretro's design. -//! -//! # Safety -//! Don't even think about using this across multiple threads. If you want to run multiple frontends, -//! it's easier to just host this crate in a runner process and fork off those runners. -use crate::frontend_impl::FRONTEND_IMPL; use crate::joypad::Joypad; -use crate::libretro_sys_new::*; -use crate::result::Result; -use std::cell::RefCell; -use std::rc::Rc; +use crate::libretro_callbacks; +use crate::result::{Error, Result}; +use ffi::CString; +use libloading::Library; +use libretro_sys::*; +use std::collections::HashMap; +use std::ffi; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; +use std::{fs, mem::MaybeUninit}; -/// Sets the callback used to update video. -pub fn set_video_update_callback(cb: impl FnMut(&[u32]) + 'static) { - unsafe { - FRONTEND_IMPL.set_video_update_callback(cb); - } -} +use tracing::{error, info}; -/// Sets the callback for video resize. -pub fn set_video_resize_callback(cb: impl FnMut(u32, u32) + 'static) { - unsafe { - FRONTEND_IMPL.set_video_resize_callback(cb); - } -} +// FIXME(lily): Rust 2024 will make a good chunk of this code illegal. +// It might be wise to just bind some "simpler" C++ code and make it safe with lifetimes here, +// or something. It's a bit of a pickle. -/// Sets the callback for audio samples. -pub fn set_audio_sample_callback(cb: impl FnMut(&[i16], usize) + 'static) { - unsafe { - FRONTEND_IMPL.set_audio_sample_callback(cb); - } -} - -/// Sets the callback for input polling. -pub fn set_input_poll_callback(cb: impl FnMut() + 'static) { - unsafe { - FRONTEND_IMPL.set_input_poll_callback(cb); - } -} - -/// Sets the given port's input device. This takes a implementation of the [crate::joypad::Joypad] -/// trait, which will provide the information needed for libretro to work and all that. -pub fn set_input_port_device(port: u32, device: Rc>) { - unsafe { - FRONTEND_IMPL.set_input_port_device(port, device); - } -} - -/// Loads a core from the given path into the global frontend state. +/// The currently running frontend. /// -/// ```rust -/// use retro_frontend::frontend; -/// frontend::load_core("./cores/gbasp.so"); -/// ``` -pub fn load_core>(path: P) -> Result<()> { - unsafe { FRONTEND_IMPL.load_core(path) } +/// # Safety +/// Libretro itself is not thread safe, so we do not try and pretend that we are. +/// Only one instance of Frontend can be active in an application. +pub(crate) static mut FRONTEND: *mut Frontend = std::ptr::null_mut(); + + +/// Interface for the frontend to call to user code. +pub trait FrontendInterface { + fn video_update(&mut self, slice: &[u32]); + fn video_resize(&mut self, width: u32, height: u32); + + // TODO(lily): This should probably return the amount of consumed frames, + // as in some cases that *might* differ? + fn audio_sample(&mut self, slice: &[i16], size: usize); + + fn input_poll(&mut self); } -/// Unloads the core currently running in the global frontend state. -/// -/// ```rust -/// use retro_frontend::frontend; -/// frontend::unload_core(); -/// ``` -pub fn unload_core() -> Result<()> { - unsafe { FRONTEND_IMPL.unload_core() } +pub struct Frontend { + /// The current core's libretro functions. + pub(crate) core_api: Option, + + /// The current core library. + pub(crate) core_library: Option>, + + pub(crate) game_loaded: bool, + + pub(crate) av_info: Option, + + /// Core requested pixel format. + pub(crate) pixel_format: PixelFormat, + + // Converted pixel buffer. We store it here so we don't keep allocating over and over. + pub(crate) converted_pixel_buffer: Vec, + + pub(crate) fb_width: u32, + pub(crate) fb_height: u32, + pub(crate) fb_pitch: u32, + + pub(crate) system_directory: CString, + pub(crate) save_directory: CString, + + pub(crate) joypads: HashMap, + + pub(crate) interface: *mut dyn FrontendInterface, } -/// Loads a ROM into the given core. This function requires that [load_core] has been called and has succeeded first. -/// -/// ```rust -/// use retro_frontend::frontend; -/// frontend::load_game("./roms/sma2.gba"); -/// ``` -pub fn load_game>(path: P) -> Result<()> { - unsafe { FRONTEND_IMPL.load_game(path) } +impl Frontend { + /// Creates a new boxed frontend instance. Note that the returned [Box] + /// must be held until this frontend is no longer used. + pub fn new(interface: *mut dyn FrontendInterface) -> Box { + let mut boxed = Box::new(Self { + core_api: None, + core_library: None, + + game_loaded: false, + + av_info: None, + + pixel_format: PixelFormat::RGB565, + converted_pixel_buffer: Vec::new(), + + fb_width: 0, + fb_height: 0, + fb_pitch: 0, + + // TODO: We should let callers set these, probably. + // For now, this is probably fine. + system_directory: CString::new("system").unwrap(), + save_directory: CString::new("save").unwrap(), + + joypads: HashMap::new(), + + interface: interface, + }); + + // Assign to the global fronend pointer + unsafe { + assert!(FRONTEND.is_null(), "Cannot have multiple sir."); + FRONTEND = &mut *boxed as *mut Frontend; + } + + boxed + } + + pub fn core_loaded(&self) -> bool { + // Ideally this logic could be simplified but just to make sure.. + self.core_library.is_some() && self.core_api.is_some() + } + + pub fn set_input_port_device(&mut self, port: u32, device: *mut dyn Joypad) { + if self.core_loaded() { + let core_api = self.core_api.as_mut().unwrap(); + + unsafe { + (core_api.retro_set_controller_port_device)(port, (*device).device_type()); + } + + self.joypads.insert(port, device); + } + } + + // clear_input_port_device? + + pub fn load_core>(&mut self, path: P) -> Result<()> { + if self.core_loaded() { + return Err(Error::CoreAlreadyLoaded); + } + + println!("load_core()"); + + unsafe { + let lib = Box::new(Library::new(path.as_ref())?); + + // bleh; CoreAPI doesn't implement Default so I can't do this in a "good" way + let mut api_uninitialized: MaybeUninit = MaybeUninit::zeroed(); + let api_ptr = api_uninitialized.as_mut_ptr(); + + // helper for DRY reasons + macro_rules! load_symbol { + ($name:ident) => { + (*api_ptr).$name = *(lib.get(stringify!($name).as_bytes())?); + }; + } + + load_symbol!(retro_set_environment); + load_symbol!(retro_set_video_refresh); + load_symbol!(retro_set_audio_sample); + load_symbol!(retro_set_audio_sample_batch); + load_symbol!(retro_set_input_poll); + load_symbol!(retro_set_input_state); + load_symbol!(retro_init); + load_symbol!(retro_deinit); + load_symbol!(retro_api_version); + load_symbol!(retro_get_system_info); + load_symbol!(retro_get_system_av_info); + load_symbol!(retro_set_controller_port_device); + load_symbol!(retro_reset); + load_symbol!(retro_run); + load_symbol!(retro_serialize_size); + load_symbol!(retro_serialize); + load_symbol!(retro_unserialize); + load_symbol!(retro_cheat_reset); + load_symbol!(retro_cheat_set); + load_symbol!(retro_load_game); + load_symbol!(retro_load_game_special); + load_symbol!(retro_unload_game); + load_symbol!(retro_get_region); + load_symbol!(retro_get_memory_data); + load_symbol!(retro_get_memory_size); + + // If we get here, then we have initalized all the core API without failing. + // We can now get an initalized CoreAPI. + let core_api = api_uninitialized.assume_init(); + + // Let's sanity check the libretro API version against bindings to make sure we can actually use this core. + // If we can't then fail the load. + let api_version = (core_api.retro_api_version)(); + if api_version != libretro_sys::API_VERSION { + error!( + "Core {} has invalid API version {api_version}; refusing to continue loading", + path.as_ref().display() + ); + return Err(Error::InvalidLibRetroAPI { + expected: libretro_sys::API_VERSION, + got: api_version, + }); + } + + // Set required libretro callbacks before calling libretro_init. + // Some cores expect some callbacks to be set before libretro_init is called, + // some cores don't. For maximum compatibility, pamper the cores which do. + (core_api.retro_set_environment)(libretro_callbacks::environment_callback); + + // Initalize the libretro core. We do this first because + // there are a Few cores which initalize resources that later + // are poked by the later callback setting that could break if we don't. + (core_api.retro_init)(); + + // Set more libretro callbacks now that we have initalized the core. + (core_api.retro_set_video_refresh)(libretro_callbacks::video_refresh_callback); + (core_api.retro_set_input_poll)(libretro_callbacks::input_poll_callback); + (core_api.retro_set_input_state)(libretro_callbacks::input_state_callback); + (core_api.retro_set_audio_sample_batch)( + libretro_callbacks::audio_sample_batch_callback, + ); + + (core_api.retro_reset)(); + + info!("Core {} loaded", path.as_ref().display()); + + // Get AV info + // Like core API, we have to MaybeUninit again. + let mut av_info: MaybeUninit = MaybeUninit::uninit(); + (core_api.retro_get_system_av_info)(av_info.as_mut_ptr()); + + self.av_info = Some(av_info.assume_init()); + + self.core_library = Some(lib); + self.core_api = Some(core_api); + } + + Ok(()) + } + + pub fn unload_core(&mut self) -> Result<()> { + if !self.core_loaded() { + return Err(Error::CoreNotLoaded); + } + + if self.game_loaded { + self.unload_game()?; + } + + // First deinitalize the libretro core before unloading the library. + if let Some(core_api) = &self.core_api { + unsafe { + (core_api.retro_deinit)(); + } + } + + // Unload the library. We don't worry about error handling right now, but + // we could. + let lib = self.core_library.take().unwrap(); + lib.close()?; + + self.core_api = None; + self.core_library = None; + + // FIXME: Do other various cleanup (when we need to do said cleanup) + self.av_info = None; + + self.fb_width = 0; + self.fb_height = 0; + self.fb_pitch = 0; + + // disconnect all currently connected joypads + self.joypads.clear(); + + Ok(()) + } + + pub fn load_game>(&mut self, path: P) -> Result<()> { + if !self.core_loaded() { + return Err(Error::CoreNotLoaded); + } + + // For now I'm only implementing the gameinfo garbage that + // makes you read the whole file in. Later on I'll look into VFS + // support; but for now, it seems more cores will probably + // play ball with this.. which sucks :( + + // I'm aware this is nasty but bleh + let slice = path.as_ref().as_os_str().as_bytes(); + let path_string = CString::new(slice).expect("shouldn't fail"); + let contents = fs::read(path)?; + + let gameinfo = GameInfo { + path: path_string.as_ptr(), + data: contents.as_ptr() as *const ffi::c_void, + size: contents.len(), + meta: std::ptr::null(), + }; + + let core_api = self.core_api.as_ref().unwrap(); + + unsafe { + if !(core_api.retro_load_game)(&gameinfo) { + return Err(Error::RomLoadFailed); + } + + self.game_loaded = true; + Ok(()) + } + } + + pub fn unload_game(&mut self) -> Result<()> { + if !self.core_loaded() { + return Err(Error::CoreNotLoaded); + } + + let core_api = self.core_api.as_ref().unwrap(); + + if self.game_loaded { + unsafe { + (core_api.retro_unload_game)(); + } + + self.game_loaded = false; + } + + Ok(()) + } + + pub fn get_av_info(&mut self) -> Result { + if !self.core_loaded() { + return Err(Error::CoreNotLoaded); + } + + if let Some(av) = self.av_info.as_ref() { + Ok(av.clone()) + } else { + Err(Error::NoAvInfo) + } + } + + pub fn get_size(&mut self) -> (u32, u32) { + (self.fb_width, self.fb_height) + } + + pub fn reset(&mut self) { + let core_api = self.core_api.as_ref().unwrap(); + + unsafe { + (core_api.retro_reset)(); + } + } + + pub fn run_frame(&mut self) { + let core_api = self.core_api.as_ref().unwrap(); + + unsafe { + (core_api.retro_run)(); + } + } } -/// Unloads a ROM from the given core. -pub fn unload_game() -> Result<()> { - unsafe { FRONTEND_IMPL.unload_game() } -} +impl Drop for Frontend { + fn drop(&mut self) { + if self.core_loaded() { + let _ = self.unload_core(); + } -/// Gets the core's current AV information. -pub fn get_av_info() -> Result { - unsafe { FRONTEND_IMPL.get_av_info() } -} - -/// Gets the current framebuffer width and height as a tuple. -pub fn get_size() -> (u32, u32) { - unsafe { FRONTEND_IMPL.get_size() } -} - -/// Runs the currently loaded core for one video frame. -pub fn run_frame() { - unsafe { FRONTEND_IMPL.run() } + unsafe { + assert!(!FRONTEND.is_null()); + FRONTEND = std::ptr::null_mut(); + } + } } diff --git a/crates/retro_frontend/src/frontend_impl.rs b/crates/retro_frontend/src/frontend_impl.rs deleted file mode 100644 index a6e1c95..0000000 --- a/crates/retro_frontend/src/frontend_impl.rs +++ /dev/null @@ -1,344 +0,0 @@ -use crate::joypad::Joypad; -use crate::libretro_callbacks; -use crate::result::{Error, Result}; -use ffi::CString; -use libloading::Library; -use libretro_sys::*; -use once_cell::sync::Lazy; -use std::collections::HashMap; -use std::os::unix::ffi::OsStrExt; -use std::path::Path; -use std::{fs, mem::MaybeUninit}; - -use std::cell::RefCell; -use std::rc::Rc; - -use std::ffi; - -use tracing::{error, info}; - -// FIXME(lily): Rust 2024 will make a good chunk of this code illegal. -// It might be wise to just bind some "simpler" C++ code and make it safe with lifetimes here, -// or something. It's a bit of a pickle. - -/// The frontend implementation. -/// -/// # Safety -/// Note that Libretro itself is not thread safe, so we do not try and pretend -/// that we are thread safe either. -pub(crate) static mut FRONTEND_IMPL: Lazy = - Lazy::new(|| FrontendStateImpl::new()); - -pub(crate) type VideoUpdateCallback = dyn FnMut(&[u32]); -pub(crate) type VideoResizeCallback = dyn FnMut(u32, u32); - -// TODO(lily): This should probably return the amount of consumed frames, -// as in some cases that *might* differ? -pub(crate) type AudioSampleCallback = dyn FnMut(&[i16], usize); - -pub(crate) type InputPollCallback = dyn FnMut(); - -pub(crate) struct FrontendStateImpl { - /// The current core's libretro functions. - pub(crate) core_api: Option, - - /// The current core library. - pub(crate) core_library: Option>, - - pub(crate) game_loaded: bool, - - pub(crate) av_info: Option, - - /// Core requested pixel format. - pub(crate) pixel_format: PixelFormat, - - // Converted pixel buffer. We store it here so we don't keep allocating over and over. - pub(crate) converted_pixel_buffer: Vec, - - pub(crate) fb_width: u32, - pub(crate) fb_height: u32, - pub(crate) fb_pitch: u32, - - pub(crate) system_directory: CString, - pub(crate) save_directory: CString, - - pub(crate) joypads: HashMap>>, - - // Callbacks that consumers can set - pub(crate) video_update_callback: Option>, - pub(crate) video_resize_callback: Option>, - pub(crate) audio_sample_callback: Option>, - pub(crate) input_poll_callback: Option>, -} - -impl FrontendStateImpl { - fn new() -> Self { - Self { - core_api: None, - core_library: None, - - game_loaded: false, - - av_info: None, - - pixel_format: PixelFormat::RGB565, - converted_pixel_buffer: Vec::new(), - - fb_width: 0, - fb_height: 0, - fb_pitch: 0, - - // TODO: We should let callers set these!! - system_directory: CString::new("system").unwrap(), - save_directory: CString::new("save").unwrap(), - - joypads: HashMap::new(), - - video_update_callback: None, - video_resize_callback: None, - audio_sample_callback: None, - input_poll_callback: None, - } - } - - pub(crate) fn core_loaded(&self) -> bool { - // Ideally this logic could be simplified but just to make sure.. - self.core_library.is_some() && self.core_api.is_some() - } - - pub(crate) fn set_video_update_callback(&mut self, cb: impl FnMut(&[u32]) + 'static) { - self.video_update_callback = Some(Box::new(cb)); - } - - pub(crate) fn set_video_resize_callback(&mut self, cb: impl FnMut(u32, u32) + 'static) { - self.video_resize_callback = Some(Box::new(cb)); - } - - pub(crate) fn set_audio_sample_callback(&mut self, cb: impl FnMut(&[i16], usize) + 'static) { - self.audio_sample_callback = Some(Box::new(cb)); - } - - pub(crate) fn set_input_poll_callback(&mut self, cb: impl FnMut() + 'static) { - self.input_poll_callback = Some(Box::new(cb)); - } - - pub(crate) fn set_input_port_device(&mut self, port: u32, device: Rc>) { - if self.core_loaded() { - let core_api = self.core_api.as_mut().unwrap(); - - unsafe { - (core_api.retro_set_controller_port_device)(port, device.borrow().device_type()); - } - - self.joypads.insert(port, device); - } - } - - // clear_input_port_device? - - pub(crate) fn load_core>(&mut self, path: P) -> Result<()> { - if self.core_loaded() { - return Err(Error::CoreAlreadyLoaded); - } - - unsafe { - let lib = Box::new(Library::new(path.as_ref())?); - - // bleh; CoreAPI doesn't implement Default so I can't do this in a "good" way - let mut api_uninitialized: MaybeUninit = MaybeUninit::zeroed(); - let api_ptr = api_uninitialized.as_mut_ptr(); - - // helper for DRY reasons - macro_rules! load_symbol { - ($name:ident) => { - (*api_ptr).$name = *(lib.get(stringify!($name).as_bytes())?); - }; - } - - load_symbol!(retro_set_environment); - load_symbol!(retro_set_video_refresh); - load_symbol!(retro_set_audio_sample); - load_symbol!(retro_set_audio_sample_batch); - load_symbol!(retro_set_input_poll); - load_symbol!(retro_set_input_state); - load_symbol!(retro_init); - load_symbol!(retro_deinit); - load_symbol!(retro_api_version); - load_symbol!(retro_get_system_info); - load_symbol!(retro_get_system_av_info); - load_symbol!(retro_set_controller_port_device); - load_symbol!(retro_reset); - load_symbol!(retro_run); - load_symbol!(retro_serialize_size); - load_symbol!(retro_serialize); - load_symbol!(retro_unserialize); - load_symbol!(retro_cheat_reset); - load_symbol!(retro_cheat_set); - load_symbol!(retro_load_game); - load_symbol!(retro_load_game_special); - load_symbol!(retro_unload_game); - load_symbol!(retro_get_region); - load_symbol!(retro_get_memory_data); - load_symbol!(retro_get_memory_size); - - // If we get here, then we have initalized all the core API without failing. - // We can now get an initalized CoreAPI. - let core_api = api_uninitialized.assume_init(); - - // Let's sanity check the libretro API version against bindings to make sure we can actually use this core. - // If we can't then fail the load. - let api_version = (core_api.retro_api_version)(); - if api_version != libretro_sys::API_VERSION { - error!( - "Core {} has invalid API version {api_version}; refusing to continue loading", - path.as_ref().display() - ); - return Err(Error::InvalidLibRetroAPI { - expected: libretro_sys::API_VERSION, - got: api_version, - }); - } - - // Set required libretro callbacks before calling libretro_init. - // Some cores expect some callbacks to be set before libretro_init is called, - // some cores don't. For maximum compatibility, pamper the cores which do. - (core_api.retro_set_environment)(libretro_callbacks::environment_callback); - - // Initalize the libretro core. We do this first because - // there are a Few cores which initalize resources that later - // are poked by the later callback setting that could break if we don't. - (core_api.retro_init)(); - - // Set more libretro callbacks now that we have initalized the core. - (core_api.retro_set_video_refresh)(libretro_callbacks::video_refresh_callback); - (core_api.retro_set_input_poll)(libretro_callbacks::input_poll_callback); - (core_api.retro_set_input_state)(libretro_callbacks::input_state_callback); - (core_api.retro_set_audio_sample_batch)( - libretro_callbacks::audio_sample_batch_callback, - ); - - info!("Core {} loaded", path.as_ref().display()); - - // Get AV info - // Like core API, we have to MaybeUninit again. - let mut av_info: MaybeUninit = MaybeUninit::uninit(); - (core_api.retro_get_system_av_info)(av_info.as_mut_ptr()); - - self.av_info = Some(av_info.assume_init()); - - self.core_library = Some(lib); - self.core_api = Some(core_api); - } - - Ok(()) - } - - pub(crate) fn unload_core(&mut self) -> Result<()> { - if !self.core_loaded() { - return Err(Error::CoreNotLoaded); - } - - if self.game_loaded { - self.unload_game()?; - } - - // First deinitalize the libretro core before unloading the library. - if let Some(core_api) = &self.core_api { - unsafe { - (core_api.retro_deinit)(); - } - } - - // Unload the library. We don't worry about error handling right now, but - // we could. - let lib = self.core_library.take().unwrap(); - lib.close()?; - - self.core_api = None; - self.core_library = None; - - // FIXME: Do other various cleanup (when we need to do said cleanup) - self.av_info = None; - - self.fb_width = 0; - self.fb_height = 0; - self.fb_pitch = 0; - - // disconnect all currently connected joypads - self.joypads.clear(); - - Ok(()) - } - - pub(crate) fn load_game>(&mut self, path: P) -> Result<()> { - if !self.core_loaded() { - return Err(Error::CoreNotLoaded); - } - - // For now I'm only implementing the gameinfo garbage that - // makes you read the whole file in. Later on I'll look into VFS - // support; but for now, it seems more cores will probably - // play ball with this.. which sucks :( - - // I'm aware this is nasty but bleh - let slice = path.as_ref().as_os_str().as_bytes(); - let path_string = CString::new(slice).expect("shouldn't fail"); - let contents = fs::read(path)?; - - let gameinfo = GameInfo { - path: path_string.as_ptr(), - data: contents.as_ptr() as *const ffi::c_void, - size: contents.len(), - meta: std::ptr::null(), - }; - - let core_api = self.core_api.as_ref().unwrap(); - - unsafe { - if !(core_api.retro_load_game)(&gameinfo) { - return Err(Error::RomLoadFailed); - } - - self.game_loaded = true; - Ok(()) - } - } - - pub(crate) fn unload_game(&mut self) -> Result<()> { - if !self.core_loaded() { - return Err(Error::CoreNotLoaded); - } - - let core_api = self.core_api.as_ref().unwrap(); - - if self.game_loaded { - unsafe { - (core_api.retro_unload_game)(); - } - - self.game_loaded = false; - } - - Ok(()) - } - - pub(crate) fn get_av_info(&mut self) -> Result { - if !self.core_loaded() { - return Err(Error::CoreNotLoaded); - } - - Ok(self.av_info.as_ref().unwrap().clone()) - } - - pub(crate) fn get_size(&mut self) -> (u32, u32) { - (self.fb_width, self.fb_height) - } - - pub(crate) fn run(&mut self) { - let core_api = self.core_api.as_ref().unwrap(); - - unsafe { - (core_api.retro_run)(); - } - } -} diff --git a/crates/retro_frontend/src/lib.rs b/crates/retro_frontend/src/lib.rs index 01381c9..b5d8e31 100644 --- a/crates/retro_frontend/src/lib.rs +++ b/crates/retro_frontend/src/lib.rs @@ -1,16 +1,11 @@ //! A libretro frontend as a reusable library crate. -mod frontend_impl; mod libretro_callbacks; mod libretro_log; pub mod libretro_sys_new; -pub mod core; - pub mod joypad; - -//#[macro_use] pub mod util; pub mod frontend; diff --git a/crates/retro_frontend/src/libretro_callbacks.rs b/crates/retro_frontend/src/libretro_callbacks.rs index 80aef0e..aa6eda9 100644 --- a/crates/retro_frontend/src/libretro_callbacks.rs +++ b/crates/retro_frontend/src/libretro_callbacks.rs @@ -1,5 +1,6 @@ +//! Callbacks for libretro use crate::libretro_sys_new::*; -use crate::{frontend_impl::*, libretro_log, util}; +use crate::{frontend::*, libretro_log, util}; use rgb565::Rgb565; @@ -68,19 +69,19 @@ pub(crate) unsafe extern "C" fn environment_callback( } ENVIRONMENT_GET_SYSTEM_DIRECTORY => { - *(data as *mut *const ffi::c_char) = FRONTEND_IMPL.system_directory.as_ptr(); + *(data as *mut *const ffi::c_char) = (*FRONTEND).system_directory.as_ptr(); return true; } ENVIRONMENT_GET_SAVE_DIRECTORY => { - *(data as *mut *const ffi::c_char) = FRONTEND_IMPL.save_directory.as_ptr(); + *(data as *mut *const ffi::c_char) = (*FRONTEND).save_directory.as_ptr(); return true; } ENVIRONMENT_SET_PIXEL_FORMAT => { let _pixel_format = *(data as *const ffi::c_uint); let pixel_format = PixelFormat::from_uint(_pixel_format).unwrap(); - FRONTEND_IMPL.pixel_format = pixel_format; + (*FRONTEND).pixel_format = pixel_format; return true; } @@ -91,12 +92,10 @@ pub(crate) unsafe extern "C" fn environment_callback( let geometry = (data as *const GameGeometry).as_ref().unwrap(); - FRONTEND_IMPL.fb_width = geometry.base_width; - FRONTEND_IMPL.fb_height = geometry.base_height; + (*FRONTEND).fb_width = geometry.base_width; + (*FRONTEND).fb_height = geometry.base_height; - if let Some(resize_callback) = &mut FRONTEND_IMPL.video_resize_callback { - resize_callback(geometry.base_width, geometry.base_height); - } + (*(*FRONTEND).interface).video_resize(geometry.base_width, geometry.base_height); return true; } @@ -110,8 +109,22 @@ pub(crate) unsafe extern "C" fn environment_callback( let var = (data as *mut Variable).as_mut().unwrap(); match ffi::CStr::from_ptr(var.key).to_str() { - Ok(_key) => { - debug!("Core wants to get variable \"{_key}\"",); + Ok(key) => { + debug!("Core wants to get variable \"{key}\""); + + // HACK for SwanStation. I really should just serialize these to TOML or something. + // (each core iirc provides its own name, so i can just do config/[core].toml) + if key == "swanstation_MemoryCards_Card1Type" + || key == "swanstation_MemoryCards_Card2Type" + { + const MC_TYPE: &'static [u8] = b"Shared\0"; + info!("Forcing {key} to be shared"); + + // what an amazing api with a lot + // *(data as *mut *const ffi::c_char) + var.value = MC_TYPE.as_ptr() as *const i8; + return true; + } return false; } Err(err) => { @@ -173,14 +186,14 @@ pub(crate) unsafe extern "C" fn video_refresh_callback( //info!("Video refresh called, {width}, {height}, {pitch}"); // bleh - FRONTEND_IMPL.fb_width = width; - FRONTEND_IMPL.fb_height = height; - FRONTEND_IMPL.fb_pitch = - pitch as u32 / util::bytes_per_pixel_from_libretro(FRONTEND_IMPL.pixel_format); + (*FRONTEND).fb_width = width; + (*FRONTEND).fb_height = height; + (*FRONTEND).fb_pitch = + pitch as u32 / util::bytes_per_pixel_from_libretro((*FRONTEND).pixel_format); - let pitch = FRONTEND_IMPL.fb_pitch as usize; + let pitch = (*FRONTEND).fb_pitch as usize; - match FRONTEND_IMPL.pixel_format { + match (*FRONTEND).pixel_format { PixelFormat::RGB565 => { let pixel_data_slice = std::slice::from_raw_parts( pixels as *const u16, @@ -188,16 +201,14 @@ pub(crate) unsafe extern "C" fn video_refresh_callback( ); // Resize the pixel buffer if we need to - if (pitch * height as usize) as usize != FRONTEND_IMPL.converted_pixel_buffer.len() { + if (pitch * height as usize) as usize != (*FRONTEND).converted_pixel_buffer.len() { info!("Resizing RGB565 -> RGBA buffer"); - FRONTEND_IMPL + (*FRONTEND) .converted_pixel_buffer .resize((pitch * height as usize) as usize, 0); // some cores are stupid - if let Some(resize_callback) = &mut FRONTEND_IMPL.video_resize_callback { - resize_callback(pitch as u32, height); - } + (*(*FRONTEND).interface).video_resize(pitch as u32, height); } // TODO: Make this convert from weird pitches to native resolution where possible. @@ -207,14 +218,12 @@ pub(crate) unsafe extern "C" fn video_refresh_callback( let comp = rgb.to_rgb888_components(); // Finally save the pixel data in the result array as an XRGB8888 value - FRONTEND_IMPL.converted_pixel_buffer[y * pitch as usize + x] = + (*FRONTEND).converted_pixel_buffer[y * pitch as usize + x] = ((comp[0] as u32) << 16) | ((comp[1] as u32) << 8) | (comp[2] as u32); } } - if let Some(update_callback) = &mut FRONTEND_IMPL.video_update_callback { - update_callback(&FRONTEND_IMPL.converted_pixel_buffer[..]); - } + (*(*FRONTEND).interface).video_update(&(*FRONTEND).converted_pixel_buffer[..]); } _ => { let pixel_data_slice = std::slice::from_raw_parts( @@ -222,17 +231,13 @@ pub(crate) unsafe extern "C" fn video_refresh_callback( (pitch * height as usize) as usize, ); - if let Some(update_callback) = &mut FRONTEND_IMPL.video_update_callback { - update_callback(&pixel_data_slice); - } + (*(*FRONTEND).interface).video_update(&pixel_data_slice); } } } pub(crate) unsafe extern "C" fn input_poll_callback() { - if let Some(poll) = &mut FRONTEND_IMPL.input_poll_callback { - poll(); - } + (*(*FRONTEND).interface).input_poll(); } pub(crate) unsafe extern "C" fn input_state_callback( @@ -241,15 +246,14 @@ pub(crate) unsafe extern "C" fn input_state_callback( _index: ffi::c_uint, // not used? button_id: ffi::c_uint, ) -> ffi::c_short { - if FRONTEND_IMPL.joypads.contains_key(&port) { - let joypad = FRONTEND_IMPL + if (*FRONTEND).joypads.contains_key(&port) { + let joypad = *(*FRONTEND) .joypads .get(&port) - .expect("How do we get here when contains_key() returns true but the key doen't exist") - .borrow(); + .expect("How do we get here when contains_key() returns true but the key doen't exist"); - if device == joypad.device_type() { - return joypad.get_button(button_id); + if device == (*joypad).device_type() { + return (*joypad).get_button(button_id); } } @@ -261,12 +265,10 @@ pub(crate) unsafe extern "C" fn audio_sample_batch_callback( samples: *const i16, frames: usize, ) -> usize { - if let Some(callback) = &mut FRONTEND_IMPL.audio_sample_callback { - let slice = std::slice::from_raw_parts(samples, frames * 2); + let slice = std::slice::from_raw_parts(samples, frames * 2); - // I might not need to give the callback the amount of frames since it can figure it out as - // slice.len() / 2, but /shrug - callback(slice, frames); - } + // I might not need to give the callback the amount of frames since it can figure it out as + // slice.len() / 2, but /shrug + (*(*FRONTEND).interface).audio_sample(slice, frames); frames } diff --git a/crates/retro_frontend/src/result.rs b/crates/retro_frontend/src/result.rs index 6961628..a0a21c9 100644 --- a/crates/retro_frontend/src/result.rs +++ b/crates/retro_frontend/src/result.rs @@ -8,6 +8,9 @@ pub enum Error { #[error(transparent)] IoError(#[from] std::io::Error), + #[error("a core has not provided AV info")] + NoAvInfo, + #[error("expected core API version {expected}, but core returned {got}")] InvalidLibRetroAPI { expected: u32, got: u32 }, diff --git a/crates/retrovnc/src/main.rs b/crates/retrovnc/src/main.rs index a9b8aa5..3430aec 100644 --- a/crates/retrovnc/src/main.rs +++ b/crates/retrovnc/src/main.rs @@ -1,14 +1,10 @@ -use std::{ - cell::RefCell, - rc::Rc, -}; +use std::path::Path; use anyhow::Result; use retro_frontend::{ - core::Core, - frontend, - joypad::{Joypad, RetroPad} + frontend::{Frontend, FrontendInterface}, + joypad::{Joypad, RetroPad}, }; use tracing::Level; use tracing_subscriber::FmtSubscriber; @@ -19,70 +15,92 @@ mod rfb; use rfb::*; struct App { + frontend: Option>, rfb_server: Box, - pad: Rc>, + pad: RetroPad, } impl App { - fn new() -> Result { - Ok(Self { + fn new() -> Result> { + let mut boxed = Box::new(Self { + frontend: None, rfb_server: RfbServer::new(RfbServerConfig { width: 640, height: 480, })?, // nasty, but idk a better way - pad: Rc::new(RefCell::new(RetroPad::new())), - }) + pad: RetroPad::new(), + }); + + // Very very nasty, but honestly it works. + // I'll look into cleaning it up later. + let obj = &mut *boxed as &mut dyn FrontendInterface; + boxed.frontend = Some(Frontend::new(obj as *mut dyn FrontendInterface)); + + Ok(boxed) } - fn new_and_init() -> Result>> { - let app = App::new()?; - let rc = Rc::new(RefCell::new(app)); - - // Initalize all the frontend callbacks and stuff. - App::init(&rc); - - Ok(rc) + fn get_frontend(&mut self) -> &mut Frontend { + self.frontend.as_mut().unwrap() } - /// Initalizes the frontend library with callbacks back to us, - /// and performs an initial window resize. - fn init(rc: &Rc>) { - let app_clone = rc.clone(); - frontend::set_video_update_callback(move |slice| { - app_clone.borrow_mut().frame_update(slice); - }); + fn init(&mut self) { + // Currently retrovnc just hardcodes the assumption of a single RetroPad. + let pad = &mut self.pad as *mut dyn Joypad; - let app_resize_clone = rc.clone(); - frontend::set_video_resize_callback(move |width, height| { - app_resize_clone.borrow_mut().resize(width, height); - }); + self.get_frontend().set_input_port_device(0, pad); - frontend::set_audio_sample_callback(|_slice, _frames| { - //println!("Got audio sample batch with {_frames} frames"); - }); + // Initalize the display + self.init_display(); + } - let app_input_poll_clone = rc.clone(); - frontend::set_input_poll_callback(move || { - app_input_poll_clone.borrow_mut().input_poll(); - }); - - // Currently retrodemo just hardcodes the assumption of a single RetroPad. - frontend::set_input_port_device(0, rc.borrow().pad.clone()); - - let av_info = frontend::get_av_info().expect("No AV info"); + fn init_display(&mut self) { + let av_info = self.get_frontend().get_av_info().expect("No AV info"); // Start VNC server. { - let server = &mut rc.borrow_mut().rfb_server; + let server = &mut self.rfb_server; tracing::info!("Starting VNC server"); server.start(); - server.resize(av_info.geometry.base_width as u16, av_info.geometry.base_height as u16); + server.resize( + av_info.geometry.base_width as u16, + av_info.geometry.base_height as u16, + ); } } - /// Called by the frontend library when a video resize needs to occur. - fn resize(&mut self, width: u32, height: u32) { + fn load_core>(&mut self, path: P) -> Result<(), retro_frontend::result::Error> { + if self.get_frontend().core_loaded() { + println!("???"); + let _ = self.get_frontend().unload_core(); + } + + self.get_frontend().load_core(path)?; + Ok(()) + } + + fn load_game>(&mut self, path: P) -> Result<(), retro_frontend::result::Error> { + self.get_frontend().load_game(path)?; + Ok(()) + } + + // Main loop + fn main_loop(&mut self) -> ! { + let frontend = self.get_frontend(); + + let av_info = frontend.get_av_info().expect("???"); + let step_ms = ((1.0 / av_info.timing.fps) * 1000.) as u64; + + // Do the main loop + loop { + frontend.run_frame(); + std::thread::sleep(std::time::Duration::from_millis(step_ms)); + } + } +} + +impl FrontendInterface for App { + fn video_resize(&mut self, width: u32, height: u32) { //let width = width * 2; //let height = height * 2; @@ -90,30 +108,25 @@ impl App { self.rfb_server.resize(width as u16, height as u16); } - /// Called by the frontend library on video frame updates - /// The framebuffer is *always* a RGBX8888 slice regardless of whatever video mode - /// the core has setup internally; this is by design to make code less annoying - fn frame_update(&mut self, slice: &[u32]) { - let framebuffer_size = frontend::get_size(); + fn video_update(&mut self, slice: &[u32]) { + let framebuffer_size = self.get_frontend().get_size(); self.rfb_server .update_buffer(&slice, framebuffer_size.0 as u16, framebuffer_size.1 as u16); } - /// Called by the frontend library during retro_run() to poll input - fn input_poll(&mut self) { - let mut pad = self.pad.borrow_mut(); + fn audio_sample(&mut self, _slice: &[i16], _size: usize) {} - pad.reset(); + fn input_poll(&mut self) { + self.pad.reset(); // Press all buttons the VNC server marked as pressed let buttons = self.rfb_server.get_buttons(); for i in 0..buttons.len() { if buttons[i] { - pad.press_button(i as u32, None); + self.pad.press_button(i as u32, None); } } } - } fn main() -> Result<()> { @@ -133,24 +146,17 @@ fn main() -> Result<()> { let core_path: &String = matches.get_one("core").unwrap(); - // Load the user's provided core - let mut core = Core::load(core_path)?; - // Initalize the app - let _app = App::new_and_init(); + let mut app = App::new()?; + + app.load_core(core_path)?; + + // Initalize app + app.init(); if let Some(rom_path) = matches.get_one::("rom") { - core.load_game(rom_path)? + app.load_game(rom_path)? } - let av_info = frontend::get_av_info()?; - let step_ms = ((1.0 / av_info.timing.fps) * 1000.) as u64; - - // Do the main loop - loop { - frontend::run_frame(); - std::thread::sleep(std::time::Duration::from_millis( - step_ms, - )); - } + app.main_loop(); }