Skip to main content

amdgpu_sysfs/gpu_handle/
mod.rs

1//! Handle on a GPU
2#[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/// A `GpuHandle` represents a handle over a single GPU device, as exposed in the Linux SysFS.
37#[derive(Clone, Debug)]
38pub struct GpuHandle {
39    sysfs_path: PathBuf,
40    /// A collection of all [HwMon](../hw_mon/struct.HwMon.html)s bound to this GPU. They are used to expose real-time data.
41    pub hw_monitors: Vec<HwMon>,
42    uevent: HashMap<String, String>,
43}
44
45impl GpuHandle {
46    /// Initializes a new `GpuHandle` from a given SysFS device path.
47    ///
48    /// Normally, the path should look akin to `/sys/class/drm/card0/device`,
49    /// and it needs to at least contain a `uevent` file.
50    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    /// Gets the kernel driver used.
84    pub fn get_driver(&self) -> &str {
85        self.uevent.get("DRIVER").unwrap()
86    }
87
88    /// Gets the **GPU's** PCI vendor and ID. This is the ID of your GPU chip, e.g. AMD Radeon RX 580.
89    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    /// Gets the **Card's** PCI vendor and ID. This is the ID of your card model, e.g. Sapphire RX 580 Pulse.
97    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    /// Gets the pci slot name of the card.
105    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        // Despite being labled NAVI10, newer generations use the same port device ids
111        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("../"); // pcie port
115
116        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    /// Gets the current PCIe link speed.
139    pub fn get_current_link_speed(&self) -> Result<String> {
140        self.get_link("current_link_speed")
141    }
142
143    /// Gets the current PCIe link width.
144    pub fn get_current_link_width(&self) -> Result<String> {
145        self.get_link("current_link_width")
146    }
147
148    /// Gets the maximum possible PCIe link speed.
149    pub fn get_max_link_speed(&self) -> Result<String> {
150        self.get_link("max_link_speed")
151    }
152
153    /// Gets the maximum possible PCIe link width.
154    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    /// Gets total VRAM size in bytes. May not be reported on some devices, such as integrated GPUs.
164    pub fn get_total_vram(&self) -> Result<u64> {
165        self.read_vram_file("mem_info_vram_total")
166    }
167
168    /// Gets how much VRAM is currently used, in bytes. May not be reported on some devices, such as integrated GPUs.
169    pub fn get_used_vram(&self) -> Result<u64> {
170        self.read_vram_file("mem_info_vram_used")
171    }
172
173    /// Returns the GPU busy percentage.
174    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    /// Returns the GPU VBIOS version.
180    pub fn get_vbios_version(&self) -> Result<String> {
181        self.read_file("vbios_version")
182    }
183
184    /// Returns the VRAM vendor
185    pub fn get_vram_vendor(&self) -> Result<String> {
186        self.read_file("mem_info_vram_vendor")
187    }
188
189    /// Returns the currently forced performance level.
190    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    /// Forces a given performance level.
196    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    /// Retuns the list of power levels and index of the currently active level for a given kind of power state.
201    /// `T` is the type that values should be deserialized into.
202    /// https://docs.kernel.org/gpu/amdgpu/thermal.html#pp-dpm
203    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 mut line in content.trim().split('\n') {
222            if let Some(stripped) = line.strip_suffix('*') {
223                line = stripped;
224
225                if let Some(identifier) = stripped.split(':').next() {
226                    if identifier.trim().eq_ignore_ascii_case("S") {
227                        continue;
228                    }
229
230                    if !invalid_active {
231                        if active.is_some() {
232                            active = None;
233                            invalid_active = true;
234                        } else {
235                            let idx = identifier
236                                .trim()
237                                .parse()
238                                .context("Unexpected power level identifier")?;
239                            active = Some(idx);
240                        }
241                    }
242                }
243            }
244            if let Some(s) = line.split(':').next_back() {
245                let parse_result = if let Some(suffix) = kind.value_suffix() {
246                    let raw_value = s.trim().to_lowercase();
247                    let value =
248                        raw_value
249                            .strip_suffix(suffix)
250                            .ok_or_else(|| ErrorKind::ParseError {
251                                msg: format!("Level did not have the expected suffix {suffix}"),
252                                line: levels.len() + 1,
253                            })?;
254                    T::from_str(value)
255                } else {
256                    let value = s.trim();
257                    T::from_str(value)
258                };
259
260                let parsed_value = parse_result.map_err(|err| ErrorKind::ParseError {
261                    msg: format!("Could not deserialize power level value: {err}"),
262                    line: levels.len() + 1,
263                })?;
264                levels.push(parsed_value);
265            }
266        }
267
268        Ok(PowerLevels { levels, active })
269    }
270
271    impl_get_clocks_levels!(get_core_clock_levels, PowerLevelKind::CoreClock, u64);
272    impl_get_clocks_levels!(get_memory_clock_levels, PowerLevelKind::MemoryClock, u64);
273    impl_get_clocks_levels!(get_pcie_clock_levels, PowerLevelKind::PcieSpeed, String);
274
275    /// Sets the enabled power levels for a power state kind to a given list of levels. This means that only the given power levels will be allowed.
276    ///
277    /// Can only be used if `power_force_performance_level` is set to `manual`.
278    pub fn set_enabled_power_levels(&self, kind: PowerLevelKind, levels: &[u8]) -> Result<()> {
279        match self.get_power_force_performance_level()? {
280            PerformanceLevel::Manual => {
281                let mut s = String::new();
282
283                for l in levels {
284                    s.push(char::from_digit((*l).into(), 10).unwrap());
285                    s.push(' ');
286                }
287
288                Ok(self.write_file(kind.filename(), s)?)
289            }
290            _ => Err(ErrorKind::NotAllowed(
291                "power_force_performance level needs to be set to 'manual' to adjust power levels"
292                    .to_string(),
293            )
294            .into()),
295        }
296    }
297
298    /// Reads the clocks table from `pp_od_clk_voltage`.
299    #[cfg(feature = "overdrive")]
300    pub fn get_clocks_table(&self) -> Result<ClocksTableGen> {
301        self.read_file_parsed("pp_od_clk_voltage")
302    }
303
304    /// Writes and commits the given clocks table to `pp_od_clk_voltage`.
305    #[cfg(feature = "overdrive")]
306    pub fn set_clocks_table(&self, new_table: &ClocksTableGen) -> Result<CommitHandle> {
307        let old_table = self.get_clocks_table()?;
308
309        let path = self.sysfs_path.join("pp_od_clk_voltage");
310        let mut file = File::create(&path)?;
311
312        new_table.write_commands(&mut file, &old_table)?;
313
314        Ok(CommitHandle::new(path))
315    }
316
317    /// Resets the clocks table to the default configuration.
318    #[cfg(feature = "overdrive")]
319    pub fn reset_clocks_table(&self) -> Result<()> {
320        let path = self.sysfs_path.join("pp_od_clk_voltage");
321        let mut file = File::create(path)?;
322        file.write_all(b"r\n")?;
323
324        Ok(())
325    }
326
327    /// Reads the list of predefined power profiles and the relevant heuristics settings for them from `pp_power_profile_mode`
328    ///
329    /// https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#pp-power-profile-mode
330    pub fn get_power_profile_modes(&self) -> Result<PowerProfileModesTable> {
331        let contents = self.read_file("pp_power_profile_mode")?;
332        PowerProfileModesTable::parse(&contents)
333    }
334
335    /// Sets the current power profile mode. You can get the available modes with [`get_power_profile_modes`].
336    /// Requires the performance level to be set to "manual" first using [`set_power_force_performance_level`]
337    pub fn set_active_power_profile_mode(&self, i: u16) -> Result<()> {
338        self.write_file("pp_power_profile_mode", format!("{i}\n"))
339    }
340
341    /// Sets a custom power profile mode. You can get the available modes, and the list of heuristic names with [`get_power_profile_modes`].
342    /// Requires the performance level to be set to "manual" first using [`set_power_force_performance_level`]
343    pub fn set_custom_power_profile_mode_heuristics(
344        &self,
345        components: &[Vec<Option<i32>>],
346    ) -> Result<()> {
347        let table = self.get_power_profile_modes()?;
348        let (index, current_custom_profile) = table
349            .modes
350            .iter()
351            .find(|(_, profile)| profile.is_custom())
352            .ok_or_else(|| {
353                ErrorKind::NotAllowed("Could not find a custom power profile".to_owned())
354            })?;
355
356        if current_custom_profile.components.len() != components.len() {
357            return Err(ErrorKind::NotAllowed(format!(
358                "Expected {} power profile components, got {}",
359                current_custom_profile.components.len(),
360                components.len()
361            ))
362            .into());
363        }
364
365        if current_custom_profile.components.len() == 1 {
366            let mut values_command = format!("{index}");
367            for heuristic in &components[0] {
368                match heuristic {
369                    Some(value) => write!(values_command, " {value}").unwrap(),
370                    None => write!(values_command, " -").unwrap(),
371                }
372            }
373
374            values_command.push('\n');
375            self.write_file("pp_power_profile_mode", values_command)
376        } else {
377            for (component_index, heuristics) in components.iter().enumerate() {
378                let mut values_command = format!("{index} {component_index}");
379                for heuristic in heuristics {
380                    match heuristic {
381                        Some(value) => write!(values_command, " {value}").unwrap(),
382                        None => write!(values_command, " -").unwrap(),
383                    }
384                }
385                values_command.push('\n');
386
387                self.write_file("pp_power_profile_mode", values_command)?;
388            }
389
390            Ok(())
391        }
392    }
393
394    fn read_fan_info(&self, file: &str, section_name: &str, range_name: &str) -> Result<FanInfo> {
395        let file_path = Path::new("gpu_od/fan_ctrl").join(file);
396        let data = self.read_file(file_path)?;
397        let contents = FanCtrlContents::parse(&data, section_name)?;
398
399        let current = contents.contents.parse()?;
400
401        let allowed_range = match contents.od_range.get(range_name) {
402            Some((raw_min, raw_max)) => {
403                let min = raw_min.parse()?;
404                let max = raw_max.parse()?;
405                Some((min, max))
406            }
407            None => None,
408        };
409
410        Ok(FanInfo {
411            current,
412            allowed_range,
413        })
414    }
415
416    /// Gets the fan acoustic limit. Values are in RPM.
417    ///
418    /// Only available on Navi3x (RDNA 3) or newer.
419    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#acoustic-limit-rpm-threshold>
420    pub fn get_fan_acoustic_limit(&self) -> Result<FanInfo> {
421        self.read_fan_info(
422            "acoustic_limit_rpm_threshold",
423            "OD_ACOUSTIC_LIMIT",
424            "ACOUSTIC_LIMIT",
425        )
426    }
427
428    /// Gets the fan acoustic target. Values are in RPM.
429    ///
430    /// Only available on Navi3x (RDNA 3) or newer.
431    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#acoustic-target-rpm-threshold>
432    pub fn get_fan_acoustic_target(&self) -> Result<FanInfo> {
433        self.read_fan_info(
434            "acoustic_target_rpm_threshold",
435            "OD_ACOUSTIC_TARGET",
436            "ACOUSTIC_TARGET",
437        )
438    }
439
440    /// Gets the fan temperature target. Values are in degrees.
441    ///
442    /// Only available on Navi3x (RDNA 3) or newer.
443    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-target-temperature>
444    pub fn get_fan_target_temperature(&self) -> Result<FanInfo> {
445        self.read_fan_info(
446            "fan_target_temperature",
447            "FAN_TARGET_TEMPERATURE",
448            "TARGET_TEMPERATURE",
449        )
450    }
451
452    /// Gets the fan minimum PWM. Values are in percentages.
453    ///
454    /// Only available on Navi3x (RDNA 3) or newer.
455    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-minimum-pwm>
456    pub fn get_fan_minimum_pwm(&self) -> Result<FanInfo> {
457        self.read_fan_info("fan_minimum_pwm", "FAN_MINIMUM_PWM", "MINIMUM_PWM")
458    }
459
460    /// Gets the current fan zero RPM mode.
461    ///
462    /// Only available on Navi3x (RDNA 3) or newer.
463    pub fn get_fan_zero_rpm_enable(&self) -> Result<bool> {
464        self.read_fan_info(
465            "fan_zero_rpm_enable",
466            "FAN_ZERO_RPM_ENABLE",
467            "ZERO_RPM_ENABLE",
468        )
469        .map(|info| info.current == 1)
470    }
471
472    /// Gets the current fan zero RPM stop temperature.
473    ///
474    /// Only available on Navi3x (RDNA 3) or newer.
475    pub fn get_fan_zero_rpm_stop_temperature(&self) -> Result<FanInfo> {
476        self.read_fan_info(
477            "fan_zero_rpm_stop_temperature",
478            "FAN_ZERO_RPM_STOP_TEMPERATURE",
479            "ZERO_RPM_STOP_TEMPERATURE",
480        )
481    }
482
483    fn set_fan_value(
484        &self,
485        file: &str,
486        value: u32,
487        section_name: &str,
488        range_name: &str,
489    ) -> Result<CommitHandle> {
490        let info = self.read_fan_info(file, section_name, range_name)?;
491        match info.allowed_range {
492            Some((min, max)) => {
493                if !(min..=max).contains(&value) {
494                    return Err(Error::not_allowed(format!(
495                        "Value {value} is out of range, should be between {min} and {max}"
496                    )));
497                }
498
499                let file_path = self.sysfs_path.join("gpu_od/fan_ctrl").join(file);
500                std::fs::write(&file_path, format!("{value}\n"))?;
501
502                Ok(CommitHandle::new(file_path))
503            }
504            None => Err(Error::not_allowed(format!(
505                "Changes to {range_name} are not allowed"
506            ))),
507        }
508    }
509
510    /// Sets the fan acoustic limit. Value is in RPM.
511    ///
512    /// Only available on Navi3x (RDNA 3) or newer.
513    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#acoustic-limit-rpm-threshold>
514    pub fn set_fan_acoustic_limit(&self, value: u32) -> Result<CommitHandle> {
515        self.set_fan_value(
516            "acoustic_limit_rpm_threshold",
517            value,
518            "OD_ACOUSTIC_LIMIT",
519            "ACOUSTIC_LIMIT",
520        )
521    }
522
523    /// Sets the fan acoustic target. Value is in RPM.
524    ///
525    /// Only available on Navi3x (RDNA 3) or newer.
526    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#acoustic-target-rpm-threshold>
527    pub fn set_fan_acoustic_target(&self, value: u32) -> Result<CommitHandle> {
528        self.set_fan_value(
529            "acoustic_target_rpm_threshold",
530            value,
531            "OD_ACOUSTIC_TARGET",
532            "ACOUSTIC_TARGET",
533        )
534    }
535
536    /// Sets the fan temperature target. Value is in degrees.
537    ///
538    /// Only available on Navi3x (RDNA 3) or newer.
539    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-target-temperature>
540    pub fn set_fan_target_temperature(&self, value: u32) -> Result<CommitHandle> {
541        self.set_fan_value(
542            "fan_target_temperature",
543            value,
544            "FAN_TARGET_TEMPERATURE",
545            "TARGET_TEMPERATURE",
546        )
547    }
548
549    /// Sets the fan minimum PWM. Value is a percentage.
550    ///
551    /// Only available on Navi3x (RDNA 3) or newer.
552    pub fn set_fan_minimum_pwm(&self, value: u32) -> Result<CommitHandle> {
553        self.set_fan_value("fan_minimum_pwm", value, "FAN_MINIMUM_PWM", "MINIMUM_PWM")
554    }
555
556    /// Sets the current fan zero RPM mode.
557    ///
558    /// Only available on Navi3x (RDNA 3) or newer.
559    pub fn set_fan_zero_rpm_enable(&self, enabled: bool) -> Result<CommitHandle> {
560        self.set_fan_value(
561            "fan_zero_rpm_enable",
562            enabled as u32,
563            "FAN_ZERO_RPM_ENABLE",
564            "ZERO_RPM_ENABLE",
565        )
566    }
567
568    /// Sets the fan zero RPM stop temperature.
569    ///
570    /// Only available on Navi3x (RDNA 3) or newer.
571    pub fn set_fan_zero_rpm_stop_temperature(&self, value: u32) -> Result<CommitHandle> {
572        self.set_fan_value(
573            "fan_zero_rpm_stop_temperature",
574            value,
575            "FAN_ZERO_RPM_STOP_TEMPERATURE",
576            "ZERO_RPM_STOP_TEMPERATURE",
577        )
578    }
579
580    fn reset_fan_value(&self, file: &str) -> Result<()> {
581        let file_path = self.sysfs_path.join("gpu_od/fan_ctrl").join(file);
582        let mut file = File::create(file_path)?;
583        writeln!(file, "r")?;
584        Ok(())
585    }
586
587    /// Resets the fan acoustic limit.
588    ///
589    /// Only available on Navi3x (RDNA 3) or newer.
590    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#acoustic-limit-rpm-threshold>
591    pub fn reset_fan_acoustic_limit(&self) -> Result<()> {
592        self.reset_fan_value("acoustic_limit_rpm_threshold")
593    }
594
595    /// Resets the fan acoustic target.
596    ///
597    /// Only available on Navi3x (RDNA 3) or newer.
598    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#acoustic-target-rpm-threshold>
599    pub fn reset_fan_acoustic_target(&self) -> Result<()> {
600        self.reset_fan_value("acoustic_target_rpm_threshold")
601    }
602
603    /// Resets the fan target temperature.
604    ///
605    /// Only available on Navi3x (RDNA 3) or newer.
606    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-target-temperature>
607    pub fn reset_fan_target_temperature(&self) -> Result<()> {
608        self.reset_fan_value("fan_target_temperature")
609    }
610
611    /// Resets the fan minimum pwm.
612    ///
613    /// Only available on Navi3x (RDNA 3) or newer.
614    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-minimum-pwm>
615    pub fn reset_fan_minimum_pwm(&self) -> Result<()> {
616        self.reset_fan_value("fan_minimum_pwm")
617    }
618
619    /// Gets the PMFW (power management firmware) fan curve.
620    /// Note: if no custom curve is used, all of the curve points may be set to 0.
621    ///
622    /// Only available on Navi3x (RDNA 3) or newer.
623    /// Older GPUs do not have a configurable fan curve in firmware, they need custom logic.
624    pub fn get_fan_curve(&self) -> Result<FanCurve> {
625        let data = self.read_file("gpu_od/fan_ctrl/fan_curve")?;
626        let contents = FanCtrlContents::parse(&data, "OD_FAN_CURVE")?;
627        let points = contents
628            .contents
629            .lines()
630            .enumerate()
631            .map(|(i, line)| {
632                let mut split = line.split(' ');
633                split.next(); // Discard index
634
635                let raw_temp = split
636                    .next()
637                    .ok_or_else(|| Error::unexpected_eol("Temperature value", i))?;
638                let temp = raw_temp.trim_end_matches('C').parse()?;
639
640                let raw_speed = split
641                    .next()
642                    .ok_or_else(|| Error::unexpected_eol("Speed value", i))?;
643                let speed = raw_speed.trim_end_matches('%').parse()?;
644
645                Ok((temp, speed))
646            })
647            .collect::<Result<_>>()?;
648
649        let temp_range = contents.od_range.get("FAN_CURVE(hotspot temp)");
650        let speed_range = contents.od_range.get("FAN_CURVE(fan speed)");
651
652        let allowed_ranges = if let Some(((min_temp, max_temp), (min_speed, max_speed))) =
653            (temp_range).zip(speed_range)
654        {
655            let min_temp: i32 = min_temp.trim_end_matches('C').parse()?;
656            let max_temp: i32 = max_temp.trim_end_matches('C').parse()?;
657
658            let min_speed: u8 = min_speed.trim_end_matches('%').parse()?;
659            let max_speed: u8 = max_speed.trim_end_matches('%').parse()?;
660
661            Some(FanCurveRanges {
662                temperature_range: min_temp..=max_temp,
663                speed_range: min_speed..=max_speed,
664            })
665        } else {
666            None
667        };
668
669        Ok(FanCurve {
670            points,
671            allowed_ranges,
672        })
673    }
674
675    /// Sets and applies the PMFW fan curve.
676    ///
677    /// Only available on Navi3x (RDNA 3) or newer.
678    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-curve>
679    pub fn set_fan_curve(&self, new_curve: &FanCurve) -> Result<CommitHandle> {
680        let current_curve = self.get_fan_curve()?;
681        let allowed_ranges = current_curve.allowed_ranges.ok_or_else(|| {
682            Error::not_allowed("Changes to the fan curve are not supported".to_owned())
683        })?;
684
685        let file_path = self.sysfs_path.join("gpu_od/fan_ctrl/fan_curve");
686
687        for (i, (temperature, speed)) in new_curve.points.iter().enumerate() {
688            if !allowed_ranges.temperature_range.contains(temperature) {
689                Err(Error::not_allowed(format!(
690                    "Temperature value {temperature} is outside of the allowed range {:?}",
691                    allowed_ranges.temperature_range
692                )))?;
693            }
694            if !allowed_ranges.speed_range.contains(speed) {
695                Err(Error::not_allowed(format!(
696                    "Speed value {speed} is outside of the allowed range {:?}",
697                    allowed_ranges.speed_range
698                )))?;
699            }
700
701            std::fs::write(&file_path, format!("{i} {temperature} {speed}\n"))?;
702        }
703
704        Ok(CommitHandle::new(file_path))
705    }
706
707    /// Resets the PMFW fan curve.
708    ///
709    /// Only available on Navi3x (RDNA 3) or newer.
710    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-curve>
711    pub fn reset_fan_curve(&self) -> Result<()> {
712        self.reset_fan_value("fan_curve")
713    }
714}
715
716impl SysFS for GpuHandle {
717    fn get_path(&self) -> &std::path::Path {
718        &self.sysfs_path
719    }
720}
721
722/// Performance level to be used by the GPU.
723///
724/// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#pp-od-clk-voltage>
725#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
726#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
727#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
728pub enum PerformanceLevel {
729    /// When auto is selected, the driver will attempt to dynamically select the optimal power profile for current conditions in the driver.
730    #[default]
731    Auto,
732    /// When low is selected, the clocks are forced to the lowest power state.
733    Low,
734    /// When high is selected, the clocks are forced to the highest power state.
735    High,
736    /// When manual is selected, power states can be manually adjusted via `pp_dpm_*` files ([`GpuHandle::set_enabled_power_levels`]) and `pp_od_clk_voltage` ([`GpuHandle::set_clocks_table`]).
737    Manual,
738}
739
740impl FromStr for PerformanceLevel {
741    type Err = Error;
742
743    fn from_str(s: &str) -> Result<Self> {
744        match s {
745            "auto" | "Automatic" => Ok(PerformanceLevel::Auto),
746            "high" | "Highest Clocks" => Ok(PerformanceLevel::High),
747            "low" | "Lowest Clocks" => Ok(PerformanceLevel::Low),
748            "manual" | "Manual" => Ok(PerformanceLevel::Manual),
749            _ => Err(ErrorKind::ParseError {
750                msg: "unrecognized GPU power profile".to_string(),
751                line: 1,
752            }
753            .into()),
754        }
755    }
756}
757
758impl fmt::Display for PerformanceLevel {
759    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
760        write!(
761            f,
762            "{}",
763            match self {
764                PerformanceLevel::Auto => "auto",
765                PerformanceLevel::High => "high",
766                PerformanceLevel::Low => "low",
767                PerformanceLevel::Manual => "manual",
768            }
769        )
770    }
771}
772
773/// For some reason files sometimes have random null bytes around lines
774fn trim_sysfs_line(line: &str) -> &str {
775    line.trim_matches(char::from(0)).trim()
776}
777
778/// Handle for committing values which were previusly written
779#[must_use]
780#[derive(Debug)]
781pub struct CommitHandle {
782    file_path: PathBuf,
783}
784
785impl CommitHandle {
786    pub(crate) fn new(file_path: PathBuf) -> Self {
787        Self { file_path }
788    }
789
790    /// Commit the previously written values
791    pub fn commit(self) -> Result<()> {
792        std::fs::write(&self.file_path, "c\n").with_context(|| {
793            format!(
794                "Could not commit values to {:?}",
795                self.file_path.file_name().unwrap()
796            )
797        })
798    }
799}
800
801#[cfg(test)]
802mod tests {
803    use super::{GpuHandle, PowerLevelKind, PowerLevels};
804    use pretty_assertions::assert_eq;
805
806    #[test]
807    fn parse_clock_levels_with_suffix() {
808        let levels = GpuHandle::parse_clock_levels::<u64>(
809            "\
8100: 500Mhz
8111: 2124Mhz *",
812            PowerLevelKind::CoreClock,
813        )
814        .unwrap();
815
816        assert_eq!(
817            PowerLevels {
818                levels: vec![500, 2124],
819                active: Some(1),
820            },
821            levels
822        );
823    }
824
825    #[test]
826    fn parse_clock_levels_without_suffix() {
827        let levels = GpuHandle::parse_clock_levels::<String>(
828            "\
8290: 2.5GT/s, x1 310Mhz
8301: 16.0GT/s, x16 619Mhz *",
831            PowerLevelKind::PcieSpeed,
832        )
833        .unwrap();
834
835        assert_eq!(
836            PowerLevels {
837                levels: vec![
838                    "2.5GT/s, x1 310Mhz".to_owned(),
839                    "16.0GT/s, x16 619Mhz".to_owned(),
840                ],
841                active: Some(1),
842            },
843            levels
844        );
845    }
846
847    #[test]
848    fn parse_clock_levels_ignores_deep_sleep() {
849        let levels = GpuHandle::parse_clock_levels::<u64>(
850            "\
851S: 19Mhz *
8520: 615Mhz
8531: 800Mhz
8542: 888Mhz
8553: 1000Mhz",
856            PowerLevelKind::CoreClock,
857        )
858        .unwrap();
859
860        assert_eq!(
861            PowerLevels {
862                levels: vec![615, 800, 888, 1000],
863                active: None,
864            },
865            levels
866        );
867    }
868
869    #[test]
870    fn parse_clock_levels_ignores_duplicate_active_markers() {
871        // driver tries to match active state by clocks value,
872        // which leads to duplicate active markers when there are multiple states with the same clocs
873        let levels = GpuHandle::parse_clock_levels::<u64>(
874            "\
8750: 400Mhz
8761: 600Mhz
8772: 687Mhz *
8783: 687Mhz *",
879            PowerLevelKind::CoreClock,
880        )
881        .unwrap();
882
883        assert_eq!(
884            PowerLevels {
885                levels: vec![400, 600, 687, 687],
886                active: None,
887            },
888            levels
889        );
890    }
891}