From ad634bf1faaae426c418060b4c78d540d888072f Mon Sep 17 00:00:00 2001 From: modeco80 Date: Sun, 4 Aug 2024 06:59:31 -0400 Subject: [PATCH] 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. --- Cargo.lock | 96 +++++++++++++++ crates/retro_frontend/Cargo.toml | 3 + crates/retro_frontend/src/frontend.rs | 114 ++++++++++++++---- crates/retro_frontend/src/lib.rs | 1 + .../retro_frontend/src/libretro_callbacks.rs | 50 ++++---- .../src/libretro_core_variable.rs | 69 +++++++++++ 6 files changed, 284 insertions(+), 49 deletions(-) create mode 100644 crates/retro_frontend/src/libretro_core_variable.rs diff --git a/Cargo.lock b/Cargo.lock index 396f005..cacdcf5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,6 +175,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.9" @@ -191,6 +197,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "home" version = "0.5.9" @@ -200,6 +212,16 @@ dependencies = [ "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]] name = "is_terminal_polyfill" version = "1.70.1" @@ -398,7 +420,9 @@ dependencies = [ "libloading", "libretro-sys", "rgb565", + "serde", "thiserror", + "toml", "tracing", ] @@ -440,6 +464,35 @@ dependencies = [ "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]] name = "sharded-slab" version = "0.1.7" @@ -508,6 +561,40 @@ dependencies = [ "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]] name = "tracing" version = "0.1.40" @@ -689,3 +776,12 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] diff --git a/crates/retro_frontend/Cargo.toml b/crates/retro_frontend/Cargo.toml index 918e978..8a1a6d3 100644 --- a/crates/retro_frontend/Cargo.toml +++ b/crates/retro_frontend/Cargo.toml @@ -10,7 +10,10 @@ libc = "0.2.155" libloading = "0.8.3" libretro-sys = "0.1.1" rgb565 = "0.1.3" +serde = { version = "1.0.204", features = ["derive"] } thiserror = "1.0.61" +toml = "0.8.19" + tracing = "0.1.40" [build-dependencies] diff --git a/crates/retro_frontend/src/frontend.rs b/crates/retro_frontend/src/frontend.rs index d5ebfae..f3387af 100644 --- a/crates/retro_frontend/src/frontend.rs +++ b/crates/retro_frontend/src/frontend.rs @@ -1,9 +1,11 @@ use crate::joypad::Joypad; use crate::libretro_callbacks; +use crate::libretro_core_variable::CoreVariable; use crate::result::{Error, Result}; use ffi::CString; use libloading::Library; use libretro_sys::*; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::ffi; use std::os::unix::ffi::OsStrExt; @@ -12,10 +14,6 @@ use std::{fs, mem::MaybeUninit}; 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. /// /// # Safety @@ -23,19 +21,29 @@ use tracing::{error, info}; /// 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 { + /// Called when video is updated. fn video_update(&mut self, slice: &[u32]); + + /// Called when resize occurs. 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); + /// Called to poll input fn input_poll(&mut self); } +/// Per-core settings +#[derive(Serialize, Deserialize)] +struct CoreSettingsFile { + #[serde(flatten)] + variables: HashMap, +} + pub struct Frontend { /// The current core's libretro functions. pub(crate) core_api: Option, @@ -47,10 +55,11 @@ pub struct Frontend { pub(crate) av_info: Option, - /// Core requested pixel format. + /// The core's requested pixel format. + /// TODO: HW accel. (or just not care) 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, pub(crate) fb_width: u32, @@ -59,6 +68,9 @@ pub struct Frontend { pub(crate) system_directory: CString, pub(crate) save_directory: CString, + pub(crate) config_directory: String, + + pub(crate) variables: HashMap, pub(crate) joypads: HashMap, @@ -88,6 +100,9 @@ impl Frontend { // For now, this is probably fine. system_directory: CString::new("system").unwrap(), save_directory: CString::new("save").unwrap(), + config_directory: "config".into(), + + variables: HashMap::new(), joypads: HashMap::new(), @@ -122,13 +137,68 @@ impl Frontend { // 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 = 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::(&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>(&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())?); @@ -174,7 +244,7 @@ impl Frontend { 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. + // If we can't, then fail the load. let api_version = (core_api.retro_api_version)(); if api_version != libretro_sys::API_VERSION { 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. // 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); + (core_api_ref.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)(); + (core_api_ref.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)( + (core_api_ref.retro_set_video_refresh)(libretro_callbacks::video_refresh_callback); + (core_api_ref.retro_set_input_poll)(libretro_callbacks::input_poll_callback); + (core_api_ref.retro_set_input_state)(libretro_callbacks::input_state_callback); + (core_api_ref.retro_set_audio_sample_batch)( libretro_callbacks::audio_sample_batch_callback, ); - (core_api.retro_reset)(); + (core_api_ref.retro_reset)(); info!("Core {} loaded", path.as_ref().display()); // Get AV info // Like core API, we have to MaybeUninit again. let mut av_info: MaybeUninit = MaybeUninit::uninit(); - (core_api.retro_get_system_av_info)(av_info.as_mut_ptr()); + (core_api_ref.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(()) diff --git a/crates/retro_frontend/src/lib.rs b/crates/retro_frontend/src/lib.rs index b5d8e31..a83cd76 100644 --- a/crates/retro_frontend/src/lib.rs +++ b/crates/retro_frontend/src/lib.rs @@ -1,6 +1,7 @@ //! A libretro frontend as a reusable library crate. mod libretro_callbacks; +mod libretro_core_variable; mod libretro_log; pub mod libretro_sys_new; diff --git a/crates/retro_frontend/src/libretro_callbacks.rs b/crates/retro_frontend/src/libretro_callbacks.rs index aa6eda9..5b34897 100644 --- a/crates/retro_frontend/src/libretro_callbacks.rs +++ b/crates/retro_frontend/src/libretro_callbacks.rs @@ -1,6 +1,6 @@ //! Callbacks for libretro -use crate::libretro_sys_new::*; use crate::{frontend::*, libretro_log, util}; +use crate::{libretro_core_variable, libretro_sys_new::*}; use rgb565::Rgb565; @@ -101,31 +101,25 @@ pub(crate) unsafe extern "C" fn environment_callback( ENVIRONMENT_GET_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() { 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) => { - 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; + if (*FRONTEND).variables.contains_key(key) { + let value = (*FRONTEND).variables.get_mut(key).unwrap(); + let value_str = value.get_value(); + libretro_variable.value = value_str.as_ptr() as *const i8; return true; + } else { + // value doesn't exist, tell the core that + libretro_variable.value = std::ptr::null(); + return false; } - return false; } Err(err) => { error!( @@ -144,22 +138,22 @@ pub(crate) unsafe extern "C" fn environment_callback( return true; } - // TODO: Fully implement, we'll need to implement above more fully. - // Ideas: - // - FrontendStateImpl can have a HashMap 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 => { 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 { 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 parsed = libretro_core_variable::CoreVariable::parse(value); + + (*FRONTEND).variables.insert(key.to_string(), parsed); + } + + // Load settings + (*FRONTEND).load_settings(); return true; } diff --git a/crates/retro_frontend/src/libretro_core_variable.rs b/crates/retro_frontend/src/libretro_core_variable.rs new file mode 100644 index 0000000..f527a89 --- /dev/null +++ b/crates/retro_frontend/src/libretro_core_variable.rs @@ -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, + + /// Value. May not be pressent; if so, assume choices[0] + pub value: Option, + + /// C value. Passed/cached to libretro. + #[serde(skip)] + c_value: Option, +} + +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; + } +}