#![allow(missing_docs)] use crate::{
error::{Error, ErrorKind},
Result,
};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PowerProfileModesTable {
pub modes: BTreeMap<u16, PowerProfile>,
pub value_names: Vec<String>,
pub active: u16,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PowerProfile {
pub name: String,
pub components: Vec<PowerProfileComponent>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize, Default, Clone))]
pub struct PowerProfileComponent {
pub clock_type: Option<String>,
pub values: Vec<Option<i32>>,
}
impl PowerProfileModesTable {
pub fn parse(s: &str) -> Result<Self> {
let mut lines = s.lines().map(|line| line.split_whitespace());
let mut split = lines
.next()
.ok_or_else(|| Error::unexpected_eol("Power profile line", 1))?;
let start = split
.next()
.ok_or_else(|| Error::unexpected_eol("Value description", 1))?;
match start {
"NUM" => Self::parse_flat(s),
"PROFILE_INDEX(NAME)" => Self::parse_nested(s),
_ if start.parse::<u16>().is_ok() => {
if lines
.next()
.and_then(|mut line| line.next())
.is_some_and(|term| term.parse::<u16>().is_ok())
{
Self::parse_basic(s)
} else {
Self::parse_rotated(s)
}
}
_ => Err(Error::basic_parse_error(
"Could not determine the type of power profile mode table",
)),
}
}
fn parse_flat(s: &str) -> Result<Self> {
let mut modes = BTreeMap::new();
let mut active = None;
let mut lines = s.lines();
let header_line = lines
.next()
.ok_or_else(|| Error::unexpected_eol("Info header", 1))?;
let mut header_split = header_line.split_whitespace();
if header_split.next() != Some("NUM") {
return Err(
ErrorKind::Unsupported("Expected header to start with 'NUM'".to_owned()).into(),
);
}
if header_split.next() != Some("MODE_NAME") {
return Err(ErrorKind::Unsupported(
"Expected header to contain 'MODE_NAME'".to_owned(),
)
.into());
}
let value_names: Vec<String> = header_split.map(str::to_owned).collect();
for (line, row) in s.lines().map(str::trim).enumerate() {
let mut split = row.split_whitespace().peekable();
if let Some(num) = split.next().and_then(|part| part.parse::<u16>().ok()) {
let name_part = split
.next()
.ok_or_else(|| Error::unexpected_eol("Mode name", line + 1))?
.trim_end_matches(':');
if let Some(next) = split.peek() {
if next.ends_with(':') {
if next.starts_with('*') {
active = Some(num);
}
split.next();
}
}
let name = if let Some(name) = name_part.strip_suffix('*') {
active = Some(num);
name.trim()
} else {
name_part
};
let values = split
.map(|value| {
if value == "-" {
Ok(None)
} else {
let parsed = value.parse().map_err(|_| {
Error::from(ErrorKind::ParseError {
msg: format!("Expected an integer, got '{value}'"),
line: line + 1,
})
})?;
Ok(Some(parsed))
}
})
.collect::<Result<_>>()?;
let power_profile = PowerProfile {
name: name.to_owned(),
components: vec![PowerProfileComponent {
clock_type: None,
values,
}],
};
modes.insert(num, power_profile);
}
}
Ok(Self {
modes,
value_names,
active: active.ok_or_else(|| Error::basic_parse_error("No active level found"))?,
})
}
fn parse_nested(s: &str) -> Result<Self> {
let mut modes = BTreeMap::new();
let mut active = None;
let mut lines = s.lines();
let header_line = lines
.next()
.ok_or_else(|| Error::unexpected_eol("Info header", 1))?;
let mut header_split = header_line.split_whitespace();
if header_split.next() != Some("PROFILE_INDEX(NAME)") {
return Err(ErrorKind::Unsupported(
"Expected header to start with 'PROFILE_INDEX(NAME)'".to_owned(),
)
.into());
}
if header_split.next() != Some("CLOCK_TYPE(NAME)") {
return Err(ErrorKind::Unsupported(
"Expected header to contain 'CLOCK_TYPE(NAME)'".to_owned(),
)
.into());
}
let value_names: Vec<String> = header_split.map(str::to_owned).collect();
let mut lines = lines.map(str::trim).enumerate().peekable();
while let Some((line, row)) = lines.next() {
if row.contains('(') {
return Err(ErrorKind::ParseError {
msg: format!("Unexpected mode heuristics line '{row}'"),
line: line + 1,
}
.into());
}
let mut split = row.split_whitespace();
if let Some(num) = split.next().and_then(|part| part.parse::<u16>().ok()) {
let name_part = split
.next()
.ok_or_else(|| Error::unexpected_eol("No name after mode number", line + 1))?
.trim_end_matches(':');
let name = if let Some(name) = name_part.strip_suffix('*') {
active = Some(num);
name.trim()
} else {
name_part
};
let mut components = Vec::new();
while lines
.peek()
.is_some_and(|(_, row)| row.contains(['(', ')']))
{
let (line, clock_type_line) = lines.next().unwrap();
let name_start = clock_type_line
.char_indices()
.position(|(_, c)| c == '(')
.ok_or_else(|| Error::unexpected_eol('(', line + 1))?;
let name_end = clock_type_line
.char_indices()
.position(|(_, c)| c == ')')
.ok_or_else(|| Error::unexpected_eol(')', line + 1))?;
let clock_type = clock_type_line[name_start + 1..name_end].trim();
let clock_type_values = clock_type_line[name_end + 1..]
.split_whitespace()
.map(str::trim)
.map(|value| {
if value == "-" {
Ok(None)
} else {
let parsed = value.parse().map_err(|_| {
Error::from(ErrorKind::ParseError {
msg: format!("Expected an integer, got '{value}'"),
line: line + 1,
})
})?;
Ok(Some(parsed))
}
})
.collect::<Result<Vec<Option<i32>>>>()?;
components.push(PowerProfileComponent {
clock_type: Some(clock_type.to_owned()),
values: clock_type_values,
})
}
let power_profile = PowerProfile {
name: name.to_owned(),
components,
};
modes.insert(num, power_profile);
}
}
Ok(Self {
modes,
value_names,
active: active.ok_or_else(|| Error::basic_parse_error("No active level found"))?,
})
}
fn parse_rotated(s: &str) -> Result<Self> {
let mut modes = BTreeMap::new();
let mut active = None;
let mut lines = s.lines().map(str::trim).enumerate();
let mut header_split = lines
.next()
.ok_or_else(|| Error::basic_parse_error("Missing header"))?
.1
.split_whitespace()
.peekable();
while let Some(raw_index) = header_split.next() {
let index: u16 = raw_index.parse().map_err(|_| {
Error::basic_parse_error(format!("Invalid mode index '{raw_index}'"))
})?;
let mut name = header_split
.next()
.ok_or_else(|| Error::unexpected_eol("Missing section name", 1))?;
if let Some(stripped) = name.strip_suffix("*") {
name = stripped;
active = Some(index);
}
if let Some(&"*") = header_split.peek() {
active = Some(index);
header_split.next();
}
modes.insert(
index,
PowerProfile {
name: name.to_owned(),
components: vec![],
},
);
}
let mut value_names = vec![];
for (i, line) in lines {
let mut split = line.split_whitespace();
let value_name = split
.next()
.ok_or_else(|| Error::unexpected_eol("Value name", i + 1))?;
value_names.push(value_name.to_owned());
for (profile_i, raw_value) in split.enumerate() {
let value = raw_value.parse().map_err(|_| {
Error::basic_parse_error(format!("Invalid mode value '{raw_value}'"))
})?;
let profile = modes.get_mut(&(profile_i as u16)).ok_or_else(|| {
Error::basic_parse_error("Could not get profile from header by index")
})?;
match profile.components.first_mut() {
Some(component) => {
component.values.push(Some(value));
}
None => {
let component = PowerProfileComponent {
clock_type: None,
values: vec![Some(value)],
};
profile.components.push(component);
}
}
}
}
Ok(Self {
modes,
value_names,
active: active.ok_or_else(|| Error::basic_parse_error("No active level found"))?,
})
}
fn parse_basic(s: &str) -> Result<Self> {
let mut modes = BTreeMap::new();
let mut active = None;
for (line, row) in s.lines().map(str::trim).enumerate() {
let mut split = row.split_whitespace();
if let Some(num) = split.next().and_then(|part| part.parse::<u16>().ok()) {
let name_part = split
.next()
.ok_or_else(|| Error::unexpected_eol("No name after mode number", line + 1))?;
let name = if let Some(name) = name_part.strip_suffix('*') {
active = Some(num);
name
} else {
name_part
};
modes.insert(
num,
PowerProfile {
name: name.to_owned(),
components: vec![],
},
);
}
}
Ok(Self {
modes,
value_names: vec![],
active: active.ok_or_else(|| Error::basic_parse_error("No active level found"))?,
})
}
}
impl PowerProfile {
pub fn is_custom(&self) -> bool {
self.name.eq_ignore_ascii_case("CUSTOM")
}
}
#[cfg(test)]
mod tests {
use super::PowerProfileModesTable;
use insta::assert_yaml_snapshot;
const TABLE_VEGA56: &str = include_test_data!("vega56/pp_power_profile_mode");
const TABLE_RX580: &str = include_test_data!("rx580/pp_power_profile_mode");
const TABLE_4800H: &str = include_test_data!("internal-4800h/pp_power_profile_mode");
const TABLE_RX6900XT: &str = include_test_data!("rx6900xt/pp_power_profile_mode");
const TABLE_RX7600S: &str = include_test_data!("rx7600s/pp_power_profile_mode");
const TABLE_RX7700S: &str = include_test_data!("rx7700s/pp_power_profile_mode");
const TABLE_RX7800XT: &str = include_test_data!("rx7800xt/pp_power_profile_mode");
#[test]
fn parse_full_vega56() {
let table = PowerProfileModesTable::parse(TABLE_VEGA56).unwrap();
assert_yaml_snapshot!(table);
}
#[test]
fn parse_full_rx580() {
let table = PowerProfileModesTable::parse(TABLE_RX580).unwrap();
assert_yaml_snapshot!(table);
}
#[test]
fn parse_full_internal_4800h() {
let table = PowerProfileModesTable::parse(TABLE_4800H).unwrap();
assert_yaml_snapshot!(table);
}
#[test]
fn parse_full_rx6900xt() {
let table = PowerProfileModesTable::parse(TABLE_RX6900XT).unwrap();
assert_yaml_snapshot!(table);
}
#[test]
fn parse_full_rx7600s() {
let table = PowerProfileModesTable::parse(TABLE_RX7600S).unwrap();
assert_yaml_snapshot!(table);
}
#[test]
fn parse_full_rx7700s() {
let table = PowerProfileModesTable::parse(TABLE_RX7700S).unwrap();
assert_yaml_snapshot!(table);
}
#[test]
fn parse_full_rx7800xt() {
let table = PowerProfileModesTable::parse(TABLE_RX7800XT).unwrap();
assert_yaml_snapshot!(table);
}
}