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::{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/// 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 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                // https://gitlab.freedesktop.org/drm/amd/-/work_items/5296
237                // handles duplicate active level lines
238                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    /// 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.
281    ///
282    /// Can only be used if `power_force_performance_level` is set to `manual`.
283    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    /// Reads the clocks table from `pp_od_clk_voltage`.
304    #[cfg(feature = "overdrive")]
305    pub fn get_clocks_table(&self) -> Result<ClocksTableGen> {
306        self.read_file_parsed("pp_od_clk_voltage")
307    }
308
309    /// Writes and commits the given clocks table to `pp_od_clk_voltage`.
310    #[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    /// Resets the clocks table to the default configuration.
323    #[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    /// Reads the list of predefined power profiles and the relevant heuristics settings for them from `pp_power_profile_mode`
333    ///
334    /// https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#pp-power-profile-mode
335    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    /// Sets the current power profile mode. You can get the available modes with [`get_power_profile_modes`].
341    /// Requires the performance level to be set to "manual" first using [`set_power_force_performance_level`]
342    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    /// Sets a custom power profile mode. You can get the available modes, and the list of heuristic names with [`get_power_profile_modes`].
347    /// Requires the performance level to be set to "manual" first using [`set_power_force_performance_level`]
348    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    /// Gets the fan acoustic limit. Values are in RPM.
422    ///
423    /// Only available on Navi3x (RDNA 3) or newer.
424    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#acoustic-limit-rpm-threshold>
425    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    /// Gets the fan acoustic target. Values are in RPM.
434    ///
435    /// Only available on Navi3x (RDNA 3) or newer.
436    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#acoustic-target-rpm-threshold>
437    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    /// Gets the fan temperature target. Values are in degrees.
446    ///
447    /// Only available on Navi3x (RDNA 3) or newer.
448    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-target-temperature>
449    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    /// Gets the fan minimum PWM. Values are in percentages.
458    ///
459    /// Only available on Navi3x (RDNA 3) or newer.
460    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-minimum-pwm>
461    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    /// Gets the current fan zero RPM mode.
466    ///
467    /// Only available on Navi3x (RDNA 3) or newer.
468    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    /// Gets the current fan zero RPM stop temperature.
478    ///
479    /// Only available on Navi3x (RDNA 3) or newer.
480    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    /// Sets the fan acoustic limit. Value is in RPM.
516    ///
517    /// Only available on Navi3x (RDNA 3) or newer.
518    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#acoustic-limit-rpm-threshold>
519    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    /// Sets the fan acoustic target. Value is in RPM.
529    ///
530    /// Only available on Navi3x (RDNA 3) or newer.
531    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#acoustic-target-rpm-threshold>
532    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    /// Sets the fan temperature target. Value is in degrees.
542    ///
543    /// Only available on Navi3x (RDNA 3) or newer.
544    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-target-temperature>
545    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    /// Sets the fan minimum PWM. Value is a percentage.
555    ///
556    /// Only available on Navi3x (RDNA 3) or newer.
557    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    /// Sets the current fan zero RPM mode.
562    ///
563    /// Only available on Navi3x (RDNA 3) or newer.
564    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    /// Sets the fan zero RPM stop temperature.
574    ///
575    /// Only available on Navi3x (RDNA 3) or newer.
576    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    /// Resets the fan acoustic limit.
593    ///
594    /// Only available on Navi3x (RDNA 3) or newer.
595    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#acoustic-limit-rpm-threshold>
596    pub fn reset_fan_acoustic_limit(&self) -> Result<()> {
597        self.reset_fan_value("acoustic_limit_rpm_threshold")
598    }
599
600    /// Resets the fan acoustic target.
601    ///
602    /// Only available on Navi3x (RDNA 3) or newer.
603    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#acoustic-target-rpm-threshold>
604    pub fn reset_fan_acoustic_target(&self) -> Result<()> {
605        self.reset_fan_value("acoustic_target_rpm_threshold")
606    }
607
608    /// Resets the fan target temperature.
609    ///
610    /// Only available on Navi3x (RDNA 3) or newer.
611    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-target-temperature>
612    pub fn reset_fan_target_temperature(&self) -> Result<()> {
613        self.reset_fan_value("fan_target_temperature")
614    }
615
616    /// Resets the fan minimum pwm.
617    ///
618    /// Only available on Navi3x (RDNA 3) or newer.
619    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-minimum-pwm>
620    pub fn reset_fan_minimum_pwm(&self) -> Result<()> {
621        self.reset_fan_value("fan_minimum_pwm")
622    }
623
624    /// Gets the PMFW (power management firmware) fan curve.
625    /// Note: if no custom curve is used, all of the curve points may be set to 0.
626    ///
627    /// Only available on Navi3x (RDNA 3) or newer.
628    /// Older GPUs do not have a configurable fan curve in firmware, they need custom logic.
629    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(); // Discard index
639
640                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    /// Sets and applies the PMFW fan curve.
681    ///
682    /// Only available on Navi3x (RDNA 3) or newer.
683    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-curve>
684    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    /// Resets the PMFW fan curve.
713    ///
714    /// Only available on Navi3x (RDNA 3) or newer.
715    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-curve>
716    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/// Performance level to be used by the GPU.
728///
729/// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#power-dpm-force-performance-level>
730#[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    /// When auto is selected, the driver will attempt to dynamically select the optimal power profile for current conditions in the driver.
735    #[default]
736    Auto,
737    /// When low is selected, the clocks are forced to the lowest power state.
738    Low,
739    /// When high is selected, the clocks are forced to the highest power state.
740    High,
741    /// 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`]).
742    Manual,
743    /// Fixed profiling mode with ASIC-specific clocks.
744    ProfileStandard,
745    /// Profiling mode that forces the core clock to the lowest level.
746    ProfileMinSclk,
747    /// Profiling mode that forces the memory clock to the lowest level.
748    ProfileMinMclk,
749    /// Profiling mode that forces clocks to the highest levels.
750    ProfilePeak,
751    /// next two are undocumented, but exist in the kernel's amd_dpm_forced_level enum
752    /// https://github.com/torvalds/linux/blob/e1914add2799225a87502051415fc5c32aeb02ae/drivers/gpu/drm/amd/include/kgd_pp_interface.h#L56-L67
753    /// Kernel extension for deterministic performance profiling.
754    PerfDeterminism,
755    /// Kernel extension used to exit profiling mode.
756    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
804/// For some reason files sometimes have random null bytes around lines
805fn trim_sysfs_line(line: &str) -> &str {
806    line.trim_matches(char::from(0)).trim()
807}
808
809/// Handle for committing values which were previusly written
810#[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    /// Commit the previously written values
822    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        // driver tries to match active state by clocks value,
951        // which leads to duplicate active markers when there are multiple states with the same clocs
952        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}