refactoring

Removes the frontend glue module, instead the frontend state struct is allowed to be created by users. This doesn't solve Rust 2024 problems (although, since we're building c++ code anyways, we can always just stash the pointer there) but it's much cleaner.Some stuff is made to use pointers, this is just because I don't want to mess around with lifetime stuff right now (it all lasts as long as the app anyways)
This commit is contained in:
Lily Tsuru 2024-08-04 02:02:29 -04:00
parent e42cab442b
commit d4329be132
9 changed files with 474 additions and 587 deletions

1
Cargo.lock generated
View file

@ -397,7 +397,6 @@ dependencies = [
"libc",
"libloading",
"libretro-sys",
"once_cell",
"rgb565",
"thiserror",
"tracing",

View file

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

View file

@ -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<P: AsRef<Path>>(path: P) -> Result<Self> {
frontend::load_core(path.as_ref())?;
Ok(Self {})
}
/// Same as [frontend::load_game].
pub fn load_game<P: AsRef<Path>>(&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();
}
}

View file

@ -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<RefCell<dyn Joypad>>) {
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<P: AsRef<std::path::Path>>(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<CoreAPI>,
/// The current core library.
pub(crate) core_library: Option<Box<Library>>,
pub(crate) game_loaded: bool,
pub(crate) av_info: Option<SystemAvInfo>,
/// 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<u32>,
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<u32 /* port */, *mut dyn Joypad>,
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<P: AsRef<std::path::Path>>(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<Self> {
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<P: AsRef<Path>>(&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<CoreAPI> = 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<SystemAvInfo> = 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<P: AsRef<Path>>(&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<SystemAvInfo> {
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<SystemAvInfo> {
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();
}
}
}

View file

@ -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<FrontendStateImpl> =
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<CoreAPI>,
/// The current core library.
pub(crate) core_library: Option<Box<Library>>,
pub(crate) game_loaded: bool,
pub(crate) av_info: Option<SystemAvInfo>,
/// 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<u32>,
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<u32 /* port */, Rc<RefCell<dyn Joypad>>>,
// Callbacks that consumers can set
pub(crate) video_update_callback: Option<Box<VideoUpdateCallback>>,
pub(crate) video_resize_callback: Option<Box<VideoResizeCallback>>,
pub(crate) audio_sample_callback: Option<Box<AudioSampleCallback>>,
pub(crate) input_poll_callback: Option<Box<InputPollCallback>>,
}
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<RefCell<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.borrow().device_type());
}
self.joypads.insert(port, device);
}
}
// clear_input_port_device?
pub(crate) fn load_core<P: AsRef<Path>>(&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<CoreAPI> = 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<SystemAvInfo> = 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<P: AsRef<Path>>(&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<SystemAvInfo> {
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)();
}
}
}

View file

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

View file

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

View file

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

View file

@ -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<Box<Frontend>>,
rfb_server: Box<RfbServer>,
pad: Rc<RefCell<RetroPad>>,
pad: RetroPad,
}
impl App {
fn new() -> Result<Self> {
Ok(Self {
fn new() -> Result<Box<Self>> {
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<Rc<RefCell<App>>> {
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<RefCell<Self>>) {
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<P: AsRef<Path>>(&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<P: AsRef<Path>>(&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::<String>("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();
}