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    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    /// 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.
263    ///
264    /// Can only be used if `power_force_performance_level` is set to `manual`.
265    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    /// Reads the clocks table from `pp_od_clk_voltage`.
286    #[cfg(feature = "overdrive")]
287    pub fn get_clocks_table(&self) -> Result<ClocksTableGen> {
288        self.read_file_parsed("pp_od_clk_voltage")
289    }
290
291    /// Writes and commits the given clocks table to `pp_od_clk_voltage`.
292    #[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    /// Resets the clocks table to the default configuration.
305    #[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    /// Reads the list of predefined power profiles and the relevant heuristics settings for them from `pp_power_profile_mode`
315    ///
316    /// https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#pp-power-profile-mode
317    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    /// Sets the current power profile mode. You can get the available modes with [`get_power_profile_modes`].
323    /// Requires the performance level to be set to "manual" first using [`set_power_force_performance_level`]
324    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    /// Sets a custom power profile mode. You can get the available modes, and the list of heuristic names with [`get_power_profile_modes`].
329    /// Requires the performance level to be set to "manual" first using [`set_power_force_performance_level`]
330    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    /// Gets the fan acoustic limit. Values are in RPM.
404    ///
405    /// Only available on Navi3x (RDNA 3) or newer.
406    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#acoustic-limit-rpm-threshold>
407    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    /// Gets the fan acoustic target. Values are in RPM.
416    ///
417    /// Only available on Navi3x (RDNA 3) or newer.
418    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#acoustic-target-rpm-threshold>
419    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    /// Gets the fan temperature target. Values are in degrees.
428    ///
429    /// Only available on Navi3x (RDNA 3) or newer.
430    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-target-temperature>
431    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    /// Gets the fan minimum PWM. Values are in percentages.
440    ///
441    /// Only available on Navi3x (RDNA 3) or newer.
442    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-minimum-pwm>
443    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    /// Gets the current fan zero RPM mode.
448    ///
449    /// Only available on Navi3x (RDNA 3) or newer.
450    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    /// Gets the current fan zero RPM stop temperature.
460    ///
461    /// Only available on Navi3x (RDNA 3) or newer.
462    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    /// Sets the fan acoustic limit. Value is in RPM.
498    ///
499    /// Only available on Navi3x (RDNA 3) or newer.
500    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#acoustic-limit-rpm-threshold>
501    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    /// Sets the fan acoustic target. 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-target-rpm-threshold>
514    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    /// Sets the fan temperature target. Value is in degrees.
524    ///
525    /// Only available on Navi3x (RDNA 3) or newer.
526    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-target-temperature>
527    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    /// Sets the fan minimum PWM. Value is a percentage.
537    ///
538    /// Only available on Navi3x (RDNA 3) or newer.
539    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    /// Sets the current fan zero RPM mode.
544    ///
545    /// Only available on Navi3x (RDNA 3) or newer.
546    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    /// Sets the fan zero RPM stop temperature.
556    ///
557    /// Only available on Navi3x (RDNA 3) or newer.
558    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    /// Resets the fan acoustic limit.
575    ///
576    /// Only available on Navi3x (RDNA 3) or newer.
577    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#acoustic-limit-rpm-threshold>
578    pub fn reset_fan_acoustic_limit(&self) -> Result<()> {
579        self.reset_fan_value("acoustic_limit_rpm_threshold")
580    }
581
582    /// Resets the fan acoustic target.
583    ///
584    /// Only available on Navi3x (RDNA 3) or newer.
585    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#acoustic-target-rpm-threshold>
586    pub fn reset_fan_acoustic_target(&self) -> Result<()> {
587        self.reset_fan_value("acoustic_target_rpm_threshold")
588    }
589
590    /// Resets the fan target temperature.
591    ///
592    /// Only available on Navi3x (RDNA 3) or newer.
593    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-target-temperature>
594    pub fn reset_fan_target_temperature(&self) -> Result<()> {
595        self.reset_fan_value("fan_target_temperature")
596    }
597
598    /// Resets the fan minimum pwm.
599    ///
600    /// Only available on Navi3x (RDNA 3) or newer.
601    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-minimum-pwm>
602    pub fn reset_fan_minimum_pwm(&self) -> Result<()> {
603        self.reset_fan_value("fan_minimum_pwm")
604    }
605
606    /// Gets the PMFW (power management firmware) fan curve.
607    /// Note: if no custom curve is used, all of the curve points may be set to 0.
608    ///
609    /// Only available on Navi3x (RDNA 3) or newer.
610    /// Older GPUs do not have a configurable fan curve in firmware, they need custom logic.
611    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(); // Discard index
621
622                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    /// Sets and applies the PMFW fan curve.
663    ///
664    /// Only available on Navi3x (RDNA 3) or newer.
665    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-curve>
666    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    /// Resets the PMFW fan curve.
695    ///
696    /// Only available on Navi3x (RDNA 3) or newer.
697    /// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#fan-curve>
698    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/// Performance level to be used by the GPU.
710///
711/// <https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#pp-od-clk-voltage>
712#[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    /// When auto is selected, the driver will attempt to dynamically select the optimal power profile for current conditions in the driver.
717    #[default]
718    Auto,
719    /// When low is selected, the clocks are forced to the lowest power state.
720    Low,
721    /// When high is selected, the clocks are forced to the highest power state.
722    High,
723    /// 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`]).
724    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
760/// For some reason files sometimes have random null bytes around lines
761fn trim_sysfs_line(line: &str) -> &str {
762    line.trim_matches(char::from(0)).trim()
763}
764
765/// Handle for committing values which were previusly written
766#[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    /// Commit the previously written values
778    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}