Add amdgpu basics and Van Gogh clockspeed control
This commit is contained in:
parent
7aa18d0294
commit
60d038c79f
14 changed files with 1426 additions and 4 deletions
|
@ -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" }
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ use super::PowerError;
|
|||
use super::primitives::Value;
|
||||
|
||||
/// Power control interface for a device
|
||||
pub trait Power<OP: PowerOp, VAL = Value> {
|
||||
pub trait Power<OP: PowerOp + ?Sized, VAL = Value> {
|
||||
/// 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<OP: PowerOp> {
|
||||
/// 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;
|
||||
}
|
||||
|
|
116
crates/core/src/primitives/clockspeed.rs
Normal file
116
crates/core/src/primitives/clockspeed.rs
Normal file
|
@ -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<Self, Self::Err> {
|
||||
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<usize> {
|
||||
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(_)));
|
||||
}
|
||||
}
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ impl <T: FromStr> FromStr for SpacedList<T> {
|
|||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
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<usize> = "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<usize> = "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]);
|
||||
}
|
||||
}
|
||||
|
|
184
crates/core/src/primitives/value_map.rs
Normal file
184
crates/core/src/primitives/value_map.rs
Normal file
|
@ -0,0 +1,184 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
/// A mapping of states (integers) to values.
|
||||
pub struct ValueMap<K: std::cmp::Eq + std::hash::Hash, V> (pub HashMap<K, V>);
|
||||
|
||||
/// ValueMap parse errors
|
||||
#[derive(Debug)]
|
||||
pub enum ValueMapParseErr<KE, VE> {
|
||||
/// Inner value type parse error
|
||||
InnerVal(VE),
|
||||
/// Inner key type parse error
|
||||
InnerKey(KE),
|
||||
/// Unrecognized char encountered at position
|
||||
UnexpectedChar(char, usize),
|
||||
}
|
||||
|
||||
impl <KE: core::fmt::Display, VE: core::fmt::Display> core::fmt::Display for ValueMapParseErr<KE, VE> {
|
||||
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 <KE: std::error::Error, VE: std::error::Error> std::error::Error for ValueMapParseErr<KE, VE> {}
|
||||
|
||||
impl <K: std::str::FromStr + std::cmp::Eq + std::hash::Hash, V: std::str::FromStr> std::str::FromStr for ValueMap<K, V> {
|
||||
type Err = ValueMapParseErr<<K as std::str::FromStr>::Err, <V as std::str::FromStr>::Err>;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
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<T> {
|
||||
/// The actual item
|
||||
pub item: T,
|
||||
/// Was there a * after it?
|
||||
pub selected: bool,
|
||||
}
|
||||
|
||||
impl <T: std::str::FromStr> std::str::FromStr for Selectable<T> {
|
||||
type Err = <T as std::str::FromStr>::Err;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
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 <K: std::cmp::Eq + std::hash::Hash, V> ValueMap<K, Selectable<V>> {
|
||||
/// 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<Item=&'_ K> + '_ {
|
||||
self.0.iter().filter(|x| x.1.selected).map(|x| x.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl <K: std::str::FromStr + std::cmp::Eq + std::hash::Hash, V: std::str::FromStr> ValueMap<K, Selectable<V>> {
|
||||
/// Assert that the map only has one selected entry
|
||||
pub fn fail_on_many_selected(self) -> Result<Self, <Self as std::str::FromStr>::Err> {
|
||||
if self.many_selected().count() > 1 {
|
||||
Err(<Self as std::str::FromStr>::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::<usize, usize>::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::<usize, Selectable<usize>>::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::<usize, Selectable<usize>>::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::<usize, Selectable<usize>>::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('*', _)));
|
||||
}
|
||||
}
|
6
crates/procbox/src/combo/amdgpu/common.rs
Normal file
6
crates/procbox/src/combo/amdgpu/common.rs
Normal file
|
@ -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)
|
||||
}
|
439
crates/procbox/src/combo/amdgpu/cpus.rs
Normal file
439
crates/procbox/src/combo/amdgpu/cpus.rs
Normal file
|
@ -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<AmdGpuCpuOp> for AmdGpu {
|
||||
fn is_on(&self) -> bool {
|
||||
self.is_enabled()
|
||||
}
|
||||
|
||||
fn is_available(&self) -> bool {
|
||||
self.is_compatible()
|
||||
}
|
||||
|
||||
fn supported_operations(&self) -> Box<dyn core::iter::Iterator<Item=AmdGpuCpuOp>> {
|
||||
Box::new(
|
||||
Power::<AmdGpuCpuFreq, _>::supported_operations(self).map(core::convert::Into::<AmdGpuCpuOp>::into)
|
||||
.chain(Power::<super::AmdGpuCommit, _>::supported_operations(self).map(core::convert::Into::<AmdGpuCpuOp>::into))
|
||||
)
|
||||
}
|
||||
|
||||
fn act(&self, op: AmdGpuCpuOp) -> Result<powerbox::primitives::Value, powerbox::PowerError> {
|
||||
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<AmdGpuCpuOp> for AmdGpu {}
|
||||
|
||||
impl CpuPowerOp for super::AmdGpuCommit {}
|
||||
impl CpuPower<super::AmdGpuCommit, ()> for AmdGpu {}
|
||||
|
||||
impl Into<AmdGpuCpuOp> 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<GetCoreMinFrequency, ClockFrequency> for AmdGpu {
|
||||
fn is_on(&self) -> bool {
|
||||
self.is_enabled()
|
||||
}
|
||||
|
||||
fn is_available(&self) -> bool {
|
||||
self.is_compatible()
|
||||
}
|
||||
|
||||
fn supported_operations(&self) -> Box<dyn core::iter::Iterator<Item=GetCoreMinFrequency>> {
|
||||
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<ClockFrequency, powerbox::PowerError> {
|
||||
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<GetCoreMinFrequency, ClockFrequency> for AmdGpu {}
|
||||
|
||||
impl Into<AmdGpuCpuFreq> 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<GetCoreMaxFrequency, ClockFrequency> for AmdGpu {
|
||||
fn is_on(&self) -> bool {
|
||||
self.is_enabled()
|
||||
}
|
||||
|
||||
fn is_available(&self) -> bool {
|
||||
self.is_compatible()
|
||||
}
|
||||
|
||||
fn supported_operations(&self) -> Box<dyn core::iter::Iterator<Item=GetCoreMaxFrequency>> {
|
||||
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<ClockFrequency, powerbox::PowerError> {
|
||||
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<GetCoreMaxFrequency, ClockFrequency> for AmdGpu {}
|
||||
|
||||
impl Into<AmdGpuCpuFreq> 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<SetCoreMinFrequency, ()> for AmdGpu {
|
||||
fn is_on(&self) -> bool {
|
||||
self.is_enabled()
|
||||
}
|
||||
|
||||
fn is_available(&self) -> bool {
|
||||
self.is_compatible()
|
||||
}
|
||||
|
||||
fn supported_operations(&self) -> Box<dyn core::iter::Iterator<Item=SetCoreMinFrequency>> {
|
||||
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<SetCoreMinFrequency, ()> 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<SetCoreMinFrequency> 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<AmdGpuCpuFreq> 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<SetCoreMaxFrequency, ()> for AmdGpu {
|
||||
fn is_on(&self) -> bool {
|
||||
self.is_enabled()
|
||||
}
|
||||
|
||||
fn is_available(&self) -> bool {
|
||||
self.is_compatible()
|
||||
}
|
||||
|
||||
fn supported_operations(&self) -> Box<dyn core::iter::Iterator<Item=SetCoreMaxFrequency>> {
|
||||
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<SetCoreMaxFrequency, ()> for AmdGpu {}
|
||||
|
||||
impl RatifiedPower<SetCoreMaxFrequency> 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<AmdGpuCpuFreq> 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<AmdGpuCpuFreq, powerbox::primitives::Value<ClockFrequency>> for AmdGpu {
|
||||
fn is_on(&self) -> bool {
|
||||
self.is_enabled()
|
||||
}
|
||||
|
||||
fn is_available(&self) -> bool {
|
||||
self.is_compatible()
|
||||
}
|
||||
|
||||
fn supported_operations(&self) -> Box<dyn core::iter::Iterator<Item=AmdGpuCpuFreq>> {
|
||||
Box::new(
|
||||
Power::<GetCoreMinFrequency, _>::supported_operations(self).map(core::convert::Into::<AmdGpuCpuFreq>::into)
|
||||
.chain(Power::<GetCoreMaxFrequency, _>::supported_operations(self).map(core::convert::Into::<AmdGpuCpuFreq>::into))
|
||||
.chain(Power::<SetCoreMinFrequency, _>::supported_operations(self).map(core::convert::Into::<AmdGpuCpuFreq>::into))
|
||||
.chain(Power::<SetCoreMaxFrequency, _>::supported_operations(self).map(core::convert::Into::<AmdGpuCpuFreq>::into))
|
||||
)
|
||||
}
|
||||
|
||||
fn act(&self, op: AmdGpuCpuFreq) -> Result<powerbox::primitives::Value<ClockFrequency>, 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<AmdGpuCpuFreq, powerbox::primitives::Value<ClockFrequency>> for AmdGpu {}
|
||||
|
||||
impl Into<AmdGpuCpuOp> for AmdGpuCpuFreq {
|
||||
fn into(self) -> AmdGpuCpuOp {
|
||||
AmdGpuCpuOp::Freq(self)
|
||||
}
|
||||
}
|
85
crates/procbox/src/combo/amdgpu/gpu.rs
Normal file
85
crates/procbox/src/combo/amdgpu/gpu.rs
Normal file
|
@ -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<AmdGpuOp> for AmdGpu {
|
||||
fn is_on(&self) -> bool {
|
||||
self.is_enabled()
|
||||
}
|
||||
|
||||
fn is_available(&self) -> bool {
|
||||
self.is_compatible()
|
||||
}
|
||||
|
||||
fn supported_operations(&self) -> Box<dyn core::iter::Iterator<Item=AmdGpuOp>> {
|
||||
// TODO
|
||||
Box::new(core::iter::empty())
|
||||
}
|
||||
|
||||
fn act(&self, op: AmdGpuOp) -> Result<powerbox::primitives::Value, powerbox::PowerError> {
|
||||
match op {
|
||||
_ => Err(powerbox::PowerError::Unknown),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuPower<AmdGpuOp> for AmdGpu {}
|
||||
|
||||
pub struct AmdGpuCommit;
|
||||
|
||||
impl PowerOp for AmdGpuCommit {
|
||||
fn is_eq_op(&self, _other: &Self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuPowerOp for AmdGpuCommit {}
|
||||
|
||||
impl Power<AmdGpuCommit, ()> for AmdGpu {
|
||||
fn is_on(&self) -> bool {
|
||||
self.is_enabled()
|
||||
}
|
||||
|
||||
fn is_available(&self) -> bool {
|
||||
self.is_compatible()
|
||||
}
|
||||
|
||||
fn supported_operations(&self) -> Box<dyn core::iter::Iterator<Item=AmdGpuCommit>> {
|
||||
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<AmdGpuCommit, ()> for AmdGpu {}
|
||||
|
||||
impl Into<AmdGpuOp> for AmdGpuCommit {
|
||||
fn into(self) -> AmdGpuOp {
|
||||
AmdGpuOp::Commit(self)
|
||||
}
|
||||
}
|
||||
|
79
crates/procbox/src/combo/amdgpu/mod.rs
Normal file
79
crates/procbox/src/combo/amdgpu/mod.rs
Normal file
|
@ -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<std::path::Path>) -> Self {
|
||||
let sysfs = BasicEntityPath::new(sys_path);
|
||||
Self {
|
||||
sysfs,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_enabled(&self) -> bool {
|
||||
self.sysfs.attribute::<String>(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::<power_dpm_force_performance_level::PowerDpmForcePerformanceLevel>(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<pp_od_clk_voltage::PpOdClkVoltageReadValues, sysfuss::EitherErr2<std::io::Error, <pp_od_clk_voltage::PpOdClkVoltageReadValues as std::str::FromStr>::Err>> {
|
||||
let initial_pdfpl_manual =
|
||||
self.sysfs.attribute::<power_dpm_force_performance_level::PowerDpmForcePerformanceLevel>(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::<dyn std::error::Error + Send + Sync>::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
|
||||
}
|
||||
}
|
|
@ -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<Self, Self::Err> {
|
||||
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(()),
|
||||
})
|
||||
}
|
||||
}
|
443
crates/procbox/src/combo/amdgpu/pp_od_clk_voltage.rs
Normal file
443
crates/procbox/src/combo/amdgpu/pp_od_clk_voltage.rs
Normal file
|
@ -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<Self, Self::Err> {
|
||||
Ok(Self::from_range_entry(s))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PpOdClkVoltageReadValues {
|
||||
pub tables: HashMap<OdTableName, ValueMap<usize, Selectable<ClockFrequency>>>,
|
||||
pub ranges: Option<ValueMap<OdTableName, SpacedList<ClockFrequency>>>,
|
||||
}
|
||||
|
||||
impl std::str::FromStr for PpOdClkVoltageReadValues {
|
||||
type Err = <ValueMap<usize, ClockFrequency> as std::str::FromStr>::Err;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
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},
|
||||
);
|
||||
}
|
||||
}
|
5
crates/procbox/src/combo/mod.rs
Normal file
5
crates/procbox/src/combo/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
//! Combined CPU and GPU devices
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub mod amdgpu;
|
||||
pub use amdgpu::AmdGpu;
|
Loading…
Reference in a new issue