Config synchronizes across app

This commit is contained in:
2026-10-31 01:10:16 -04:00
parent 9bcd99e437
commit 36cfd961da
5 changed files with 124 additions and 51 deletions
+5 -4
View File
@@ -23,7 +23,7 @@ use winit::{
};
use crate::{
config::{AppConfig, CliArgs},
config::{AppConfig, CliArgs, Config},
controller::ControllerManager,
emulator::{EmulatorClient, EmulatorCommand, SimId},
filesystem::Persistence,
@@ -94,7 +94,7 @@ impl Application {
handle: OwnedDisplayHandle,
persistence: Persistence,
args: CliArgs,
config: AppConfig,
config: Config<AppConfig>,
) -> Self {
let wgpu = WgpuState::new(handle, config);
let icon = load_icon().ok().map(Arc::new);
@@ -504,11 +504,11 @@ impl ApplicationHandler<UserEvent> for Application {
struct WgpuState {
instance: wgpu::Instance,
device_descriptor: Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>,
config: AppConfig,
config: Config<AppConfig>,
}
impl WgpuState {
fn new(handle: OwnedDisplayHandle, config: AppConfig) -> Self {
fn new(handle: OwnedDisplayHandle, config: Config<AppConfig>) -> Self {
#[allow(unused_variables)]
let egui_wgpu::WgpuSetupCreateNew {
instance_descriptor: wgpu::InstanceDescriptor { backends, .. },
@@ -531,6 +531,7 @@ impl WgpuState {
}
fn init(&mut self, window: &Window) -> egui_wgpu::WgpuSetupExisting {
self.config.refresh();
let power_preference =
wgpu::PowerPreference::from_env().unwrap_or(self.config.power_preference);
debug!("power preference: {power_preference:?}");
+15 -24
View File
@@ -1,9 +1,9 @@
use anyhow::Result;
use bytemuck::NoUninit;
use clap::{Parser, ValueEnum};
use egui::{Color32, Pos2, Vec2};
use serde::{Deserialize, Serialize};
pub use crate::filesystem::Config;
use crate::{emulator::SimId, filesystem::Persistence, window::DisplayMode};
use std::path::PathBuf;
@@ -116,38 +116,31 @@ pub struct AppConfig {
#[serde(default = "default_save_data_location")]
pub save_data_location: SaveDataLocation,
}
impl AppConfig {
pub fn load(persistence: &Persistence) -> Self {
if let Ok(config) = persistence.load_config(APP_CONFIG_FILENAME) {
return config;
}
impl Default for AppConfig {
fn default() -> Self {
Self {
power_preference: default_power_preference(),
force_fallback: false,
save_data_location: default_save_data_location(),
}
}
pub fn save(&self, persistence: &Persistence) -> Result<()> {
persistence.save_config(APP_CONFIG_FILENAME, &self)
}
pub fn update(&mut self, args: &CliArgs) -> bool {
let mut updated = false;
impl AppConfig {
pub fn load(persistence: &Persistence) -> Config<Self> {
persistence.config(APP_CONFIG_FILENAME)
}
pub fn apply_updates(&mut self, args: &CliArgs) {
if let Some(pref) = args.wgpu_power_preference {
self.power_preference = pref.into();
updated = true;
}
if let Some(force_fallback) = args.wgpu_force_fallback {
self.force_fallback = force_fallback;
updated = true;
}
if let Some(save_data_location) = args.save_data_location {
self.save_data_location = save_data_location;
updated = true;
}
updated
}
}
@@ -171,12 +164,8 @@ pub struct SimConfig {
#[serde(default)]
pub position: Option<Pos2>,
}
impl SimConfig {
pub fn load(persistence: &Persistence, sim_id: SimId) -> Self {
if let Ok(config) = persistence.load_config(sim_config_filename(sim_id)) {
return config;
}
impl Default for SimConfig {
fn default() -> Self {
Self {
display_mode: DisplayMode::Anaglyph,
colors: COLOR_PRESETS[0],
@@ -187,9 +176,11 @@ impl SimConfig {
position: None,
}
}
}
pub fn save(&self, persistence: &Persistence, sim_id: SimId) -> Result<()> {
persistence.save_config(sim_config_filename(sim_id), self)
impl SimConfig {
pub fn load(persistence: &Persistence, sim_id: SimId) -> Config<Self> {
persistence.config(sim_config_filename(sim_id))
}
}
+92 -1
View File
@@ -1,4 +1,13 @@
use std::{fs, path::PathBuf};
use std::{
collections::HashMap,
fs,
ops::Deref,
path::PathBuf,
sync::{
Arc, Mutex,
atomic::{AtomicU64, Ordering},
},
};
use anyhow::{Result, bail};
use directories::ProjectDirs;
@@ -7,12 +16,42 @@ use serde::{Deserialize, Serialize};
#[derive(Clone)]
pub struct Persistence {
config_dir: Option<PathBuf>,
versions: Arc<Mutex<HashMap<String, Arc<AtomicU64>>>>,
}
impl Persistence {
pub fn new() -> Self {
Self {
config_dir: init_config_dir(),
versions: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn config<T: ConfigCompatible>(&self, file: &str) -> Config<T> {
let latest_version = self
.versions
.lock()
.unwrap()
.entry(file.to_string())
.or_default()
.clone();
let path = self
.config_dir
.as_ref()
.map(|p| p.join(file).with_extension("json"));
let value = if let Some(p) = path.as_ref()
&& let Ok(bytes) = fs::read(p)
&& let Ok(v) = serde_json::from_slice(&bytes)
{
v
} else {
T::default()
};
Config {
value,
version: latest_version.load(Ordering::Relaxed),
latest_version,
path,
}
}
@@ -50,3 +89,55 @@ pub fn init_data_dir() -> Option<PathBuf> {
fn project_dirs() -> Option<ProjectDirs> {
ProjectDirs::from("com", "virtual-boy", "Lemur")
}
pub trait ConfigCompatible: Clone + Default + Eq + Serialize + for<'a> Deserialize<'a> {}
impl<T: Clone + Default + Eq + Serialize + for<'a> Deserialize<'a>> ConfigCompatible for T {}
// A wrapper for a T (probably) stored on the filesystem.
// Cheap to dereference, easy to sync changes to all copies.
pub struct Config<T> {
value: T,
version: u64,
latest_version: Arc<AtomicU64>,
path: Option<PathBuf>,
}
impl<T> Deref for Config<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<T: ConfigCompatible> Config<T> {
pub fn refresh(&mut self) {
let version = self.latest_version.load(Ordering::Relaxed);
if self.version == version {
return;
}
if let Some(path) = &self.path
&& let Ok(bytes) = fs::read(path)
&& let Ok(latest) = serde_json::from_slice(&bytes)
{
self.value = latest;
}
self.version = version;
}
pub fn update(&mut self, update: impl FnOnce(&mut T)) {
let mut new_value = self.value.clone();
update(&mut new_value);
if self.value == new_value {
return;
}
if let Some(path) = &self.path
&& let Ok(bytes) = serde_json::to_vec(&new_value)
{
let _ = fs::write(path, bytes);
}
let version = self.latest_version.fetch_add(1, Ordering::Relaxed) + 1;
self.value = new_value.clone();
self.version = version;
}
}
+2 -3
View File
@@ -94,9 +94,8 @@ fn main() -> Result<()> {
let persistence = Persistence::new();
let mut config = AppConfig::load(&persistence);
if config.update(&args) {
let _ = config.save(&persistence);
}
config.update(|c| c.apply_updates(&args));
let p1 = SimConfig::load(&persistence, SimId::Player1);
let p2 = SimConfig::load(&persistence, SimId::Player2);
+11 -20
View File
@@ -7,7 +7,7 @@ use std::{
use crate::{
app::UserEvent,
config::{AppConfig, COLOR_PRESETS, SaveDataLocation, SimConfig},
config::{AppConfig, COLOR_PRESETS, Config, SaveDataLocation, SimConfig},
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState},
filepicker::FilePicker,
filesystem::Persistence,
@@ -42,7 +42,7 @@ pub struct GameWindow {
persistence: Persistence,
shortcuts: ShortcutProvider,
sim_id: SimId,
config: SimConfig,
config: Config<SimConfig>,
toasts: Toasts,
screen: Option<GameScreen>,
messages: mpsc::Receiver<Toast>,
@@ -334,7 +334,7 @@ impl GameWindow {
}
let watch_rom = self.client.is_rom_watched(self.sim_id);
if ui.selectable_button(watch_rom, "Watch ROM").clicked() {
self.update_config(|c| c.watch_rom = !watch_rom);
self.config.update(|c| c.watch_rom = !watch_rom);
self.client
.send_command(EmulatorCommand::WatchRom(self.sim_id, !watch_rom));
}
@@ -395,7 +395,7 @@ impl GameWindow {
ui.separator();
let low_battery = self.config.low_battery;
if ui.selectable_button(low_battery, "Low Battery").clicked() {
self.update_config(|c| c.low_battery = !low_battery);
self.config.update(|c| c.low_battery = !low_battery);
let _ = self
.proxy
.send_event(UserEvent::LowBattery(self.sim_id, !low_battery));
@@ -551,7 +551,7 @@ impl GameWindow {
ui.send_viewport_cmd(ViewportCommand::InnerSize(current_dims * scale));
}
self.update_config(|c| {
self.config.update(|c| {
c.display_mode = display_mode;
c.dimensions = current_dims * scale;
});
@@ -559,7 +559,7 @@ impl GameWindow {
ui.menu_button("Colors", |ui| {
for preset in COLOR_PRESETS {
if ui.color_pair_button(preset[0], preset[1]).clicked() {
self.update_config(|c| c.colors = preset);
self.config.update(|c| c.colors = preset);
}
}
ui.with_layout(ui.layout().with_cross_align(egui::Align::Center), |ui| {
@@ -589,7 +589,7 @@ impl GameWindow {
.selectable_button(self.config.audio_enabled, "Enabled")
.clicked()
{
self.update_config(|c| c.audio_enabled = !c.audio_enabled);
self.config.update(|c| c.audio_enabled = !c.audio_enabled);
self.client.send_command(EmulatorCommand::SetAudioEnabled(
self.sim_id,
self.config.audio_enabled,
@@ -611,8 +611,7 @@ impl GameWindow {
return;
};
let mut app_config = AppConfig::load(&self.persistence);
app_config.save_data_location = new_location;
let _ = app_config.save(&self.persistence);
app_config.update(|c| c.save_data_location = new_location);
self.client
.send_command(EmulatorCommand::SetSaveDataLocation(new_location));
};
@@ -662,19 +661,10 @@ impl GameWindow {
self.color_picker = None;
}
if updated {
self.update_config(|c| c.colors = colors);
self.config.update(|c| c.colors = colors);
}
}
fn update_config(&mut self, update: impl FnOnce(&mut SimConfig)) {
let mut new_config = self.config.clone();
update(&mut new_config);
if self.config != new_config {
let _ = new_config.save(&self.persistence, self.sim_id);
}
self.config = new_config;
}
fn button_for(&self, ctx: &Context, text: &str, command: Command) -> Button<'_> {
let button = Button::new(text);
match self.shortcuts.shortcut_for(command) {
@@ -701,6 +691,7 @@ impl AppWindow for GameWindow {
}
fn show(&mut self, ui: &mut Ui) {
self.config.refresh();
let has_player_2 = self.client.has_player_2();
if !self.show_player_name && has_player_2 {
ui.send_viewport_cmd(ViewportCommand::Title(format!("Lemur ({})", self.sim_id)));
@@ -731,7 +722,7 @@ impl AppWindow for GameWindow {
bounds.max - bounds.min
};
let position = ui.input(|i| i.viewport().outer_rect.map(|r| r.min));
self.update_config(|c| {
self.config.update(|c| {
c.dimensions = dimensions;
c.position = position;
});