diff --git a/Cargo.lock b/Cargo.lock index dfcfc2f..1fdeeaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -232,6 +232,9 @@ name = "arrayvec" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +dependencies = [ + "serde", +] [[package]] name = "as-raw-xcb-connection" @@ -1571,6 +1574,8 @@ dependencies = [ "allocator-api2", "equivalent", "foldhash 0.2.0", + "serde", + "serde_core", ] [[package]] @@ -2238,6 +2243,22 @@ dependencies = [ "zerocopy-derive", ] +[[package]] +name = "macro_rules_attribute" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ae8f6d608c795738406608304d30a2dfbdc8e58e44f7ba43236da5208ded3c" +dependencies = [ + "macro_rules_attribute-proc_macro", + "pastey", +] + +[[package]] +name = "macro_rules_attribute-proc_macro" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc04a4c58212d57930a24bf47d3fa87485264a3a054e9c10e042eb373573ad3c" + [[package]] name = "matchers" version = "0.2.0" @@ -2999,6 +3020,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "pdb-addr2line" version = "0.11.2" @@ -5094,6 +5121,7 @@ dependencies = [ "hashbrown 0.16.1", "indexmap", "log", + "macro_rules_attribute", "naga", "once_cell", "parking_lot", @@ -5101,6 +5129,7 @@ dependencies = [ "profiling", "raw-window-handle", "rustc-hash 1.1.0", + "serde", "smallvec", "thiserror 2.0.18", "wgpu-core-deps-apple", @@ -5223,6 +5252,7 @@ dependencies = [ "js-sys", "log", "raw-window-handle", + "serde", "web-sys", ] diff --git a/Cargo.toml b/Cargo.toml index 96def43..18addd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ thread-priority = "3" tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync", "time"] } tracing = { version = "0.1", features = ["release_max_level_debug"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] } -wgpu = "29" +wgpu = { version = "29", features = ["serde"] } wholesym = "0.8" winit = { version = "0.30", features = ["serde"] } diff --git a/src/app.rs b/src/app.rs index 66233c6..008682b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -23,7 +23,7 @@ use winit::{ }; use crate::{ - config::CliArgs, + config::{AppConfig, CliArgs}, controller::ControllerManager, emulator::{EmulatorClient, EmulatorCommand, SimId}, images::ImageTextureLoader, @@ -94,8 +94,9 @@ impl Application { handle: OwnedDisplayHandle, persistence: Persistence, args: CliArgs, + config: AppConfig, ) -> Self { - let wgpu = WgpuState::new(handle); + let wgpu = WgpuState::new(handle, config); let icon = load_icon().ok().map(Arc::new); let mappings = MappingProvider::new(persistence.clone(), args.player2_controller); let shortcuts = ShortcutProvider::new(persistence.clone()); @@ -496,10 +497,11 @@ impl ApplicationHandler for Application { struct WgpuState { instance: wgpu::Instance, device_descriptor: Arc wgpu::DeviceDescriptor<'static> + Send + Sync>, + config: AppConfig, } impl WgpuState { - fn new(handle: OwnedDisplayHandle) -> Self { + fn new(handle: OwnedDisplayHandle, config: AppConfig) -> Self { #[allow(unused_variables)] let egui_wgpu::WgpuSetupCreateNew { instance_descriptor: wgpu::InstanceDescriptor { backends, .. }, @@ -517,16 +519,19 @@ impl WgpuState { Self { instance, device_descriptor, + config, } } fn init(&mut self, window: &Window) -> egui_wgpu::WgpuSetupExisting { let power_preference = - wgpu::PowerPreference::from_env().unwrap_or(wgpu::PowerPreference::LowPower); + wgpu::PowerPreference::from_env().unwrap_or(self.config.power_preference); debug!("power preference: {power_preference:?}"); let force_fallback_adapter = std::env::var("WGPU_FORCE_FALLBACK") - .is_ok_and(|f| f.eq_ignore_ascii_case("true") || f.eq_ignore_ascii_case("1")); + .map_or(self.config.force_fallback, |f| { + f.eq_ignore_ascii_case("true") || f.eq_ignore_ascii_case("1") + }); debug!("force fallback: {force_fallback_adapter}"); let surface = self diff --git a/src/config.rs b/src/config.rs index fc623af..05cb217 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use clap::Parser; +use clap::{Parser, ValueEnum}; use egui::{Color32, Pos2, Vec2}; use serde::{Deserialize, Serialize}; @@ -47,6 +47,28 @@ pub struct CliArgs { /// Map the first connected controller to Player 2 #[arg(long)] pub player2_controller: bool, + /// Force the application to render with CPU, rather than GPU. + #[arg(long)] + pub wgpu_force_fallback: Option, + /// Set a preference for whether to use a low-power or high-performance GPU adapter + #[arg(long)] + pub wgpu_power_preference: Option, +} + +#[derive(ValueEnum, Clone, Copy)] +pub enum PowerPreferenceWrapper { + Low, + High, + None, +} +impl From for wgpu::PowerPreference { + fn from(value: PowerPreferenceWrapper) -> Self { + match value { + PowerPreferenceWrapper::Low => wgpu::PowerPreference::LowPower, + PowerPreferenceWrapper::High => wgpu::PowerPreference::HighPerformance, + PowerPreferenceWrapper::None => wgpu::PowerPreference::None, + } + } } pub const COLOR_PRESETS: [[Color32; 2]; 3] = [ @@ -64,6 +86,49 @@ pub const COLOR_PRESETS: [[Color32; 2]; 3] = [ ], ]; +const fn default_power_preference() -> wgpu::PowerPreference { + wgpu::PowerPreference::LowPower +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct AppConfig { + #[serde(default = "default_power_preference")] + pub power_preference: wgpu::PowerPreference, + #[serde(default)] + pub force_fallback: bool, +} + +impl AppConfig { + pub fn load(persistence: &Persistence) -> Self { + if let Ok(config) = persistence.load_config(APP_CONFIG_FILENAME) { + return config; + } + Self { + power_preference: default_power_preference(), + force_fallback: false, + } + } + + 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; + 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; + } + updated + } +} + +const APP_CONFIG_FILENAME: &str = "config"; + const fn default_audio_enabled() -> bool { true } @@ -81,7 +146,7 @@ pub struct SimConfig { impl SimConfig { pub fn load(persistence: &Persistence, sim_id: SimId) -> Self { - if let Ok(config) = persistence.load_config(config_filename(sim_id)) { + if let Ok(config) = persistence.load_config(sim_config_filename(sim_id)) { return config; } Self { @@ -94,11 +159,11 @@ impl SimConfig { } pub fn save(&self, persistence: &Persistence, sim_id: SimId) -> Result<()> { - persistence.save_config(config_filename(sim_id), self) + persistence.save_config(sim_config_filename(sim_id), self) } } -fn config_filename(sim_id: SimId) -> &'static str { +fn sim_config_filename(sim_id: SimId) -> &'static str { match sim_id { SimId::Player1 => "config_p1", SimId::Player2 => "config_p2", diff --git a/src/main.rs b/src/main.rs index 6d60eae..57912ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::Subscribe use winit::event_loop::{ControlFlow, EventLoop}; use crate::{ - config::{CliArgs, SimConfig}, + config::{AppConfig, CliArgs, SimConfig}, emulator::SimId, persistence::Persistence, }; @@ -110,6 +110,10 @@ fn main() -> Result<()> { if args.profile { builder = builder.start_paused(true) } + let mut config = AppConfig::load(&persistence); + if config.update(&args) { + let _ = config.save(&persistence); + } let p1 = SimConfig::load(&persistence, SimId::Player1); let p2 = SimConfig::load(&persistence, SimId::Player2); @@ -143,6 +147,7 @@ fn main() -> Result<()> { handle, persistence, args, + config, ))?; Ok(()) }