From d7454a03b53f3f92d82fcb3bbf9538a8c166518a Mon Sep 17 00:00:00 2001 From: William Edwards Date: Thu, 12 Jun 2025 14:31:52 -0700 Subject: [PATCH] feat(hwmon): add hwmon interface implementation for TDP control --- Cargo.lock | 15 +- Cargo.toml | 1 + src/performance/gpu/amd/hwmon.rs | 260 +++++++++++++++++++++++ src/performance/gpu/amd/mod.rs | 1 + src/performance/gpu/amd/tdp.rs | 86 +++++++- src/performance/gpu/platform/hardware.rs | 7 +- src/performance/gpu/tdp.rs | 8 + 7 files changed, 371 insertions(+), 7 deletions(-) create mode 100644 src/performance/gpu/amd/hwmon.rs diff --git a/Cargo.lock b/Cargo.lock index bab4085..1ec6a1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1232,6 +1232,7 @@ dependencies = [ "simple_logger", "tokio", "toml", + "udev 0.9.3", "xdg", "zbus 3.15.2", "zbus_macros 3.15.2", @@ -1372,7 +1373,7 @@ dependencies = [ "rusb", "serde", "typeshare", - "udev", + "udev 0.8.0", "zbus 5.1.1", ] @@ -1803,6 +1804,18 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "udev" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4e37e9ea4401fc841ff54b9ddfc9be1079b1e89434c1a6a865dd68980f7e9f" +dependencies = [ + "io-lifetimes", + "libc", + "libudev-sys", + "pkg-config", +] + [[package]] name = "uds_windows" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 32cd4c3..adf9018 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ rog_platform = { git = "https://gitlab.com/asus-linux/asusctl.git", default-feat xdg = "2.5.2" toml = "0.7.8" serde = { version = "1.0", features = ["derive"] } +udev = { version = "0.9.3", features = ["send", "sync"] } [target.'cfg(target_arch = "x86_64")'.dependencies] libryzenadj = { git = "https://gitlab.com/shadowapex/libryzenadj-rs.git" } diff --git a/src/performance/gpu/amd/hwmon.rs b/src/performance/gpu/amd/hwmon.rs new file mode 100644 index 0000000..5bd2b97 --- /dev/null +++ b/src/performance/gpu/amd/hwmon.rs @@ -0,0 +1,260 @@ +use std::{collections::HashMap, fs, io, ops::Add, path::PathBuf, str::FromStr}; + +use udev::Device; + +use crate::performance::gpu::{ + platform::hardware::Hardware, + tdp::{HardwareAccess, TDPDevice, TDPError, TDPResult}, +}; + +/// Amount to scale the TDP values by. E.g. 15 == 15000000 +const TDP_SCALE: f64 = 1000000.0; + +/// Hwmon implementation of TDP control +pub struct Hwmon { + /// Detected hardware TDP limits + hardware: Option, + /// Udev device used to set/get sysfs properties + device: Device, + /// Mapping of attribute labels to their attribute path. In the hwmon + /// interface there are typically "*_label" attributes which name a particular + /// set of attributes that denotes its function. For example, an interface + /// with the attributes: + /// ["power1_cap", "power1_label, power2_cap, power2_label"] + /// Would have this mapping created: + /// {"slowPPT": "power1", "fastPPT": "power2"} + label_map: HashMap, +} + +impl Hwmon { + pub fn new(path: &str) -> Result { + // Discover the hwmon path for the device + let mut hwmon_path = None; + let search_path = PathBuf::from(format!("{path}/device/hwmon")); + let dir = fs::read_dir(search_path.as_path())?; + for entry in dir { + let path = entry?.path(); + if !path.is_dir() { + continue; + } + hwmon_path = Some(search_path.join(path)); + } + + // Ensure a valid hwmon path was found + let Some(hwmon_path) = hwmon_path else { + return Err(io::Error::new( + io::ErrorKind::NotFound, + "No valid hwmon interface found", + )); + }; + log::debug!("Found hwmon interface: {hwmon_path:?}"); + + // Use udev to read/write attributes + let device = Device::from_syspath(hwmon_path.as_path())?; + + // Create a mapping of attribute labels to their corresponding attribute. + let mut label_map = HashMap::new(); + for attrib in device.attributes() { + log::debug!( + "Found device attribute: {:?}: {:?}", + attrib.name(), + attrib.value() + ); + let name = attrib.name().to_string_lossy(); + if !name.ends_with("_label") { + continue; + } + + let key = attrib.value().to_string_lossy().to_string(); + let Some(value) = name.strip_suffix("_label").map(String::from) else { + continue; + }; + + label_map.insert(key, value); + } + + // Get the hardware limits + let hardware = Self::get_limits(&device, &label_map); + + let hwmon = Self { + hardware, + device, + label_map, + }; + + Ok(hwmon) + } + + /// Returns the detected TDP limits + fn get_limits(device: &Device, label_map: &HashMap) -> Option { + let prefix = label_map.get("fastPPT")?; + + let cap_max = format!("{prefix}_cap_max"); + let max_value = device.attribute_value(cap_max)?.to_str()?; + let max_value: f64 = max_value.parse().ok()?; + let cap_min = format!("{prefix}_cap_min"); + let min_value = device.attribute_value(cap_min)?.to_str()?; + let min_value: f64 = min_value.parse().ok()?; + + let hardware = Hardware { + min_tdp: (min_value / TDP_SCALE), + max_tdp: (max_value / TDP_SCALE), + max_boost: 0.0, + }; + + Some(hardware) + } + + /// Returns the current slowPPT value + fn get_slow_ppt_cap(&self) -> Option + where + F: FromStr, + { + self.get_label_value("slowPPT", "cap") + } + + /// Set the slowPPT to the given value + fn set_slow_ppt_cap(&mut self, value: S) -> io::Result<()> + where + S: ToString + Add, + { + self.set_label_value("slowPPT", "cap", value) + } + + /// Returns the current fastPPT value + fn get_fast_ppt_cap(&self) -> Option + where + F: FromStr, + { + self.get_label_value("fastPPT", "cap") + } + + /// Set the fastPPT to the given value + fn set_fast_ppt_cap(&mut self, value: S) -> io::Result<()> + where + S: ToString + Add, + { + self.set_label_value("fastPPT", "cap", value) + } + + /// Returns the value of the attribute with the given label name found + /// in the `*_label` attribute. For example, if `power1_label` is "slowPPT", + /// and you want to get the value of `power1_cap`, you can use this method + /// to get the value using the label instead of the attribute name: + /// E.g. `self.get_label_value::("slowPPT", "cap")` + fn get_label_value(&self, label: &str, attribute: &str) -> Option + where + F: FromStr, + { + let prefix = self.label_map.get(label)?; + let attribute = format!("{prefix}_{attribute}"); + let value = self.device.attribute_value(attribute)?.to_str()?; + value.parse().ok() + } + + /// Similar to [Hwmon::get_label_value], this method can be used to write + /// a value to the attribute with the given label. + fn set_label_value(&mut self, label: &str, attribute: &str, value: S) -> io::Result<()> + where + S: ToString, + { + let prefix = self.label_map.get(label).unwrap(); + let attribute = format!("{prefix}_{attribute}"); + self.device + .set_attribute_value(attribute, value.to_string().as_str()) + } +} + +impl HardwareAccess for Hwmon { + fn hardware(&self) -> Option<&Hardware> { + self.hardware.as_ref() + } +} + +impl TDPDevice for Hwmon { + async fn tdp(&self) -> TDPResult { + let Some(value) = self.get_slow_ppt_cap::() else { + return Err(TDPError::FeatureUnsupported); + }; + + // 15000000 == 15 + Ok(value / TDP_SCALE) + } + + async fn set_tdp(&mut self, value: f64) -> TDPResult<()> { + log::debug!("Setting TDP to: {value}"); + if value < 1.0 { + log::warn!("Cowardly refusing to set TDP less than 1W"); + return Err(TDPError::InvalidArgument(format!( + "Cowardly refusing to set TDP less than 1W: provided {value}W", + ))); + } + + // Get the current boost value before updating. We will + // use this value to also adjust the Fast PPT Limit. + let boost = self.boost().await? as u64; + let slow_ppt = (value * TDP_SCALE) as u64; // 15 == 15000000 + let fast_ppt = (value * TDP_SCALE) as u64 + boost; + + self.set_slow_ppt_cap(slow_ppt)?; + self.set_fast_ppt_cap(fast_ppt)?; + + Ok(()) + } + + async fn boost(&self) -> TDPResult { + let Some(slow_ppt) = self.get_slow_ppt_cap::() else { + return Err(TDPError::FeatureUnsupported); + }; + let Some(fast_ppt) = self.get_fast_ppt_cap::() else { + return Err(TDPError::FeatureUnsupported); + }; + + // Boost is the difference between fastPPT and slowPPT + let boost = (fast_ppt - slow_ppt).max(0.0); + + Ok(boost / TDP_SCALE) + } + + async fn set_boost(&mut self, value: f64) -> TDPResult<()> { + log::debug!("Setting boost to: {value}"); + if value < 0.0 { + log::warn!("Cowardly refusing to set TDP Boost less than 0W"); + return Err(TDPError::InvalidArgument(format!( + "Cowardly refusing to set TDP Boost less than 0W: {}W provided", + value + ))); + } + + let Some(slow_ppt_raw) = self.get_slow_ppt_cap::() else { + return Err(TDPError::FeatureUnsupported); + }; + let slow_ppt_scaled = slow_ppt_raw / TDP_SCALE; + let fast_ppt_scaled = slow_ppt_scaled + value; + let fast_ppt_raw = (fast_ppt_scaled * TDP_SCALE) as u64; + + self.set_fast_ppt_cap(fast_ppt_raw)?; + + Ok(()) + } + + async fn thermal_throttle_limit_c(&self) -> TDPResult { + Err(TDPError::FeatureUnsupported) + } + + async fn set_thermal_throttle_limit_c(&mut self, _limit: f64) -> TDPResult<()> { + Err(TDPError::FeatureUnsupported) + } + + async fn power_profile(&self) -> TDPResult { + Err(TDPError::FeatureUnsupported) + } + + async fn power_profiles_available(&self) -> TDPResult> { + Err(TDPError::FeatureUnsupported) + } + + async fn set_power_profile(&mut self, _profile: String) -> TDPResult<()> { + Err(TDPError::FeatureUnsupported) + } +} diff --git a/src/performance/gpu/amd/mod.rs b/src/performance/gpu/amd/mod.rs index f939d63..a8ec09e 100644 --- a/src/performance/gpu/amd/mod.rs +++ b/src/performance/gpu/amd/mod.rs @@ -1,3 +1,4 @@ pub mod amdgpu; +pub mod hwmon; pub mod ryzenadj; pub mod tdp; diff --git a/src/performance/gpu/amd/tdp.rs b/src/performance/gpu/amd/tdp.rs index f9d6578..4770f4a 100644 --- a/src/performance/gpu/amd/tdp.rs +++ b/src/performance/gpu/amd/tdp.rs @@ -5,6 +5,7 @@ use crate::performance::gpu::{ tdp::{HardwareAccess, TDPDevice, TDPError, TDPResult}, }; +use super::hwmon::Hwmon; #[cfg(target_arch = "x86_64")] use super::ryzenadj::RyzenAdjTdp; @@ -14,6 +15,7 @@ pub struct Tdp { acpi: Option, #[cfg(target_arch = "x86_64")] ryzenadj: Option, + hwmon: Option, hardware: Option, } @@ -42,6 +44,17 @@ impl Tdp { None => None, }; + let hwmon = match Hwmon::new(path) { + Ok(hwmon) => { + log::info!("Found hwmon interface for TDP control"); + Some(hwmon) + } + Err(e) => { + log::debug!("Unable to find hwmon interface: {e}"); + None + } + }; + #[cfg(target_arch = "x86_64")] let ryzenadj = match RyzenAdjTdp::new(path.to_string(), device_id.to_string()) { Ok(ryzenadj) => { @@ -59,7 +72,14 @@ impl Tdp { log::info!("Found Hardware interface for TDP control"); Some(hardware) } - None => None, + None => { + if let Some(hwmon) = hwmon.as_ref() { + log::info!("Found Hardware interface from hwmon for TDP control"); + hwmon.hardware().cloned() + } else { + None + } + } }; Tdp { @@ -67,6 +87,7 @@ impl Tdp { acpi, #[cfg(target_arch = "x86_64")] ryzenadj, + hwmon, hardware, } } @@ -89,6 +110,20 @@ impl TDPDevice for Tdp { } }; }; + + // TODO: set platform profile based on % of max TDP. + if let Some(hwmon) = self.hwmon.as_ref() { + match hwmon.tdp().await { + Ok(tdp) => { + log::info!("TDP is currently {tdp}"); + return Ok(tdp); + } + Err(e) => { + log::warn!("Failed to read current TDP using hwmon: {e:?}"); + } + }; + } + // TODO: set platform profile based on % of max TDP. #[cfg(target_arch = "x86_64")] if self.ryzenadj.is_some() { @@ -103,6 +138,7 @@ impl TDPDevice for Tdp { } }; }; + Err(TDPError::FailedOperation( "No TDP Interface available to read TDP.".into(), )) @@ -122,6 +158,19 @@ impl TDPDevice for Tdp { } }; }; + + if let Some(hwmon) = self.hwmon.as_mut() { + match hwmon.set_tdp(value).await { + Ok(tdp) => { + log::info!("TDP set to {value}"); + return Ok(tdp); + } + Err(e) => { + log::warn!("Failed to set TDP using hwmon: {e:?}"); + } + }; + } + #[cfg(target_arch = "x86_64")] if self.ryzenadj.is_some() { let ryzenadj = self.ryzenadj.as_mut().unwrap(); @@ -135,6 +184,7 @@ impl TDPDevice for Tdp { } }; }; + Err(TDPError::FailedOperation( "No TDP Interface available to set TDP.".into(), )) @@ -154,6 +204,19 @@ impl TDPDevice for Tdp { } }; }; + + if let Some(hwmon) = self.hwmon.as_ref() { + match hwmon.boost().await { + Ok(boost) => { + log::info!("Boost is currently {boost}"); + return Ok(boost); + } + Err(e) => { + log::warn!("Failed to read current boost using hwmon: {e:?}"); + } + }; + }; + #[cfg(target_arch = "x86_64")] if self.ryzenadj.is_some() { let ryzenadj = self.ryzenadj.as_ref().unwrap(); @@ -167,6 +230,7 @@ impl TDPDevice for Tdp { } }; }; + Err(TDPError::FailedOperation( "No TDP Interface available to read boost.".into(), )) @@ -186,6 +250,19 @@ impl TDPDevice for Tdp { } }; }; + + if let Some(hwmon) = self.hwmon.as_mut() { + match hwmon.set_boost(value).await { + Ok(_) => { + log::info!("Boost set to {value}"); + return Ok(()); + } + Err(e) => { + log::warn!("Failed to set boost using hwmon: {e:?}"); + } + }; + }; + #[cfg(target_arch = "x86_64")] if self.ryzenadj.is_some() { let ryzenadj = self.ryzenadj.as_mut().unwrap(); @@ -199,6 +276,7 @@ impl TDPDevice for Tdp { } }; }; + Err(TDPError::FailedOperation( "No TDP Interface available to set boost.".into(), )) @@ -219,6 +297,7 @@ impl TDPDevice for Tdp { } }; }; + Err(TDPError::FailedOperation( "No TDP Interface available to read thermal throttle limit.".into(), )) @@ -239,6 +318,7 @@ impl TDPDevice for Tdp { } }; }; + Err(TDPError::FailedOperation( "No TDP Interface available to set thermal throttle limit.".into(), )) @@ -272,6 +352,7 @@ impl TDPDevice for Tdp { } }; }; + Err(TDPError::FailedOperation( "No TDP Interface available to read power profile.".into(), )) @@ -305,6 +386,7 @@ impl TDPDevice for Tdp { } }; }; + Err(TDPError::FailedOperation( "No TDP Interface available to set power profile.".into(), )) @@ -323,6 +405,7 @@ impl TDPDevice for Tdp { } }; }; + #[cfg(target_arch = "x86_64")] if self.ryzenadj.is_some() { let ryzenadj = self.ryzenadj.as_ref().unwrap(); @@ -336,6 +419,7 @@ impl TDPDevice for Tdp { } }; }; + Err(TDPError::FailedOperation( "No TDP Interface available to list available power profiles.".into(), )) diff --git a/src/performance/gpu/platform/hardware.rs b/src/performance/gpu/platform/hardware.rs index 1fbe967..72d2201 100644 --- a/src/performance/gpu/platform/hardware.rs +++ b/src/performance/gpu/platform/hardware.rs @@ -4,6 +4,7 @@ use std::fs; use std::io::ErrorKind; use std::path::Path; +#[derive(Default, Clone)] pub struct Hardware { pub min_tdp: f64, pub max_tdp: f64, @@ -22,11 +23,7 @@ impl Hardware { Ok(hardware) => Some(hardware), Err(e) => { log::error!("Failed to load hardware configuration: {}", e); - Some(Self { - min_tdp: 0.0, - max_tdp: 0.0, - max_boost: 0.0, - }) + None } } } diff --git a/src/performance/gpu/tdp.rs b/src/performance/gpu/tdp.rs index b342fb7..fd13d63 100644 --- a/src/performance/gpu/tdp.rs +++ b/src/performance/gpu/tdp.rs @@ -1,3 +1,5 @@ +use std::io; + #[derive(Debug)] pub enum TDPError { FeatureUnsupported, @@ -12,6 +14,12 @@ impl From for String { } } +impl From for TDPError { + fn from(value: io::Error) -> Self { + Self::IOError(value.to_string()) + } +} + pub type TDPResult = Result; // Helper trait to simplify access to hardware information