From 60d038c79f08d5410b284b42d9584a7a55469106 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Wed, 10 Jul 2024 20:58:37 -0400 Subject: [PATCH] Add amdgpu basics and Van Gogh clockspeed control --- crates/batbox/Cargo.toml | 2 +- crates/core/src/lib.rs | 2 +- crates/core/src/power_trait.rs | 11 +- crates/core/src/primitives/clockspeed.rs | 116 +++++ crates/core/src/primitives/mod.rs | 6 + .../src/primitives/space_separated_list.rs | 8 +- crates/core/src/primitives/value_map.rs | 184 ++++++++ crates/procbox/src/combo/amdgpu/common.rs | 6 + crates/procbox/src/combo/amdgpu/cpus.rs | 439 +++++++++++++++++ crates/procbox/src/combo/amdgpu/gpu.rs | 85 ++++ crates/procbox/src/combo/amdgpu/mod.rs | 79 ++++ .../power_dpm_force_performance_level.rs | 44 ++ .../src/combo/amdgpu/pp_od_clk_voltage.rs | 443 ++++++++++++++++++ crates/procbox/src/combo/mod.rs | 5 + 14 files changed, 1426 insertions(+), 4 deletions(-) create mode 100644 crates/core/src/primitives/clockspeed.rs create mode 100644 crates/core/src/primitives/value_map.rs create mode 100644 crates/procbox/src/combo/amdgpu/common.rs create mode 100644 crates/procbox/src/combo/amdgpu/cpus.rs create mode 100644 crates/procbox/src/combo/amdgpu/gpu.rs create mode 100644 crates/procbox/src/combo/amdgpu/mod.rs create mode 100644 crates/procbox/src/combo/amdgpu/power_dpm_force_performance_level.rs create mode 100644 crates/procbox/src/combo/amdgpu/pp_od_clk_voltage.rs create mode 100644 crates/procbox/src/combo/mod.rs diff --git a/crates/batbox/Cargo.toml b/crates/batbox/Cargo.toml index c92c503..3cb925a 100644 --- a/crates/batbox/Cargo.toml +++ b/crates/batbox/Cargo.toml @@ -7,4 +7,4 @@ description = "Power toolbox for power supply devices" [dependencies] powerbox = { version = "0.1", path = "../core" } -sysfuss = { version = "0.3", path = "../../../sysfs-nav" } +sysfuss = { version = "0.4", path = "../../../sysfs-nav" } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 0302120..18b08bd 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -5,7 +5,7 @@ mod power_error; pub use power_error::PowerError; mod power_trait; -pub use power_trait::{Power, PowerOp}; +pub use power_trait::{Power, PowerOp, RatifiedPower}; pub mod primitives; diff --git a/crates/core/src/power_trait.rs b/crates/core/src/power_trait.rs index 79eeb28..9ae0cc2 100644 --- a/crates/core/src/power_trait.rs +++ b/crates/core/src/power_trait.rs @@ -2,7 +2,7 @@ use super::PowerError; use super::primitives::Value; /// Power control interface for a device -pub trait Power { +pub trait Power { /// Is this device online? fn is_on(&self) -> bool; /// Is this device connected? @@ -19,3 +19,12 @@ pub trait PowerOp { /// This should ignore values of the operation. fn is_eq_op(&self, other: &Self) -> bool; } + +/// Power control device operation validation +pub trait RatifiedPower { + /// Is this operation within the rules of the device? + fn is_possible(&self, op: &OP) -> bool; + /// Set operation parameters to nearest allowed values. + /// Returns false if that is not possible. + fn clamp(&self, op: &mut OP) -> bool; +} diff --git a/crates/core/src/primitives/clockspeed.rs b/crates/core/src/primitives/clockspeed.rs new file mode 100644 index 0000000..df7b80b --- /dev/null +++ b/crates/core/src/primitives/clockspeed.rs @@ -0,0 +1,116 @@ +/// A clock speed +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub struct ClockFrequency { + /// The speed as parsed + pub value: usize, + /// The SI prefix for the speed value. + /// This is always true: value = 10^(3n) for some integer n + pub si_prefix: usize, +} + +impl ClockFrequency { + /// The clock speed in Hz + pub fn in_hz(&self) -> usize { + self.value * self.si_prefix + } +} + +/// Clock speed parse errors +#[derive(Debug)] +pub enum ClockFrequencyParseErr { + /// Integer parse error + Int(core::num::ParseIntError), + /// Bad unit prefix + UnknownSiPrefix(char), + /// Missing unit + MissingHertz, +} + +impl core::fmt::Display for ClockFrequencyParseErr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Int(i_e) => write!(f, "integer error: {}", i_e), + Self::UnknownSiPrefix(c) => write!(f, "unknown SI unit prefix {}", c), + Self::MissingHertz => write!(f, "missing ending hz"), + } + } +} + +impl std::error::Error for ClockFrequencyParseErr {} + +impl std::str::FromStr for ClockFrequency { + type Err = ClockFrequencyParseErr; + + fn from_str(s: &str) -> Result { + println!("clockspeed in str: '{}'", s); + let mut chars: Vec<_> = s.chars().collect(); + if let Some(last_char) = chars.pop() { + if last_char != 'z' { + return Err(ClockFrequencyParseErr::MissingHertz); + } + } else { + return Err(ClockFrequencyParseErr::MissingHertz); + } + if let Some(second_last_char) = chars.pop() { + if !(second_last_char == 'H' || second_last_char == 'h') { + return Err(ClockFrequencyParseErr::MissingHertz); + } + } else { + return Err(ClockFrequencyParseErr::MissingHertz); + } + let prefix_char = chars.pop(); + let si_prefix = if let Some(prefix_char) = prefix_char { + if prefix_char.is_ascii_digit() { + chars.push(prefix_char); + 1 + } else { + if let Some(prefix) = si_prefix_char_to_number(prefix_char) { + prefix + } else { + return Err(ClockFrequencyParseErr::UnknownSiPrefix(prefix_char)); + } + } + } else { + 1 + }; + let int_str: String = chars.into_iter().collect(); + let value = usize::from_str(&int_str).map_err(ClockFrequencyParseErr::Int)?; + Ok(Self { + value, + si_prefix, + }) + } +} + +fn si_prefix_char_to_number(si_prefix: char) -> Option { + match si_prefix { + 'G' => Some(1_000_000_000), + 'M' => Some(1_000_000), + 'K' => Some(1_000), + _ => None + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use super::*; + + #[test] + fn clockspeed_parse_valid() { + let val = ClockFrequency::from_str("2468Mhz").expect("parse should succeed"); + assert_eq!(val.value, 2468); + assert_eq!(val.si_prefix, 1_000_000); + } + + #[test] + fn clockspeed_parse_subtly_bad() { + let err = if let Err(e) = ClockFrequency::from_str("2468 Mhz") { + e + } else { + panic!("parse should fail"); + }; + assert!(matches!(err, ClockFrequencyParseErr::Int(_))); + } +} diff --git a/crates/core/src/primitives/mod.rs b/crates/core/src/primitives/mod.rs index 6ed6da5..8a6c0ed 100644 --- a/crates/core/src/primitives/mod.rs +++ b/crates/core/src/primitives/mod.rs @@ -3,6 +3,12 @@ mod boolean_number; pub use boolean_number::BoolNum; +mod clockspeed; +pub use clockspeed::{ClockFrequency, ClockFrequencyParseErr}; + +mod value_map; +pub use value_map::{ValueMap, Selectable, ValueMapParseErr}; + mod range; pub use range::{Range, RangeList, RangeListParseErr, RangeListItem, RangeListIter}; diff --git a/crates/core/src/primitives/space_separated_list.rs b/crates/core/src/primitives/space_separated_list.rs index 956eeba..a6cc228 100644 --- a/crates/core/src/primitives/space_separated_list.rs +++ b/crates/core/src/primitives/space_separated_list.rs @@ -8,7 +8,7 @@ impl FromStr for SpacedList { fn from_str(s: &str) -> Result { let mut results = Vec::new(); - for chars in s.split(' ') { + for chars in s.split(|c: char| c == ' ' || c == '\t') { if !chars.is_empty() { results.push(T::from_str(chars)?); } @@ -32,4 +32,10 @@ mod test { let strings: SpacedList = "1 2 3 4 5 6 7 8 9 10 11".parse().expect("fail"); assert_eq!(strings.0, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); } + + #[test] + fn parse_list_with_tabs_num() { + let strings: SpacedList = "1 2 3 4\t5\t\t \t6 7 8 9 10 11".parse().expect("fail"); + assert_eq!(strings.0, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); + } } diff --git a/crates/core/src/primitives/value_map.rs b/crates/core/src/primitives/value_map.rs new file mode 100644 index 0000000..e556dca --- /dev/null +++ b/crates/core/src/primitives/value_map.rs @@ -0,0 +1,184 @@ +use std::collections::HashMap; + +/// A mapping of states (integers) to values. +pub struct ValueMap (pub HashMap); + +/// ValueMap parse errors +#[derive(Debug)] +pub enum ValueMapParseErr { + /// Inner value type parse error + InnerVal(VE), + /// Inner key type parse error + InnerKey(KE), + /// Unrecognized char encountered at position + UnexpectedChar(char, usize), +} + +impl core::fmt::Display for ValueMapParseErr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InnerVal(inner) => write!(f, "inner value parse error: {}", inner), + Self::InnerKey(inner) => write!(f, "inner key parse error: {}", inner), + Self::UnexpectedChar(c, pos) => write!(f, "unexpected {} as index {}", c, pos), + } + } +} + +impl std::error::Error for ValueMapParseErr {} + +impl std::str::FromStr for ValueMap { + type Err = ValueMapParseErr<::Err, ::Err>; + + fn from_str(s: &str) -> Result { + let mut line_start = 0; + let mut last_char_boundary = 0; + let mut value_start = 0; + let mut state_key = None; + let mut mappings = HashMap::new(); + for i in 1..s.len()-1 { + if s.is_char_boundary(i) { + let last_char = &s[last_char_boundary..i]; + match last_char { + ":" => { + state_key = Some(K::from_str(&s[line_start..last_char_boundary]).map_err(ValueMapParseErr::InnerKey)?); + value_start = i; + }, + "\n" => { + if let Some(state) = state_key.take() { + let value_str = &s[value_start..last_char_boundary]; + let value = V::from_str(value_str).map_err(ValueMapParseErr::InnerVal)?; + mappings.insert(state, value); + } else { + return Err(ValueMapParseErr::UnexpectedChar('\n', i)); + } + line_start = i; + }, + "\t" | " " => if value_start == last_char_boundary { value_start = i }, + _ => {}, + } + last_char_boundary = i; + } + } + // final state parse + if let Some(state) = state_key.take() { + let value_str = &s[value_start..s.len()]; + let value = V::from_str(value_str).map_err(ValueMapParseErr::InnerVal)?; + mappings.insert(state, value); + } else { + return Err(ValueMapParseErr::UnexpectedChar('\0', s.len())); + } + Ok(Self(mappings)) + } +} + +/// An item possibly with * appended to it, usually used to indicate the item is active +#[derive(PartialEq, Eq, Debug)] +pub struct Selectable { + /// The actual item + pub item: T, + /// Was there a * after it? + pub selected: bool, +} + +impl std::str::FromStr for Selectable { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + let star_check_start = s.len() - 1; + let star_check_end = s.len(); + let is_starred; + let value_str = if s.is_char_boundary(star_check_start) + && s.is_char_boundary(star_check_end) + && &s[star_check_start..star_check_end] == "*" + { + let mut value_end = star_check_start; + for i in (0..star_check_start).rev() { + if s.is_char_boundary(i) { + if &s[i..value_end] == " " { + value_end = i; + } else { + break; + } + } + } + is_starred = true; + &s[0..value_end] + } else { + is_starred = false; + &s[0..s.len()] + }; + let value = T::from_str(value_str)?; + Ok(Self { + item: value, + selected: is_starred, + }) + } +} + +impl ValueMap> { + /// Get first selected entry in the map + pub fn selected(&self) -> Option<&'_ K> { + self.0.iter().find(|x| x.1.selected).map(|x| x.0) + } + + /// Get all entries which are marked as selected in the map + pub fn many_selected(&self) -> impl core::iter::Iterator + '_ { + self.0.iter().filter(|x| x.1.selected).map(|x| x.0) + } +} + +impl ValueMap> { + /// Assert that the map only has one selected entry + pub fn fail_on_many_selected(self) -> Result::Err> { + if self.many_selected().count() > 1 { + Err(::Err::UnexpectedChar('*', 0)) + } else { + Ok(self) + } + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use super::*; + + #[test] + fn parse_map_unselected() { + let int_map = ValueMap::::from_str(r#"0: 800 +1: 1100 +2: 2700"#).expect("parse should succeed"); + assert_eq!(int_map.0.len(), 3); + } + + #[test] + fn parse_map_good_selected() { + let int_map = ValueMap::>::from_str(r#"0: 800 +1: 1100 * +2: 2700"#).expect("parse should succeed"); + assert_eq!(int_map.0.len(), 3); + assert_eq!(int_map.selected(), Some(&1)); + } + + #[test] + fn parse_map_good_selected_lots_of_spaces() { + let int_map = ValueMap::>::from_str(r#"0: 800 +1: 1100 * +2: 2700"#).expect("parse should succeed"); + assert_eq!(int_map.0.len(), 3); + assert_eq!(int_map.selected(), Some(&1)); + } + + #[test] + fn parse_map_good_too_many_selected() { + let e = if let Err(e) = ValueMap::>::from_str(r#"0: 800 +1: 1100 * +2: 2700 *"#).and_then(ValueMap::fail_on_many_selected) { + e + } else { + panic!("parse should fail"); + }; + assert!(matches!(e, ValueMapParseErr::UnexpectedChar('*', _))); + } +} diff --git a/crates/procbox/src/combo/amdgpu/common.rs b/crates/procbox/src/combo/amdgpu/common.rs new file mode 100644 index 0000000..91df31b --- /dev/null +++ b/crates/procbox/src/combo/amdgpu/common.rs @@ -0,0 +1,6 @@ +use sysfuss::{BasicEntityPath, SysEntityAttributesExt}; + +pub fn write_confirm_pp_od_clk_voltage(sysfs: &BasicEntityPath) -> Result<(), powerbox::PowerError> { + sysfs.set(super::DEVICE_PP_OD_CLK_VOLTAGE, super::pp_od_clk_voltage::PpOdClkVoltageWriteValues::C.sysfs_str()) + .map_err(powerbox::PowerError::Io) +} diff --git a/crates/procbox/src/combo/amdgpu/cpus.rs b/crates/procbox/src/combo/amdgpu/cpus.rs new file mode 100644 index 0000000..d75cf63 --- /dev/null +++ b/crates/procbox/src/combo/amdgpu/cpus.rs @@ -0,0 +1,439 @@ +//! CPU power management implementation + +use powerbox::{Power, PowerOp, RatifiedPower}; +use powerbox::primitives::ClockFrequency; +use sysfuss::SysEntityAttributesExt; + +use crate::cpu::{CpuPower, CpuPowerOp}; +use super::AmdGpu; + +pub enum AmdGpuCpuOp { + Freq(AmdGpuCpuFreq), + Commit(super::AmdGpuCommit), +} + +impl PowerOp for AmdGpuCpuOp { + fn is_eq_op(&self, other: &Self) -> bool { + match self { + Self::Freq(f) => if let Self::Freq(other) = other { f.is_eq_op(other) } else { false }, + Self::Commit(c) => if let Self::Commit(other) = other { c.is_eq_op(other) } else { false }, + } + } +} + +impl CpuPowerOp for AmdGpuCpuOp {} + +impl Power for AmdGpu { + fn is_on(&self) -> bool { + self.is_enabled() + } + + fn is_available(&self) -> bool { + self.is_compatible() + } + + fn supported_operations(&self) -> Box> { + Box::new( + Power::::supported_operations(self).map(core::convert::Into::::into) + .chain(Power::::supported_operations(self).map(core::convert::Into::::into)) + ) + } + + fn act(&self, op: AmdGpuCpuOp) -> Result { + match op { + AmdGpuCpuOp::Freq(freq) => self.act(freq).map(powerbox::primitives::Value::into_any), + AmdGpuCpuOp::Commit(c) => self.act(c).map(|_| powerbox::primitives::Value::Unknown), + } + } +} + +impl CpuPower for AmdGpu {} + +impl CpuPowerOp for super::AmdGpuCommit {} +impl CpuPower for AmdGpu {} + +impl Into for super::AmdGpuCommit { + fn into(self) -> AmdGpuCpuOp { + AmdGpuCpuOp::Commit(self) + } +} + +pub struct GetCoreMinFrequency { + pub core: usize, +} + +impl PowerOp for GetCoreMinFrequency { + fn is_eq_op(&self, _other: &Self) -> bool { + true + } +} + +impl CpuPowerOp for GetCoreMinFrequency {} + +impl Power for AmdGpu { + fn is_on(&self) -> bool { + self.is_enabled() + } + + fn is_available(&self) -> bool { + self.is_compatible() + } + + fn supported_operations(&self) -> Box> { + if let Ok(pp_od_clk_info) = self.read_pp_od_clk_voltage() { + let keys: Vec<_> = pp_od_clk_info.tables.keys().map(|k| k.to_owned()).collect(); + Box::new(keys.into_iter().filter_map(|key| { + if let super::pp_od_clk_voltage::OdTableName::CCLK_RANGE_Core(core) = key { + Some(GetCoreMinFrequency{ core }) + } else { + None + } + })) + } else { + Box::new(core::iter::empty()) + } + } + + fn act(&self, op: GetCoreMinFrequency) -> Result { + let mut pp_od_clk_info = self.read_pp_od_clk_voltage().map_err(|e| match e { + sysfuss::EitherErr2::First(io) => powerbox::PowerError::Io(io), + sysfuss::EitherErr2::Second(_parse) => powerbox::PowerError::Unknown, + })?; + if let Some(table) = pp_od_clk_info.tables.remove(&super::pp_od_clk_voltage::OdTableName::CCLK_RANGE_Core(op.core)) { + if let Some(min_clock) = table.0.into_values().min_by_key(|val| val.item.in_hz()) { + return Ok(min_clock.item); + } + } + Err(powerbox::PowerError::InvalidInput) + } +} + +impl CpuPower for AmdGpu {} + +impl Into for GetCoreMinFrequency { + fn into(self) -> AmdGpuCpuFreq { + AmdGpuCpuFreq::GetMin(self) + } +} + +pub struct GetCoreMaxFrequency { + pub core: usize, +} + +impl PowerOp for GetCoreMaxFrequency { + fn is_eq_op(&self, _other: &Self) -> bool { + true + } +} + +impl CpuPowerOp for GetCoreMaxFrequency {} + +impl Power for AmdGpu { + fn is_on(&self) -> bool { + self.is_enabled() + } + + fn is_available(&self) -> bool { + self.is_compatible() + } + + fn supported_operations(&self) -> Box> { + if let Ok(pp_od_clk_info) = self.read_pp_od_clk_voltage() { + let keys: Vec<_> = pp_od_clk_info.tables.keys().map(|k| k.to_owned()).collect(); + Box::new(keys.into_iter().filter_map(|key| { + if let super::pp_od_clk_voltage::OdTableName::CCLK_RANGE_Core(core) = key { + Some(GetCoreMaxFrequency{ core }) + } else { + None + } + })) + } else { + Box::new(core::iter::empty()) + } + } + + fn act(&self, op: GetCoreMaxFrequency) -> Result { + let mut pp_od_clk_info = self.read_pp_od_clk_voltage().map_err(|e| match e { + sysfuss::EitherErr2::First(io) => powerbox::PowerError::Io(io), + sysfuss::EitherErr2::Second(_parse) => powerbox::PowerError::Unknown, + })?; + if let Some(table) = pp_od_clk_info.tables.remove(&super::pp_od_clk_voltage::OdTableName::CCLK_RANGE_Core(op.core)) { + if let Some(max_clock) = table.0.into_values().max_by_key(|val| val.item.in_hz()) { + return Ok(max_clock.item); + } + } + Err(powerbox::PowerError::InvalidInput) + } +} + +impl CpuPower for AmdGpu {} + +impl Into for GetCoreMaxFrequency { + fn into(self) -> AmdGpuCpuFreq { + AmdGpuCpuFreq::GetMax(self) + } +} + +pub struct SetCoreMinFrequency { + pub core: usize, + pub clock: ClockFrequency, + pub no_commit: bool, +} + +impl PowerOp for SetCoreMinFrequency { + fn is_eq_op(&self, _other: &Self) -> bool { + true + } +} + +impl CpuPowerOp for SetCoreMinFrequency {} + +impl Power for AmdGpu { + fn is_on(&self) -> bool { + self.is_enabled() + } + + fn is_available(&self) -> bool { + self.is_compatible() + } + + fn supported_operations(&self) -> Box> { + if let Ok(pp_od_clk_info) = self.read_pp_od_clk_voltage() { + let keys: Vec<_> = pp_od_clk_info.tables.keys().map(|k| k.to_owned()).collect(); + Box::new(keys.into_iter().filter_map(|key| { + if let super::pp_od_clk_voltage::OdTableName::CCLK_RANGE_Core(core) = key { + Some(SetCoreMinFrequency{ core, clock: ClockFrequency { value: 1000, si_prefix: 1_000_000 }, no_commit: false }) + } else { + None + } + })) + } else { + Box::new(core::iter::empty()) + } + } + + fn act(&self, op: SetCoreMinFrequency) -> Result<(), powerbox::PowerError> { + let payload = super::pp_od_clk_voltage::PpOdClkVoltageWriteValues::CCLK { + core: op.core, + limit: super::pp_od_clk_voltage::MinMax::Min, + clockspeed: op.clock, + }; + if !self.is_pdfpl_manual() { + self.sysfs.set( + super::DEVICE_POWER_DPM_FORCE_LIMITS_ATTRIBUTE, + super::power_dpm_force_performance_level::PowerDpmForcePerformanceLevel::Manual.sysfs_str()) + .map_err(powerbox::PowerError::Io)?; + } + self.sysfs.set(super::DEVICE_PP_OD_CLK_VOLTAGE, payload.sysfs_str()) + .map_err(powerbox::PowerError::Io)?; + if !op.no_commit { + super::common::write_confirm_pp_od_clk_voltage(&self.sysfs) + } else { + Ok(()) + } + } +} + +impl CpuPower for AmdGpu {} + +fn get_clocks_range(pp_od_clk_info: &super::pp_od_clk_voltage::PpOdClkVoltageReadValues) -> Option<(ClockFrequency, ClockFrequency)> { + if let Some(ranges) = &pp_od_clk_info.ranges { + if let Some(range) = ranges.0.get(&super::pp_od_clk_voltage::OdTableName::CCLK) { + Some((range.0[0], range.0[1])) + } else { + None + } + } else { + None + } +} + +impl RatifiedPower for AmdGpu { + fn is_possible(&self, op: &SetCoreMinFrequency) -> bool { + if let Ok(pp_od_clk_info) = self.read_pp_od_clk_voltage() { + let is_valid_core = pp_od_clk_info.tables.get(&super::pp_od_clk_voltage::OdTableName::CCLK_RANGE_Core(op.core)).is_some(); + if let Some((min, max)) = get_clocks_range(&pp_od_clk_info) { + let op_clock = op.clock.in_hz(); + is_valid_core && op_clock >= min.in_hz() && op_clock <= max.in_hz() + } else { + false + } + } else { + false + } + } + + fn clamp(&self, op: &mut SetCoreMinFrequency) -> bool { + if let Ok(pp_od_clk_info) = self.read_pp_od_clk_voltage() { + if let Some((min, max)) = get_clocks_range(&pp_od_clk_info) { + let raw_clock = op.clock.in_hz().clamp(min.in_hz(), max.in_hz()); + op.clock.value = raw_clock / op.clock.si_prefix; + true + } else { + false + } + } else { + false + } + } +} + +impl Into for SetCoreMinFrequency { + fn into(self) -> AmdGpuCpuFreq { + AmdGpuCpuFreq::SetMin(self) + } +} + +pub struct SetCoreMaxFrequency { + pub core: usize, + pub clock: ClockFrequency, + pub no_commit: bool, +} + +impl PowerOp for SetCoreMaxFrequency { + fn is_eq_op(&self, _other: &Self) -> bool { + true + } +} + +impl CpuPowerOp for SetCoreMaxFrequency {} + +impl Power for AmdGpu { + fn is_on(&self) -> bool { + self.is_enabled() + } + + fn is_available(&self) -> bool { + self.is_compatible() + } + + fn supported_operations(&self) -> Box> { + if let Ok(pp_od_clk_info) = self.read_pp_od_clk_voltage() { + let keys: Vec<_> = pp_od_clk_info.tables.keys().map(|k| k.to_owned()).collect(); + Box::new(keys.into_iter().filter_map(|key| { + if let super::pp_od_clk_voltage::OdTableName::CCLK_RANGE_Core(core) = key { + Some(SetCoreMaxFrequency{ core, clock: ClockFrequency { value: 1000, si_prefix: 1_000_000 }, no_commit: false }) + } else { + None + } + })) + } else { + Box::new(core::iter::empty()) + } + } + + fn act(&self, op: SetCoreMaxFrequency) -> Result<(), powerbox::PowerError> { + let payload = super::pp_od_clk_voltage::PpOdClkVoltageWriteValues::CCLK { + core: op.core, + limit: super::pp_od_clk_voltage::MinMax::Max, + clockspeed: op.clock, + }; + if !self.is_pdfpl_manual() { + self.sysfs.set( + super::DEVICE_POWER_DPM_FORCE_LIMITS_ATTRIBUTE, + super::power_dpm_force_performance_level::PowerDpmForcePerformanceLevel::Manual.sysfs_str()) + .map_err(powerbox::PowerError::Io)?; + } + self.sysfs.set(super::DEVICE_PP_OD_CLK_VOLTAGE, payload.sysfs_str()) + .map_err(powerbox::PowerError::Io)?; + if !op.no_commit { + super::common::write_confirm_pp_od_clk_voltage(&self.sysfs) + } else { + Ok(()) + } + } +} + +impl CpuPower for AmdGpu {} + +impl RatifiedPower for AmdGpu { + fn is_possible(&self, op: &SetCoreMaxFrequency) -> bool { + if let Ok(pp_od_clk_info) = self.read_pp_od_clk_voltage() { + let is_valid_core = pp_od_clk_info.tables.get(&super::pp_od_clk_voltage::OdTableName::CCLK_RANGE_Core(op.core)).is_some(); + if let Some((min, max)) = get_clocks_range(&pp_od_clk_info) { + let op_clock = op.clock.in_hz(); + is_valid_core && op_clock >= min.in_hz() && op_clock <= max.in_hz() + } else { + false + } + } else { + false + } + } + + fn clamp(&self, op: &mut SetCoreMaxFrequency) -> bool { + if let Ok(pp_od_clk_info) = self.read_pp_od_clk_voltage() { + if let Some((min, max)) = get_clocks_range(&pp_od_clk_info) { + let raw_clock = op.clock.in_hz().clamp(min.in_hz(), max.in_hz()); + op.clock.value = raw_clock / op.clock.si_prefix; + true + } else { + false + } + } else { + false + } + } +} + +impl Into for SetCoreMaxFrequency { + fn into(self) -> AmdGpuCpuFreq { + AmdGpuCpuFreq::SetMax(self) + } +} + +pub enum AmdGpuCpuFreq { + GetMin(GetCoreMinFrequency), + GetMax(GetCoreMaxFrequency), + SetMin(SetCoreMinFrequency), + SetMax(SetCoreMaxFrequency), +} + +impl PowerOp for AmdGpuCpuFreq { + fn is_eq_op(&self, other: &Self) -> bool { + match self { + Self::GetMin(_) => matches!(other, Self::GetMin(_)), + Self::GetMax(_) => matches!(other, Self::GetMax(_)), + Self::SetMin(_) => matches!(other, Self::SetMin(_)), + Self::SetMax(_) => matches!(other, Self::SetMax(_)), + } + } +} + +impl CpuPowerOp for AmdGpuCpuFreq {} + +impl Power> for AmdGpu { + fn is_on(&self) -> bool { + self.is_enabled() + } + + fn is_available(&self) -> bool { + self.is_compatible() + } + + fn supported_operations(&self) -> Box> { + Box::new( + Power::::supported_operations(self).map(core::convert::Into::::into) + .chain(Power::::supported_operations(self).map(core::convert::Into::::into)) + .chain(Power::::supported_operations(self).map(core::convert::Into::::into)) + .chain(Power::::supported_operations(self).map(core::convert::Into::::into)) + ) + } + + fn act(&self, op: AmdGpuCpuFreq) -> Result, powerbox::PowerError> { + match op { + AmdGpuCpuFreq::GetMin(x) => self.act(x).map(powerbox::primitives::Value::Custom), + AmdGpuCpuFreq::GetMax(x) => self.act(x).map(powerbox::primitives::Value::Custom), + AmdGpuCpuFreq::SetMin(x) => self.act(x).map(|_| powerbox::primitives::Value::Unknown), + AmdGpuCpuFreq::SetMax(x) => self.act(x).map(|_| powerbox::primitives::Value::Unknown), + } + } +} + +impl CpuPower> for AmdGpu {} + +impl Into for AmdGpuCpuFreq { + fn into(self) -> AmdGpuCpuOp { + AmdGpuCpuOp::Freq(self) + } +} diff --git a/crates/procbox/src/combo/amdgpu/gpu.rs b/crates/procbox/src/combo/amdgpu/gpu.rs new file mode 100644 index 0000000..126756c --- /dev/null +++ b/crates/procbox/src/combo/amdgpu/gpu.rs @@ -0,0 +1,85 @@ +//! CPU power management implementation + +use powerbox::{Power, PowerOp}; +use sysfuss::SysEntityAttributesExt; + +use crate::gpu::{GpuPower, GpuPowerOp}; +use super::AmdGpu; + +pub enum AmdGpuOp { + Commit(AmdGpuCommit), +} + +impl PowerOp for AmdGpuOp { + fn is_eq_op(&self, other: &Self) -> bool { + match self { + Self::Commit(_) => matches!(other, Self::Commit(_)), + } + } +} + +impl GpuPowerOp for AmdGpuOp {} + +impl Power for AmdGpu { + fn is_on(&self) -> bool { + self.is_enabled() + } + + fn is_available(&self) -> bool { + self.is_compatible() + } + + fn supported_operations(&self) -> Box> { + // TODO + Box::new(core::iter::empty()) + } + + fn act(&self, op: AmdGpuOp) -> Result { + match op { + _ => Err(powerbox::PowerError::Unknown), + } + } +} + +impl GpuPower for AmdGpu {} + +pub struct AmdGpuCommit; + +impl PowerOp for AmdGpuCommit { + fn is_eq_op(&self, _other: &Self) -> bool { + true + } +} + +impl GpuPowerOp for AmdGpuCommit {} + +impl Power for AmdGpu { + fn is_on(&self) -> bool { + self.is_enabled() + } + + fn is_available(&self) -> bool { + self.is_compatible() + } + + fn supported_operations(&self) -> Box> { + if self.sysfs.exists(&super::DEVICE_PP_OD_CLK_VOLTAGE) { + Box::new(core::iter::once(AmdGpuCommit)) + } else { + Box::new(core::iter::empty()) + } + } + + fn act(&self, _: AmdGpuCommit) -> Result<(), powerbox::PowerError> { + super::common::write_confirm_pp_od_clk_voltage(&self.sysfs) + } +} + +impl GpuPower for AmdGpu {} + +impl Into for AmdGpuCommit { + fn into(self) -> AmdGpuOp { + AmdGpuOp::Commit(self) + } +} + diff --git a/crates/procbox/src/combo/amdgpu/mod.rs b/crates/procbox/src/combo/amdgpu/mod.rs new file mode 100644 index 0000000..c929d3b --- /dev/null +++ b/crates/procbox/src/combo/amdgpu/mod.rs @@ -0,0 +1,79 @@ +//! The amdgpu driver can be used to control AMD's APUs (including the CPU part of them!). +//! +//! Based on https://www.kernel.org/doc/html/latest/gpu/amdgpu/thermal.html + +mod common; +mod cpus; +mod gpu; +mod power_dpm_force_performance_level; +mod pp_od_clk_voltage; + +pub use cpus::{AmdGpuCpuOp, AmdGpuCpuFreq, GetCoreMinFrequency, GetCoreMaxFrequency, SetCoreMinFrequency, SetCoreMaxFrequency}; +pub use gpu::{AmdGpuOp, AmdGpuCommit}; +//pub use pp_od_clk_voltage::MinMax; + +use sysfuss::{BasicEntityPath, SysEntityAttributesExt}; + +//const DEVICE_FOLDER: &str = "device/"; +const DEVICE_ENABLE: &str = "device/enable"; +const DEVICE_POWER_DPM_FORCE_LIMITS_ATTRIBUTE: &str = "device/power_dpm_force_performance_level"; +const DEVICE_PP_OD_CLK_VOLTAGE: &str = "device/pp_od_clk_voltage"; + +/// amdgpu device +pub struct AmdGpu { + /// amdgpu sysfs entity + pub(super) sysfs: BasicEntityPath, +} + +impl AmdGpu { + /// Instantiate the power control interface. + /// + /// NOTE: This does not verify that `sys_path` is actually compatible. + /// Use try_into() for that instead. + pub fn new(sys_path: impl AsRef) -> Self { + let sysfs = BasicEntityPath::new(sys_path); + Self { + sysfs, + } + } + + pub(super) fn is_enabled(&self) -> bool { + self.sysfs.attribute::(DEVICE_ENABLE).is_ok_and(|val: String| val == "1") + } + + pub(super) fn is_compatible(&self) -> bool { + self.sysfs.exists(&DEVICE_POWER_DPM_FORCE_LIMITS_ATTRIBUTE) + } + + pub(super) fn is_pdfpl_manual(&self) -> bool { + self.sysfs.attribute::(DEVICE_POWER_DPM_FORCE_LIMITS_ATTRIBUTE).is_ok_and(|val| val == power_dpm_force_performance_level::PowerDpmForcePerformanceLevel::Manual) + } + + pub(super) fn read_pp_od_clk_voltage(&self) -> Result::Err>> { + let initial_pdfpl_manual = + self.sysfs.attribute::(DEVICE_POWER_DPM_FORCE_LIMITS_ATTRIBUTE) + .map_err(|e| match e { + sysfuss::EitherErr2::First(io) => sysfuss::EitherErr2::First(io), + sysfuss::EitherErr2::Second(_) => sysfuss::EitherErr2::First( + std::io::Error::new( + std::io::ErrorKind::Unsupported, + Box::::from("Invalid performance level mode") + ) + ) + })?; + if initial_pdfpl_manual != power_dpm_force_performance_level::PowerDpmForcePerformanceLevel::Manual { + self.sysfs.set( + DEVICE_POWER_DPM_FORCE_LIMITS_ATTRIBUTE, + power_dpm_force_performance_level::PowerDpmForcePerformanceLevel::Manual.sysfs_str() + ).map_err(|e| sysfuss::EitherErr2::First(e))?; + } + let result = self.sysfs.attribute(DEVICE_PP_OD_CLK_VOLTAGE); + if initial_pdfpl_manual != power_dpm_force_performance_level::PowerDpmForcePerformanceLevel::Manual { + self.sysfs.set( + DEVICE_POWER_DPM_FORCE_LIMITS_ATTRIBUTE, + power_dpm_force_performance_level::PowerDpmForcePerformanceLevel::Manual.sysfs_str() + ).map_err(|e| sysfuss::EitherErr2::First(e))?; + } + result + } +} diff --git a/crates/procbox/src/combo/amdgpu/power_dpm_force_performance_level.rs b/crates/procbox/src/combo/amdgpu/power_dpm_force_performance_level.rs new file mode 100644 index 0000000..646b98c --- /dev/null +++ b/crates/procbox/src/combo/amdgpu/power_dpm_force_performance_level.rs @@ -0,0 +1,44 @@ +#[derive(PartialEq, Eq, Debug)] +pub enum PowerDpmForcePerformanceLevel { + Auto, + Low, + High, + Manual, + ProfileStandard, + ProfileMinSclk, + ProfileMinMclk, + ProfilePeak, +} + +impl PowerDpmForcePerformanceLevel { + pub const fn sysfs_str(&self) -> &'static str { + match self { + Self::Auto => "auto", + Self::Low => "low", + Self::High => "high", + Self::Manual => "manual", + Self::ProfileStandard => "profile_standard", + Self::ProfileMinSclk => "profile_min_sclk", + Self::ProfileMinMclk => "profile_min_mclk", + Self::ProfilePeak => "profile_peak", + } + } +} + +impl std::str::FromStr for PowerDpmForcePerformanceLevel { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(match s { + "auto" => Self::Auto, + "low" => Self::Low, + "high" => Self::High, + "manual" => Self::Manual, + "profile_standard" => Self::ProfileStandard, + "profile_min_sclk" => Self::ProfileMinSclk, + "profile_min_mclk" => Self::ProfileMinMclk, + "profile_peak" => Self::ProfilePeak, + _ => return Err(()), + }) + } +} diff --git a/crates/procbox/src/combo/amdgpu/pp_od_clk_voltage.rs b/crates/procbox/src/combo/amdgpu/pp_od_clk_voltage.rs new file mode 100644 index 0000000..ebfe080 --- /dev/null +++ b/crates/procbox/src/combo/amdgpu/pp_od_clk_voltage.rs @@ -0,0 +1,443 @@ +use std::{borrow::Cow, collections::HashMap}; + +use powerbox::primitives::{ValueMap, ClockFrequency, Selectable, SpacedList, ValueMapParseErr}; + +#[allow(non_camel_case_types)] +#[derive(PartialEq, Eq, Hash, Clone)] +pub enum OdTableName { + SCLK, + MCLK, + CCLK, + VDDC, + Custom { + od_table_name: String, + od_range_entry: String, + }, + RANGE, + CCLK_RANGE_Core(usize), +} + +impl OdTableName { + fn from_table_name(name: &str) -> Self { + match name { + "OD_SCLK" => OdTableName::SCLK, + "OD_MCLK" => OdTableName::MCLK, + "OD_CCLK" => OdTableName::CCLK, + "OD_VDDC_CURVE" => OdTableName::VDDC, + "OD_RANGE" => OdTableName::RANGE, + cclk_core if cclk_core.starts_with("CCLK_RANGE") => { + if let Ok(x) = cclk_core.replacen("CCLK_RANGE in Core", "", 1).parse() { + OdTableName::CCLK_RANGE_Core(x) + } else { + OdTableName::Custom { + od_table_name: cclk_core.to_owned(), + od_range_entry: cclk_core.replacen("OD_", "", 1).to_owned(), + } + } + } + n => OdTableName::Custom { + od_table_name: n.to_owned(), + od_range_entry: n.replacen("OD_", "", 1).to_owned(), + } + } + } + + fn from_range_entry(key: &str) -> Self { + match key { + "SCLK" => OdTableName::SCLK, + "MCLK" => OdTableName::MCLK, + "CCLK" => OdTableName::CCLK, + "VDDC_CURVE" => OdTableName::VDDC, + k => OdTableName::Custom { + od_table_name: format!("OD_{}", k), + od_range_entry: k.to_owned(), + } + } + } + + fn table_name(&self) -> Cow<'_, str> { + match self { + Self::SCLK => Cow::from("OD_SCLK"), + Self::MCLK => Cow::from("OD_MCLK"), + Self::CCLK => Cow::from("OD_CCLK"), + Self::VDDC => Cow::from("OD_VDDC_CURVE"), + Self::Custom { od_table_name, .. } => Cow::from(od_table_name), + Self::RANGE => Cow::from("OD_RANGE"), + Self::CCLK_RANGE_Core(core) => Cow::from(format!("CCLK_RANGE in Core{}", core)), + } + } + + #[allow(dead_code)] + fn range_name(&self) -> &'_ str { + match self { + Self::SCLK => "SCLK", + Self::MCLK => "MCLK", + Self::CCLK => "CCLK", + Self::VDDC => "VDDC_CURVE", + Self::Custom { od_range_entry, .. } => od_range_entry, + Self::RANGE => panic!("cannot get range entry name of range table"), + Self::CCLK_RANGE_Core(_) => "CCLK", // TODO is this correct ??? + } + } +} + +impl std::str::FromStr for OdTableName { + type Err = std::convert::Infallible; + + fn from_str(s: &str) -> Result { + Ok(Self::from_range_entry(s)) + } +} + +pub struct PpOdClkVoltageReadValues { + pub tables: HashMap>>, + pub ranges: Option>>, +} + +impl std::str::FromStr for PpOdClkVoltageReadValues { + type Err = as std::str::FromStr>::Err; + + fn from_str(s: &str) -> Result { + let mut table_start = 0; + let mut lines_iter = s.split('\n'); + let mut tables = HashMap::new(); + let mut range_table = None; + let mut table_name_line = if let Some(table_name_line) = lines_iter.next() { + table_name_line + } else { + return Ok(Self { + tables, + ranges: range_table, + }) + }; + loop { + table_start += table_name_line.len() + 1; + let mut table_end = table_start; + let table_name = table_name_line.trim_end().trim_end_matches(|c: char| c == ':'); + let table_name = OdTableName::from_table_name(table_name); + println!("table name: {}", table_name.table_name()); + let next_table_name_line; + if let OdTableName::RANGE = table_name { + println!("range table special case!"); + 'range_table_loop: loop { + let line = lines_iter.next(); + if let Some(line) = line.and_then(|line| is_valid_range_table_entry_line(line).then_some(line)) { + table_end += line.len() + 1; + } else { + next_table_name_line = line; + break 'range_table_loop; + } + } + + let table_str = &s[table_start..table_end-1]; + println!("table str: '{}'", table_str); + match table_str.parse() { + Ok(r_t) => range_table = Some(r_t), + Err(ValueMapParseErr::InnerVal(v)) => return Err(ValueMapParseErr::InnerVal(v)), + Err(ValueMapParseErr::InnerKey(_)) => panic!("infallible error encountered"), + Err(ValueMapParseErr::UnexpectedChar(x, y)) => return Err(ValueMapParseErr::UnexpectedChar(x, y)), + } + table_start = table_end; + } else { + 'regular_table_loop: loop { + let line = lines_iter.next(); + if let Some(line) = line.and_then(|line| line.chars().next().is_some_and(|c| c.is_ascii_digit()).then_some(line)) { + table_end += line.len() + 1; + } else { + next_table_name_line = line; + break 'regular_table_loop; + } + } + + let table_str = &s[table_start..table_end-1]; + println!("table str: '{}'", table_str); + let new_table = table_str.parse()?; + tables.insert(table_name, new_table); + table_start = table_end; + } + + if let Some(next_table_name_line) = next_table_name_line { + table_name_line = next_table_name_line; + } else { + return Ok(Self { + tables, + ranges: range_table, + }) + } + } + } +} + +fn is_valid_range_table_entry_line(s: &str) -> bool { + let mut has_non_digits = false; + let mut chars_iter = s.chars(); + for c in &mut chars_iter { + match c { + ':' => return has_non_digits && chars_iter.next().is_some(), + '0'..='9' => {}, + _ => has_non_digits = true, + } + } + return false; +} + +#[repr(u8)] +pub(super) enum MinMax { + Min = 0, + Max = 1, +} + +#[allow(dead_code)] +pub(super) enum PpOdClkVoltageWriteValues { + /// Something clock + SCLK { + limit: MinMax, + clockspeed: ClockFrequency, + }, + /// Memory clock + MCLK { + limit: MinMax, + clockspeed: ClockFrequency, + }, + /// Core clock + CCLK { + core: usize, + limit: MinMax, + clockspeed: ClockFrequency, + }, + /// Voltage curve + VC { + point: usize, + clockspeed: ClockFrequency, + millivolts: usize, + }, + /// Voltage offset + VO { + millivolts: isize, + }, + /// Confirm (required to apply another other written value) + C, +} + +impl PpOdClkVoltageWriteValues { + pub fn sysfs_str(&self) -> String { + match self { + Self::SCLK { limit, clockspeed } => format!("s {} {}", Self::limit_num(limit), Self::mhz_clock(clockspeed)), + Self::MCLK { limit, clockspeed } => format!("m {} {}", Self::limit_num(limit), Self::mhz_clock(clockspeed)), + Self::CCLK { core, limit, clockspeed } => format!("p {} {} {}", core, Self::limit_num(limit), Self::mhz_clock(clockspeed)), + Self::VC { point, clockspeed, millivolts } => format!("vc {} {} {}", point, Self::mhz_clock(clockspeed), millivolts), + Self::VO { millivolts } => format!("vo {}", millivolts), + Self::C => "c".to_owned(), + } + } + + fn mhz_clock(clk: &ClockFrequency) -> usize { + if clk.si_prefix == 1_000_000 /* MHz */ { + clk.value + } else { + clk.in_hz() / 1_000_000 + } + } + + fn limit_num(limit: &MinMax) -> u8 { + match limit { + MinMax::Min => 0, + MinMax::Max => 1, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn framework_13_amd_parse_test() { + const DATA: &str = +r#"OD_SCLK: +0: 800Mhz +1: 2700Mhz +OD_RANGE: +SCLK: 800Mhz 2700Mhz +"#; + let pp_od_clk_volt: PpOdClkVoltageReadValues = DATA.trim_end().parse().expect("failed to parse"); + + assert!(pp_od_clk_volt.tables.contains_key(&OdTableName::SCLK)); + assert!(pp_od_clk_volt.ranges.is_some()); + assert_eq!( + pp_od_clk_volt.ranges.as_ref() + .expect("missing ranges table") + .0 + .get(&OdTableName::SCLK) + .expect("missing SCLK entry in range table") + .0, + vec![ClockFrequency { value: 800, si_prefix: 1_000_000}, ClockFrequency { value: 2700, si_prefix: 1_000_000}] + ); + assert_eq!( + pp_od_clk_volt.tables + .get(&OdTableName::SCLK) + .expect("missing SCLK table") + .0 + .get(&0) + .expect("missing 0th entry in SCLK table"), + &Selectable { item: ClockFrequency { value: 800, si_prefix: 1_000_000}, selected: false}, + ); + assert_eq!( + pp_od_clk_volt.tables + .get(&OdTableName::SCLK) + .expect("missing SCLK table") + .0 + .get(&1) + .expect("missing 0th entry in SCLK table"), + &Selectable { item: ClockFrequency { value: 2700, si_prefix: 1_000_000}, selected: false}, + ); + } + + #[test] + fn steam_deck_lcd_parse_test() { + const DATA: &str = +r#"OD_SCLK: +0: 200Mhz +1: 1600Mhz +OD_RANGE: +SCLK: 200Mhz 1600Mhz +CCLK: 1400Mhz 3500Mhz +CCLK_RANGE in Core0: +0: 1400Mhz +1: 3500Mhz +"#; + let pp_od_clk_volt: PpOdClkVoltageReadValues = DATA.trim_end().parse().expect("failed to parse"); + + assert!(pp_od_clk_volt.tables.contains_key(&OdTableName::SCLK)); + assert!(pp_od_clk_volt.tables.contains_key(&OdTableName::CCLK_RANGE_Core(0))); + assert!(pp_od_clk_volt.ranges.is_some()); + assert_eq!( + pp_od_clk_volt.ranges.as_ref() + .expect("missing ranges table") + .0 + .get(&OdTableName::SCLK) + .expect("missing SCLK entry in range table") + .0, + vec![ClockFrequency { value: 200, si_prefix: 1_000_000}, ClockFrequency { value: 1600, si_prefix: 1_000_000}] + ); + assert_eq!( + pp_od_clk_volt.ranges.as_ref() + .expect("missing ranges table") + .0 + .get(&OdTableName::CCLK) + .expect("missing CCLK entry in range table") + .0, + vec![ClockFrequency { value: 1400, si_prefix: 1_000_000}, ClockFrequency { value: 3500, si_prefix: 1_000_000}] + ); + assert_eq!( + pp_od_clk_volt.tables + .get(&OdTableName::SCLK) + .expect("missing SCLK table") + .0 + .get(&0) + .expect("missing 0th entry in SCLK table"), + &Selectable { item: ClockFrequency { value: 200, si_prefix: 1_000_000}, selected: false}, + ); + assert_eq!( + pp_od_clk_volt.tables + .get(&OdTableName::SCLK) + .expect("missing SCLK table") + .0 + .get(&1) + .expect("missing 0th entry in SCLK table"), + &Selectable { item: ClockFrequency { value: 1600, si_prefix: 1_000_000}, selected: false}, + ); + assert_eq!( + pp_od_clk_volt.tables + .get(&OdTableName::CCLK_RANGE_Core(0)) + .expect("missing CCLK in Core0 table") + .0 + .get(&0) + .expect("missing 0th entry in CCLK Core0 table"), + &Selectable { item: ClockFrequency { value: 1400, si_prefix: 1_000_000}, selected: false}, + ); + assert_eq!( + pp_od_clk_volt.tables + .get(&OdTableName::CCLK_RANGE_Core(0)) + .expect("missing CCLK in Core0 table") + .0 + .get(&1) + .expect("missing 0th entry in CCLK Core0 table"), + &Selectable { item: ClockFrequency { value: 3500, si_prefix: 1_000_000}, selected: false}, + ); + } + + #[test] + fn steam_deck_oled_parse_test() { + const DATA: &str = +r#"OD_SCLK: +0: 200Mhz +1: 1600Mhz +OD_RANGE: +SCLK: 200Mhz 1600Mhz +CCLK: 1400Mhz 3500Mhz +CCLK_RANGE in Core0: +0: 1400Mhz +1: 3500Mhz +"#; + let pp_od_clk_volt: PpOdClkVoltageReadValues = DATA.trim_end().parse().expect("failed to parse"); + + assert!(pp_od_clk_volt.tables.contains_key(&OdTableName::SCLK)); + assert!(pp_od_clk_volt.tables.contains_key(&OdTableName::CCLK_RANGE_Core(0))); + assert!(pp_od_clk_volt.ranges.is_some()); + assert_eq!( + pp_od_clk_volt.ranges.as_ref() + .expect("missing ranges table") + .0 + .get(&OdTableName::SCLK) + .expect("missing SCLK entry in range table") + .0, + vec![ClockFrequency { value: 200, si_prefix: 1_000_000}, ClockFrequency { value: 1600, si_prefix: 1_000_000}] + ); + assert_eq!( + pp_od_clk_volt.ranges.as_ref() + .expect("missing ranges table") + .0 + .get(&OdTableName::CCLK) + .expect("missing CCLK entry in range table") + .0, + vec![ClockFrequency { value: 1400, si_prefix: 1_000_000}, ClockFrequency { value: 3500, si_prefix: 1_000_000}] + ); + assert_eq!( + pp_od_clk_volt.tables + .get(&OdTableName::SCLK) + .expect("missing SCLK table") + .0 + .get(&0) + .expect("missing 0th entry in SCLK table"), + &Selectable { item: ClockFrequency { value: 200, si_prefix: 1_000_000}, selected: false}, + ); + assert_eq!( + pp_od_clk_volt.tables + .get(&OdTableName::SCLK) + .expect("missing SCLK table") + .0 + .get(&1) + .expect("missing 0th entry in SCLK table"), + &Selectable { item: ClockFrequency { value: 1600, si_prefix: 1_000_000}, selected: false}, + ); + assert_eq!( + pp_od_clk_volt.tables + .get(&OdTableName::CCLK_RANGE_Core(0)) + .expect("missing CCLK in Core0 table") + .0 + .get(&0) + .expect("missing 0th entry in CCLK Core0 table"), + &Selectable { item: ClockFrequency { value: 1400, si_prefix: 1_000_000}, selected: false}, + ); + assert_eq!( + pp_od_clk_volt.tables + .get(&OdTableName::CCLK_RANGE_Core(0)) + .expect("missing CCLK in Core0 table") + .0 + .get(&1) + .expect("missing 0th entry in CCLK Core0 table"), + &Selectable { item: ClockFrequency { value: 3500, si_prefix: 1_000_000}, selected: false}, + ); + } +} diff --git a/crates/procbox/src/combo/mod.rs b/crates/procbox/src/combo/mod.rs new file mode 100644 index 0000000..1c12f0f --- /dev/null +++ b/crates/procbox/src/combo/mod.rs @@ -0,0 +1,5 @@ +//! Combined CPU and GPU devices + +#[allow(missing_docs)] +pub mod amdgpu; +pub use amdgpu::AmdGpu;