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:
parent
1624810e55
commit
ad634bf1fa
6 changed files with 284 additions and 49 deletions
96
Cargo.lock
generated
96
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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<String, CoreVariable>,
|
||||
}
|
||||
|
||||
pub struct Frontend {
|
||||
/// The current core's libretro functions.
|
||||
pub(crate) core_api: Option<CoreAPI>,
|
||||
|
@ -47,10 +55,11 @@ pub struct Frontend {
|
|||
|
||||
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,
|
||||
|
||||
// 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) 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<String, CoreVariable>,
|
||||
|
||||
pub(crate) joypads: HashMap<u32 /* port */, *mut dyn Joypad>,
|
||||
|
||||
|
@ -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<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<()> {
|
||||
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<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.core_library = Some(lib);
|
||||
self.core_api = Some(core_api);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<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 => {
|
||||
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;
|
||||
}
|
||||
|
|
69
crates/retro_frontend/src/libretro_core_variable.rs
Normal file
69
crates/retro_frontend/src/libretro_core_variable.rs
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue