mirror of
https://github.com/ilya-zlobintsev/LACT.git
synced 2025-02-25 18:55:26 -06:00
feat: add support for custom fan curves and static fan speed on RDNA3
This commit is contained in:
parent
a8c2c60f4a
commit
f4ed59469c
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -41,9 +41,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "amdgpu-sysfs"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04b3c8f53aaed3c9a8f9b71c7d261a4343703ed3a5758eda16646e9b57c4e263"
|
||||
version = "0.14.0"
|
||||
source = "git+https://github.com/ilya-zlobintsev/amdgpu-sysfs-rs?branch=feature/rnda3-fan-curve#0d270af398dc863d7f3f976887bb5810ed754abf"
|
||||
dependencies = [
|
||||
"enum_dispatch",
|
||||
"serde",
|
||||
|
@ -1,5 +1,8 @@
|
||||
use anyhow::anyhow;
|
||||
use lact_schema::{amdgpu_sysfs::hw_mon::Temperature, default_fan_curve, FanCurveMap};
|
||||
use lact_schema::{
|
||||
amdgpu_sysfs::{gpu_handle::fan_control::FanCurve as PmfwCurve, hw_mon::Temperature},
|
||||
default_fan_curve, FanCurveMap,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::warn;
|
||||
|
||||
@ -39,6 +42,29 @@ impl FanCurve {
|
||||
|
||||
(f32::from(u8::MAX) * percentage) as u8
|
||||
}
|
||||
|
||||
pub fn into_pmfw_curve(self, current_pmfw_curve: PmfwCurve) -> anyhow::Result<PmfwCurve> {
|
||||
if current_pmfw_curve.points.len() != self.0.len() {
|
||||
return Err(anyhow!(
|
||||
"The GPU only supports {} curve points, given {}",
|
||||
current_pmfw_curve.points.len(),
|
||||
self.0.len()
|
||||
));
|
||||
}
|
||||
let points = self
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|(temp, ratio)| {
|
||||
let pwm = (f32::from(u8::MAX) * ratio) as u8;
|
||||
(temp, pwm)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(PmfwCurve {
|
||||
points,
|
||||
allowed_ranges: current_pmfw_curve.allowed_ranges,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FanCurve {
|
||||
@ -60,7 +86,7 @@ impl Default for FanCurve {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::FanCurve;
|
||||
use super::{FanCurve, PmfwCurve};
|
||||
use lact_schema::amdgpu_sysfs::hw_mon::Temperature;
|
||||
|
||||
fn simple_pwm(temp: f32) -> u8 {
|
||||
@ -147,9 +173,7 @@ mod tests {
|
||||
};
|
||||
curve.pwm_at_temp(temp)
|
||||
};
|
||||
assert_eq!(pwm_at_temp(20.0), 0);
|
||||
assert_eq!(pwm_at_temp(30.0), 0);
|
||||
assert_eq!(pwm_at_temp(33.0), 15);
|
||||
assert_eq!(pwm_at_temp(40.0), 51);
|
||||
assert_eq!(pwm_at_temp(60.0), 127);
|
||||
assert_eq!(pwm_at_temp(65.0), 159);
|
||||
assert_eq!(pwm_at_temp(70.0), 191);
|
||||
@ -158,4 +182,16 @@ mod tests {
|
||||
assert_eq!(pwm_at_temp(100.0), 255);
|
||||
assert_eq!(pwm_at_temp(-5.0), 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_curve_to_pmfw() {
|
||||
let curve = FanCurve::default();
|
||||
let current_pmfw_curve = PmfwCurve {
|
||||
points: Box::new([(0, 0); 5]),
|
||||
allowed_ranges: None,
|
||||
};
|
||||
let pmfw_curve = curve.into_pmfw_curve(current_pmfw_curve).unwrap();
|
||||
let expected_points = [(40, 51), (50, 89), (60, 127), (70, 191), (80, 255)];
|
||||
assert_eq!(&expected_points, pmfw_curve.points.as_ref());
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ use lact_schema::{
|
||||
amdgpu_sysfs::{
|
||||
error::Error,
|
||||
gpu_handle::{
|
||||
fan_control::FanCurve as PmfwCurve,
|
||||
overdrive::{ClocksTable, ClocksTableGen},
|
||||
GpuHandle, PerformanceLevel, PowerLevelKind, PowerLevels,
|
||||
},
|
||||
@ -331,27 +332,47 @@ impl GpuController {
|
||||
// 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")?;
|
||||
|
||||
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
|
||||
let static_speed_converted = (f64::from(u8::MAX) * static_speed) as u8;
|
||||
let static_pwm = (f64::from(u8::MAX) * static_speed) as u8;
|
||||
|
||||
hw_mon
|
||||
.set_fan_pwm(static_speed_converted)
|
||||
.context("could not set fan speed")?;
|
||||
// Use PMFW curve functionality for static speed when it is available
|
||||
if let Ok(current_curve) = self.handle.get_fan_curve() {
|
||||
let allowed_ranges = current_curve.allowed_ranges.ok_or_else(|| {
|
||||
anyhow!("The GPU does not allow setting custom fan values (is overdrive enabled?)")
|
||||
})?;
|
||||
let temperature = allowed_ranges.temperature_range.end();
|
||||
let points =
|
||||
vec![(*temperature, static_pwm); current_curve.points.len()].into_boxed_slice();
|
||||
let new_curve = PmfwCurve {
|
||||
points,
|
||||
allowed_ranges: Some(allowed_ranges),
|
||||
};
|
||||
|
||||
debug!("set fan speed to {}", static_speed);
|
||||
self.handle
|
||||
.set_fan_curve(&new_curve)
|
||||
.context("Could not set fan curve")?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
} else {
|
||||
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")?;
|
||||
|
||||
hw_mon
|
||||
.set_fan_pwm(static_pwm)
|
||||
.context("could not set fan speed")?;
|
||||
|
||||
debug!("set fan speed to {}", static_speed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_curve_fan_control(
|
||||
@ -359,6 +380,33 @@ impl GpuController {
|
||||
curve: FanCurve,
|
||||
temp_key: String,
|
||||
interval: Duration,
|
||||
) -> anyhow::Result<()> {
|
||||
// Use the PMFW curve functionality when it is available
|
||||
// Otherwise, fall back to manual fan control via a task
|
||||
match self.handle.get_fan_curve() {
|
||||
Ok(current_curve) => {
|
||||
let new_curve = curve
|
||||
.into_pmfw_curve(current_curve)
|
||||
.context("Invalid fan curve")?;
|
||||
|
||||
self.handle
|
||||
.set_fan_curve(&new_curve)
|
||||
.context("Could not set fan curve")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => {
|
||||
self.start_curve_fan_control_task(curve, temp_key, interval)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_curve_fan_control_task(
|
||||
&self,
|
||||
curve: FanCurve,
|
||||
temp_key: String,
|
||||
interval: Duration,
|
||||
) -> anyhow::Result<()> {
|
||||
// Stop existing task to re-apply new curve
|
||||
self.stop_fan_control(false).await?;
|
||||
@ -500,35 +548,6 @@ 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 {
|
||||
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_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"
|
||||
));
|
||||
}
|
||||
} else {
|
||||
self.stop_fan_control(true).await?;
|
||||
}
|
||||
|
||||
if let Some(cap) = config.power_cap {
|
||||
let hw_mon = self.first_hw_mon()?;
|
||||
|
||||
@ -629,7 +648,34 @@ impl GpuController {
|
||||
.with_context(|| format!("Could not set {kind:?} power states"))?;
|
||||
}
|
||||
|
||||
if !config.fan_control_enabled {
|
||||
if config.fan_control_enabled {
|
||||
if let Some(ref settings) = config.fan_control_settings {
|
||||
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_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"
|
||||
));
|
||||
}
|
||||
} else {
|
||||
self.stop_fan_control(true).await?;
|
||||
|
||||
let pmfw = &config.pmfw_options;
|
||||
if let Some(acoustic_limit) = pmfw.acoustic_limit {
|
||||
self.handle
|
||||
|
@ -7,7 +7,9 @@ edition = "2021"
|
||||
args = ["clap"]
|
||||
|
||||
[dependencies]
|
||||
amdgpu-sysfs = { version = "0.13.0", features = ["serde"] }
|
||||
amdgpu-sysfs = { git = "https://github.com/ilya-zlobintsev/amdgpu-sysfs-rs", branch = "feature/rnda3-fan-curve", features = [
|
||||
"serde",
|
||||
] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
indexmap = { version = "*", features = ["serde"] }
|
||||
clap = { version = "4.4.11", features = ["derive"], optional = true }
|
||||
|
@ -50,15 +50,7 @@ impl FromStr for FanControlMode {
|
||||
pub type FanCurveMap = BTreeMap<i32, f32>;
|
||||
|
||||
pub fn default_fan_curve() -> FanCurveMap {
|
||||
[
|
||||
(30, 0.0),
|
||||
(40, 0.2),
|
||||
(50, 0.35),
|
||||
(60, 0.5),
|
||||
(70, 0.75),
|
||||
(80, 1.0),
|
||||
]
|
||||
.into()
|
||||
[(40, 0.2), (50, 0.35), (60, 0.5), (70, 0.75), (80, 1.0)].into()
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
Loading…
Reference in New Issue
Block a user