Skip to content

Commit 12e9500

Browse files
committed
feat(hwmon): add hwmon interface implementation for TDP control
1 parent 854f9aa commit 12e9500

File tree

7 files changed

+371
-7
lines changed

7 files changed

+371
-7
lines changed

Cargo.lock

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ rog_platform = { git = "https://gitlab.com/asus-linux/asusctl.git", default-feat
2727
xdg = "2.5.2"
2828
toml = "0.7.8"
2929
serde = { version = "1.0", features = ["derive"] }
30+
udev = { version = "0.9.3", features = ["send", "sync"] }
3031

3132
[target.'cfg(target_arch = "x86_64")'.dependencies]
3233
libryzenadj = { git = "https://gitlab.com/shadowapex/libryzenadj-rs.git" }

src/performance/gpu/amd/hwmon.rs

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
use std::{collections::HashMap, fs, io, ops::Add, path::PathBuf, str::FromStr};
2+
3+
use udev::Device;
4+
5+
use crate::performance::gpu::{
6+
platform::hardware::Hardware,
7+
tdp::{HardwareAccess, TDPDevice, TDPError, TDPResult},
8+
};
9+
10+
/// Amount to scale the TDP values by. E.g. 15 == 15000000
11+
const TDP_SCALE: f64 = 1000000.0;
12+
13+
/// Hwmon implementation of TDP control
14+
pub struct Hwmon {
15+
/// Detected hardware TDP limits
16+
hardware: Option<Hardware>,
17+
/// Udev device used to set/get sysfs properties
18+
device: Device,
19+
/// Mapping of attribute labels to their attribute path. In the hwmon
20+
/// interface there are typically "*_label" attributes which name a particular
21+
/// set of attributes that denotes its function. For example, an interface
22+
/// with the attributes:
23+
/// ["power1_cap", "power1_label, power2_cap, power2_label"]
24+
/// Would have this mapping created:
25+
/// {"slowPPT": "power1", "fastPPT": "power2"}
26+
label_map: HashMap<String, String>,
27+
}
28+
29+
impl Hwmon {
30+
pub fn new(path: &str) -> Result<Self, io::Error> {
31+
// Discover the hwmon path for the device
32+
let mut hwmon_path = None;
33+
let search_path = PathBuf::from(format!("{path}/device/hwmon"));
34+
let dir = fs::read_dir(search_path.as_path())?;
35+
for entry in dir {
36+
let path = entry?.path();
37+
if !path.is_dir() {
38+
continue;
39+
}
40+
hwmon_path = Some(search_path.join(path));
41+
}
42+
43+
// Ensure a valid hwmon path was found
44+
let Some(hwmon_path) = hwmon_path else {
45+
return Err(io::Error::new(
46+
io::ErrorKind::NotFound,
47+
"No valid hwmon interface found",
48+
));
49+
};
50+
log::debug!("Found hwmon interface: {hwmon_path:?}");
51+
52+
// Use udev to read/write attributes
53+
let device = Device::from_syspath(hwmon_path.as_path())?;
54+
55+
// Create a mapping of attribute labels to their corresponding attribute.
56+
let mut label_map = HashMap::new();
57+
for attrib in device.attributes() {
58+
log::debug!(
59+
"Found device attribute: {:?}: {:?}",
60+
attrib.name(),
61+
attrib.value()
62+
);
63+
let name = attrib.name().to_string_lossy();
64+
if !name.ends_with("_label") {
65+
continue;
66+
}
67+
68+
let key = attrib.value().to_string_lossy().to_string();
69+
let Some(value) = name.strip_suffix("_label").map(String::from) else {
70+
continue;
71+
};
72+
73+
label_map.insert(key, value);
74+
}
75+
76+
// Get the hardware limits
77+
let hardware = Self::get_limits(&device, &label_map);
78+
79+
let hwmon = Self {
80+
hardware,
81+
device,
82+
label_map,
83+
};
84+
85+
Ok(hwmon)
86+
}
87+
88+
/// Returns the detected TDP limits
89+
fn get_limits(device: &Device, label_map: &HashMap<String, String>) -> Option<Hardware> {
90+
let prefix = label_map.get("fastPPT")?;
91+
92+
let cap_max = format!("{prefix}_cap_max");
93+
let max_value = device.attribute_value(cap_max)?.to_str()?;
94+
let max_value: f64 = max_value.parse().ok()?;
95+
let cap_min = format!("{prefix}_cap_min");
96+
let min_value = device.attribute_value(cap_min)?.to_str()?;
97+
let min_value: f64 = min_value.parse().ok()?;
98+
99+
let hardware = Hardware {
100+
min_tdp: (min_value / TDP_SCALE),
101+
max_tdp: (max_value / TDP_SCALE),
102+
max_boost: 0.0,
103+
};
104+
105+
Some(hardware)
106+
}
107+
108+
/// Returns the current slowPPT value
109+
fn get_slow_ppt_cap<F>(&self) -> Option<F>
110+
where
111+
F: FromStr,
112+
{
113+
self.get_label_value("slowPPT", "cap")
114+
}
115+
116+
/// Set the slowPPT to the given value
117+
fn set_slow_ppt_cap<S>(&mut self, value: S) -> io::Result<()>
118+
where
119+
S: ToString + Add,
120+
{
121+
self.set_label_value("slowPPT", "cap", value)
122+
}
123+
124+
/// Returns the current fastPPT value
125+
fn get_fast_ppt_cap<F>(&self) -> Option<F>
126+
where
127+
F: FromStr,
128+
{
129+
self.get_label_value("fastPPT", "cap")
130+
}
131+
132+
/// Set the fastPPT to the given value
133+
fn set_fast_ppt_cap<S>(&mut self, value: S) -> io::Result<()>
134+
where
135+
S: ToString + Add,
136+
{
137+
self.set_label_value("fastPPT", "cap", value)
138+
}
139+
140+
/// Returns the value of the attribute with the given label name found
141+
/// in the `*_label` attribute. For example, if `power1_label` is "slowPPT",
142+
/// and you want to get the value of `power1_cap`, you can use this method
143+
/// to get the value using the label instead of the attribute name:
144+
/// E.g. `self.get_label_value::<f64>("slowPPT", "cap")`
145+
fn get_label_value<F>(&self, label: &str, attribute: &str) -> Option<F>
146+
where
147+
F: FromStr,
148+
{
149+
let prefix = self.label_map.get(label)?;
150+
let attribute = format!("{prefix}_{attribute}");
151+
let value = self.device.attribute_value(attribute)?.to_str()?;
152+
value.parse().ok()
153+
}
154+
155+
/// Similar to [Hwmon::get_label_value], this method can be used to write
156+
/// a value to the attribute with the given label.
157+
fn set_label_value<S>(&mut self, label: &str, attribute: &str, value: S) -> io::Result<()>
158+
where
159+
S: ToString,
160+
{
161+
let prefix = self.label_map.get(label).unwrap();
162+
let attribute = format!("{prefix}_{attribute}");
163+
self.device
164+
.set_attribute_value(attribute, value.to_string().as_str())
165+
}
166+
}
167+
168+
impl HardwareAccess for Hwmon {
169+
fn hardware(&self) -> Option<&Hardware> {
170+
self.hardware.as_ref()
171+
}
172+
}
173+
174+
impl TDPDevice for Hwmon {
175+
async fn tdp(&self) -> TDPResult<f64> {
176+
let Some(value) = self.get_slow_ppt_cap::<f64>() else {
177+
return Err(TDPError::FeatureUnsupported);
178+
};
179+
180+
// 15000000 == 15
181+
Ok(value / TDP_SCALE)
182+
}
183+
184+
async fn set_tdp(&mut self, value: f64) -> TDPResult<()> {
185+
log::debug!("Setting TDP to: {value}");
186+
if value < 1.0 {
187+
log::warn!("Cowardly refusing to set TDP less than 1W");
188+
return Err(TDPError::InvalidArgument(format!(
189+
"Cowardly refusing to set TDP less than 1W: provided {value}W",
190+
)));
191+
}
192+
193+
// Get the current boost value before updating. We will
194+
// use this value to also adjust the Fast PPT Limit.
195+
let boost = self.boost().await? as u64;
196+
let slow_ppt = (value * TDP_SCALE) as u64; // 15 == 15000000
197+
let fast_ppt = (value * TDP_SCALE) as u64 + boost;
198+
199+
self.set_slow_ppt_cap(slow_ppt)?;
200+
self.set_fast_ppt_cap(fast_ppt)?;
201+
202+
Ok(())
203+
}
204+
205+
async fn boost(&self) -> TDPResult<f64> {
206+
let Some(slow_ppt) = self.get_slow_ppt_cap::<f64>() else {
207+
return Err(TDPError::FeatureUnsupported);
208+
};
209+
let Some(fast_ppt) = self.get_fast_ppt_cap::<f64>() else {
210+
return Err(TDPError::FeatureUnsupported);
211+
};
212+
213+
// Boost is the difference between fastPPT and slowPPT
214+
let boost = (fast_ppt - slow_ppt).max(0.0);
215+
216+
Ok(boost / TDP_SCALE)
217+
}
218+
219+
async fn set_boost(&mut self, value: f64) -> TDPResult<()> {
220+
log::debug!("Setting boost to: {value}");
221+
if value < 0.0 {
222+
log::warn!("Cowardly refusing to set TDP Boost less than 0W");
223+
return Err(TDPError::InvalidArgument(format!(
224+
"Cowardly refusing to set TDP Boost less than 0W: {}W provided",
225+
value
226+
)));
227+
}
228+
229+
let Some(slow_ppt_raw) = self.get_slow_ppt_cap::<f64>() else {
230+
return Err(TDPError::FeatureUnsupported);
231+
};
232+
let slow_ppt_scaled = slow_ppt_raw / TDP_SCALE;
233+
let fast_ppt_scaled = slow_ppt_scaled + value;
234+
let fast_ppt_raw = (fast_ppt_scaled * TDP_SCALE) as u64;
235+
236+
self.set_fast_ppt_cap(fast_ppt_raw)?;
237+
238+
Ok(())
239+
}
240+
241+
async fn thermal_throttle_limit_c(&self) -> TDPResult<f64> {
242+
Err(TDPError::FeatureUnsupported)
243+
}
244+
245+
async fn set_thermal_throttle_limit_c(&mut self, _limit: f64) -> TDPResult<()> {
246+
Err(TDPError::FeatureUnsupported)
247+
}
248+
249+
async fn power_profile(&self) -> TDPResult<String> {
250+
Err(TDPError::FeatureUnsupported)
251+
}
252+
253+
async fn power_profiles_available(&self) -> TDPResult<Vec<String>> {
254+
Err(TDPError::FeatureUnsupported)
255+
}
256+
257+
async fn set_power_profile(&mut self, _profile: String) -> TDPResult<()> {
258+
Err(TDPError::FeatureUnsupported)
259+
}
260+
}

src/performance/gpu/amd/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod amdgpu;
2+
pub mod hwmon;
23
pub mod ryzenadj;
34
pub mod tdp;

0 commit comments

Comments
 (0)