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",
|
||||
"libloading",
|
||||
"libretro-sys",
|
||||
"once_cell",
|
||||
"rgb565",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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::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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
||||
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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue