Add amdgpu basics and Van Gogh clockspeed control

This commit is contained in:
NGnius (Graham) 2024-07-10 20:58:37 -04:00
parent 7aa18d0294
commit 60d038c79f
14 changed files with 1426 additions and 4 deletions

View file

@ -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" }

View file

@ -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;

View file

@ -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;
}

View 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(_)));
}
}

View file

@ -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};

View file

@ -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]);
}
}

View 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('*', _)));
}
}

View 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)
}

View 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)
}
}

View 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)
}
}

View 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
}
}

View file

@ -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(()),
})
}
}

View 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},
);
}
}

View file

@ -0,0 +1,5 @@
//! Combined CPU and GPU devices
#[allow(missing_docs)]
pub mod amdgpu;
pub use amdgpu::AmdGpu;