amdgpu_sysfs/gpu_handle/
power_profile_mode.rs

1//! `pp-power-profile-mode`
2#![allow(missing_docs)] // temp
3use crate::{
4    error::{Error, ErrorKind},
5    Result,
6};
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeMap;
10
11/// Table of predefined power profile modes
12
13/// https://kernel.org/doc/html/latest/gpu/amdgpu/thermal.html#pp-power-profile-mode
14#[derive(Debug)]
15#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
16pub struct PowerProfileModesTable {
17    /// List of available modes
18    pub modes: BTreeMap<u16, PowerProfile>,
19    /// Names for the values in [`PowerProfile`]
20    pub value_names: Vec<String>,
21    /// The currently active mode
22    pub active: u16,
23}
24
25#[derive(Debug)]
26#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
27pub struct PowerProfile {
28    pub name: String,
29    /// On RDNA and newer, each profile has multiple components for different clock types.
30    /// Older generations have only one set of values.
31    pub components: Vec<PowerProfileComponent>,
32}
33
34#[derive(Debug)]
35#[cfg_attr(feature = "serde", derive(Serialize, Deserialize, Default, Clone))]
36pub struct PowerProfileComponent {
37    /// Filled on RDNA and newer
38    pub clock_type: Option<String>,
39    pub values: Vec<Option<i32>>,
40}
41
42impl PowerProfileModesTable {
43    /// Parse the table from a given string
44    pub fn parse(s: &str) -> Result<Self> {
45        let mut lines = s.lines().map(|line| line.split_whitespace());
46
47        let mut split = lines
48            .next()
49            .ok_or_else(|| Error::unexpected_eol("Power profile line", 1))?;
50        let start = split
51            .next()
52            .ok_or_else(|| Error::unexpected_eol("Value description", 1))?;
53
54        match start {
55            "NUM" => Self::parse_flat(s),
56            "PROFILE_INDEX(NAME)" => Self::parse_nested(s),
57            _ if start.parse::<u16>().is_ok() => {
58                if lines
59                    .next()
60                    .and_then(|mut line| line.next())
61                    .is_some_and(|term| term.parse::<u16>().is_ok())
62                {
63                    Self::parse_basic(s)
64                } else {
65                    Self::parse_rotated(s)
66                }
67            }
68            _ => Err(Error::basic_parse_error(
69                "Could not determine the type of power profile mode table",
70            )),
71        }
72    }
73
74    /// Parse the format used by pre-RDNA GPUs
75    fn parse_flat(s: &str) -> Result<Self> {
76        let mut modes = BTreeMap::new();
77        let mut active = None;
78
79        let mut lines = s.lines();
80
81        let header_line = lines
82            .next()
83            .ok_or_else(|| Error::unexpected_eol("Info header", 1))?;
84        let mut header_split = header_line.split_whitespace();
85
86        if header_split.next() != Some("NUM") {
87            return Err(
88                ErrorKind::Unsupported("Expected header to start with 'NUM'".to_owned()).into(),
89            );
90        }
91        if header_split.next() != Some("MODE_NAME") {
92            return Err(ErrorKind::Unsupported(
93                "Expected header to contain 'MODE_NAME'".to_owned(),
94            )
95            .into());
96        }
97
98        let value_names: Vec<String> = header_split.map(str::to_owned).collect();
99
100        for (line, row) in s.lines().map(str::trim).enumerate() {
101            let mut split = row.split_whitespace().peekable();
102            if let Some(num) = split.next().and_then(|part| part.parse::<u16>().ok()) {
103                let name_part = split
104                    .next()
105                    .ok_or_else(|| Error::unexpected_eol("Mode name", line + 1))?
106                    .trim_end_matches(':');
107
108                // Handle space within the mode name:
109                // `3D_FULL_SCREEN *:`
110                if let Some(next) = split.peek() {
111                    if next.ends_with(':') {
112                        if next.starts_with('*') {
113                            active = Some(num);
114                        }
115                        split.next();
116                    }
117                }
118
119                let name = if let Some(name) = name_part.strip_suffix('*') {
120                    active = Some(num);
121                    name.trim()
122                } else {
123                    name_part
124                };
125
126                let values = split
127                    .map(|value| {
128                        if value == "-" {
129                            Ok(None)
130                        } else {
131                            let parsed = value.parse().map_err(|_| {
132                                Error::from(ErrorKind::ParseError {
133                                    msg: format!("Expected an integer, got '{value}'"),
134                                    line: line + 1,
135                                })
136                            })?;
137                            Ok(Some(parsed))
138                        }
139                    })
140                    .collect::<Result<_>>()?;
141
142                let power_profile = PowerProfile {
143                    name: name.to_owned(),
144                    components: vec![PowerProfileComponent {
145                        clock_type: None,
146                        values,
147                    }],
148                };
149                modes.insert(num, power_profile);
150            }
151        }
152
153        Ok(Self {
154            modes,
155            value_names,
156            active: active.ok_or_else(|| Error::basic_parse_error("No active level found"))?,
157        })
158    }
159
160    /// Parse the format used by RDNA and higher
161    fn parse_nested(s: &str) -> Result<Self> {
162        let mut modes = BTreeMap::new();
163        let mut active = None;
164
165        let mut lines = s.lines();
166
167        let header_line = lines
168            .next()
169            .ok_or_else(|| Error::unexpected_eol("Info header", 1))?;
170        let mut header_split = header_line.split_whitespace();
171
172        if header_split.next() != Some("PROFILE_INDEX(NAME)") {
173            return Err(ErrorKind::Unsupported(
174                "Expected header to start with 'PROFILE_INDEX(NAME)'".to_owned(),
175            )
176            .into());
177        }
178        if header_split.next() != Some("CLOCK_TYPE(NAME)") {
179            return Err(ErrorKind::Unsupported(
180                "Expected header to contain 'CLOCK_TYPE(NAME)'".to_owned(),
181            )
182            .into());
183        }
184
185        let value_names: Vec<String> = header_split.map(str::to_owned).collect();
186
187        let mut lines = lines.map(str::trim).enumerate().peekable();
188        while let Some((line, row)) = lines.next() {
189            if row.contains('(') {
190                return Err(ErrorKind::ParseError {
191                    msg: format!("Unexpected mode heuristics line '{row}'"),
192                    line: line + 1,
193                }
194                .into());
195            }
196
197            let mut split = row.split_whitespace();
198            if let Some(num) = split.next().and_then(|part| part.parse::<u16>().ok()) {
199                let name_part = split
200                    .next()
201                    .ok_or_else(|| Error::unexpected_eol("No name after mode number", line + 1))?
202                    .trim_end_matches(':');
203
204                let name = if let Some(name) = name_part.strip_suffix('*') {
205                    active = Some(num);
206                    name.trim()
207                } else {
208                    name_part
209                };
210
211                let mut components = Vec::new();
212
213                while lines
214                    .peek()
215                    .is_some_and(|(_, row)| row.contains(['(', ')']))
216                {
217                    let (line, clock_type_line) = lines.next().unwrap();
218
219                    let name_start = clock_type_line
220                        .char_indices()
221                        .position(|(_, c)| c == '(')
222                        .ok_or_else(|| Error::unexpected_eol('(', line + 1))?;
223
224                    let name_end = clock_type_line
225                        .char_indices()
226                        .position(|(_, c)| c == ')')
227                        .ok_or_else(|| Error::unexpected_eol(')', line + 1))?;
228
229                    let clock_type = clock_type_line[name_start + 1..name_end].trim();
230
231                    let clock_type_values = clock_type_line[name_end + 1..]
232                        .split_whitespace()
233                        .map(str::trim)
234                        .map(|value| {
235                            if value == "-" {
236                                Ok(None)
237                            } else {
238                                let parsed = value.parse().map_err(|_| {
239                                    Error::from(ErrorKind::ParseError {
240                                        msg: format!("Expected an integer, got '{value}'"),
241                                        line: line + 1,
242                                    })
243                                })?;
244                                Ok(Some(parsed))
245                            }
246                        })
247                        .collect::<Result<Vec<Option<i32>>>>()?;
248
249                    components.push(PowerProfileComponent {
250                        clock_type: Some(clock_type.to_owned()),
251                        values: clock_type_values,
252                    })
253                }
254
255                let power_profile = PowerProfile {
256                    name: name.to_owned(),
257                    components,
258                };
259                modes.insert(num, power_profile);
260            }
261        }
262
263        Ok(Self {
264            modes,
265            value_names,
266            active: active.ok_or_else(|| Error::basic_parse_error("No active level found"))?,
267        })
268    }
269
270    /// Parse "rotated" format (with columns as profiles, and rows as values).
271    /// Used at least by RDNA3 laptop GPUs (example data: 7700s)
272    fn parse_rotated(s: &str) -> Result<Self> {
273        let mut modes = BTreeMap::new();
274        let mut active = None;
275
276        let mut lines = s.lines().map(str::trim).enumerate();
277
278        let mut header_split = lines
279            .next()
280            .ok_or_else(|| Error::basic_parse_error("Missing header"))?
281            .1
282            .split_whitespace()
283            .peekable();
284
285        while let Some(raw_index) = header_split.next() {
286            let index: u16 = raw_index.parse().map_err(|_| {
287                Error::basic_parse_error(format!("Invalid mode index '{raw_index}'"))
288            })?;
289
290            let mut name = header_split
291                .next()
292                .ok_or_else(|| Error::unexpected_eol("Missing section name", 1))?;
293
294            if let Some(stripped) = name.strip_suffix("*") {
295                name = stripped;
296                active = Some(index);
297            }
298
299            if let Some(&"*") = header_split.peek() {
300                active = Some(index);
301                header_split.next();
302            }
303
304            modes.insert(
305                index,
306                PowerProfile {
307                    name: name.to_owned(),
308                    components: vec![],
309                },
310            );
311        }
312
313        let mut value_names = vec![];
314
315        for (i, line) in lines {
316            let mut split = line.split_whitespace();
317            let value_name = split
318                .next()
319                .ok_or_else(|| Error::unexpected_eol("Value name", i + 1))?;
320
321            value_names.push(value_name.to_owned());
322
323            for (profile_i, raw_value) in split.enumerate() {
324                let value = raw_value.parse().map_err(|_| {
325                    Error::basic_parse_error(format!("Invalid mode value '{raw_value}'"))
326                })?;
327
328                let profile = modes.get_mut(&(profile_i as u16)).ok_or_else(|| {
329                    Error::basic_parse_error("Could not get profile from header by index")
330                })?;
331
332                match profile.components.first_mut() {
333                    Some(component) => {
334                        component.values.push(Some(value));
335                    }
336                    None => {
337                        let component = PowerProfileComponent {
338                            clock_type: None,
339                            values: vec![Some(value)],
340                        };
341                        profile.components.push(component);
342                    }
343                }
344            }
345        }
346
347        Ok(Self {
348            modes,
349            value_names,
350            active: active.ok_or_else(|| Error::basic_parse_error("No active level found"))?,
351        })
352    }
353
354    /// Parse the format used by integrated GPUs
355    fn parse_basic(s: &str) -> Result<Self> {
356        let mut modes = BTreeMap::new();
357        let mut active = None;
358
359        for (line, row) in s.lines().map(str::trim).enumerate() {
360            let mut split = row.split_whitespace();
361            if let Some(num) = split.next().and_then(|part| part.parse::<u16>().ok()) {
362                let name_part = split
363                    .next()
364                    .ok_or_else(|| Error::unexpected_eol("No name after mode number", line + 1))?;
365
366                let name = if let Some(name) = name_part.strip_suffix('*') {
367                    active = Some(num);
368                    name
369                } else {
370                    name_part
371                };
372
373                modes.insert(
374                    num,
375                    PowerProfile {
376                        name: name.to_owned(),
377                        components: vec![],
378                    },
379                );
380            }
381        }
382
383        Ok(Self {
384            modes,
385            value_names: vec![],
386            active: active.ok_or_else(|| Error::basic_parse_error("No active level found"))?,
387        })
388    }
389}
390
391impl PowerProfile {
392    /// If this is the custom profile (checked by name)
393    pub fn is_custom(&self) -> bool {
394        self.name.eq_ignore_ascii_case("CUSTOM")
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::PowerProfileModesTable;
401    use insta::assert_yaml_snapshot;
402
403    const TABLE_VEGA56: &str = include_test_data!("vega56/pp_power_profile_mode");
404    const TABLE_RX580: &str = include_test_data!("rx580/pp_power_profile_mode");
405    const TABLE_4800H: &str = include_test_data!("internal-4800h/pp_power_profile_mode");
406    const TABLE_RX6900XT: &str = include_test_data!("rx6900xt/pp_power_profile_mode");
407    const TABLE_RX7600S: &str = include_test_data!("rx7600s/pp_power_profile_mode");
408    const TABLE_RX7700S: &str = include_test_data!("rx7700s/pp_power_profile_mode");
409    const TABLE_RX7800XT: &str = include_test_data!("rx7800xt/pp_power_profile_mode");
410
411    #[test]
412    fn parse_full_vega56() {
413        let table = PowerProfileModesTable::parse(TABLE_VEGA56).unwrap();
414        assert_yaml_snapshot!(table);
415    }
416
417    #[test]
418    fn parse_full_rx580() {
419        let table = PowerProfileModesTable::parse(TABLE_RX580).unwrap();
420        assert_yaml_snapshot!(table);
421    }
422
423    #[test]
424    fn parse_full_internal_4800h() {
425        let table = PowerProfileModesTable::parse(TABLE_4800H).unwrap();
426        assert_yaml_snapshot!(table);
427    }
428
429    #[test]
430    fn parse_full_rx6900xt() {
431        let table = PowerProfileModesTable::parse(TABLE_RX6900XT).unwrap();
432        assert_yaml_snapshot!(table);
433    }
434
435    #[test]
436    fn parse_full_rx7600s() {
437        let table = PowerProfileModesTable::parse(TABLE_RX7600S).unwrap();
438        assert_yaml_snapshot!(table);
439    }
440
441    #[test]
442    fn parse_full_rx7700s() {
443        let table = PowerProfileModesTable::parse(TABLE_RX7700S).unwrap();
444        assert_yaml_snapshot!(table);
445    }
446
447    #[test]
448    fn parse_full_rx7800xt() {
449        let table = PowerProfileModesTable::parse(TABLE_RX7800XT).unwrap();
450        assert_yaml_snapshot!(table);
451    }
452}