1#![allow(missing_docs)] use crate::{
4 error::{Error, ErrorKind},
5 Result,
6};
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeMap;
10
11#[derive(Debug)]
15#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
16pub struct PowerProfileModesTable {
17 pub modes: BTreeMap<u16, PowerProfile>,
19 pub value_names: Vec<String>,
21 pub active: u16,
23}
24
25#[derive(Debug)]
26#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
27pub struct PowerProfile {
28 pub name: String,
29 pub components: Vec<PowerProfileComponent>,
32}
33
34#[derive(Debug)]
35#[cfg_attr(feature = "serde", derive(Serialize, Deserialize, Default, Clone))]
36pub struct PowerProfileComponent {
37 pub clock_type: Option<String>,
39 pub values: Vec<Option<i32>>,
40}
41
42impl PowerProfileModesTable {
43 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 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 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 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 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 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 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}