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
284        while let Some(raw_index) = header_split.next() {
285            let index: u16 = raw_index.parse()?;
286
287            let mut name = header_split
288                .next()
289                .ok_or_else(|| Error::unexpected_eol("Missing section name", 1))?;
290
291            if let Some(stripped) = name.strip_suffix("*") {
292                name = stripped;
293                active = Some(index);
294            }
295
296            modes.insert(
297                index,
298                PowerProfile {
299                    name: name.to_owned(),
300                    components: vec![],
301                },
302            );
303        }
304
305        let mut value_names = vec![];
306
307        for (i, line) in lines {
308            let mut split = line.split_whitespace();
309            let value_name = split
310                .next()
311                .ok_or_else(|| Error::unexpected_eol("Value name", i + 1))?;
312
313            value_names.push(value_name.to_owned());
314
315            for (profile_i, raw_value) in split.enumerate() {
316                let value = raw_value.parse()?;
317
318                let profile = modes.get_mut(&(profile_i as u16)).ok_or_else(|| {
319                    Error::basic_parse_error("Could not get profile from header by index")
320                })?;
321
322                match profile.components.first_mut() {
323                    Some(component) => {
324                        component.values.push(Some(value));
325                    }
326                    None => {
327                        let component = PowerProfileComponent {
328                            clock_type: None,
329                            values: vec![Some(value)],
330                        };
331                        profile.components.push(component);
332                    }
333                }
334            }
335        }
336
337        Ok(Self {
338            modes,
339            value_names,
340            active: active.ok_or_else(|| Error::basic_parse_error("No active level found"))?,
341        })
342    }
343
344    /// Parse the format used by integrated GPUs
345    fn parse_basic(s: &str) -> Result<Self> {
346        let mut modes = BTreeMap::new();
347        let mut active = None;
348
349        for (line, row) in s.lines().map(str::trim).enumerate() {
350            let mut split = row.split_whitespace();
351            if let Some(num) = split.next().and_then(|part| part.parse::<u16>().ok()) {
352                let name_part = split
353                    .next()
354                    .ok_or_else(|| Error::unexpected_eol("No name after mode number", line + 1))?;
355
356                let name = if let Some(name) = name_part.strip_suffix('*') {
357                    active = Some(num);
358                    name
359                } else {
360                    name_part
361                };
362
363                modes.insert(
364                    num,
365                    PowerProfile {
366                        name: name.to_owned(),
367                        components: vec![],
368                    },
369                );
370            }
371        }
372
373        Ok(Self {
374            modes,
375            value_names: vec![],
376            active: active.ok_or_else(|| Error::basic_parse_error("No active level found"))?,
377        })
378    }
379}
380
381impl PowerProfile {
382    /// If this is the custom profile (checked by name)
383    pub fn is_custom(&self) -> bool {
384        self.name.eq_ignore_ascii_case("CUSTOM")
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use super::PowerProfileModesTable;
391    use insta::assert_yaml_snapshot;
392
393    const TABLE_VEGA56: &str = include_test_data!("vega56/pp_power_profile_mode");
394    const TABLE_RX580: &str = include_test_data!("rx580/pp_power_profile_mode");
395    const TABLE_4800H: &str = include_test_data!("internal-4800h/pp_power_profile_mode");
396    const TABLE_RX6900XT: &str = include_test_data!("rx6900xt/pp_power_profile_mode");
397    const TABLE_RX7700S: &str = include_test_data!("rx7700s/pp_power_profile_mode");
398    const TABLE_RX7800XT: &str = include_test_data!("rx7800xt/pp_power_profile_mode");
399
400    #[test]
401    fn parse_full_vega56() {
402        let table = PowerProfileModesTable::parse(TABLE_VEGA56).unwrap();
403        assert_yaml_snapshot!(table);
404    }
405
406    #[test]
407    fn parse_full_rx580() {
408        let table = PowerProfileModesTable::parse(TABLE_RX580).unwrap();
409        assert_yaml_snapshot!(table);
410    }
411
412    #[test]
413    fn parse_full_internal_4800h() {
414        let table = PowerProfileModesTable::parse(TABLE_4800H).unwrap();
415        assert_yaml_snapshot!(table);
416    }
417
418    #[test]
419    fn parse_full_rx6900xt() {
420        let table = PowerProfileModesTable::parse(TABLE_RX6900XT).unwrap();
421        assert_yaml_snapshot!(table);
422    }
423
424    #[test]
425    fn parse_full_rx7700s() {
426        let table = PowerProfileModesTable::parse(TABLE_RX7700S).unwrap();
427        assert_yaml_snapshot!(table);
428    }
429
430    #[test]
431    fn parse_full_rx7800xt() {
432        let table = PowerProfileModesTable::parse(TABLE_RX7800XT).unwrap();
433        assert_yaml_snapshot!(table);
434    }
435}