1#[cfg(feature = "overdrive")]
3pub mod overdrive;
4#[macro_use]
5mod power_levels;
6pub mod fan_control;
7pub mod power_profile_mode;
8
9pub use power_levels::{PowerLevelKind, PowerLevels};
10
11use self::fan_control::{FanCurve, FanCurveRanges, FanInfo};
12use crate::{
13 error::{Error, ErrorContext, ErrorKind},
14 gpu_handle::fan_control::FanCtrlContents,
15 hw_mon::HwMon,
16 sysfs::SysFS,
17 Result,
18};
19use power_profile_mode::PowerProfileModesTable;
20#[cfg(feature = "serde")]
21use serde::{Deserialize, Serialize};
22use std::{
23 collections::HashMap,
24 fmt::{self, Display, Write as _},
25 fs,
26 io::Write,
27 path::{Path, PathBuf},
28 str::FromStr,
29};
30#[cfg(feature = "overdrive")]
31use {
32 self::overdrive::{ClocksTable, ClocksTableGen},
33 std::fs::File,
34};
35
36#[derive(Clone, Debug)]
38pub struct GpuHandle {
39 sysfs_path: PathBuf,
40 pub hw_monitors: Vec<HwMon>,
42 uevent: HashMap<String, String>,
43}
44
45impl GpuHandle {
46 pub fn new_from_path(sysfs_path: PathBuf) -> Result<Self> {
51 let mut hw_monitors = Vec::new();
52
53 if let Ok(hw_mons_iter) = fs::read_dir(sysfs_path.join("hwmon")) {
54 for hw_mon_dir in hw_mons_iter.flatten() {
55 if let Ok(hw_mon) = HwMon::new_from_path(hw_mon_dir.path()) {
56 hw_monitors.push(hw_mon);
57 }
58 }
59 }
60
61 let uevent_raw = fs::read_to_string(sysfs_path.join("uevent"))?.replace(char::from(0), "");
62
63 let mut uevent = HashMap::new();
64
65 for (i, line) in uevent_raw.trim().lines().enumerate() {
66 let (key, value) = line
67 .split_once('=')
68 .ok_or_else(|| Error::unexpected_eol("=", i))?;
69
70 uevent.insert(key.to_owned(), value.to_owned());
71 }
72
73 match uevent.get("DRIVER") {
74 Some(_) => Ok(Self {
75 sysfs_path,
76 hw_monitors,
77 uevent,
78 }),
79 None => Err(ErrorKind::InvalidSysFS.into()),
80 }
81 }
82
83 pub fn get_driver(&self) -> &str {
85 self.uevent.get("DRIVER").unwrap()
86 }
87
88 pub fn get_pci_id(&self) -> Option<(&str, &str)> {
90 match self.uevent.get("PCI_ID") {
91 Some(pci_str) => pci_str.split_once(':'),
92 None => None,
93 }
94 }
95
96 pub fn get_pci_subsys_id(&self) -> Option<(&str, &str)> {
98 match self.uevent.get("PCI_SUBSYS_ID") {
99 Some(pci_str) => pci_str.split_once(':'),
100 None => None,
101 }
102 }
103
104 pub fn get_pci_slot_name(&self) -> Option<&str> {
106 self.uevent.get("PCI_SLOT_NAME").map(|s| s.as_str())
107 }
108
109 fn get_link(&self, file_name: &str) -> Result<String> {
110 const NAVI10_UPSTREAM_PORT: &str = "0x1478\n";
112 const NAVI10_DOWNSTREAM_PORT: &str = "0x1479\n";
113
114 let mut sysfs_path = std::fs::canonicalize(self.get_path())?.join("../"); for _ in 0..2 {
117 let Ok(did) = std::fs::read_to_string(sysfs_path.join("device")) else {
118 break;
119 };
120
121 if did == NAVI10_UPSTREAM_PORT || did == NAVI10_DOWNSTREAM_PORT {
122 sysfs_path.push("../");
123 } else {
124 break;
125 }
126 }
127
128 sysfs_path.pop();
129
130 Self {
131 sysfs_path,
132 hw_monitors: Vec::new(),
133 uevent: HashMap::new(),
134 }
135 .read_file(file_name)
136 }
137
138 pub fn get_current_link_speed(&self) -> Result<String> {
140 self.get_link("current_link_speed")
141 }
142
143 pub fn get_current_link_width(&self) -> Result<String> {
145 self.get_link("current_link_width")
146 }
147
148 pub fn get_max_link_speed(&self) -> Result<String> {
150 self.get_link("max_link_speed")
151 }
152
153 pub fn get_max_link_width(&self) -> Result<String> {
155 self.get_link("max_link_width")
156 }
157
158 fn read_vram_file(&self, file: &str) -> Result<u64> {
159 let raw_vram = self.read_file(file)?;
160 Ok(raw_vram.parse()?)
161 }
162
163 pub fn get_total_vram(&self) -> Result<u64> {
165 self.read_vram_file("mem_info_vram_total")
166 }
167
168 pub fn get_used_vram(&self) -> Result<u64> {
170 self.read_vram_file("mem_info_vram_used")
171 }
172
173 pub fn get_busy_percent(&self) -> Result<u8> {
175 let raw_busy = self.read_file("gpu_busy_percent")?;
176 Ok(raw_busy.parse()?)
177 }
178
179 pub fn get_vbios_version(&self) -> Result<String> {
181 self.read_file("vbios_version")
182 }
183
184 pub fn get_vram_vendor(&self) -> Result<String> {
186 self.read_file("mem_info_vram_vendor")
187 }
188
189 pub fn get_power_force_performance_level(&self) -> Result<PerformanceLevel> {
191 let raw_level = self.read_file("power_dpm_force_performance_level")?;
192 PerformanceLevel::from_str(&raw_level)
193 }
194
195 pub fn set_power_force_performance_level(&self, level: PerformanceLevel) -> Result<()> {
197 self.write_file("power_dpm_force_performance_level", level.to_string())
198 }
199
200 pub fn get_clock_levels<T>(&self, kind: PowerLevelKind) -> Result<PowerLevels<T>>
203 where
204 T: FromStr,
205 <T as FromStr>::Err: Display,
206 {
207 self.read_file(kind.filename()).and_then(|content| {
208 let mut levels = Vec::new();
209 let mut active = None;
210 let mut invalid_active = false;
211
212 for mut line in content.trim().split('\n') {
213 if let Some(stripped) = line.strip_suffix('*') {
214 line = stripped;
215
216 if let Some(identifier) = stripped.split(':').next() {
217 if !invalid_active {
218 if active.is_some() {
219 active = None;
220 invalid_active = true;
221 } else {
222 let idx = identifier
223 .trim()
224 .parse()
225 .context("Unexpected power level identifier")?;
226 active = Some(idx);
227 }
228 }
229 }
230 }
231 if let Some(s) = line.split(':').next_back() {
232 let parse_result = if let Some(suffix) = kind.value_suffix() {
233 let raw_value = s.trim().to_lowercase();
234 let value = raw_value.strip_suffix(suffix).ok_or_else(|| {
235 ErrorKind::ParseError {
236 msg: format!("Level did not have the expected suffix {suffix}"),
237 line: levels.len() + 1,
238 }
239 })?;
240 T::from_str(value)
241 } else {
242 let value = s.trim();
243 T::from_str(value)
244 };
245
246 let parsed_value = parse_result.map_err(|err| ErrorKind::ParseError {
247 msg: format!("Could not deserialize power level value: {err}"),
248 line: levels.len() + 1,
249 })?;
250 levels.push(parsed_value);
251 }
252 }
253
254 Ok(PowerLevels { levels, active })
255 })
256 }
257
258 impl_get_clocks_levels!(get_core_clock_levels, PowerLevelKind::CoreClock, u64);
259 impl_get_clocks_levels!(get_memory_clock_levels, PowerLevelKind::MemoryClock, u64);
260 impl_get_clocks_levels!(get_pcie_clock_levels, PowerLevelKind::PcieSpeed, String);
261
262 pub fn set_enabled_power_levels(&self, kind: PowerLevelKind, levels: &[u8]) -> Result<()> {
266 match self.get_power_force_performance_level()? {
267 PerformanceLevel::Manual => {
268 let mut s = String::new();
269
270 for l in levels {
271 s.push(char::from_digit((*l).into(), 10).unwrap());
272 s.push(' ');
273 }
274
275 Ok(self.write_file(kind.filename(), s)?)
276 }
277 _ => Err(ErrorKind::NotAllowed(
278 "power_force_performance level needs to be set to 'manual' to adjust power levels"
279 .to_string(),
280 )
281 .into()),
282 }
283 }
284
285 #[cfg(feature = "overdrive")]
287 pub fn get_clocks_table(&self) -> Result<ClocksTableGen> {
288 self.read_file_parsed("pp_od_clk_voltage")
289 }
290
291 #[cfg(feature = "overdrive")]
293 pub fn set_clocks_table(&self, new_table: &ClocksTableGen) -> Result<CommitHandle> {
294 let old_table = self.get_clocks_table()?;
295
296 let path = self.sysfs_path.join("pp_od_clk_voltage");
297 let mut file = File::create(&path)?;
298
299 new_table.write_commands(&mut file, &old_table)?;
300
301 Ok(CommitHandle::new(path))
302 }
303
304 #[cfg(feature = "overdrive")]
306 pub fn reset_clocks_table(&self) -> Result<()> {
307 let path = self.sysfs_path.join("pp_od_clk_voltage");
308 let mut file = File::create(path)?;
309 file.write_all(b"r\n")?;
310
311 Ok(())
312 }
313
314 pub fn get_power_profile_modes(&self) -> Result<PowerProfileModesTable> {
318 let contents = self.read_file("pp_power_profile_mode")?;
319 PowerProfileModesTable::parse(&contents)
320 }
321
322 pub fn set_active_power_profile_mode(&self, i: u16) -> Result<()> {
325 self.write_file("pp_power_profile_mode", format!("{i}\n"))
326 }
327
328 pub fn set_custom_power_profile_mode_heuristics(
331 &self,
332 components: &[Vec<Option<i32>>],
333 ) -> Result<()> {
334 let table = self.get_power_profile_modes()?;
335 let (index, current_custom_profile) = table
336 .modes
337 .iter()
338 .find(|(_, profile)| profile.is_custom())
339 .ok_or_else(|| {
340 ErrorKind::NotAllowed("Could not find a custom power profile".to_owned())
341 })?;
342
343 if current_custom_profile.components.len() != components.len() {
344 return Err(ErrorKind::NotAllowed(format!(
345 "Expected {} power profile components, got {}",
346 current_custom_profile.components.len(),
347 components.len()
348 ))
349 .into());
350 }
351
352 if current_custom_profile.components.len() == 1 {
353 let mut values_command = format!("{index}");
354 for heuristic in &components[0] {
355 match heuristic {
356 Some(value) => write!(values_command, " {value}").unwrap(),
357 None => write!(values_command, " -").unwrap(),
358 }
359 }
360
361 values_command.push('\n');
362 self.write_file("pp_power_profile_mode", values_command)
363 } else {
364 for (component_index, heuristics) in components.iter().enumerate() {
365 let mut values_command = format!("{index} {component_index}");
366 for heuristic in heuristics {
367 match heuristic {
368 Some(value) => write!(values_command, " {value}").unwrap(),
369 None => write!(values_command, " -").unwrap(),
370 }
371 }
372 values_command.push('\n');
373
374 self.write_file("pp_power_profile_mode", values_command)?;
375 }
376
377 Ok(())
378 }
379 }
380
381 fn read_fan_info(&self, file: &str, section_name: &str, range_name: &str) -> Result<FanInfo> {
382 let file_path = Path::new("gpu_od/fan_ctrl").join(file);
383 let data = self.read_file(file_path)?;
384 let contents = FanCtrlContents::parse(&data, section_name)?;
385
386 let current = contents.contents.parse()?;
387
388 let allowed_range = match contents.od_range.get(range_name) {
389 Some((raw_min, raw_max)) => {
390 let min = raw_min.parse()?;
391 let max = raw_max.parse()?;
392 Some((min, max))
393 }
394 None => None,
395 };
396
397 Ok(FanInfo {
398 current,
399 allowed_range,
400 })
401 }
402
403 pub fn get_fan_acoustic_limit(&self) -> Result<FanInfo> {
408 self.read_fan_info(
409 "acoustic_limit_rpm_threshold",
410 "OD_ACOUSTIC_LIMIT",
411 "ACOUSTIC_LIMIT",
412 )
413 }
414
415 pub fn get_fan_acoustic_target(&self) -> Result<FanInfo> {
420 self.read_fan_info(
421 "acoustic_target_rpm_threshold",
422 "OD_ACOUSTIC_TARGET",
423 "ACOUSTIC_TARGET",
424 )
425 }
426
427 pub fn get_fan_target_temperature(&self) -> Result<FanInfo> {
432 self.read_fan_info(
433 "fan_target_temperature",
434 "FAN_TARGET_TEMPERATURE",
435 "TARGET_TEMPERATURE",
436 )
437 }
438
439 pub fn get_fan_minimum_pwm(&self) -> Result<FanInfo> {
444 self.read_fan_info("fan_minimum_pwm", "FAN_MINIMUM_PWM", "MINIMUM_PWM")
445 }
446
447 pub fn get_fan_zero_rpm_enable(&self) -> Result<bool> {
451 self.read_fan_info(
452 "fan_zero_rpm_enable",
453 "FAN_ZERO_RPM_ENABLE",
454 "ZERO_RPM_ENABLE",
455 )
456 .map(|info| info.current == 1)
457 }
458
459 pub fn get_fan_zero_rpm_stop_temperature(&self) -> Result<FanInfo> {
463 self.read_fan_info(
464 "fan_zero_rpm_stop_temperature",
465 "FAN_ZERO_RPM_STOP_TEMPERATURE",
466 "ZERO_RPM_STOP_TEMPERATURE",
467 )
468 }
469
470 fn set_fan_value(
471 &self,
472 file: &str,
473 value: u32,
474 section_name: &str,
475 range_name: &str,
476 ) -> Result<CommitHandle> {
477 let info = self.read_fan_info(file, section_name, range_name)?;
478 match info.allowed_range {
479 Some((min, max)) => {
480 if !(min..=max).contains(&value) {
481 return Err(Error::not_allowed(format!(
482 "Value {value} is out of range, should be between {min} and {max}"
483 )));
484 }
485
486 let file_path = self.sysfs_path.join("gpu_od/fan_ctrl").join(file);
487 std::fs::write(&file_path, format!("{value}\n"))?;
488
489 Ok(CommitHandle::new(file_path))
490 }
491 None => Err(Error::not_allowed(format!(
492 "Changes to {range_name} are not allowed"
493 ))),
494 }
495 }
496
497 pub fn set_fan_acoustic_limit(&self, value: u32) -> Result<CommitHandle> {
502 self.set_fan_value(
503 "acoustic_limit_rpm_threshold",
504 value,
505 "OD_ACOUSTIC_LIMIT",
506 "ACOUSTIC_LIMIT",
507 )
508 }
509
510 pub fn set_fan_acoustic_target(&self, value: u32) -> Result<CommitHandle> {
515 self.set_fan_value(
516 "acoustic_target_rpm_threshold",
517 value,
518 "OD_ACOUSTIC_TARGET",
519 "ACOUSTIC_TARGET",
520 )
521 }
522
523 pub fn set_fan_target_temperature(&self, value: u32) -> Result<CommitHandle> {
528 self.set_fan_value(
529 "fan_target_temperature",
530 value,
531 "FAN_TARGET_TEMPERATURE",
532 "TARGET_TEMPERATURE",
533 )
534 }
535
536 pub fn set_fan_minimum_pwm(&self, value: u32) -> Result<CommitHandle> {
540 self.set_fan_value("fan_minimum_pwm", value, "FAN_MINIMUM_PWM", "MINIMUM_PWM")
541 }
542
543 pub fn set_fan_zero_rpm_enable(&self, enabled: bool) -> Result<CommitHandle> {
547 self.set_fan_value(
548 "fan_zero_rpm_enable",
549 enabled as u32,
550 "FAN_ZERO_RPM_ENABLE",
551 "ZERO_RPM_ENABLE",
552 )
553 }
554
555 pub fn set_fan_zero_rpm_stop_temperature(&self, value: u32) -> Result<CommitHandle> {
559 self.set_fan_value(
560 "fan_zero_rpm_stop_temperature",
561 value,
562 "FAN_ZERO_RPM_STOP_TEMPERATURE",
563 "ZERO_RPM_STOP_TEMPERATURE",
564 )
565 }
566
567 fn reset_fan_value(&self, file: &str) -> Result<()> {
568 let file_path = self.sysfs_path.join("gpu_od/fan_ctrl").join(file);
569 let mut file = File::create(file_path)?;
570 writeln!(file, "r")?;
571 Ok(())
572 }
573
574 pub fn reset_fan_acoustic_limit(&self) -> Result<()> {
579 self.reset_fan_value("acoustic_limit_rpm_threshold")
580 }
581
582 pub fn reset_fan_acoustic_target(&self) -> Result<()> {
587 self.reset_fan_value("acoustic_target_rpm_threshold")
588 }
589
590 pub fn reset_fan_target_temperature(&self) -> Result<()> {
595 self.reset_fan_value("fan_target_temperature")
596 }
597
598 pub fn reset_fan_minimum_pwm(&self) -> Result<()> {
603 self.reset_fan_value("fan_minimum_pwm")
604 }
605
606 pub fn get_fan_curve(&self) -> Result<FanCurve> {
612 let data = self.read_file("gpu_od/fan_ctrl/fan_curve")?;
613 let contents = FanCtrlContents::parse(&data, "OD_FAN_CURVE")?;
614 let points = contents
615 .contents
616 .lines()
617 .enumerate()
618 .map(|(i, line)| {
619 let mut split = line.split(' ');
620 split.next(); let raw_temp = split
623 .next()
624 .ok_or_else(|| Error::unexpected_eol("Temperature value", i))?;
625 let temp = raw_temp.trim_end_matches('C').parse()?;
626
627 let raw_speed = split
628 .next()
629 .ok_or_else(|| Error::unexpected_eol("Speed value", i))?;
630 let speed = raw_speed.trim_end_matches('%').parse()?;
631
632 Ok((temp, speed))
633 })
634 .collect::<Result<_>>()?;
635
636 let temp_range = contents.od_range.get("FAN_CURVE(hotspot temp)");
637 let speed_range = contents.od_range.get("FAN_CURVE(fan speed)");
638
639 let allowed_ranges = if let Some(((min_temp, max_temp), (min_speed, max_speed))) =
640 (temp_range).zip(speed_range)
641 {
642 let min_temp: i32 = min_temp.trim_end_matches('C').parse()?;
643 let max_temp: i32 = max_temp.trim_end_matches('C').parse()?;
644
645 let min_speed: u8 = min_speed.trim_end_matches('%').parse()?;
646 let max_speed: u8 = max_speed.trim_end_matches('%').parse()?;
647
648 Some(FanCurveRanges {
649 temperature_range: min_temp..=max_temp,
650 speed_range: min_speed..=max_speed,
651 })
652 } else {
653 None
654 };
655
656 Ok(FanCurve {
657 points,
658 allowed_ranges,
659 })
660 }
661
662 pub fn set_fan_curve(&self, new_curve: &FanCurve) -> Result<CommitHandle> {
667 let current_curve = self.get_fan_curve()?;
668 let allowed_ranges = current_curve.allowed_ranges.ok_or_else(|| {
669 Error::not_allowed("Changes to the fan curve are not supported".to_owned())
670 })?;
671
672 let file_path = self.sysfs_path.join("gpu_od/fan_ctrl/fan_curve");
673
674 for (i, (temperature, speed)) in new_curve.points.iter().enumerate() {
675 if !allowed_ranges.temperature_range.contains(temperature) {
676 Err(Error::not_allowed(format!(
677 "Temperature value {temperature} is outside of the allowed range {:?}",
678 allowed_ranges.temperature_range
679 )))?;
680 }
681 if !allowed_ranges.speed_range.contains(speed) {
682 Err(Error::not_allowed(format!(
683 "Speed value {speed} is outside of the allowed range {:?}",
684 allowed_ranges.speed_range
685 )))?;
686 }
687
688 std::fs::write(&file_path, format!("{i} {temperature} {speed}\n"))?;
689 }
690
691 Ok(CommitHandle::new(file_path))
692 }
693
694 pub fn reset_fan_curve(&self) -> Result<()> {
699 self.reset_fan_value("fan_curve")
700 }
701}
702
703impl SysFS for GpuHandle {
704 fn get_path(&self) -> &std::path::Path {
705 &self.sysfs_path
706 }
707}
708
709#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
713#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
714#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
715pub enum PerformanceLevel {
716 #[default]
718 Auto,
719 Low,
721 High,
723 Manual,
725}
726
727impl FromStr for PerformanceLevel {
728 type Err = Error;
729
730 fn from_str(s: &str) -> Result<Self> {
731 match s {
732 "auto" | "Automatic" => Ok(PerformanceLevel::Auto),
733 "high" | "Highest Clocks" => Ok(PerformanceLevel::High),
734 "low" | "Lowest Clocks" => Ok(PerformanceLevel::Low),
735 "manual" | "Manual" => Ok(PerformanceLevel::Manual),
736 _ => Err(ErrorKind::ParseError {
737 msg: "unrecognized GPU power profile".to_string(),
738 line: 1,
739 }
740 .into()),
741 }
742 }
743}
744
745impl fmt::Display for PerformanceLevel {
746 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
747 write!(
748 f,
749 "{}",
750 match self {
751 PerformanceLevel::Auto => "auto",
752 PerformanceLevel::High => "high",
753 PerformanceLevel::Low => "low",
754 PerformanceLevel::Manual => "manual",
755 }
756 )
757 }
758}
759
760fn trim_sysfs_line(line: &str) -> &str {
762 line.trim_matches(char::from(0)).trim()
763}
764
765#[must_use]
767#[derive(Debug)]
768pub struct CommitHandle {
769 file_path: PathBuf,
770}
771
772impl CommitHandle {
773 pub(crate) fn new(file_path: PathBuf) -> Self {
774 Self { file_path }
775 }
776
777 pub fn commit(self) -> Result<()> {
779 std::fs::write(&self.file_path, "c\n").with_context(|| {
780 format!(
781 "Could not commit values to {:?}",
782 self.file_path.file_name().unwrap()
783 )
784 })
785 }
786}