feat: added static fan control option (#198)

* fix: fixed the confirm apply dialog timer not stopping after choosing an option

* feat: added static fan control to the daemon and GUI

* fix: cleanup and validate static speed
This commit is contained in:
bisspector
2023-09-30 14:28:39 +02:00
committed by GitHub
parent 1aef539546
commit 9533234f7e
10 changed files with 325 additions and 113 deletions

View File

@@ -8,8 +8,8 @@ use nix::unistd::getuid;
use schema::{
amdgpu_sysfs::gpu_handle::{power_profile_mode::PowerProfileModesTable, PerformanceLevel},
request::{ConfirmCommand, SetClocksCommand},
ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanCurveMap, Request, Response,
SystemInfo,
ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanControlMode, FanCurveMap, Request,
Response, SystemInfo,
};
use serde::Deserialize;
use std::{
@@ -106,10 +106,18 @@ impl DaemonClient {
&self,
id: &str,
enabled: bool,
mode: Option<FanControlMode>,
static_speed: Option<f64>,
curve: Option<FanCurveMap>,
) -> anyhow::Result<u64> {
self.make_request(Request::SetFanControl { id, enabled, curve })?
.inner()
self.make_request(Request::SetFanControl {
id,
enabled,
mode,
static_speed,
curve,
})?
.inner()
}
pub fn set_power_cap(&self, id: &str, cap: Option<f64>) -> anyhow::Result<u64> {

View File

@@ -1,6 +1,9 @@
use crate::server::gpu_controller::fan_control::FanCurve;
use anyhow::Context;
use lact_schema::{amdgpu_sysfs::gpu_handle::PerformanceLevel, request::SetClocksCommand};
use lact_schema::{
amdgpu_sysfs::gpu_handle::PerformanceLevel, default_fan_curve, request::SetClocksCommand,
FanControlMode,
};
use nix::unistd::getuid;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
@@ -102,11 +105,31 @@ impl Gpu {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FanControlSettings {
#[serde(default)]
pub mode: FanControlMode,
#[serde(default = "default_fan_static_speed")]
pub static_speed: f64,
pub temperature_key: String,
pub interval_ms: u64,
pub curve: FanCurve,
}
impl Default for FanControlSettings {
fn default() -> Self {
Self {
mode: FanControlMode::default(),
static_speed: default_fan_static_speed(),
temperature_key: "edge".to_owned(),
interval_ms: 500,
curve: FanCurve(default_fan_curve()),
}
}
}
pub fn default_fan_static_speed() -> f64 {
0.5
}
impl Config {
pub fn load() -> anyhow::Result<Option<Self>> {
let path = get_path();
@@ -159,6 +182,8 @@ fn default_apply_settings_timer() -> u64 {
#[cfg(test)]
mod tests {
use lact_schema::FanControlMode;
use super::{Config, Daemon, FanControlSettings, Gpu};
use crate::server::gpu_controller::fan_control::FanCurve;
@@ -174,6 +199,8 @@ mod tests {
curve: FanCurve::default(),
temperature_key: "edge".to_owned(),
interval_ms: 500,
mode: FanControlMode::Curve,
static_speed: 0.5,
}),
..Default::default()
},

View File

@@ -231,15 +231,17 @@ impl GpuController {
}
pub fn get_stats(&self, gpu_config: Option<&config::Gpu>) -> anyhow::Result<DeviceStats> {
let fan_control_enabled = self
.fan_control_handle
.try_borrow()
.map_err(|err| anyhow!("Could not lock fan control mutex: {err}"))?
.is_some();
Ok(DeviceStats {
fan: FanStats {
control_enabled: fan_control_enabled,
control_enabled: gpu_config
.map(|config| config.fan_control_enabled)
.unwrap_or_default(),
control_mode: gpu_config
.and_then(|config| config.fan_control_settings.as_ref())
.map(|settings| settings.mode.clone()),
static_speed: gpu_config
.and_then(|config| config.fan_control_settings.as_ref())
.map(|settings| settings.static_speed),
curve: gpu_config
.and_then(|config| config.fan_control_settings.as_ref())
.map(|settings| settings.curve.0.clone()),
@@ -291,7 +293,33 @@ impl GpuController {
self.handle.hw_monitors.first().map(f)
}
async fn start_fan_control(
async fn set_static_fan_control(&self, static_speed: f64) -> anyhow::Result<()> {
// Stop existing task to set static speed
self.stop_fan_control(false).await?;
let hw_mon = self
.handle
.hw_monitors
.first()
.cloned()
.context("This GPU has no monitor")?;
hw_mon
.set_fan_control_method(FanControlMethod::Manual)
.context("Could not set fan control method")?;
let static_speed_converted = (f64::from(u8::MAX) * static_speed) as u8;
hw_mon
.set_fan_pwm(static_speed_converted)
.context("could not set fan speed")?;
debug!("set fan speed to {}", static_speed);
Ok(())
}
async fn start_curve_fan_control(
&self,
curve: FanCurve,
temp_key: String,
@@ -359,18 +387,18 @@ impl GpuController {
if let Some((notify, handle)) = maybe_notify {
notify.notify_one();
handle.await?;
}
if reset_mode {
let hw_mon = self
.handle
.hw_monitors
.first()
.cloned()
.context("This GPU has no monitor")?;
hw_mon
.set_fan_control_method(FanControlMethod::Auto)
.context("Could not set fan control back to automatic")?;
}
if reset_mode {
let hw_mon = self
.handle
.hw_monitors
.first()
.cloned()
.context("This GPU has no monitor")?;
hw_mon
.set_fan_control_method(FanControlMethod::Auto)
.context("Could not set fan control back to automatic")?;
}
Ok(())
@@ -379,17 +407,24 @@ impl GpuController {
pub async fn apply_config(&self, config: &config::Gpu) -> anyhow::Result<()> {
if config.fan_control_enabled {
if let Some(ref settings) = config.fan_control_settings {
if settings.curve.0.is_empty() {
return Err(anyhow!("Cannot use empty fan curve"));
}
match settings.mode {
lact_schema::FanControlMode::Static => {
self.set_static_fan_control(settings.static_speed).await?
}
lact_schema::FanControlMode::Curve => {
if settings.curve.0.is_empty() {
return Err(anyhow!("Cannot use empty fan curve"));
}
let interval = Duration::from_millis(settings.interval_ms);
self.start_fan_control(
settings.curve.clone(),
settings.temperature_key.clone(),
interval,
)
.await?;
let interval = Duration::from_millis(settings.interval_ms);
self.start_curve_fan_control(
settings.curve.clone(),
settings.temperature_key.clone(),
interval,
)
.await?;
}
}
} else {
return Err(anyhow!(
"Trying to enable fan control with no settings provided"

View File

@@ -1,10 +1,11 @@
use super::gpu_controller::{fan_control::FanCurve, GpuController};
use crate::config::{self, Config, FanControlSettings};
use crate::config::{self, default_fan_static_speed, Config, FanControlSettings};
use anyhow::{anyhow, Context};
use lact_schema::{
amdgpu_sysfs::gpu_handle::{power_profile_mode::PowerProfileModesTable, PerformanceLevel},
default_fan_curve,
request::{ConfirmCommand, SetClocksCommand},
ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanCurveMap,
ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanControlMode, FanCurveMap,
};
use std::{cell::RefCell, collections::BTreeMap, env, path::PathBuf, rc::Rc, time::Duration};
use tokio::{sync::oneshot, time::sleep};
@@ -207,36 +208,69 @@ impl<'a> Handler {
&'a self,
id: &str,
enabled: bool,
mode: Option<FanControlMode>,
static_speed: Option<f64>,
curve: Option<FanCurveMap>,
) -> anyhow::Result<u64> {
let settings = match curve {
Some(raw_curve) => {
let curve = FanCurve(raw_curve);
curve.validate()?;
let settings = {
let mut config_guard = self
.config
.try_borrow_mut()
.map_err(|err| anyhow!("{err}"))?;
let gpu_config = config_guard.gpus.entry(id.to_owned()).or_default();
let mut config_guard = self
.config
.try_borrow_mut()
.map_err(|err| anyhow!("{err}"))?;
let gpu_config = config_guard.gpus.entry(id.to_owned()).or_default();
match mode {
Some(mode) => match mode {
FanControlMode::Static => {
if matches!(static_speed, Some(speed) if !(0.0..=1.0).contains(&speed)) {
return Err(anyhow!("static speed value out of range"));
}
if let Some(mut existing_settings) = gpu_config.fan_control_settings.clone() {
existing_settings.curve = curve;
Some(existing_settings)
} else {
Some(FanControlSettings {
curve,
temperature_key: "edge".to_owned(),
interval_ms: 500,
})
}
if let Some(mut existing_settings) = gpu_config.fan_control_settings.clone()
{
existing_settings.mode = mode;
if let Some(static_speed) = static_speed {
existing_settings.static_speed = static_speed;
}
Some(existing_settings)
} else {
Some(FanControlSettings {
mode,
static_speed: static_speed.unwrap_or_else(default_fan_static_speed),
..Default::default()
})
}
}
FanControlMode::Curve => {
if let Some(mut existing_settings) = gpu_config.fan_control_settings.clone()
{
existing_settings.mode = mode;
if let Some(raw_curve) = curve {
let curve = FanCurve(raw_curve);
curve.validate()?;
existing_settings.curve = curve;
}
Some(existing_settings)
} else {
let curve = FanCurve(curve.unwrap_or_else(default_fan_curve));
curve.validate()?;
Some(FanControlSettings {
mode,
curve,
..Default::default()
})
}
}
},
None => None,
}
None => None,
};
self.edit_gpu_config(id.to_owned(), |config| {
config.fan_control_enabled = enabled;
config.fan_control_settings = settings;
if let Some(settings) = settings {
config.fan_control_settings = Some(settings);
}
})
.await
}

View File

@@ -88,9 +88,17 @@ async fn handle_request<'a>(request: Request<'a>, handler: &'a Handler) -> anyho
Request::DevicePowerProfileModes { id } => {
ok_response(handler.get_power_profile_modes(id)?)
}
Request::SetFanControl { id, enabled, curve } => {
ok_response(handler.set_fan_control(id, enabled, curve).await?)
}
Request::SetFanControl {
id,
enabled,
mode,
static_speed,
curve,
} => ok_response(
handler
.set_fan_control(id, enabled, mode, static_speed, curve)
.await?,
),
Request::SetPowerCap { id, cap } => ok_response(handler.set_power_cap(id, cap).await?),
Request::SetPerformanceLevel {
id,

View File

@@ -335,6 +335,8 @@ impl App {
.set_fan_control(
&gpu_id,
thermals_settings.manual_fan_control,
thermals_settings.mode,
thermals_settings.static_speed,
thermals_settings.curve,
)
.context("Could not set fan control")?;

View File

@@ -21,7 +21,6 @@ pub struct FanCurveFrame {
impl FanCurveFrame {
pub fn new() -> Self {
let root_box = Box::new(Orientation::Vertical, 5);
root_box.hide();
let hbox = Box::new(Orientation::Horizontal, 5);

View File

@@ -1,16 +1,19 @@
mod fan_curve_frame;
use fan_curve_frame::FanCurveFrame;
use glib::clone;
use gtk::prelude::*;
use gtk::*;
use lact_client::schema::{default_fan_curve, DeviceStats, FanCurveMap};
use lact_client::schema::{default_fan_curve, DeviceStats, FanControlMode, FanCurveMap};
use super::{label_row, section_box, values_grid, values_row};
use self::fan_curve_frame::FanCurveFrame;
use super::{label_row, section_box, values_grid};
#[derive(Debug)]
pub struct ThermalsSettings {
pub manual_fan_control: bool,
pub mode: Option<FanControlMode>,
pub static_speed: Option<f64>,
pub curve: Option<FanCurveMap>,
}
@@ -19,8 +22,10 @@ pub struct ThermalsPage {
pub container: Box,
temperatures_label: Label,
fan_speed_label: Label,
fan_control_enabled_switch: Switch,
fan_static_speed_adjustment: Adjustment,
fan_curve_frame: FanCurveFrame,
fan_control_mode_stack: Stack,
fan_control_mode_stack_switcher: StackSwitcher,
}
impl ThermalsPage {
@@ -37,51 +42,53 @@ impl ThermalsPage {
container.append(&stats_section);
let fan_control_section = section_box("Fan control");
let fan_control_grid = values_grid();
let fan_control_enabled_switch = Switch::builder()
.active(true)
.halign(Align::End)
.hexpand(true)
.sensitive(false)
.build();
values_row(
"Automatic fan control:",
&fan_control_grid,
&fan_control_enabled_switch,
0,
0,
);
fan_control_section.append(&fan_control_grid);
let fan_curve_frame = FanCurveFrame::new();
fan_control_section.append(&fan_curve_frame.container);
let fan_static_speed_frame = Box::builder()
.orientation(Orientation::Horizontal)
.spacing(5)
.valign(Align::Start)
.build();
let fan_static_speed_adjustment = static_speed_adj(&fan_static_speed_frame);
// Show/hide fan curve when the switch is toggled
{
let fan_curve_frame = fan_curve_frame.clone();
fan_control_enabled_switch.connect_state_set(move |_, state| {
if state {
show_fan_control_warning();
fan_curve_frame.container.hide();
} else {
fan_curve_frame.container.show();
}
Inhibit(false)
});
}
let fan_control_section = section_box("Fan control");
let fan_control_mode_stack = Stack::builder().build();
let fan_control_mode_stack_switcher = StackSwitcher::builder()
.stack(&fan_control_mode_stack)
.visible(false)
.sensitive(false)
.build();
fan_control_mode_stack.add_titled(
&Box::new(Orientation::Vertical, 15),
Some("automatic"),
"Automatic",
);
fan_control_mode_stack.add_titled(&fan_curve_frame.container, Some("curve"), "Curve");
fan_control_mode_stack.add_titled(&fan_static_speed_frame, Some("static"), "Static");
fan_control_section.append(&fan_control_mode_stack_switcher);
fan_control_section.append(&fan_control_mode_stack);
container.append(&fan_control_section);
fan_control_mode_stack.connect_visible_child_name_notify(|stack| {
if stack.visible_child_name() == Some("automatic".into()) {
show_fan_control_warning()
}
});
Self {
container,
temperatures_label,
fan_speed_label,
fan_control_enabled_switch,
fan_static_speed_adjustment,
fan_curve_frame,
fan_control_mode_stack,
fan_control_mode_stack_switcher,
}
}
@@ -114,20 +121,31 @@ impl ThermalsPage {
}
if initial {
self.fan_control_enabled_switch.set_visible(true);
self.fan_control_enabled_switch
self.fan_control_mode_stack_switcher.set_visible(true);
self.fan_control_mode_stack_switcher
.set_sensitive(stats.fan.speed_current.is_some());
self.fan_control_enabled_switch
.set_active(!stats.fan.control_enabled);
let child_name = match stats.fan.control_mode {
Some(mode) if stats.fan.control_enabled => match mode {
FanControlMode::Static => "static",
FanControlMode::Curve => "curve",
},
_ => "automatic",
};
self.fan_control_mode_stack
.set_visible_child_name(child_name);
if let Some(static_speed) = &stats.fan.static_speed {
self.fan_static_speed_adjustment
.set_value(*static_speed * 100.0);
}
if let Some(curve) = &stats.fan.curve {
self.fan_curve_frame.set_curve(curve);
}
if stats.fan.control_enabled {
self.fan_curve_frame.container.show();
} else {
self.fan_curve_frame.container.hide();
if !stats.fan.control_enabled {
if self.fan_curve_frame.get_curve().is_empty() {
self.fan_curve_frame.set_curve(&default_fan_curve());
}
@@ -136,10 +154,14 @@ impl ThermalsPage {
}
pub fn connect_settings_changed<F: Fn() + 'static + Clone>(&self, f: F) {
self.fan_control_enabled_switch
.connect_state_set(clone!(@strong f => move |_, _| {
self.fan_control_mode_stack
.connect_visible_child_name_notify(clone!(@strong f => move |_| {
f();
}));
self.fan_static_speed_adjustment
.connect_value_changed(clone!(@strong f => move |_| {
f();
Inhibit(false)
}));
self.fan_curve_frame.connect_adjusted(move || {
@@ -148,13 +170,26 @@ impl ThermalsPage {
}
pub fn get_thermals_settings(&self) -> Option<ThermalsSettings> {
if self.fan_control_enabled_switch.is_sensitive() {
let manual_fan_control = !self.fan_control_enabled_switch.state();
if self.fan_control_mode_stack_switcher.is_sensitive() {
let name = self.fan_control_mode_stack.visible_child_name();
let name = name
.as_ref()
.map(|name| name.as_str())
.expect("No name on the visible child");
let (manual_fan_control, mode) = match name {
"automatic" => (false, None),
"curve" => (true, Some(FanControlMode::Curve)),
"static" => (true, Some(FanControlMode::Static)),
_ => unreachable!(),
};
let static_speed = Some(self.fan_static_speed_adjustment.value() / 100.0);
let curve = self.fan_curve_frame.get_curve();
let curve = if curve.is_empty() { None } else { Some(curve) };
Some(ThermalsSettings {
manual_fan_control,
mode,
static_speed,
curve,
})
} else {
@@ -163,6 +198,45 @@ impl ThermalsPage {
}
}
fn static_speed_adj(parent_box: &Box) -> Adjustment {
let label = Label::builder()
.label("Speed (in %)")
.halign(Align::Start)
.build();
let adjustment = Adjustment::new(0.0, 0.0, 100.0, 0.1, 1.0, 0.0);
let scale = Scale::builder()
.orientation(Orientation::Horizontal)
.adjustment(&adjustment)
.hexpand(true)
.margin_start(5)
.margin_end(5)
.build();
let value_selector = SpinButton::new(Some(&adjustment), 1.0, 1);
let value_label = Label::new(None);
let popover = Popover::builder().child(&value_selector).build();
let value_button = MenuButton::builder()
.popover(&popover)
.child(&value_label)
.build();
adjustment.connect_value_changed(clone!(@strong value_label => move |adjustment| {
let value = adjustment.value();
value_label.set_text(&format!("{value:.1}"));
}));
adjustment.set_value(50.0);
parent_box.append(&label);
parent_box.append(&scale);
parent_box.append(&value_button);
adjustment
}
fn show_fan_control_warning() {
let diag = MessageDialog::new(None::<&Window>, DialogFlags::empty(), MessageType::Warning, ButtonsType::Ok,
"Warning! Due to a driver bug, a reboot may be required for fan control to properly switch back to automatic.");

View File

@@ -22,8 +22,29 @@ use serde::{Deserialize, Serialize};
use std::{
borrow::Cow,
collections::{BTreeMap, HashMap},
str::FromStr,
};
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum FanControlMode {
Static,
#[default]
Curve,
}
impl FromStr for FanControlMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"curve" => Ok(Self::Curve),
"static" => Ok(Self::Static),
_ => Err("unknown fan control mode".to_string()),
}
}
}
pub type FanCurveMap = BTreeMap<i32, f32>;
pub fn default_fan_curve() -> FanCurveMap {
@@ -165,6 +186,8 @@ pub struct DeviceStats {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FanStats {
pub control_enabled: bool,
pub control_mode: Option<FanControlMode>,
pub static_speed: Option<f64>,
pub curve: Option<FanCurveMap>,
pub speed_current: Option<u32>,
pub speed_max: Option<u32>,

View File

@@ -1,4 +1,4 @@
use crate::FanCurveMap;
use crate::{FanControlMode, FanCurveMap};
use amdgpu_sysfs::gpu_handle::PerformanceLevel;
use serde::{Deserialize, Serialize};
@@ -23,6 +23,8 @@ pub enum Request<'a> {
SetFanControl {
id: &'a str,
enabled: bool,
mode: Option<FanControlMode>,
static_speed: Option<f64>,
curve: Option<FanCurveMap>,
},
SetPowerCap {