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:
parent
e42cab442b
commit
d4329be132
9 changed files with 474 additions and 587 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -397,7 +397,6 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"libloading",
|
"libloading",
|
||||||
"libretro-sys",
|
"libretro-sys",
|
||||||
"once_cell",
|
|
||||||
"rgb565",
|
"rgb565",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
|
@ -9,7 +9,6 @@ edition = "2021"
|
||||||
libc = "0.2.155"
|
libc = "0.2.155"
|
||||||
libloading = "0.8.3"
|
libloading = "0.8.3"
|
||||||
libretro-sys = "0.1.1"
|
libretro-sys = "0.1.1"
|
||||||
once_cell = "1.19.0"
|
|
||||||
rgb565 = "0.1.3"
|
rgb565 = "0.1.3"
|
||||||
thiserror = "1.0.61"
|
thiserror = "1.0.61"
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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::joypad::Joypad;
|
||||||
use crate::libretro_sys_new::*;
|
use crate::libretro_callbacks;
|
||||||
use crate::result::Result;
|
use crate::result::{Error, Result};
|
||||||
use std::cell::RefCell;
|
use ffi::CString;
|
||||||
use std::rc::Rc;
|
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.
|
use tracing::{error, info};
|
||||||
pub fn set_video_update_callback(cb: impl FnMut(&[u32]) + 'static) {
|
|
||||||
unsafe {
|
|
||||||
FRONTEND_IMPL.set_video_update_callback(cb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the callback for video resize.
|
// FIXME(lily): Rust 2024 will make a good chunk of this code illegal.
|
||||||
pub fn set_video_resize_callback(cb: impl FnMut(u32, u32) + 'static) {
|
// It might be wise to just bind some "simpler" C++ code and make it safe with lifetimes here,
|
||||||
unsafe {
|
// or something. It's a bit of a pickle.
|
||||||
FRONTEND_IMPL.set_video_resize_callback(cb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the callback for audio samples.
|
/// The currently running frontend.
|
||||||
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.
|
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// # Safety
|
||||||
/// use retro_frontend::frontend;
|
/// Libretro itself is not thread safe, so we do not try and pretend that we are.
|
||||||
/// frontend::load_core("./cores/gbasp.so");
|
/// Only one instance of Frontend can be active in an application.
|
||||||
/// ```
|
pub(crate) static mut FRONTEND: *mut Frontend = std::ptr::null_mut();
|
||||||
pub fn load_core<P: AsRef<std::path::Path>>(path: P) -> Result<()> {
|
|
||||||
unsafe { FRONTEND_IMPL.load_core(path) }
|
|
||||||
|
/// 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.
|
pub struct Frontend {
|
||||||
///
|
/// The current core's libretro functions.
|
||||||
/// ```rust
|
pub(crate) core_api: Option<CoreAPI>,
|
||||||
/// use retro_frontend::frontend;
|
|
||||||
/// frontend::unload_core();
|
/// The current core library.
|
||||||
/// ```
|
pub(crate) core_library: Option<Box<Library>>,
|
||||||
pub fn unload_core() -> Result<()> {
|
|
||||||
unsafe { FRONTEND_IMPL.unload_core() }
|
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.
|
impl Frontend {
|
||||||
///
|
/// Creates a new boxed frontend instance. Note that the returned [Box]
|
||||||
/// ```rust
|
/// must be held until this frontend is no longer used.
|
||||||
/// use retro_frontend::frontend;
|
pub fn new(interface: *mut dyn FrontendInterface) -> Box<Self> {
|
||||||
/// frontend::load_game("./roms/sma2.gba");
|
let mut boxed = Box::new(Self {
|
||||||
/// ```
|
core_api: None,
|
||||||
pub fn load_game<P: AsRef<std::path::Path>>(path: P) -> Result<()> {
|
core_library: None,
|
||||||
unsafe { FRONTEND_IMPL.load_game(path) }
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unloads a ROM from the given core.
|
boxed
|
||||||
pub fn unload_game() -> Result<()> {
|
|
||||||
unsafe { FRONTEND_IMPL.unload_game() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the core's current AV information.
|
pub fn core_loaded(&self) -> bool {
|
||||||
pub fn get_av_info() -> Result<SystemAvInfo> {
|
// Ideally this logic could be simplified but just to make sure..
|
||||||
unsafe { FRONTEND_IMPL.get_av_info() }
|
self.core_library.is_some() && self.core_api.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the current framebuffer width and height as a tuple.
|
pub fn set_input_port_device(&mut self, port: u32, device: *mut dyn Joypad) {
|
||||||
pub fn get_size() -> (u32, u32) {
|
if self.core_loaded() {
|
||||||
unsafe { FRONTEND_IMPL.get_size() }
|
let core_api = self.core_api.as_mut().unwrap();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
(core_api.retro_set_controller_port_device)(port, (*device).device_type());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs the currently loaded core for one video frame.
|
self.joypads.insert(port, device);
|
||||||
pub fn run_frame() {
|
}
|
||||||
unsafe { FRONTEND_IMPL.run() }
|
}
|
||||||
|
|
||||||
|
// 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)();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Frontend {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if self.core_loaded() {
|
||||||
|
let _ = self.unload_core();
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
assert!(!FRONTEND.is_null());
|
||||||
|
FRONTEND = std::ptr::null_mut();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +1,11 @@
|
||||||
//! A libretro frontend as a reusable library crate.
|
//! A libretro frontend as a reusable library crate.
|
||||||
|
|
||||||
mod frontend_impl;
|
|
||||||
mod libretro_callbacks;
|
mod libretro_callbacks;
|
||||||
mod libretro_log;
|
mod libretro_log;
|
||||||
|
|
||||||
pub mod libretro_sys_new;
|
pub mod libretro_sys_new;
|
||||||
|
|
||||||
pub mod core;
|
|
||||||
|
|
||||||
pub mod joypad;
|
pub mod joypad;
|
||||||
|
|
||||||
//#[macro_use]
|
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
pub mod frontend;
|
pub mod frontend;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
//! Callbacks for libretro
|
||||||
use crate::libretro_sys_new::*;
|
use crate::libretro_sys_new::*;
|
||||||
use crate::{frontend_impl::*, libretro_log, util};
|
use crate::{frontend::*, libretro_log, util};
|
||||||
|
|
||||||
use rgb565::Rgb565;
|
use rgb565::Rgb565;
|
||||||
|
|
||||||
|
@ -68,19 +69,19 @@ pub(crate) unsafe extern "C" fn environment_callback(
|
||||||
}
|
}
|
||||||
|
|
||||||
ENVIRONMENT_GET_SYSTEM_DIRECTORY => {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ENVIRONMENT_GET_SAVE_DIRECTORY => {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ENVIRONMENT_SET_PIXEL_FORMAT => {
|
ENVIRONMENT_SET_PIXEL_FORMAT => {
|
||||||
let _pixel_format = *(data as *const ffi::c_uint);
|
let _pixel_format = *(data as *const ffi::c_uint);
|
||||||
let pixel_format = PixelFormat::from_uint(_pixel_format).unwrap();
|
let pixel_format = PixelFormat::from_uint(_pixel_format).unwrap();
|
||||||
FRONTEND_IMPL.pixel_format = pixel_format;
|
(*FRONTEND).pixel_format = pixel_format;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,12 +92,10 @@ pub(crate) unsafe extern "C" fn environment_callback(
|
||||||
|
|
||||||
let geometry = (data as *const GameGeometry).as_ref().unwrap();
|
let geometry = (data as *const GameGeometry).as_ref().unwrap();
|
||||||
|
|
||||||
FRONTEND_IMPL.fb_width = geometry.base_width;
|
(*FRONTEND).fb_width = geometry.base_width;
|
||||||
FRONTEND_IMPL.fb_height = geometry.base_height;
|
(*FRONTEND).fb_height = geometry.base_height;
|
||||||
|
|
||||||
if let Some(resize_callback) = &mut FRONTEND_IMPL.video_resize_callback {
|
(*(*FRONTEND).interface).video_resize(geometry.base_width, geometry.base_height);
|
||||||
resize_callback(geometry.base_width, geometry.base_height);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,8 +109,22 @@ pub(crate) unsafe extern "C" fn environment_callback(
|
||||||
let var = (data as *mut Variable).as_mut().unwrap();
|
let var = (data as *mut Variable).as_mut().unwrap();
|
||||||
|
|
||||||
match ffi::CStr::from_ptr(var.key).to_str() {
|
match ffi::CStr::from_ptr(var.key).to_str() {
|
||||||
Ok(_key) => {
|
Ok(key) => {
|
||||||
debug!("Core wants to get variable \"{_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;
|
return false;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
@ -173,14 +186,14 @@ pub(crate) unsafe extern "C" fn video_refresh_callback(
|
||||||
//info!("Video refresh called, {width}, {height}, {pitch}");
|
//info!("Video refresh called, {width}, {height}, {pitch}");
|
||||||
|
|
||||||
// bleh
|
// bleh
|
||||||
FRONTEND_IMPL.fb_width = width;
|
(*FRONTEND).fb_width = width;
|
||||||
FRONTEND_IMPL.fb_height = height;
|
(*FRONTEND).fb_height = height;
|
||||||
FRONTEND_IMPL.fb_pitch =
|
(*FRONTEND).fb_pitch =
|
||||||
pitch as u32 / util::bytes_per_pixel_from_libretro(FRONTEND_IMPL.pixel_format);
|
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 => {
|
PixelFormat::RGB565 => {
|
||||||
let pixel_data_slice = std::slice::from_raw_parts(
|
let pixel_data_slice = std::slice::from_raw_parts(
|
||||||
pixels as *const u16,
|
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
|
// 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");
|
info!("Resizing RGB565 -> RGBA buffer");
|
||||||
FRONTEND_IMPL
|
(*FRONTEND)
|
||||||
.converted_pixel_buffer
|
.converted_pixel_buffer
|
||||||
.resize((pitch * height as usize) as usize, 0);
|
.resize((pitch * height as usize) as usize, 0);
|
||||||
|
|
||||||
// some cores are stupid
|
// some cores are stupid
|
||||||
if let Some(resize_callback) = &mut FRONTEND_IMPL.video_resize_callback {
|
(*(*FRONTEND).interface).video_resize(pitch as u32, height);
|
||||||
resize_callback(pitch as u32, height);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make this convert from weird pitches to native resolution where possible.
|
// 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();
|
let comp = rgb.to_rgb888_components();
|
||||||
|
|
||||||
// Finally save the pixel data in the result array as an XRGB8888 value
|
// 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);
|
((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 {
|
(*(*FRONTEND).interface).video_update(&(*FRONTEND).converted_pixel_buffer[..]);
|
||||||
update_callback(&FRONTEND_IMPL.converted_pixel_buffer[..]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let pixel_data_slice = std::slice::from_raw_parts(
|
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,
|
(pitch * height as usize) as usize,
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(update_callback) = &mut FRONTEND_IMPL.video_update_callback {
|
(*(*FRONTEND).interface).video_update(&pixel_data_slice);
|
||||||
update_callback(&pixel_data_slice);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) unsafe extern "C" fn input_poll_callback() {
|
pub(crate) unsafe extern "C" fn input_poll_callback() {
|
||||||
if let Some(poll) = &mut FRONTEND_IMPL.input_poll_callback {
|
(*(*FRONTEND).interface).input_poll();
|
||||||
poll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) unsafe extern "C" fn input_state_callback(
|
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?
|
_index: ffi::c_uint, // not used?
|
||||||
button_id: ffi::c_uint,
|
button_id: ffi::c_uint,
|
||||||
) -> ffi::c_short {
|
) -> ffi::c_short {
|
||||||
if FRONTEND_IMPL.joypads.contains_key(&port) {
|
if (*FRONTEND).joypads.contains_key(&port) {
|
||||||
let joypad = FRONTEND_IMPL
|
let joypad = *(*FRONTEND)
|
||||||
.joypads
|
.joypads
|
||||||
.get(&port)
|
.get(&port)
|
||||||
.expect("How do we get here when contains_key() returns true but the key doen't exist")
|
.expect("How do we get here when contains_key() returns true but the key doen't exist");
|
||||||
.borrow();
|
|
||||||
|
|
||||||
if device == joypad.device_type() {
|
if device == (*joypad).device_type() {
|
||||||
return joypad.get_button(button_id);
|
return (*joypad).get_button(button_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -261,12 +265,10 @@ pub(crate) unsafe extern "C" fn audio_sample_batch_callback(
|
||||||
samples: *const i16,
|
samples: *const i16,
|
||||||
frames: usize,
|
frames: usize,
|
||||||
) -> 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
|
// I might not need to give the callback the amount of frames since it can figure it out as
|
||||||
// slice.len() / 2, but /shrug
|
// slice.len() / 2, but /shrug
|
||||||
callback(slice, frames);
|
(*(*FRONTEND).interface).audio_sample(slice, frames);
|
||||||
}
|
|
||||||
frames
|
frames
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,9 @@ pub enum Error {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
IoError(#[from] std::io::Error),
|
IoError(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("a core has not provided AV info")]
|
||||||
|
NoAvInfo,
|
||||||
|
|
||||||
#[error("expected core API version {expected}, but core returned {got}")]
|
#[error("expected core API version {expected}, but core returned {got}")]
|
||||||
InvalidLibRetroAPI { expected: u32, got: u32 },
|
InvalidLibRetroAPI { expected: u32, got: u32 },
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
use std::{
|
use std::path::Path;
|
||||||
cell::RefCell,
|
|
||||||
rc::Rc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use retro_frontend::{
|
use retro_frontend::{
|
||||||
core::Core,
|
frontend::{Frontend, FrontendInterface},
|
||||||
frontend,
|
joypad::{Joypad, RetroPad},
|
||||||
joypad::{Joypad, RetroPad}
|
|
||||||
};
|
};
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
use tracing_subscriber::FmtSubscriber;
|
use tracing_subscriber::FmtSubscriber;
|
||||||
|
@ -19,70 +15,92 @@ mod rfb;
|
||||||
use rfb::*;
|
use rfb::*;
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
|
frontend: Option<Box<Frontend>>,
|
||||||
rfb_server: Box<RfbServer>,
|
rfb_server: Box<RfbServer>,
|
||||||
pad: Rc<RefCell<RetroPad>>,
|
pad: RetroPad,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn new() -> Result<Self> {
|
fn new() -> Result<Box<Self>> {
|
||||||
Ok(Self {
|
let mut boxed = Box::new(Self {
|
||||||
|
frontend: None,
|
||||||
rfb_server: RfbServer::new(RfbServerConfig {
|
rfb_server: RfbServer::new(RfbServerConfig {
|
||||||
width: 640,
|
width: 640,
|
||||||
height: 480,
|
height: 480,
|
||||||
})?,
|
})?,
|
||||||
// nasty, but idk a better way
|
// 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>>> {
|
fn get_frontend(&mut self) -> &mut Frontend {
|
||||||
let app = App::new()?;
|
self.frontend.as_mut().unwrap()
|
||||||
let rc = Rc::new(RefCell::new(app));
|
|
||||||
|
|
||||||
// Initalize all the frontend callbacks and stuff.
|
|
||||||
App::init(&rc);
|
|
||||||
|
|
||||||
Ok(rc)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initalizes the frontend library with callbacks back to us,
|
fn init(&mut self) {
|
||||||
/// and performs an initial window resize.
|
// Currently retrovnc just hardcodes the assumption of a single RetroPad.
|
||||||
fn init(rc: &Rc<RefCell<Self>>) {
|
let pad = &mut self.pad as *mut dyn Joypad;
|
||||||
let app_clone = rc.clone();
|
|
||||||
frontend::set_video_update_callback(move |slice| {
|
|
||||||
app_clone.borrow_mut().frame_update(slice);
|
|
||||||
});
|
|
||||||
|
|
||||||
let app_resize_clone = rc.clone();
|
self.get_frontend().set_input_port_device(0, pad);
|
||||||
frontend::set_video_resize_callback(move |width, height| {
|
|
||||||
app_resize_clone.borrow_mut().resize(width, height);
|
|
||||||
});
|
|
||||||
|
|
||||||
frontend::set_audio_sample_callback(|_slice, _frames| {
|
// Initalize the display
|
||||||
//println!("Got audio sample batch with {_frames} frames");
|
self.init_display();
|
||||||
});
|
}
|
||||||
|
|
||||||
let app_input_poll_clone = rc.clone();
|
fn init_display(&mut self) {
|
||||||
frontend::set_input_poll_callback(move || {
|
let av_info = self.get_frontend().get_av_info().expect("No AV info");
|
||||||
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");
|
|
||||||
|
|
||||||
// Start VNC server.
|
// Start VNC server.
|
||||||
{
|
{
|
||||||
let server = &mut rc.borrow_mut().rfb_server;
|
let server = &mut self.rfb_server;
|
||||||
tracing::info!("Starting VNC server");
|
tracing::info!("Starting VNC server");
|
||||||
server.start();
|
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 load_core<P: AsRef<Path>>(&mut self, path: P) -> Result<(), retro_frontend::result::Error> {
|
||||||
fn resize(&mut self, width: u32, height: u32) {
|
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 width = width * 2;
|
||||||
//let height = height * 2;
|
//let height = height * 2;
|
||||||
|
|
||||||
|
@ -90,30 +108,25 @@ impl App {
|
||||||
self.rfb_server.resize(width as u16, height as u16);
|
self.rfb_server.resize(width as u16, height as u16);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called by the frontend library on video frame updates
|
fn video_update(&mut self, slice: &[u32]) {
|
||||||
/// The framebuffer is *always* a RGBX8888 slice regardless of whatever video mode
|
let framebuffer_size = self.get_frontend().get_size();
|
||||||
/// 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();
|
|
||||||
self.rfb_server
|
self.rfb_server
|
||||||
.update_buffer(&slice, framebuffer_size.0 as u16, framebuffer_size.1 as u16);
|
.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 audio_sample(&mut self, _slice: &[i16], _size: usize) {}
|
||||||
fn input_poll(&mut self) {
|
|
||||||
let mut pad = self.pad.borrow_mut();
|
|
||||||
|
|
||||||
pad.reset();
|
fn input_poll(&mut self) {
|
||||||
|
self.pad.reset();
|
||||||
|
|
||||||
// Press all buttons the VNC server marked as pressed
|
// Press all buttons the VNC server marked as pressed
|
||||||
let buttons = self.rfb_server.get_buttons();
|
let buttons = self.rfb_server.get_buttons();
|
||||||
for i in 0..buttons.len() {
|
for i in 0..buttons.len() {
|
||||||
if buttons[i] {
|
if buttons[i] {
|
||||||
pad.press_button(i as u32, None);
|
self.pad.press_button(i as u32, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
|
@ -133,24 +146,17 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
let core_path: &String = matches.get_one("core").unwrap();
|
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
|
// 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") {
|
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()?;
|
app.main_loop();
|
||||||
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,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue