feat: add support for multiple settings profiles (#327)

* feat: initial support for multiple settings profiles

* feat: initial profiles management support

* feat: basic profile management ui
This commit is contained in:
Ilya Zlobintsev 2024-09-28 09:51:35 +03:00 committed by GitHub
parent fa0eb3083c
commit 538cea3aa5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 683 additions and 132 deletions

5
Cargo.lock generated
View File

@ -1468,9 +1468,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.4.0" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
@ -1611,6 +1611,7 @@ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"futures", "futures",
"indexmap",
"lact-schema", "lact-schema",
"libdrm_amdgpu_sys", "libdrm_amdgpu_sys",
"libflate", "libflate",

View File

@ -23,6 +23,7 @@ futures = { version = "0.3.30", default-features = false }
tokio = { version = "1.35.1", default-features = false } tokio = { version = "1.35.1", default-features = false }
nix = { version = "0.29.0", default-features = false } nix = { version = "0.29.0", default-features = false }
chrono = "0.4.31" chrono = "0.4.31"
indexmap = { version = "2.5.0", features = ["serde"] }
[profile.release] [profile.release]
strip = "symbols" strip = "symbols"

View File

@ -3,6 +3,7 @@ mod connection;
mod macros; mod macros;
pub use lact_schema as schema; pub use lact_schema as schema;
use lact_schema::request::ProfileBase;
use amdgpu_sysfs::gpu_handle::{ use amdgpu_sysfs::gpu_handle::{
power_profile_mode::PowerProfileModesTable, PerformanceLevel, PowerLevelKind, power_profile_mode::PowerProfileModesTable, PerformanceLevel, PowerLevelKind,
@ -12,8 +13,8 @@ use connection::{tcp::TcpConnection, unix::UnixConnection, DaemonConnection};
use nix::unistd::getuid; use nix::unistd::getuid;
use schema::{ use schema::{
request::{ConfirmCommand, SetClocksCommand}, request::{ConfirmCommand, SetClocksCommand},
ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanOptions, PowerStates, Request, ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanOptions, PowerStates, ProfilesInfo,
Response, SystemInfo, Request, Response, SystemInfo,
}; };
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::{
@ -116,6 +117,7 @@ impl DaemonClient {
request_plain!(disable_overdrive, DisableOverdrive, String); request_plain!(disable_overdrive, DisableOverdrive, String);
request_plain!(generate_debug_snapshot, GenerateSnapshot, String); request_plain!(generate_debug_snapshot, GenerateSnapshot, String);
request_plain!(reset_config, RestConfig, ()); request_plain!(reset_config, RestConfig, ());
request_plain!(list_profiles, ListProfiles, ProfilesInfo);
request_with_id!(get_device_info, DeviceInfo, DeviceInfo); request_with_id!(get_device_info, DeviceInfo, DeviceInfo);
request_with_id!(get_device_stats, DeviceStats, DeviceStats); request_with_id!(get_device_stats, DeviceStats, DeviceStats);
request_with_id!(get_device_clocks_info, DeviceClocksInfo, ClocksInfo); 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!(reset_pmfw, ResetPmfw, u64);
request_with_id!(dump_vbios, VbiosDump, Vec<u8>); request_with_id!(dump_vbios, VbiosDump, Vec<u8>);
pub async fn set_profile(&self, name: Option<String>) -> 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( pub async fn set_performance_level(
&self, &self,
id: &str, id: &str,

View File

@ -27,6 +27,7 @@ tokio = { workspace = true, features = [
"sync", "sync",
] } ] }
futures = { workspace = true } futures = { workspace = true }
indexmap = { workspace = true }
pciid-parser = { version = "0.7", features = ["serde"] } pciid-parser = { version = "0.7", features = ["serde"] }
serde_yaml = "0.9" serde_yaml = "0.9"

View File

@ -1,6 +1,7 @@
use crate::server::gpu_controller::fan_control::FanCurve; use crate::server::gpu_controller::fan_control::FanCurve;
use amdgpu_sysfs::gpu_handle::{PerformanceLevel, PowerLevelKind}; use amdgpu_sysfs::gpu_handle::{PerformanceLevel, PowerLevelKind};
use anyhow::Context; use anyhow::Context;
use indexmap::IndexMap;
use lact_schema::{default_fan_curve, request::SetClocksCommand, FanControlMode, PmfwOptions}; use lact_schema::{default_fan_curve, request::SetClocksCommand, FanControlMode, PmfwOptions};
use nix::unistd::getuid; use nix::unistd::getuid;
use notify::{RecommendedWatcher, Watcher}; use notify::{RecommendedWatcher, Watcher};
@ -27,7 +28,11 @@ pub struct Config {
#[serde(default = "default_apply_settings_timer")] #[serde(default = "default_apply_settings_timer")]
pub apply_settings_timer: u64, pub apply_settings_timer: u64,
#[serde(default, skip_serializing_if = "HashMap::is_empty")] #[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub gpus: HashMap<String, Gpu>, gpus: HashMap<String, Gpu>,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub profiles: IndexMap<String, Profile>,
#[serde(default)]
pub current_profile: Option<String>,
} }
impl Default for Config { impl Default for Config {
@ -36,6 +41,8 @@ impl Default for Config {
daemon: Daemon::default(), daemon: Daemon::default(),
apply_settings_timer: default_apply_settings_timer(), apply_settings_timer: default_apply_settings_timer(),
gpus: HashMap::new(), 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<String, Gpu>,
}
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct Gpu { pub struct Gpu {
@ -178,6 +191,54 @@ impl Config {
Ok(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<String, Gpu>> {
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<String, Gpu>> {
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<Mutex<Instant>>) -> mpsc::UnboundedReceiver<Config> { pub fn start_watcher(config_last_saved: Arc<Mutex<Instant>>) -> mpsc::UnboundedReceiver<Config> {

View File

@ -18,7 +18,7 @@ use tokio::{
signal::unix::{signal, SignalKind}, signal::unix::{signal, SignalKind},
task::LocalSet, 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 /// RDNA3, minimum family that supports the new pmfw interface
pub const AMDGPU_FAMILY_GC_11_0_0: u32 = 145; pub const AMDGPU_FAMILY_GC_11_0_0: u32 = 145;
@ -109,9 +109,15 @@ async fn listen_config_changes(handler: Handler) {
while let Some(new_config) = rx.recv().await { while let Some(new_config) = rx.recv().await {
info!("config file was changed, reloading"); info!("config file was changed, reloading");
handler.config.replace(new_config); handler.config.replace(new_config);
handler.apply_current_config().await; match handler.apply_current_config().await {
Ok(()) => {
info!("configuration reloaded"); info!("configuration reloaded");
} }
Err(err) => {
error!("could not apply new config: {err:#}");
}
}
}
} }
async fn ensure_sufficient_uptime() { async fn ensure_sufficient_uptime() {

View File

@ -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?) ok_response(handler.set_enabled_power_states(id, kind, states).await?)
} }
Request::VbiosDump { id } => ok_response(handler.vbios_dump(id)?), 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::EnableOverdrive => ok_response(system::enable_overdrive().await?),
Request::DisableOverdrive => ok_response(system::disable_overdrive().await?), Request::DisableOverdrive => ok_response(system::disable_overdrive().await?),
Request::GenerateSnapshot => ok_response(handler.generate_snapshot().await?), Request::GenerateSnapshot => ok_response(handler.generate_snapshot().await?),

View File

@ -2,17 +2,17 @@ use super::{
gpu_controller::{fan_control::FanCurve, GpuController}, gpu_controller::{fan_control::FanCurve, GpuController},
system::{self, detect_initramfs_type, PP_FEATURE_MASK_PATH}, 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::{ use amdgpu_sysfs::{
gpu_handle::{power_profile_mode::PowerProfileModesTable, PerformanceLevel, PowerLevelKind}, gpu_handle::{power_profile_mode::PowerProfileModesTable, PerformanceLevel, PowerLevelKind},
sysfs::SysFS, sysfs::SysFS,
}; };
use anyhow::{anyhow, Context}; use anyhow::{anyhow, bail, Context};
use lact_schema::{ use lact_schema::{
default_fan_curve, default_fan_curve,
request::{ConfirmCommand, SetClocksCommand}, request::{ConfirmCommand, ProfileBase, SetClocksCommand},
ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanControlMode, FanOptions, PmfwOptions, ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanControlMode, FanOptions, PmfwOptions,
PowerStates, PowerStates, ProfilesInfo,
}; };
use libflate::gzip; use libflate::gzip;
use nix::libc; use nix::libc;
@ -135,7 +135,9 @@ impl<'a> Handler {
confirm_config_tx: Rc::new(RefCell::new(None)), confirm_config_tx: Rc::new(RefCell::new(None)),
config_last_saved: Arc::new(Mutex::new(Instant::now())), 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 // Eagerly release memory
// `load_controllers` allocates and deallocates the entire PCI ID database, // `load_controllers` allocates and deallocates the entire PCI ID database,
@ -148,10 +150,11 @@ impl<'a> Handler {
Ok(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 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 Some(controller) = self.gpu_controllers.get(id) {
if let Err(err) = controller.apply_config(gpu_config).await { if let Err(err) = controller.apply_config(gpu_config).await {
error!("could not apply existing config for gpu {id}: {err}"); 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"); info!("could not find GPU with id {id} defined in configuration");
} }
} }
Ok(())
} }
async fn edit_gpu_config<F: FnOnce(&mut config::Gpu)>( async fn edit_gpu_config<F: FnOnce(&mut config::Gpu)>(
@ -181,7 +186,7 @@ impl<'a> Handler {
let (gpu_config, apply_timer) = { let (gpu_config, apply_timer) = {
let config = self.config.try_borrow().map_err(|err| anyhow!("{err}"))?; let config = self.config.try_borrow().map_err(|err| anyhow!("{err}"))?;
let apply_timer = config.apply_settings_timer; 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) (gpu_config, apply_timer)
}; };
@ -243,7 +248,12 @@ impl<'a> Handler {
*handler.config_last_saved.lock().unwrap() = Instant::now(); *handler.config_last_saved.lock().unwrap() = Instant::now();
let mut config_guard = handler.config.borrow_mut(); 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() { if let Err(err) = config_guard.save() {
error!("{err:#}"); error!("{err:#}");
@ -302,7 +312,7 @@ impl<'a> Handler {
.config .config
.try_borrow() .try_borrow()
.map_err(|err| anyhow!("Could not read config: {err:?}"))?; .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)) Ok(self.controller_by_id(id)?.get_stats(gpu_config))
} }
@ -316,7 +326,10 @@ impl<'a> Handler {
.config .config
.try_borrow_mut() .try_borrow_mut()
.map_err(|err| anyhow!("{err}"))?; .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 { match opts.mode {
Some(mode) => match mode { Some(mode) => match mode {
@ -412,7 +425,7 @@ impl<'a> Handler {
.config .config
.try_borrow() .try_borrow()
.map_err(|err| anyhow!("Could not read config: {err:?}"))?; .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); let states = self.controller_by_id(id)?.get_power_states(gpu_config);
Ok(states) Ok(states)
@ -593,6 +606,53 @@ impl<'a> Handler {
Ok(out_path) 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<String>) -> 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<()> { pub fn confirm_pending_config(&self, command: ConfirmCommand) -> anyhow::Result<()> {
if let Some(tx) = self if let Some(tx) = self
.confirm_config_tx .confirm_config_tx
@ -611,7 +671,7 @@ impl<'a> Handler {
self.cleanup().await; self.cleanup().await;
let mut config = self.config.borrow_mut(); let mut config = self.config.borrow_mut();
config.gpus.clear(); config.clear();
*self.config_last_saved.lock().unwrap() = Instant::now(); *self.config_last_saved.lock().unwrap() = Instant::now();
if let Err(err) = config.save() { if let Err(err) = config.save() {

View File

@ -10,7 +10,9 @@ pub async fn listen_events(handler: Handler) {
Ok(mut stream) => { Ok(mut stream) => {
while stream.next().await.is_some() { while stream.next().await.is_some() {
info!("suspend/resume event detected, reloading config"); 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:#}"), Err(err) => error!("could not subscribe to suspend events: {err:#}"),

View File

@ -24,7 +24,7 @@ use gtk::{
ApplicationWindow, ButtonsType, FileChooserAction, FileChooserDialog, MessageDialog, ApplicationWindow, ButtonsType, FileChooserAction, FileChooserDialog, MessageDialog,
MessageType, ResponseType, MessageType, ResponseType,
}; };
use header::Header; use header::{Header, HeaderMsg};
use lact_client::DaemonClient; use lact_client::DaemonClient;
use lact_daemon::MODULE_CONF_PATH; use lact_daemon::MODULE_CONF_PATH;
use lact_schema::{ use lact_schema::{
@ -181,7 +181,7 @@ impl AsyncComponent for AppModel {
show_embedded_info(&root, err); show_embedded_info(&root, err);
} }
sender.input(AppMsg::ReloadData { full: true }); sender.input(AppMsg::ReloadProfiles);
AsyncComponentParts { model, widgets } AsyncComponentParts { model, widgets }
} }
@ -210,6 +210,11 @@ impl AppModel {
) -> Result<(), Rc<anyhow::Error>> { ) -> Result<(), Rc<anyhow::Error>> {
match msg { match msg {
AppMsg::Error(err) => Err(err), AppMsg::Error(err) => Err(err),
AppMsg::ReloadProfiles => {
self.reload_profiles().await?;
sender.input(AppMsg::ReloadData { full: false });
Ok(())
}
AppMsg::ReloadData { full } => { AppMsg::ReloadData { full } => {
let gpu_id = self.current_gpu_id()?; let gpu_id = self.current_gpu_id()?;
if full { if full {
@ -219,6 +224,24 @@ impl AppModel {
} }
Ok(()) 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) => { AppMsg::Stats(stats) => {
self.root_stack.info_page.set_stats(&stats); self.root_stack.info_page.set_stats(&stats);
self.root_stack.thermals_page.set_stats(&stats, false); self.root_stack.thermals_page.set_stats(&stats, false);
@ -308,6 +331,12 @@ impl AppModel {
.context("No GPU selected") .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( async fn update_gpu_data_full(
&mut self, &mut self,
gpu_id: String, gpu_id: String,
@ -334,6 +363,8 @@ impl AppModel {
self.root_stack.thermals_page.set_info(&info); self.root_stack.thermals_page.set_info(&info);
self.graphs_window.clear();
Ok(()) Ok(())
} }
@ -434,8 +465,6 @@ impl AppModel {
.send(ApplyRevealerMsg::Hide) .send(ApplyRevealerMsg::Hide)
.unwrap(); .unwrap();
self.graphs_window.clear();
self.stats_task_handle = Some(start_stats_update_loop( self.stats_task_handle = Some(start_stats_update_loop(
gpu_id.to_owned(), gpu_id.to_owned(),
self.daemon_client.clone(), self.daemon_client.clone(),

View File

@ -1,21 +1,39 @@
mod new_profile_dialog;
use super::{AppMsg, DebugSnapshot, DisableOverdrive, DumpVBios, ResetConfig, ShowGraphsWindow}; use super::{AppMsg, DebugSnapshot, DisableOverdrive, DumpVBios, ResetConfig, ShowGraphsWindow};
use glib::clone;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::*; use gtk::*;
use lact_client::schema::DeviceListEntry; use lact_client::schema::DeviceListEntry;
use lact_schema::ProfilesInfo;
use new_profile_dialog::NewProfileDialog;
use relm4::{ 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 { pub struct Header {
gpu_selector: Controller<SimpleComboBox<DeviceListEntry>>, gpu_selector: TypedListView<GpuListItem, gtk::SingleSelection>,
profile_selector: TypedListView<ProfileListItem, gtk::SingleSelection>,
selector_label: String,
}
#[derive(Debug)]
pub enum HeaderMsg {
Profiles(ProfilesInfo),
SelectProfile,
SelectGpu,
CreateProfile,
DeleteProfile,
} }
#[relm4::component(pub)] #[relm4::component(pub)]
impl SimpleComponent for Header { impl Component for Header {
type Init = (Vec<DeviceListEntry>, gtk::Stack); type Init = (Vec<DeviceListEntry>, gtk::Stack);
type Input = (); type Input = HeaderMsg;
type Output = AppMsg; type Output = AppMsg;
type CommandOutput = ();
view! { view! {
gtk::HeaderBar { gtk::HeaderBar {
@ -26,14 +44,80 @@ impl SimpleComponent for Header {
set_stack: Some(&stack), set_stack: Some(&stack),
}, },
#[name = "menu_button"]
pack_start = &gtk::MenuButton {
#[watch]
set_label: &model.selector_label,
#[wrap(Some)]
set_popover = &gtk::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] #[local_ref]
pack_start = gpu_selector -> ComboBoxText, 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 = &gtk::MenuButton { pack_end = &gtk::MenuButton {
set_icon_name: "open-menu-symbolic", set_icon_name: "open-menu-symbolic",
set_menu_model: Some(&app_menu), set_menu_model: Some(&app_menu),
} }
} },
} }
menu! { menu! {
@ -57,114 +141,225 @@ impl SimpleComponent for Header {
root: Self::Root, root: Self::Root,
sender: ComponentSender<Self>, sender: ComponentSender<Self>,
) -> ComponentParts<Self> { ) -> ComponentParts<Self> {
let gpu_selector = SimpleComboBox::builder() sender.input(HeaderMsg::SelectGpu);
.launch(SimpleComboBox {
variants,
active_index: Some(0),
})
.forward(sender.output_sender(), |_| AppMsg::ReloadData {
full: true,
});
// limits the length of gpu names in combobox let mut gpu_selector = TypedListView::<_, gtk::SingleSelection>::new();
for cell in gpu_selector.widget().cells() { gpu_selector.extend_from_iter(variants.into_iter().map(GpuListItem));
cell.set_property("width-chars", 10);
cell.set_property("ellipsize", pango::EllipsizeMode::End); gpu_selector
.selection_model
.connect_selection_changed(clone!(
#[strong]
sender,
move |_, _, _| {
sender.input(HeaderMsg::SelectGpu);
} }
));
let model = Self { gpu_selector }; let profile_selector = TypedListView::<_, gtk::SingleSelection>::new();
profile_selector
.selection_model
.connect_selection_changed(clone!(
#[strong]
sender,
move |_, _, _| {
sender.input(HeaderMsg::SelectProfile);
}
));
let gpu_selector = model.gpu_selector.widget(); 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!(); let widgets = view_output!();
widgets.menu_button.connect_label_notify(|menu_button| {
let label_box = menu_button
.first_child()
.unwrap()
.downcast::<gtk::ToggleButton>()
.unwrap()
.child()
.unwrap()
.downcast::<gtk::Box>()
.unwrap();
// Limits the length of text in the menu button
let selector_label = label_box
.first_child()
.unwrap()
.downcast::<Label>()
.unwrap();
selector_label.set_ellipsize(pango::EllipsizeMode::End);
selector_label.set_width_chars(14);
});
ComponentParts { model, widgets } ComponentParts { model, widgets }
} }
fn update_with_view(
&mut self,
widgets: &mut Self::Widgets,
msg: Self::Input,
sender: ComponentSender<Self>,
_root: &Self::Root,
) {
match msg {
HeaderMsg::Profiles(profiles_info) => {
let selected_index = match &profiles_info.current_profile {
Some(profile) => {
profiles_info
.profiles
.iter()
.position(|value| value == profile)
.expect("Active profile is not in the list")
+ 1
}
None => 0,
};
self.profile_selector.clear();
self.profile_selector.append(ProfileListItem::Default);
for profile in profiles_info.profiles {
self.profile_selector
.append(ProfileListItem::Profile(profile));
}
self.profile_selector
.selection_model
.set_selected(selected_index as u32);
}
HeaderMsg::SelectGpu => sender.output(AppMsg::ReloadData { full: true }).unwrap(),
HeaderMsg::SelectProfile => {
let selected_profile = self.selected_profile();
sender
.output(AppMsg::SelectProfile(selected_profile))
.unwrap();
}
HeaderMsg::CreateProfile => {
let mut diag_controller = NewProfileDialog::builder()
.launch(self.custom_profiles())
.forward(sender.output_sender(), |(name, base)| {
AppMsg::CreateProfile(name, base)
});
diag_controller.detach_runtime();
}
HeaderMsg::DeleteProfile => {
if let Some(selected_profile) = self.selected_profile() {
let msg =
format!("Are you sure you want to delete profile \"{selected_profile}\"");
sender
.output(AppMsg::ask_confirmation(
AppMsg::DeleteProfile(selected_profile),
"Delete profile",
&msg,
gtk::ButtonsType::OkCancel,
))
.unwrap();
}
}
}
self.update_label();
self.update_view(widgets, sender);
}
} }
impl Header { impl Header {
pub fn selected_gpu_id(&self) -> Option<String> { pub fn selected_gpu_id(&self) -> Option<String> {
let selected = self.gpu_selector.selection_model.selected();
self.gpu_selector self.gpu_selector
.model() .get(selected)
.get_active_elem() .as_ref()
.map(|model| model.id.clone()) .map(|item| item.borrow().0.id.clone())
}
fn custom_profiles(&self) -> Vec<String> {
let mut profiles = Vec::with_capacity(self.profile_selector.len() as usize);
for i in 0..self.profile_selector.len() {
let item = self.profile_selector.get(i).unwrap();
let item = item.borrow();
if let ProfileListItem::Profile(name) = &*item {
profiles.push(name.clone());
}
}
profiles
}
fn selected_profile(&self) -> Option<String> {
let selected_index = self.profile_selector.selection_model.selected();
let item = self
.profile_selector
.get(selected_index)
.expect("Invalid item selected");
let item = item.borrow().clone();
match item {
ProfileListItem::Default => None,
ProfileListItem::Profile(name) => Some(name),
} }
} }
/*#[derive(Clone)] fn update_label(&mut self) {
pub struct Header { let gpu_index = self.gpu_selector.selection_model.selected();
pub container: HeaderBar, let profile = self
gpu_selector: ComboBoxText, .profile_selector
switcher: StackSwitcher, .get(self.profile_selector.selection_model.selected())
} .as_ref()
.map(|item| item.borrow().to_string())
.unwrap_or_else(|| "<Unknown>".to_owned());
impl Header { self.selector_label = format!("GPU {gpu_index} | {profile}");
pub fn new(system_info: &SystemInfo) -> Self {
let container = HeaderBar::new();
container.set_show_title_buttons(true);
let switcher = StackSwitcher::new();
container.set_title_widget(Some(&switcher));
let gpu_selector = ComboBoxText::new();
container.pack_start(&gpu_selector);
let menu = gio::Menu::new();
menu.append(
Some("Show historical charts"),
Some("app.show-graphs-window"),
);
menu.append(Some("Dump VBIOS"), Some("app.dump-vbios"));
menu.append(
Some("Generate debug snapshot"),
Some("app.generate-debug-snapshot"),
);
if system_info.amdgpu_overdrive_enabled == Some(true) {
menu.append(
Some("Disable overclocking support"),
Some("app.disable-overdrive"),
)
}
menu.append(Some("Reset all configuration"), Some("app.reset-config"));
let menu_button = MenuButton::builder()
.icon_name("open-menu-symbolic")
.menu_model(&menu)
.build();
container.pack_end(&menu_button);
Header {
container,
gpu_selector,
switcher,
} }
} }
pub fn set_switcher_stack(&self, stack: &Stack) { struct GpuListItem(DeviceListEntry);
self.switcher.set_stack(Some(stack));
impl RelmListItem for GpuListItem {
type Root = gtk::Label;
type Widgets = gtk::Label;
fn setup(_list_item: &gtk::ListItem) -> (Self::Root, Self::Widgets) {
let label = gtk::Label::new(None);
label.set_margin_all(5);
(label.clone(), label)
} }
pub fn set_devices(&self, gpus: &[DeviceListEntry<'_>]) { fn bind(&mut self, widgets: &mut Self::Widgets, _root: &mut Self::Root) {
for (i, entry) in gpus.iter().enumerate() { widgets.set_label(self.0.name.as_deref().unwrap_or(&self.0.id));
let name = format!("{i}: {}", entry.name.unwrap_or_default()); }
self.gpu_selector.append(Some(entry.id), &name);
} }
//limits the length of gpu names in combobox #[derive(Clone)]
for cell in self.gpu_selector.cells() { enum ProfileListItem {
cell.set_property("width-chars", 10); Default,
cell.set_property("ellipsize", EllipsizeMode::End); Profile(String),
} }
self.gpu_selector.set_active(Some(0)); impl fmt::Display for ProfileListItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = match self {
ProfileListItem::Default => "Default",
ProfileListItem::Profile(name) => name,
};
text.fmt(f)
}
} }
pub fn connect_gpu_selection_changed<F: Fn(String) + 'static>(&self, f: F) { impl RelmListItem for ProfileListItem {
self.gpu_selector.connect_changed(move |gpu_selector| { type Root = Label;
if let Some(selected_id) = gpu_selector.active_id() { type Widgets = Label;
f(selected_id.to_string());
fn setup(_list_item: &gtk::ListItem) -> (Self::Root, Self::Widgets) {
let label = gtk::Label::new(None);
label.set_margin_all(5);
(label.clone(), label)
}
fn bind(&mut self, widgets: &mut Self::Widgets, _root: &mut Self::Root) {
widgets.set_label(&self.to_string());
} }
});
} }
}*/

View File

@ -0,0 +1,129 @@
use gtk::prelude::*;
use lact_schema::request::ProfileBase;
use relm4::{
Component, ComponentController, ComponentParts, ComponentSender, Controller, RelmWidgetExt,
};
use relm4_components::simple_combo_box::SimpleComboBox;
pub struct NewProfileDialog {
name_buffer: gtk::EntryBuffer,
base_selector: Controller<SimpleComboBox<ProfileBase>>,
}
#[derive(Debug)]
pub enum NewProfileDialogMsg {
Create,
}
#[relm4::component(pub)]
impl Component for NewProfileDialog {
type Init = Vec<String>;
type Input = NewProfileDialogMsg;
type Output = (String, ProfileBase);
type CommandOutput = ();
view! {
gtk::Window {
set_default_size: (250, 130),
set_title: Some("Create Profile"),
set_hide_on_close: true,
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
set_margin_all: 10,
gtk::Entry {
set_placeholder_text: Some("Name"),
set_buffer: &model.name_buffer,
},
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 5,
gtk::Label {
set_label: "Base profile:",
},
#[local_ref]
base_selector -> gtk::ComboBoxText {
set_margin_horizontal: 5,
set_hexpand: true,
set_halign: gtk::Align::End,
},
},
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 5,
set_hexpand: true,
set_vexpand: true,
set_valign: gtk::Align::End,
gtk::Button {
set_label: "Cancel",
set_hexpand: true,
connect_clicked[root] => move |_| {
root.hide();
},
},
gtk::Button {
set_label: "Create",
set_hexpand: true,
connect_clicked => NewProfileDialogMsg::Create,
},
}
},
}
}
fn init(
current_profiles: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let mut variants = vec![ProfileBase::Empty, ProfileBase::Default];
variants.extend(current_profiles.into_iter().map(ProfileBase::Profile));
let base_selector = SimpleComboBox::<ProfileBase>::builder()
.launch(SimpleComboBox {
variants,
active_index: Some(1),
})
.detach();
let model = Self {
base_selector,
name_buffer: gtk::EntryBuffer::default(),
};
let base_selector = model.base_selector.widget();
let widgets = view_output!();
root.present();
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>, root: &Self::Root) {
match msg {
NewProfileDialogMsg::Create => {
if self.name_buffer.length() != 0 {
if let Some(selected) = self.base_selector.model().active_index {
let base = self.base_selector.model().variants[selected].clone();
sender
.output((self.name_buffer.text().to_string(), base))
.unwrap();
root.hide();
}
}
}
}
}
}

View File

@ -1,5 +1,5 @@
use super::confirmation_dialog::ConfirmationOptions; use super::confirmation_dialog::ConfirmationOptions;
use lact_schema::DeviceStats; use lact_schema::{request::ProfileBase, DeviceStats};
use std::rc::Rc; use std::rc::Rc;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -17,6 +17,10 @@ pub enum AppMsg {
EnableOverdrive, EnableOverdrive,
DisableOverdrive, DisableOverdrive,
ResetConfig, ResetConfig,
ReloadProfiles,
SelectProfile(Option<String>),
CreateProfile(String, ProfileBase),
DeleteProfile(String),
AskConfirmation(ConfirmationOptions, Box<AppMsg>), AskConfirmation(ConfirmationOptions, Box<AppMsg>),
} }

View File

@ -10,10 +10,10 @@ args = ["clap"]
amdgpu-sysfs = { workspace = true } amdgpu-sysfs = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_with = { workspace = true } serde_with = { workspace = true }
serde-error = "=0.1.2"
anyhow = { workspace = true } anyhow = { workspace = true }
indexmap = { workspace = true }
indexmap = { version = "*", features = ["serde"] } serde-error = "=0.1.2"
clap = { version = "4.4.18", features = ["derive"], optional = true } clap = { version = "4.4.18", features = ["derive"], optional = true }
[build-dependencies] [build-dependencies]

View File

@ -306,3 +306,9 @@ pub struct FanOptions<'a> {
pub spindown_delay_ms: Option<u64>, pub spindown_delay_ms: Option<u64>,
pub change_threshold: Option<u64>, pub change_threshold: Option<u64>,
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
pub struct ProfilesInfo {
pub profiles: Vec<String>,
pub current_profile: Option<String>,
}

View File

@ -1,3 +1,5 @@
use std::fmt;
use crate::FanOptions; use crate::FanOptions;
use amdgpu_sysfs::gpu_handle::{PerformanceLevel, PowerLevelKind}; use amdgpu_sysfs::gpu_handle::{PerformanceLevel, PowerLevelKind};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -57,6 +59,17 @@ pub enum Request<'a> {
VbiosDump { VbiosDump {
id: &'a str, id: &'a str,
}, },
ListProfiles,
SetProfile {
name: Option<String>,
},
CreateProfile {
name: String,
base: ProfileBase,
},
DeleteProfile {
name: String,
},
EnableOverdrive, EnableOverdrive,
DisableOverdrive, DisableOverdrive,
GenerateSnapshot, GenerateSnapshot,
@ -83,3 +96,22 @@ pub enum SetClocksCommand {
VoltageOffset(i32), VoltageOffset(i32),
Reset, Reset,
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum ProfileBase {
Empty,
Default,
Profile(String),
}
impl fmt::Display for ProfileBase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = match self {
ProfileBase::Empty => "Empty",
ProfileBase::Default => "Default",
ProfileBase::Profile(name) => name,
};
text.fmt(f)
}
}