diff --git a/Cargo.lock b/Cargo.lock index 38514a3..4f8776d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1468,9 +1468,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown", @@ -1611,6 +1611,7 @@ dependencies = [ "anyhow", "chrono", "futures", + "indexmap", "lact-schema", "libdrm_amdgpu_sys", "libflate", diff --git a/Cargo.toml b/Cargo.toml index df20b6a..c4da4ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ futures = { version = "0.3.30", default-features = false } tokio = { version = "1.35.1", default-features = false } nix = { version = "0.29.0", default-features = false } chrono = "0.4.31" +indexmap = { version = "2.5.0", features = ["serde"] } [profile.release] strip = "symbols" diff --git a/lact-client/src/lib.rs b/lact-client/src/lib.rs index 4c65a99..702c39b 100644 --- a/lact-client/src/lib.rs +++ b/lact-client/src/lib.rs @@ -3,6 +3,7 @@ mod connection; mod macros; pub use lact_schema as schema; +use lact_schema::request::ProfileBase; use amdgpu_sysfs::gpu_handle::{ power_profile_mode::PowerProfileModesTable, PerformanceLevel, PowerLevelKind, @@ -12,8 +13,8 @@ use connection::{tcp::TcpConnection, unix::UnixConnection, DaemonConnection}; use nix::unistd::getuid; use schema::{ request::{ConfirmCommand, SetClocksCommand}, - ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanOptions, PowerStates, Request, - Response, SystemInfo, + ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanOptions, PowerStates, ProfilesInfo, + Request, Response, SystemInfo, }; use serde::Deserialize; use std::{ @@ -116,6 +117,7 @@ impl DaemonClient { request_plain!(disable_overdrive, DisableOverdrive, String); request_plain!(generate_debug_snapshot, GenerateSnapshot, String); request_plain!(reset_config, RestConfig, ()); + request_plain!(list_profiles, ListProfiles, ProfilesInfo); request_with_id!(get_device_info, DeviceInfo, DeviceInfo); request_with_id!(get_device_stats, DeviceStats, DeviceStats); request_with_id!(get_device_clocks_info, DeviceClocksInfo, ClocksInfo); @@ -128,6 +130,24 @@ impl DaemonClient { request_with_id!(reset_pmfw, ResetPmfw, u64); request_with_id!(dump_vbios, VbiosDump, Vec); + pub async fn set_profile(&self, name: Option) -> anyhow::Result<()> { + self.make_request(Request::SetProfile { name }) + .await? + .inner() + } + + pub async fn create_profile(&self, name: String, base: ProfileBase) -> anyhow::Result<()> { + self.make_request(Request::CreateProfile { name, base }) + .await? + .inner() + } + + pub async fn delete_profile(&self, name: String) -> anyhow::Result<()> { + self.make_request(Request::DeleteProfile { name }) + .await? + .inner() + } + pub async fn set_performance_level( &self, id: &str, diff --git a/lact-daemon/Cargo.toml b/lact-daemon/Cargo.toml index 34593bf..c7f808a 100644 --- a/lact-daemon/Cargo.toml +++ b/lact-daemon/Cargo.toml @@ -27,6 +27,7 @@ tokio = { workspace = true, features = [ "sync", ] } futures = { workspace = true } +indexmap = { workspace = true } pciid-parser = { version = "0.7", features = ["serde"] } serde_yaml = "0.9" diff --git a/lact-daemon/src/config.rs b/lact-daemon/src/config.rs index b4e8efc..c90f203 100644 --- a/lact-daemon/src/config.rs +++ b/lact-daemon/src/config.rs @@ -1,6 +1,7 @@ use crate::server::gpu_controller::fan_control::FanCurve; use amdgpu_sysfs::gpu_handle::{PerformanceLevel, PowerLevelKind}; use anyhow::Context; +use indexmap::IndexMap; use lact_schema::{default_fan_curve, request::SetClocksCommand, FanControlMode, PmfwOptions}; use nix::unistd::getuid; use notify::{RecommendedWatcher, Watcher}; @@ -27,7 +28,11 @@ pub struct Config { #[serde(default = "default_apply_settings_timer")] pub apply_settings_timer: u64, #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub gpus: HashMap, + gpus: HashMap, + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub profiles: IndexMap, + #[serde(default)] + pub current_profile: Option, } impl Default for Config { @@ -36,6 +41,8 @@ impl Default for Config { daemon: Daemon::default(), apply_settings_timer: default_apply_settings_timer(), gpus: HashMap::new(), + profiles: IndexMap::new(), + current_profile: None, } } } @@ -61,6 +68,12 @@ impl Default for Daemon { } } +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct Profile { + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub gpus: HashMap, +} + #[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] pub struct Gpu { @@ -178,6 +191,54 @@ impl Config { Ok(config) } } + + /// Gets the GPU configs according to the current profile. Returns an error if the current profile could not be found. + pub fn gpus(&self) -> anyhow::Result<&HashMap> { + match &self.current_profile { + Some(profile) => { + let profile = self + .profiles + .get(profile) + .with_context(|| format!("Could not find profile '{profile}'"))?; + Ok(&profile.gpus) + } + None => Ok(&self.gpus), + } + } + + /// Same as [`gpus`], but with a mutable reference + pub fn gpus_mut(&mut self) -> anyhow::Result<&mut HashMap> { + match &self.current_profile { + Some(profile) => { + let profile = self + .profiles + .get_mut(profile) + .with_context(|| format!("Could not find profile '{profile}'"))?; + Ok(&mut profile.gpus) + } + None => Ok(&mut self.gpus), + } + } + + /// Get a specific profile + pub fn profile(&self, profile: &str) -> anyhow::Result<&Profile> { + self.profiles + .get(profile) + .with_context(|| format!("Profile {profile} not found")) + } + + /// Get the settings for "default" profile (aka no profile) + pub fn default_profile(&self) -> Profile { + Profile { + gpus: self.gpus.clone(), + } + } + + pub fn clear(&mut self) { + self.gpus.clear(); + self.profiles.clear(); + self.current_profile = None; + } } pub fn start_watcher(config_last_saved: Arc>) -> mpsc::UnboundedReceiver { diff --git a/lact-daemon/src/lib.rs b/lact-daemon/src/lib.rs index 5048328..8f73c41 100644 --- a/lact-daemon/src/lib.rs +++ b/lact-daemon/src/lib.rs @@ -18,7 +18,7 @@ use tokio::{ signal::unix::{signal, SignalKind}, task::LocalSet, }; -use tracing::{debug, debug_span, info, warn, Instrument, Level}; +use tracing::{debug, debug_span, error, info, warn, Instrument, Level}; /// RDNA3, minimum family that supports the new pmfw interface pub const AMDGPU_FAMILY_GC_11_0_0: u32 = 145; @@ -109,8 +109,14 @@ async fn listen_config_changes(handler: Handler) { while let Some(new_config) = rx.recv().await { info!("config file was changed, reloading"); handler.config.replace(new_config); - handler.apply_current_config().await; - info!("configuration reloaded"); + match handler.apply_current_config().await { + Ok(()) => { + info!("configuration reloaded"); + } + Err(err) => { + error!("could not apply new config: {err:#}"); + } + } } } diff --git a/lact-daemon/src/server/mod.rs b/lact-daemon/src/server.rs similarity index 95% rename from lact-daemon/src/server/mod.rs rename to lact-daemon/src/server.rs index a450056..7668f81 100644 --- a/lact-daemon/src/server/mod.rs +++ b/lact-daemon/src/server.rs @@ -164,6 +164,10 @@ async fn handle_request<'a>(request: Request<'a>, handler: &'a Handler) -> anyho ok_response(handler.set_enabled_power_states(id, kind, states).await?) } Request::VbiosDump { id } => ok_response(handler.vbios_dump(id)?), + Request::ListProfiles => ok_response(handler.list_profiles()), + Request::SetProfile { name } => ok_response(handler.set_profile(name).await?), + Request::CreateProfile { name, base } => ok_response(handler.create_profile(name, base)?), + Request::DeleteProfile { name } => ok_response(handler.delete_profile(name).await?), Request::EnableOverdrive => ok_response(system::enable_overdrive().await?), Request::DisableOverdrive => ok_response(system::disable_overdrive().await?), Request::GenerateSnapshot => ok_response(handler.generate_snapshot().await?), diff --git a/lact-daemon/src/server/handler.rs b/lact-daemon/src/server/handler.rs index d5f0b60..0ce2525 100644 --- a/lact-daemon/src/server/handler.rs +++ b/lact-daemon/src/server/handler.rs @@ -2,17 +2,17 @@ use super::{ gpu_controller::{fan_control::FanCurve, GpuController}, system::{self, detect_initramfs_type, PP_FEATURE_MASK_PATH}, }; -use crate::config::{self, default_fan_static_speed, Config, FanControlSettings}; +use crate::config::{self, default_fan_static_speed, Config, FanControlSettings, Profile}; use amdgpu_sysfs::{ gpu_handle::{power_profile_mode::PowerProfileModesTable, PerformanceLevel, PowerLevelKind}, sysfs::SysFS, }; -use anyhow::{anyhow, Context}; +use anyhow::{anyhow, bail, Context}; use lact_schema::{ default_fan_curve, - request::{ConfirmCommand, SetClocksCommand}, + request::{ConfirmCommand, ProfileBase, SetClocksCommand}, ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanControlMode, FanOptions, PmfwOptions, - PowerStates, + PowerStates, ProfilesInfo, }; use libflate::gzip; use nix::libc; @@ -135,7 +135,9 @@ impl<'a> Handler { confirm_config_tx: Rc::new(RefCell::new(None)), config_last_saved: Arc::new(Mutex::new(Instant::now())), }; - handler.apply_current_config().await; + if let Err(err) = handler.apply_current_config().await { + error!("could not apply config: {err:#}"); + } // Eagerly release memory // `load_controllers` allocates and deallocates the entire PCI ID database, @@ -148,10 +150,11 @@ impl<'a> Handler { Ok(handler) } - pub async fn apply_current_config(&self) { + pub async fn apply_current_config(&self) -> anyhow::Result<()> { let config = self.config.borrow().clone(); // Clone to avoid locking the RwLock on an await point - for (id, gpu_config) in &config.gpus { + let gpus = config.gpus()?; + for (id, gpu_config) in gpus { if let Some(controller) = self.gpu_controllers.get(id) { if let Err(err) = controller.apply_config(gpu_config).await { error!("could not apply existing config for gpu {id}: {err}"); @@ -160,6 +163,8 @@ impl<'a> Handler { info!("could not find GPU with id {id} defined in configuration"); } } + + Ok(()) } async fn edit_gpu_config( @@ -181,7 +186,7 @@ impl<'a> Handler { let (gpu_config, apply_timer) = { let config = self.config.try_borrow().map_err(|err| anyhow!("{err}"))?; let apply_timer = config.apply_settings_timer; - let gpu_config = config.gpus.get(&id).cloned().unwrap_or_default(); + let gpu_config = config.gpus()?.get(&id).cloned().unwrap_or_default(); (gpu_config, apply_timer) }; @@ -243,7 +248,12 @@ impl<'a> Handler { *handler.config_last_saved.lock().unwrap() = Instant::now(); let mut config_guard = handler.config.borrow_mut(); - config_guard.gpus.insert(id, new_config); + match config_guard.gpus_mut() { + Ok(gpus) => { + gpus.insert(id, new_config); + } + Err(err) => error!("{err:#}"), + } if let Err(err) = config_guard.save() { error!("{err:#}"); @@ -302,7 +312,7 @@ impl<'a> Handler { .config .try_borrow() .map_err(|err| anyhow!("Could not read config: {err:?}"))?; - let gpu_config = config.gpus.get(id); + let gpu_config = config.gpus()?.get(id); Ok(self.controller_by_id(id)?.get_stats(gpu_config)) } @@ -316,7 +326,10 @@ impl<'a> Handler { .config .try_borrow_mut() .map_err(|err| anyhow!("{err}"))?; - let gpu_config = config_guard.gpus.entry(opts.id.to_owned()).or_default(); + let gpu_config = config_guard + .gpus_mut()? + .entry(opts.id.to_owned()) + .or_default(); match opts.mode { Some(mode) => match mode { @@ -412,7 +425,7 @@ impl<'a> Handler { .config .try_borrow() .map_err(|err| anyhow!("Could not read config: {err:?}"))?; - let gpu_config = config.gpus.get(id); + let gpu_config = config.gpus()?.get(id); let states = self.controller_by_id(id)?.get_power_states(gpu_config); Ok(states) @@ -593,6 +606,53 @@ impl<'a> Handler { Ok(out_path) } + pub fn list_profiles(&self) -> ProfilesInfo { + let config = self.config.borrow(); + ProfilesInfo { + profiles: config.profiles.keys().cloned().collect(), + current_profile: config.current_profile.clone(), + } + } + + pub async fn set_profile(&self, name: Option) -> anyhow::Result<()> { + if let Some(name) = &name { + self.config.borrow().profile(name)?; + } + + self.cleanup().await; + self.config.borrow_mut().current_profile = name; + + self.apply_current_config().await?; + self.config.borrow_mut().save()?; + + Ok(()) + } + + pub fn create_profile(&self, name: String, base: ProfileBase) -> anyhow::Result<()> { + let mut config = self.config.borrow_mut(); + if config.profiles.contains_key(&name) { + bail!("Profile {name} already exists"); + } + + let profile = match base { + ProfileBase::Empty => Profile::default(), + ProfileBase::Default => config.default_profile(), + ProfileBase::Profile(name) => config.profile(&name)?.clone(), + }; + config.profiles.insert(name, profile); + config.save()?; + Ok(()) + } + + pub async fn delete_profile(&self, name: String) -> anyhow::Result<()> { + if self.config.borrow().current_profile.as_ref() == Some(&name) { + self.set_profile(None).await?; + } + self.config.borrow_mut().profiles.shift_remove(&name); + self.config.borrow().save()?; + Ok(()) + } + pub fn confirm_pending_config(&self, command: ConfirmCommand) -> anyhow::Result<()> { if let Some(tx) = self .confirm_config_tx @@ -611,7 +671,7 @@ impl<'a> Handler { self.cleanup().await; let mut config = self.config.borrow_mut(); - config.gpus.clear(); + config.clear(); *self.config_last_saved.lock().unwrap() = Instant::now(); if let Err(err) = config.save() { diff --git a/lact-daemon/src/suspend.rs b/lact-daemon/src/suspend.rs index 208aabd..8727480 100644 --- a/lact-daemon/src/suspend.rs +++ b/lact-daemon/src/suspend.rs @@ -10,7 +10,9 @@ pub async fn listen_events(handler: Handler) { Ok(mut stream) => { while stream.next().await.is_some() { info!("suspend/resume event detected, reloading config"); - handler.apply_current_config().await; + if let Err(err) = handler.apply_current_config().await { + error!("could not reapply config: {err:#}"); + } } } Err(err) => error!("could not subscribe to suspend events: {err:#}"), diff --git a/lact-gui/src/app/mod.rs b/lact-gui/src/app.rs similarity index 96% rename from lact-gui/src/app/mod.rs rename to lact-gui/src/app.rs index b266921..3dfbe3b 100644 --- a/lact-gui/src/app/mod.rs +++ b/lact-gui/src/app.rs @@ -24,7 +24,7 @@ use gtk::{ ApplicationWindow, ButtonsType, FileChooserAction, FileChooserDialog, MessageDialog, MessageType, ResponseType, }; -use header::Header; +use header::{Header, HeaderMsg}; use lact_client::DaemonClient; use lact_daemon::MODULE_CONF_PATH; use lact_schema::{ @@ -181,7 +181,7 @@ impl AsyncComponent for AppModel { show_embedded_info(&root, err); } - sender.input(AppMsg::ReloadData { full: true }); + sender.input(AppMsg::ReloadProfiles); AsyncComponentParts { model, widgets } } @@ -210,6 +210,11 @@ impl AppModel { ) -> Result<(), Rc> { match msg { AppMsg::Error(err) => Err(err), + AppMsg::ReloadProfiles => { + self.reload_profiles().await?; + sender.input(AppMsg::ReloadData { full: false }); + Ok(()) + } AppMsg::ReloadData { full } => { let gpu_id = self.current_gpu_id()?; if full { @@ -219,6 +224,24 @@ impl AppModel { } Ok(()) } + AppMsg::SelectProfile(profile) => { + self.daemon_client.set_profile(profile).await?; + sender.input(AppMsg::ReloadData { full: false }); + Ok(()) + } + AppMsg::CreateProfile(name, base) => { + self.daemon_client + .create_profile(name.clone(), base) + .await?; + self.daemon_client.set_profile(Some(name)).await?; + sender.input(AppMsg::ReloadProfiles); + Ok(()) + } + AppMsg::DeleteProfile(profile) => { + self.daemon_client.delete_profile(profile).await?; + sender.input(AppMsg::ReloadProfiles); + Ok(()) + } AppMsg::Stats(stats) => { self.root_stack.info_page.set_stats(&stats); self.root_stack.thermals_page.set_stats(&stats, false); @@ -308,6 +331,12 @@ impl AppModel { .context("No GPU selected") } + async fn reload_profiles(&mut self) -> anyhow::Result<()> { + let profiles = self.daemon_client.list_profiles().await?.inner()?; + self.header.emit(HeaderMsg::Profiles(profiles)); + Ok(()) + } + async fn update_gpu_data_full( &mut self, gpu_id: String, @@ -334,6 +363,8 @@ impl AppModel { self.root_stack.thermals_page.set_info(&info); + self.graphs_window.clear(); + Ok(()) } @@ -434,8 +465,6 @@ impl AppModel { .send(ApplyRevealerMsg::Hide) .unwrap(); - self.graphs_window.clear(); - self.stats_task_handle = Some(start_stats_update_loop( gpu_id.to_owned(), self.daemon_client.clone(), diff --git a/lact-gui/src/app/header.rs b/lact-gui/src/app/header.rs index 48e7fb0..b7e4dc1 100644 --- a/lact-gui/src/app/header.rs +++ b/lact-gui/src/app/header.rs @@ -1,21 +1,39 @@ +mod new_profile_dialog; + use super::{AppMsg, DebugSnapshot, DisableOverdrive, DumpVBios, ResetConfig, ShowGraphsWindow}; +use glib::clone; use gtk::prelude::*; use gtk::*; use lact_client::schema::DeviceListEntry; +use lact_schema::ProfilesInfo; +use new_profile_dialog::NewProfileDialog; use relm4::{ - Component, ComponentController, ComponentParts, ComponentSender, Controller, SimpleComponent, + typed_view::list::{RelmListItem, TypedListView}, + Component, ComponentController, ComponentParts, ComponentSender, RelmWidgetExt, }; -use relm4_components::simple_combo_box::SimpleComboBox; +use std::fmt; pub struct Header { - gpu_selector: Controller>, + gpu_selector: TypedListView, + profile_selector: TypedListView, + selector_label: String, +} + +#[derive(Debug)] +pub enum HeaderMsg { + Profiles(ProfilesInfo), + SelectProfile, + SelectGpu, + CreateProfile, + DeleteProfile, } #[relm4::component(pub)] -impl SimpleComponent for Header { +impl Component for Header { type Init = (Vec, gtk::Stack); - type Input = (); + type Input = HeaderMsg; type Output = AppMsg; + type CommandOutput = (); view! { gtk::HeaderBar { @@ -26,14 +44,80 @@ impl SimpleComponent for Header { set_stack: Some(&stack), }, - #[local_ref] - pack_start = gpu_selector -> ComboBoxText, + #[name = "menu_button"] + pack_start = >k::MenuButton { + #[watch] + set_label: &model.selector_label, + #[wrap(Some)] + set_popover = >k::Popover { + set_margin_all: 5, + set_autohide: false, + + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 5, + + gtk::Frame { + set_label: Some("GPU"), + set_label_align: 0.05, + set_margin_all: 5, + + gtk::ScrolledWindow { + set_policy: (gtk::PolicyType::Never, gtk::PolicyType::Automatic), + set_propagate_natural_height: true, + + #[local_ref] + gpu_selector -> gtk::ListView { } + } + }, + + gtk::Frame { + set_label: Some("Settings Profile"), + set_label_align: 0.05, + set_margin_all: 5, + + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 5, + + gtk::ScrolledWindow { + set_policy: (gtk::PolicyType::Never, gtk::PolicyType::Automatic), + set_propagate_natural_height: true, + + #[local_ref] + profile_selector -> gtk::ListView { } + }, + + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 5, + + gtk::Button { + set_expand: true, + set_icon_name: "list-add-symbolic", + connect_clicked => HeaderMsg::CreateProfile, + }, + + gtk::Button { + set_expand: true, + set_icon_name: "list-remove-symbolic", + connect_clicked => HeaderMsg::DeleteProfile, + #[watch] + set_sensitive: model.profile_selector.selection_model.selected() != 0, + }, + } + } + }, + } + }, + }, pack_end = >k::MenuButton { set_icon_name: "open-menu-symbolic", set_menu_model: Some(&app_menu), } - } + }, + } menu! { @@ -57,114 +141,225 @@ impl SimpleComponent for Header { root: Self::Root, sender: ComponentSender, ) -> ComponentParts { - let gpu_selector = SimpleComboBox::builder() - .launch(SimpleComboBox { - variants, - active_index: Some(0), - }) - .forward(sender.output_sender(), |_| AppMsg::ReloadData { - full: true, - }); + sender.input(HeaderMsg::SelectGpu); - // limits the length of gpu names in combobox - for cell in gpu_selector.widget().cells() { - cell.set_property("width-chars", 10); - cell.set_property("ellipsize", pango::EllipsizeMode::End); - } + let mut gpu_selector = TypedListView::<_, gtk::SingleSelection>::new(); + gpu_selector.extend_from_iter(variants.into_iter().map(GpuListItem)); - let model = Self { gpu_selector }; + gpu_selector + .selection_model + .connect_selection_changed(clone!( + #[strong] + sender, + move |_, _, _| { + sender.input(HeaderMsg::SelectGpu); + } + )); - let gpu_selector = model.gpu_selector.widget(); + let profile_selector = TypedListView::<_, gtk::SingleSelection>::new(); + profile_selector + .selection_model + .connect_selection_changed(clone!( + #[strong] + sender, + move |_, _, _| { + sender.input(HeaderMsg::SelectProfile); + } + )); + + let model = Self { + gpu_selector, + profile_selector, + selector_label: String::new(), + }; + + let gpu_selector = &model.gpu_selector.view; + let profile_selector = &model.profile_selector.view; let widgets = view_output!(); + widgets.menu_button.connect_label_notify(|menu_button| { + let label_box = menu_button + .first_child() + .unwrap() + .downcast::() + .unwrap() + .child() + .unwrap() + .downcast::() + .unwrap(); + // Limits the length of text in the menu button + let selector_label = label_box + .first_child() + .unwrap() + .downcast::