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::{PowerLevel, PowerLevelKind, PowerLevels, PowerLevelId};
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>>
204 where
205 T: FromStr,
206 <T as FromStr>::Err: Display,
207 {
208 self.read_file(kind.filename())
209 .and_then(|content| Self::parse_clock_levels(&content, kind))
210 }
211
212 fn parse_clock_levels<T>(content: &str, kind: PowerLevelKind) -> Result<PowerLevels<T>>
213 where
214 T: FromStr,
215 <T as FromStr>::Err: Display,
216 {
217 let mut levels = Vec::new();
218 let mut active = None;
219 let mut invalid_active = false;
220
221 for line in content.trim().split('\n') {
222 let (line, is_active) = match line.strip_suffix('*') {
223 Some(stripped) => (stripped, true),
224 None => (line, false),
225 };
226
227 if let Some((identifier, s)) = line.split_once(':') {
228 let id = match identifier.trim() {
229 "S" => PowerLevelId::Sleep,
230 identifier => PowerLevelId::Index(
231 identifier
232 .parse()
233 .context("Unexpected power level identifier")?,
234 ),
235 };
236 if is_active && !invalid_active {
239 if active.is_some() {
240 active = None;
241 invalid_active = true;
242 } else {
243 active = Some(id);
244 }
245 }
246
247 let parse_result = if let Some(suffix) = kind.value_suffix() {
248 let raw_value = s.trim().to_lowercase();
249 let value =
250 raw_value
251 .strip_suffix(suffix)
252 .ok_or_else(|| ErrorKind::ParseError {
253 msg: format!("Level did not have the expected suffix {suffix}"),
254 line: levels.len() + 1,
255 })?;
256 T::from_str(value)
257 } else {
258 let value = s.trim();
259 T::from_str(value)
260 };
261
262 let parsed_value = parse_result.map_err(|err| ErrorKind::ParseError {
263 msg: format!("Could not deserialize power level value: {err}"),
264 line: levels.len() + 1,
265 })?;
266 levels.push(PowerLevel {
267 id,
268 value: parsed_value,
269 });
270 }
271 }
272
273 Ok(PowerLevels { levels, active })
274 }
275
276 impl_get_clocks_levels!(get_core_clock_levels, PowerLevelKind::CoreClock, u64);
277 impl_get_clocks_levels!(get_memory_clock_levels, PowerLevelKind::MemoryClock, u64);
278 impl_get_clocks_levels!(get_pcie_clock_levels, PowerLevelKind::PcieSpeed, String);
279
280 pub fn set_enabled_power_levels(&self, kind: PowerLevelKind, levels: &[u8]) -> Result<()> {
284 match self.get_power_force_performance_level()? {
285 PerformanceLevel::Manual => {
286 let mut s = String::new();
287
288 for l in levels {
289 s.push(char::from_digit((*l).into(), 10).unwrap());
290 s.push(' ');
291 }
292
293 Ok(self.write_file(kind.filename(), s)?)
294 }
295 _ => Err(ErrorKind::NotAllowed(
296 "power_force_performance level needs to be set to 'manual' to adjust power levels"
297 .to_string(),
298 )
299 .into()),
300 }
301 }
302
303 #[cfg(feature = "overdrive")]
305 pub fn get_clocks_table(&self) -> Result<ClocksTableGen> {
306 self.read_file_parsed("pp_od_clk_voltage")
307 }
308
309 #[cfg(feature = "overdrive")]
311 pub fn set_clocks_table(&self, new_table: &ClocksTableGen) -> Result<CommitHandle> {
312 let old_table = self.get_clocks_table()?;
313
314 let path = self.sysfs_path.join("pp_od_clk_voltage");
315 let mut file = File::create(&path)?;
316
317 new_table.write_commands(&mut file, &old_table)?;
318
319 Ok(CommitHandle::new(path))
320 }
321
322 #[cfg(feature = "overdrive")]
324 pub fn reset_clocks_table(&self) -> Result<()> {
325 let path = self.sysfs_path.join("pp_od_clk_voltage");
326 let mut file = File::create(path)?;
327 file.write_all(b"r\n")?;
328
329 Ok(())
330 }
331
332 pub fn get_power_profile_modes(&self) -> Result<PowerProfileModesTable> {
336 let contents = self.read_file("pp_power_profile_mode")?;
337 PowerProfileModesTable::parse(&contents)
338 }
339
340 pub fn set_active_power_profile_mode(&self, i: u16) -> Result<()> {
343 self.write_file("pp_power_profile_mode", format!("{i}\n"))
344 }
345
346 pub fn set_custom_power_profile_mode_heuristics(
349 &self,
350 components: &[Vec<Option<i32>>],
351 ) -> Result<()> {
352 let table = self.get_power_profile_modes()?;
353 let (index, current_custom_profile) = table
354 .modes
355 .iter()
356 .find(|(_, profile)| profile.is_custom())
357 .ok_or_else(|| {
358 ErrorKind::NotAllowed("Could not find a custom power profile".to_owned())
359 })?;
360
361 if current_custom_profile.components.len() != components.len() {
362 return Err(ErrorKind::NotAllowed(format!(
363 "Expected {} power profile components, got {}",
364 current_custom_profile.components.len(),
365 components.len()
366 ))
367 .into());
368 }
369
370 if current_custom_profile.components.len() == 1 {
371 let mut values_command = format!("{index}");
372 for heuristic in &components[0] {
373 match heuristic {
374 Some(value) => write!(values_command, " {value}").unwrap(),
375 None => write!(values_command, " -").unwrap(),
376 }
377 }
378
379 values_command.push('\n');
380 self.write_file("pp_power_profile_mode", values_command)
381 } else {
382 for (component_index, heuristics) in components.iter().enumerate() {
383 let mut values_command = format!("{index} {component_index}");
384 for heuristic in heuristics {
385 match heuristic {
386 Some(value) => write!(values_command, " {value}").unwrap(),
387 None => write!(values_command, " -").unwrap(),
388 }
389 }
390 values_command.push('\n');
391
392 self.write_file("pp_power_profile_mode", values_command)?;
393 }
394
395 Ok(())
396 }
397 }
398
399 fn read_fan_info(&self, file: &str, section_name: &str, range_name: &str) -> Result<FanInfo> {
400 let file_path = Path::new("gpu_od/fan_ctrl").join(file);
401 let data = self.read_file(file_path)?;
402 let contents = FanCtrlContents::parse(&data, section_name)?;
403
404 let current = contents.contents.parse()?;
405
406 let allowed_range = match contents.od_range.get(range_name) {
407 Some((raw_min, raw_max)) => {
408 let min = raw_min.parse()?;
409 let max = raw_max.parse()?;
410 Some((min, max))
411 }
412 None => None,
413 };
414
415 Ok(FanInfo {
416 current,
417 allowed_range,
418 })
419 }
420
421 pub fn get_fan_acoustic_limit(&self) -> Result<FanInfo> {
426 self.read_fan_info(
427 "acoustic_limit_rpm_threshold",
428 "OD_ACOUSTIC_LIMIT",
429 "ACOUSTIC_LIMIT",
430 )
431 }
432
433 pub fn get_fan_acoustic_target(&self) -> Result<FanInfo> {
438 self.read_fan_info(
439 "acoustic_target_rpm_threshold",
440 "OD_ACOUSTIC_TARGET",
441 "ACOUSTIC_TARGET",
442 )
443 }
444
445 pub fn get_fan_target_temperature(&self) -> Result<FanInfo> {
450 self.read_fan_info(
451 "fan_target_temperature",
452 "FAN_TARGET_TEMPERATURE",
453 "TARGET_TEMPERATURE",
454 )
455 }
456
457 pub fn get_fan_minimum_pwm(&self) -> Result<FanInfo> {
462 self.read_fan_info("fan_minimum_pwm", "FAN_MINIMUM_PWM", "MINIMUM_PWM")
463 }
464
465 pub fn get_fan_zero_rpm_enable(&self) -> Result<bool> {
469 self.read_fan_info(
470 "fan_zero_rpm_enable",
471 "FAN_ZERO_RPM_ENABLE",
472 "ZERO_RPM_ENABLE",
473 )
474 .map(|info| info.current == 1)
475 }
476
477 pub fn get_fan_zero_rpm_stop_temperature(&self) -> Result<FanInfo> {
481 self.read_fan_info(
482 "fan_zero_rpm_stop_temperature",
483 "FAN_ZERO_RPM_STOP_TEMPERATURE",
484 "ZERO_RPM_STOP_TEMPERATURE",
485 )
486 }
487
488 fn set_fan_value(
489 &self,
490 file: &str,
491 value: u32,
492 section_name: &str,
493 range_name: &str,
494 ) -> Result<CommitHandle> {
495 let info = self.read_fan_info(file, section_name, range_name)?;
496 match info.allowed_range {
497 Some((min, max)) => {
498 if !(min..=max).contains(&value) {
499 return Err(Error::not_allowed(format!(
500 "Value {value} is out of range, should be between {min} and {max}"
501 )));
502 }
503
504 let file_path = self.sysfs_path.join("gpu_od/fan_ctrl").join(file);
505 std::fs::write(&file_path, format!("{value}\n"))?;
506
507 Ok(CommitHandle::new(file_path))
508 }
509 None => Err(Error::not_allowed(format!(
510 "Changes to {range_name} are not allowed"
511 ))),
512 }
513 }
514
515 pub fn set_fan_acoustic_limit(&self, value: u32) -> Result<CommitHandle> {
520 self.set_fan_value(
521 "acoustic_limit_rpm_threshold",
522 value,
523 "OD_ACOUSTIC_LIMIT",
524 "ACOUSTIC_LIMIT",
525 )
526 }
527
528 pub fn set_fan_acoustic_target(&self, value: u32) -> Result<CommitHandle> {
533 self.set_fan_value(
534 "acoustic_target_rpm_threshold",
535 value,
536 "OD_ACOUSTIC_TARGET",
537 "ACOUSTIC_TARGET",
538 )
539 }
540
541 pub fn set_fan_target_temperature(&self, value: u32) -> Result<CommitHandle> {
546 self.set_fan_value(
547 "fan_target_temperature",
548 value,
549 "FAN_TARGET_TEMPERATURE",
550 "TARGET_TEMPERATURE",
551 )
552 }
553
554 pub fn set_fan_minimum_pwm(&self, value: u32) -> Result<CommitHandle> {
558 self.set_fan_value("fan_minimum_pwm", value, "FAN_MINIMUM_PWM", "MINIMUM_PWM")
559 }
560
561 pub fn set_fan_zero_rpm_enable(&self, enabled: bool) -> Result<CommitHandle> {
565 self.set_fan_value(
566 "fan_zero_rpm_enable",
567 enabled as u32,
568 "FAN_ZERO_RPM_ENABLE",
569 "ZERO_RPM_ENABLE",
570 )
571 }
572
573 pub fn set_fan_zero_rpm_stop_temperature(&self, value: u32) -> Result<CommitHandle> {
577 self.set_fan_value(
578 "fan_zero_rpm_stop_temperature",
579 value,
580 "FAN_ZERO_RPM_STOP_TEMPERATURE",
581 "ZERO_RPM_STOP_TEMPERATURE",
582 )
583 }
584
585 fn reset_fan_value(&self, file: &str) -> Result<()> {
586 let file_path = self.sysfs_path.join("gpu_od/fan_ctrl").join(file);
587 let mut file = File::create(file_path)?;
588 writeln!(file, "r")?;
589 Ok(())
590 }
591
592 pub fn reset_fan_acoustic_limit(&self) -> Result<()> {
597 self.reset_fan_value("acoustic_limit_rpm_threshold")
598 }
599
600 pub fn reset_fan_acoustic_target(&self) -> Result<()> {
605 self.reset_fan_value("acoustic_target_rpm_threshold")
606 }
607
608 pub fn reset_fan_target_temperature(&self) -> Result<()> {
613 self.reset_fan_value("fan_target_temperature")
614 }
615
616 pub fn reset_fan_minimum_pwm(&self) -> Result<()> {
621 self.reset_fan_value("fan_minimum_pwm")
622 }
623
624 pub fn get_fan_curve(&self) -> Result<FanCurve> {
630 let data = self.read_file("gpu_od/fan_ctrl/fan_curve")?;
631 let contents = FanCtrlContents::parse(&data, "OD_FAN_CURVE")?;
632 let points = contents
633 .contents
634 .lines()
635 .enumerate()
636 .map(|(i, line)| {
637 let mut split = line.split(' ');
638 split.next(); let raw_temp = split
641 .next()
642 .ok_or_else(|| Error::unexpected_eol("Temperature value", i))?;
643 let temp = raw_temp.trim_end_matches('C').parse()?;
644
645 let raw_speed = split
646 .next()
647 .ok_or_else(|| Error::unexpected_eol("Speed value", i))?;
648 let speed = raw_speed.trim_end_matches('%').parse()?;
649
650 Ok((temp, speed))
651 })
652 .collect::<Result<_>>()?;
653
654 let temp_range = contents.od_range.get("FAN_CURVE(hotspot temp)");
655 let speed_range = contents.od_range.get("FAN_CURVE(fan speed)");
656
657 let allowed_ranges = if let Some(((min_temp, max_temp), (min_speed, max_speed))) =
658 (temp_range).zip(speed_range)
659 {
660 let min_temp: i32 = min_temp.trim_end_matches('C').parse()?;
661 let max_temp: i32 = max_temp.trim_end_matches('C').parse()?;
662
663 let min_speed: u8 = min_speed.trim_end_matches('%').parse()?;
664 let max_speed: u8 = max_speed.trim_end_matches('%').parse()?;
665
666 Some(FanCurveRanges {
667 temperature_range: min_temp..=max_temp,
668 speed_range: min_speed..=max_speed,
669 })
670 } else {
671 None
672 };
673
674 Ok(FanCurve {
675 points,
676 allowed_ranges,
677 })
678 }
679
680 pub fn set_fan_curve(&self, new_curve: &FanCurve) -> Result<CommitHandle> {
685 let current_curve = self.get_fan_curve()?;
686 let allowed_ranges = current_curve.allowed_ranges.ok_or_else(|| {
687 Error::not_allowed("Changes to the fan curve are not supported".to_owned())
688 })?;
689
690 let file_path = self.sysfs_path.join("gpu_od/fan_ctrl/fan_curve");
691
692 for (i, (temperature, speed)) in new_curve.points.iter().enumerate() {
693 if !allowed_ranges.temperature_range.contains(temperature) {
694 Err(Error::not_allowed(format!(
695 "Temperature value {temperature} is outside of the allowed range {:?}",
696 allowed_ranges.temperature_range
697 )))?;
698 }
699 if !allowed_ranges.speed_range.contains(speed) {
700 Err(Error::not_allowed(format!(
701 "Speed value {speed} is outside of the allowed range {:?}",
702 allowed_ranges.speed_range
703 )))?;
704 }
705
706 std::fs::write(&file_path, format!("{i} {temperature} {speed}\n"))?;
707 }
708
709 Ok(CommitHandle::new(file_path))
710 }
711
712 pub fn reset_fan_curve(&self) -> Result<()> {
717 self.reset_fan_value("fan_curve")
718 }
719}
720
721impl SysFS for GpuHandle {
722 fn get_path(&self) -> &std::path::Path {
723 &self.sysfs_path
724 }
725}
726
727#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
731#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
732#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
733pub enum PerformanceLevel {
734 #[default]
736 Auto,
737 Low,
739 High,
741 Manual,
743 ProfileStandard,
745 ProfileMinSclk,
747 ProfileMinMclk,
749 ProfilePeak,
751 PerfDeterminism,
755 ProfileExit,
757}
758
759impl FromStr for PerformanceLevel {
760 type Err = Error;
761
762 fn from_str(s: &str) -> Result<Self> {
763 match s {
764 "auto" => Ok(PerformanceLevel::Auto),
765 "high" => Ok(PerformanceLevel::High),
766 "low" => Ok(PerformanceLevel::Low),
767 "manual" => Ok(PerformanceLevel::Manual),
768 "profile_standard" => Ok(PerformanceLevel::ProfileStandard),
769 "profile_min_sclk" => Ok(PerformanceLevel::ProfileMinSclk),
770 "profile_min_mclk" => Ok(PerformanceLevel::ProfileMinMclk),
771 "profile_peak" => Ok(PerformanceLevel::ProfilePeak),
772 "perf_determinism" => Ok(PerformanceLevel::PerfDeterminism),
773 "profile_exit" => Ok(PerformanceLevel::ProfileExit),
774 _ => Err(ErrorKind::ParseError {
775 msg: "unrecognized GPU power profile".to_string(),
776 line: 1,
777 }
778 .into()),
779 }
780 }
781}
782
783impl fmt::Display for PerformanceLevel {
784 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
785 write!(
786 f,
787 "{}",
788 match self {
789 PerformanceLevel::Auto => "auto",
790 PerformanceLevel::High => "high",
791 PerformanceLevel::Low => "low",
792 PerformanceLevel::Manual => "manual",
793 PerformanceLevel::ProfileStandard => "profile_standard",
794 PerformanceLevel::ProfileMinSclk => "profile_min_sclk",
795 PerformanceLevel::ProfileMinMclk => "profile_min_mclk",
796 PerformanceLevel::ProfilePeak => "profile_peak",
797 PerformanceLevel::PerfDeterminism => "perf_determinism",
798 PerformanceLevel::ProfileExit => "profile_exit",
799 }
800 )
801 }
802}
803
804fn trim_sysfs_line(line: &str) -> &str {
806 line.trim_matches(char::from(0)).trim()
807}
808
809#[must_use]
811#[derive(Debug)]
812pub struct CommitHandle {
813 file_path: PathBuf,
814}
815
816impl CommitHandle {
817 pub(crate) fn new(file_path: PathBuf) -> Self {
818 Self { file_path }
819 }
820
821 pub fn commit(self) -> Result<()> {
823 std::fs::write(&self.file_path, "c\n").with_context(|| {
824 format!(
825 "Could not commit values to {:?}",
826 self.file_path.file_name().unwrap()
827 )
828 })
829 }
830}
831
832#[cfg(test)]
833mod tests {
834 use super::{GpuHandle, PowerLevel, PowerLevelKind, PowerLevels, PowerLevelId};
835 use pretty_assertions::assert_eq;
836
837 fn level<T>(id: u8, value: T) -> PowerLevel<T> {
838 PowerLevel {
839 id: PowerLevelId::Index(id),
840 value,
841 }
842 }
843
844 fn sleep<T>(value: T) -> PowerLevel<T> {
845 PowerLevel {
846 id: PowerLevelId::Sleep,
847 value,
848 }
849 }
850
851 #[test]
852 fn parse_clock_levels_with_suffix() {
853 let levels = GpuHandle::parse_clock_levels::<u64>(
854 "\
8550: 500Mhz
8561: 2124Mhz *",
857 PowerLevelKind::CoreClock,
858 )
859 .unwrap();
860
861 assert_eq!(
862 PowerLevels {
863 levels: vec![level(0, 500), level(1, 2124)],
864 active: Some(PowerLevelId::Index(1)),
865 },
866 levels
867 );
868 }
869
870 #[test]
871 fn parse_clock_levels_without_suffix() {
872 let levels = GpuHandle::parse_clock_levels::<String>(
873 "\
8740: 2.5GT/s, x1 310Mhz
8751: 16.0GT/s, x16 619Mhz *",
876 PowerLevelKind::PcieSpeed,
877 )
878 .unwrap();
879
880 assert_eq!(
881 PowerLevels {
882 levels: vec![
883 level(0, "2.5GT/s, x1 310Mhz".to_owned()),
884 level(1, "16.0GT/s, x16 619Mhz".to_owned()),
885 ],
886 active: Some(PowerLevelId::Index(1)),
887 },
888 levels
889 );
890 }
891
892 #[test]
893 fn parse_clock_levels_marks_deep_sleep_active() {
894 let levels = GpuHandle::parse_clock_levels::<u64>(
895 "\
896S: 19Mhz *
8970: 615Mhz
8981: 800Mhz
8992: 888Mhz
9003: 1000Mhz",
901 PowerLevelKind::CoreClock,
902 )
903 .unwrap();
904
905 assert_eq!(
906 PowerLevels {
907 levels: vec![
908 sleep(19),
909 level(0, 615),
910 level(1, 800),
911 level(2, 888),
912 level(3, 1000)
913 ],
914 active: Some(PowerLevelId::Sleep),
915 },
916 levels
917 );
918 }
919
920 #[test]
921 fn parse_clock_levels_marks_deep_sleep_inactive() {
922 let levels = GpuHandle::parse_clock_levels::<u64>(
923 "\
924S: 19Mhz
9250: 615Mhz *
9261: 800Mhz
9272: 888Mhz
9283: 1000Mhz",
929 PowerLevelKind::CoreClock,
930 )
931 .unwrap();
932
933 assert_eq!(
934 PowerLevels {
935 levels: vec![
936 sleep(19),
937 level(0, 615),
938 level(1, 800),
939 level(2, 888),
940 level(3, 1000)
941 ],
942 active: Some(PowerLevelId::Index(0)),
943 },
944 levels
945 );
946 }
947
948 #[test]
949 fn parse_clock_levels_ignores_duplicate_active_markers() {
950 let levels = GpuHandle::parse_clock_levels::<u64>(
953 "\
9540: 400Mhz
9551: 600Mhz
9562: 687Mhz *
9573: 687Mhz *",
958 PowerLevelKind::CoreClock,
959 )
960 .unwrap();
961
962 assert_eq!(
963 PowerLevels {
964 levels: vec![level(0, 400), level(1, 600), level(2, 687), level(3, 687)],
965 active: None,
966 },
967 levels
968 );
969 }
970}