retro_frontend: actually implement variable storage

I'm not certain I'll keep it like this (it should probably be in `FrontendInterface`, so the driver code can serialize how it sees fit, rather than us), but it works to get it done.

There's also a lot of janky code I need to clean up before I'm really happy about it.
This commit is contained in:
Lily Tsuru 2024-08-04 06:59:31 -04:00
parent 1624810e55
commit ad634bf1fa
6 changed files with 284 additions and 49 deletions

96
Cargo.lock generated
View file

@ -175,6 +175,12 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.9" version = "0.3.9"
@ -191,6 +197,12 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]] [[package]]
name = "home" name = "home"
version = "0.5.9" version = "0.5.9"
@ -200,6 +212,16 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "indexmap"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.1" version = "1.70.1"
@ -398,7 +420,9 @@ dependencies = [
"libloading", "libloading",
"libretro-sys", "libretro-sys",
"rgb565", "rgb565",
"serde",
"thiserror", "thiserror",
"toml",
"tracing", "tracing",
] ]
@ -440,6 +464,35 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "serde"
version = "1.0.204"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.204"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_spanned"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.7" version = "0.1.7"
@ -508,6 +561,40 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "toml"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.40" version = "0.1.40"
@ -689,3 +776,12 @@ name = "windows_x86_64_msvc"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
dependencies = [
"memchr",
]

View file

@ -10,7 +10,10 @@ libc = "0.2.155"
libloading = "0.8.3" libloading = "0.8.3"
libretro-sys = "0.1.1" libretro-sys = "0.1.1"
rgb565 = "0.1.3" rgb565 = "0.1.3"
serde = { version = "1.0.204", features = ["derive"] }
thiserror = "1.0.61" thiserror = "1.0.61"
toml = "0.8.19"
tracing = "0.1.40" tracing = "0.1.40"
[build-dependencies] [build-dependencies]

View file

@ -1,9 +1,11 @@
use crate::joypad::Joypad; use crate::joypad::Joypad;
use crate::libretro_callbacks; use crate::libretro_callbacks;
use crate::libretro_core_variable::CoreVariable;
use crate::result::{Error, Result}; use crate::result::{Error, Result};
use ffi::CString; use ffi::CString;
use libloading::Library; use libloading::Library;
use libretro_sys::*; use libretro_sys::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::ffi; use std::ffi;
use std::os::unix::ffi::OsStrExt; use std::os::unix::ffi::OsStrExt;
@ -12,10 +14,6 @@ use std::{fs, mem::MaybeUninit};
use tracing::{error, info}; 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 currently running frontend. /// The currently running frontend.
/// ///
/// # Safety /// # Safety
@ -23,19 +21,29 @@ use tracing::{error, info};
/// Only one instance of Frontend can be active in an application. /// Only one instance of Frontend can be active in an application.
pub(crate) static mut FRONTEND: *mut Frontend = std::ptr::null_mut(); pub(crate) static mut FRONTEND: *mut Frontend = std::ptr::null_mut();
/// Interface for the frontend to call to user code. /// Interface for the frontend to call to user code.
pub trait FrontendInterface { pub trait FrontendInterface {
/// Called when video is updated.
fn video_update(&mut self, slice: &[u32]); fn video_update(&mut self, slice: &[u32]);
/// Called when resize occurs.
fn video_resize(&mut self, width: u32, height: u32); fn video_resize(&mut self, width: u32, height: u32);
// TODO(lily): This should probably return the amount of consumed frames, // TODO(lily): This should probably return the amount of consumed frames,
// as in some cases that *might* differ? // as in some cases that *might* differ?
fn audio_sample(&mut self, slice: &[i16], size: usize); fn audio_sample(&mut self, slice: &[i16], size: usize);
/// Called to poll input
fn input_poll(&mut self); fn input_poll(&mut self);
} }
/// Per-core settings
#[derive(Serialize, Deserialize)]
struct CoreSettingsFile {
#[serde(flatten)]
variables: HashMap<String, CoreVariable>,
}
pub struct Frontend { pub struct Frontend {
/// The current core's libretro functions. /// The current core's libretro functions.
pub(crate) core_api: Option<CoreAPI>, pub(crate) core_api: Option<CoreAPI>,
@ -47,10 +55,11 @@ pub struct Frontend {
pub(crate) av_info: Option<SystemAvInfo>, pub(crate) av_info: Option<SystemAvInfo>,
/// Core requested pixel format. /// The core's requested pixel format.
/// TODO: HW accel. (or just not care)
pub(crate) pixel_format: PixelFormat, pub(crate) pixel_format: PixelFormat,
// Converted pixel buffer. We store it here so we don't keep allocating over and over. /// 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) converted_pixel_buffer: Vec<u32>,
pub(crate) fb_width: u32, pub(crate) fb_width: u32,
@ -59,6 +68,9 @@ pub struct Frontend {
pub(crate) system_directory: CString, pub(crate) system_directory: CString,
pub(crate) save_directory: CString, pub(crate) save_directory: CString,
pub(crate) config_directory: String,
pub(crate) variables: HashMap<String, CoreVariable>,
pub(crate) joypads: HashMap<u32 /* port */, *mut dyn Joypad>, pub(crate) joypads: HashMap<u32 /* port */, *mut dyn Joypad>,
@ -88,6 +100,9 @@ impl Frontend {
// For now, this is probably fine. // For now, this is probably fine.
system_directory: CString::new("system").unwrap(), system_directory: CString::new("system").unwrap(),
save_directory: CString::new("save").unwrap(), save_directory: CString::new("save").unwrap(),
config_directory: "config".into(),
variables: HashMap::new(),
joypads: HashMap::new(), joypads: HashMap::new(),
@ -122,13 +137,68 @@ impl Frontend {
// clear_input_port_device? // clear_input_port_device?
fn get_config_file_path(&self) -> String {
let path = unsafe {
let core_api = self.core_api.as_ref().unwrap();
let mut system_info: MaybeUninit<SystemInfo> = MaybeUninit::uninit();
(core_api.retro_get_system_info)(system_info.as_mut_ptr());
let info = system_info.assume_init();
let c_name = ffi::CStr::from_ptr(info.library_name);
format!(
"{}/{}.toml",
self.config_directory,
c_name.to_str().expect("ughh")
)
};
path
}
// TODO: make this a bit less janky (and use Results)
pub fn load_settings(&mut self) {
let path = self.get_config_file_path();
match fs::exists(path.clone()) {
Ok(exists) => {
if exists {
let data = fs::read_to_string(path).expect("Could not read config");
let config =
toml::from_str::<CoreSettingsFile>(&data).expect("Could not parse config");
self.variables = config.variables;
} else {
// Save the core's initial settings to disk
self.save_settings();
}
}
Err(e) => {
error!("Can't seem to read {path}: {}", e);
}
}
}
pub fn save_settings(&mut self) {
let path = self.get_config_file_path();
let settings = CoreSettingsFile {
variables: self.variables.clone(),
};
let string = toml::to_string(&settings).expect("Could not serialize settings");
fs::write(path.clone(), string).expect("Could not save settings to disk");
info!("Saved settings to {path}");
}
pub fn load_core<P: AsRef<Path>>(&mut self, path: P) -> Result<()> { pub fn load_core<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
if self.core_loaded() { if self.core_loaded() {
return Err(Error::CoreAlreadyLoaded); return Err(Error::CoreAlreadyLoaded);
} }
println!("load_core()");
unsafe { unsafe {
let lib = Box::new(Library::new(path.as_ref())?); let lib = Box::new(Library::new(path.as_ref())?);
@ -174,7 +244,7 @@ impl Frontend {
let core_api = api_uninitialized.assume_init(); 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. // 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. // If we can't, then fail the load.
let api_version = (core_api.retro_api_version)(); let api_version = (core_api.retro_api_version)();
if api_version != libretro_sys::API_VERSION { if api_version != libretro_sys::API_VERSION {
error!( error!(
@ -187,37 +257,39 @@ impl Frontend {
}); });
} }
self.core_library = Some(lib);
self.core_api = Some(core_api);
let core_api_ref = self.core_api.as_ref().unwrap();
// Set required libretro callbacks before calling libretro_init. // Set required libretro callbacks before calling libretro_init.
// Some cores expect some callbacks to be set before libretro_init is called, // 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. // some cores don't. For maximum compatibility, pamper the cores which do.
(core_api.retro_set_environment)(libretro_callbacks::environment_callback); (core_api_ref.retro_set_environment)(libretro_callbacks::environment_callback);
// Initalize the libretro core. We do this first because // Initalize the libretro core. We do this first because
// there are a Few cores which initalize resources that later // there are a Few cores which initalize resources that later
// are poked by the later callback setting that could break if we don't. // are poked by the later callback setting that could break if we don't.
(core_api.retro_init)(); (core_api_ref.retro_init)();
// Set more libretro callbacks now that we have initalized the core. // Set more libretro callbacks now that we have initalized the core.
(core_api.retro_set_video_refresh)(libretro_callbacks::video_refresh_callback); (core_api_ref.retro_set_video_refresh)(libretro_callbacks::video_refresh_callback);
(core_api.retro_set_input_poll)(libretro_callbacks::input_poll_callback); (core_api_ref.retro_set_input_poll)(libretro_callbacks::input_poll_callback);
(core_api.retro_set_input_state)(libretro_callbacks::input_state_callback); (core_api_ref.retro_set_input_state)(libretro_callbacks::input_state_callback);
(core_api.retro_set_audio_sample_batch)( (core_api_ref.retro_set_audio_sample_batch)(
libretro_callbacks::audio_sample_batch_callback, libretro_callbacks::audio_sample_batch_callback,
); );
(core_api.retro_reset)(); (core_api_ref.retro_reset)();
info!("Core {} loaded", path.as_ref().display()); info!("Core {} loaded", path.as_ref().display());
// Get AV info // Get AV info
// Like core API, we have to MaybeUninit again. // Like core API, we have to MaybeUninit again.
let mut av_info: MaybeUninit<SystemAvInfo> = MaybeUninit::uninit(); let mut av_info: MaybeUninit<SystemAvInfo> = MaybeUninit::uninit();
(core_api.retro_get_system_av_info)(av_info.as_mut_ptr()); (core_api_ref.retro_get_system_av_info)(av_info.as_mut_ptr());
self.av_info = Some(av_info.assume_init()); self.av_info = Some(av_info.assume_init());
self.core_library = Some(lib);
self.core_api = Some(core_api);
} }
Ok(()) Ok(())

View file

@ -1,6 +1,7 @@
//! A libretro frontend as a reusable library crate. //! A libretro frontend as a reusable library crate.
mod libretro_callbacks; mod libretro_callbacks;
mod libretro_core_variable;
mod libretro_log; mod libretro_log;
pub mod libretro_sys_new; pub mod libretro_sys_new;

View file

@ -1,6 +1,6 @@
//! Callbacks for libretro //! Callbacks for libretro
use crate::libretro_sys_new::*;
use crate::{frontend::*, libretro_log, util}; use crate::{frontend::*, libretro_log, util};
use crate::{libretro_core_variable, libretro_sys_new::*};
use rgb565::Rgb565; use rgb565::Rgb565;
@ -101,32 +101,26 @@ pub(crate) unsafe extern "C" fn environment_callback(
ENVIRONMENT_GET_VARIABLE => { ENVIRONMENT_GET_VARIABLE => {
// Make sure the core actually is giving us a pointer to a *Variable // Make sure the core actually is giving us a pointer to a *Variable
// so we can (if we have it!) fill it in. // so we can fill it in.
if data.is_null() { if data.is_null() {
return false; return false;
} }
let var = (data as *mut Variable).as_mut().unwrap(); let libretro_variable = (data as *mut Variable).as_mut().unwrap();
match ffi::CStr::from_ptr(var.key).to_str() { match ffi::CStr::from_ptr(libretro_variable.key).to_str() {
Ok(key) => { Ok(key) => {
debug!("Core wants to get variable \"{key}\""); if (*FRONTEND).variables.contains_key(key) {
let value = (*FRONTEND).variables.get_mut(key).unwrap();
// HACK for SwanStation. I really should just serialize these to TOML or something. let value_str = value.get_value();
// (each core iirc provides its own name, so i can just do config/[core].toml) libretro_variable.value = value_str.as_ptr() as *const i8;
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 true;
} } else {
// value doesn't exist, tell the core that
libretro_variable.value = std::ptr::null();
return false; return false;
} }
}
Err(err) => { Err(err) => {
error!( error!(
"Core gave an invalid key for ENVIRONMENT_GET_VARIABLE: {:?}", "Core gave an invalid key for ENVIRONMENT_GET_VARIABLE: {:?}",
@ -144,22 +138,22 @@ pub(crate) unsafe extern "C" fn environment_callback(
return true; return true;
} }
// TODO: Fully implement, we'll need to implement above more fully.
// Ideas:
// - FrontendStateImpl can have a HashMap<CString, CString> which will then
// be where we can store stuff. Also the consumer application could in theory
// use that to save/restore (by injecting keys from another source)
ENVIRONMENT_SET_VARIABLES => { ENVIRONMENT_SET_VARIABLES => {
let ptr = data as *const Variable; let ptr = data as *const Variable;
let slice = util::terminated_array(ptr, |item| item.key.is_null());
let _slice = util::terminated_array(ptr, |item| item.key.is_null()); // populate variables hashmap
/*
for var in slice { for var in slice {
let key = std::ffi::CStr::from_ptr(var.key).to_str().unwrap(); let key = std::ffi::CStr::from_ptr(var.key).to_str().unwrap();
let value = std::ffi::CStr::from_ptr(var.value).to_str().unwrap(); let value = std::ffi::CStr::from_ptr(var.value).to_str().unwrap();
}*/
let parsed = libretro_core_variable::CoreVariable::parse(value);
(*FRONTEND).variables.insert(key.to_string(), parsed);
}
// Load settings
(*FRONTEND).load_settings();
return true; return true;
} }

View file

@ -0,0 +1,69 @@
//! Helpers for dealing with Libretro configuration values.
use serde::{Deserialize, Serialize};
use std::ffi::CString;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CoreVariable {
/// Description of variable
pub description: String,
/// possible choices
pub choices: Vec<String>,
/// Value. May not be pressent; if so, assume choices[0]
pub value: Option<String>,
/// C value. Passed/cached to libretro.
#[serde(skip)]
c_value: Option<CString>,
}
impl CoreVariable {
/// Parses this core variable.
pub fn parse(str: &str) -> Self {
let string = str.to_string();
match string.find(';') {
Some(index) => {
let name = &string[0..index];
if string.chars().nth(index + 1).unwrap() != ' ' {
panic!("Improperly formatted core variable");
}
let raw_choices = string[index + 2..].to_string();
let choices = raw_choices.split('|').map(|s| s.to_string()).collect();
Self {
description: name.to_string(),
choices: choices,
value: None,
c_value: None,
}
}
None => panic!("??? Couldn't find"),
}
}
/// Gets this variable's value
pub fn get_value(&mut self) -> &CString {
let rust_value = if self.value.is_some() {
self.value.as_ref().unwrap()
} else {
&self.choices[0]
};
if self.c_value.is_none() {
self.c_value = Some(CString::new(rust_value.as_bytes()).expect("aaa"));
}
self.c_value.as_ref().unwrap()
}
/// Sets a new value
pub fn set_value(&mut self, value: &String) {
self.value = Some(value.clone());
self.c_value = None;
}
}