#[cfg(feature = "overdrive")]
pub mod overdrive;
#[macro_use]
mod power_levels;
pub mod power_profile_mode;
pub use power_levels::{PowerLevelKind, PowerLevels};
use crate::{
error::{Error, ErrorContext, ErrorKind},
hw_mon::HwMon,
sysfs::SysFS,
Result,
};
use power_profile_mode::PowerProfileModesTable;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fmt::{self, Display},
fs,
path::PathBuf,
str::FromStr,
};
#[cfg(feature = "overdrive")]
use {
self::overdrive::{ClocksTable, ClocksTableGen},
std::fs::File,
};
#[derive(Clone, Debug)]
pub struct GpuHandle {
sysfs_path: PathBuf,
pub hw_monitors: Vec<HwMon>,
uevent: HashMap<String, String>,
}
impl GpuHandle {
pub fn new_from_path(sysfs_path: PathBuf) -> Result<Self> {
let mut hw_monitors = Vec::new();
if let Ok(hw_mons_iter) = fs::read_dir(sysfs_path.join("hwmon")) {
for hw_mon_dir in hw_mons_iter.flatten() {
if let Ok(hw_mon) = HwMon::new_from_path(hw_mon_dir.path()) {
hw_monitors.push(hw_mon);
}
}
}
let uevent_raw = fs::read_to_string(sysfs_path.join("uevent"))?.replace(char::from(0), "");
let mut uevent = HashMap::new();
for (i, line) in uevent_raw.trim().lines().enumerate() {
let (key, value) = line
.split_once('=')
.ok_or_else(|| Error::unexpected_eol("=", i))?;
uevent.insert(key.to_owned(), value.to_owned());
}
match uevent.get("DRIVER") {
Some(_) => Ok(Self {
sysfs_path,
hw_monitors,
uevent,
}),
None => Err(ErrorKind::InvalidSysFS.into()),
}
}
pub fn get_driver(&self) -> &str {
self.uevent.get("DRIVER").unwrap()
}
pub fn get_pci_id(&self) -> Option<(&str, &str)> {
match self.uevent.get("PCI_ID") {
Some(pci_str) => pci_str.split_once(':'),
None => None,
}
}
pub fn get_pci_subsys_id(&self) -> Option<(&str, &str)> {
match self.uevent.get("PCI_SUBSYS_ID") {
Some(pci_str) => pci_str.split_once(':'),
None => None,
}
}
pub fn get_pci_slot_name(&self) -> Option<&str> {
self.uevent.get("PCI_SLOT_NAME").map(|s| s.as_str())
}
pub fn get_current_link_speed(&self) -> Result<String> {
self.read_file("current_link_speed")
}
pub fn get_current_link_width(&self) -> Result<String> {
self.read_file("current_link_width")
}
pub fn get_max_link_speed(&self) -> Result<String> {
self.read_file("max_link_speed")
}
pub fn get_max_link_width(&self) -> Result<String> {
self.read_file("max_link_width")
}
fn read_vram_file(&self, file: &str) -> Result<u64> {
let raw_vram = self.read_file(file)?;
Ok(raw_vram.parse()?)
}
pub fn get_total_vram(&self) -> Result<u64> {
self.read_vram_file("mem_info_vram_total")
}
pub fn get_used_vram(&self) -> Result<u64> {
self.read_vram_file("mem_info_vram_used")
}
pub fn get_busy_percent(&self) -> Result<u8> {
let raw_busy = self.read_file("gpu_busy_percent")?;
Ok(raw_busy.parse()?)
}
pub fn get_vbios_version(&self) -> Result<String> {
self.read_file("vbios_version")
}
pub fn get_power_force_performance_level(&self) -> Result<PerformanceLevel> {
let raw_level = self.read_file("power_dpm_force_performance_level")?;
PerformanceLevel::from_str(&raw_level)
}
pub fn set_power_force_performance_level(&self, level: PerformanceLevel) -> Result<()> {
self.write_file("power_dpm_force_performance_level", level.to_string())
}
pub fn get_clock_levels<T>(&self, kind: PowerLevelKind) -> Result<PowerLevels<T>>
where
T: FromStr,
<T as FromStr>::Err: Display,
{
self.read_file(kind.filename()).and_then(|content| {
let mut levels = Vec::new();
let mut active = None;
for mut line in content.trim().split('\n') {
if let Some(stripped) = line.strip_suffix('*') {
line = stripped;
if let Some(identifier) = stripped.split(':').next() {
active = Some(
identifier
.trim()
.parse()
.context("Unexpected power level identifier")?,
);
}
}
if let Some(s) = line.split(':').last() {
let parse_result = if let Some(suffix) = kind.value_suffix() {
let raw_value = s.trim().to_lowercase();
let value = raw_value.strip_suffix(suffix).ok_or_else(|| {
ErrorKind::ParseError {
msg: format!("Level did not have the expected suffix {suffix}"),
line: levels.len() + 1,
}
})?;
T::from_str(value)
} else {
let value = s.trim();
T::from_str(value)
};
let parsed_value = parse_result.map_err(|err| ErrorKind::ParseError {
msg: format!("Could not deserialize power level value: {err}"),
line: levels.len() + 1,
})?;
levels.push(parsed_value);
}
}
Ok(PowerLevels { levels, active })
})
}
impl_get_clocks_levels!(get_core_clock_levels, PowerLevelKind::CoreClock, u64);
impl_get_clocks_levels!(get_memory_clock_levels, PowerLevelKind::MemoryClock, u64);
impl_get_clocks_levels!(get_pcie_clock_levels, PowerLevelKind::PcieSpeed, String);
pub fn set_enabled_power_levels(&self, kind: PowerLevelKind, levels: &[u8]) -> Result<()> {
match self.get_power_force_performance_level()? {
PerformanceLevel::Manual => {
let mut s = String::new();
for l in levels {
s.push(char::from_digit((*l).into(), 10).unwrap());
s.push(' ');
}
Ok(self.write_file(kind.filename(), s)?)
}
_ => Err(ErrorKind::NotAllowed(
"power_force_performance level needs to be set to 'manual' to adjust power levels"
.to_string(),
)
.into()),
}
}
#[cfg(feature = "overdrive")]
pub fn get_clocks_table(&self) -> Result<ClocksTableGen> {
self.read_file_parsed("pp_od_clk_voltage")
}
#[cfg(feature = "overdrive")]
pub fn set_clocks_table(&self, table: &ClocksTableGen) -> Result<()> {
use std::io::Write;
let path = self.sysfs_path.join("pp_od_clk_voltage");
let mut file = File::create(path)?;
table.write_commands(&mut file)?;
file.write_all(b"c\n")?;
Ok(())
}
#[cfg(feature = "overdrive")]
pub fn reset_clocks_table(&self) -> Result<()> {
use std::io::Write;
let path = self.sysfs_path.join("pp_od_clk_voltage");
let mut file = File::create(path)?;
file.write_all(b"r\n")?;
Ok(())
}
pub fn get_power_profile_modes(&self) -> Result<PowerProfileModesTable> {
let contents = self.read_file("pp_power_profile_mode")?;
PowerProfileModesTable::parse(&contents)
}
pub fn set_active_power_profile_mode(&self, i: u16) -> Result<()> {
self.write_file("pp_power_profile_mode", format!("{i}\n"))
}
}
impl SysFS for GpuHandle {
fn get_path(&self) -> &std::path::Path {
&self.sysfs_path
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
pub enum PerformanceLevel {
#[default]
Auto,
Low,
High,
Manual,
}
impl FromStr for PerformanceLevel {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"auto" | "Automatic" => Ok(PerformanceLevel::Auto),
"high" | "Highest Clocks" => Ok(PerformanceLevel::High),
"low" | "Lowest Clocks" => Ok(PerformanceLevel::Low),
"manual" | "Manual" => Ok(PerformanceLevel::Manual),
_ => Err(ErrorKind::ParseError {
msg: "unrecognized GPU power profile".to_string(),
line: 1,
}
.into()),
}
}
}
impl fmt::Display for PerformanceLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
PerformanceLevel::Auto => "auto",
PerformanceLevel::High => "high",
PerformanceLevel::Low => "low",
PerformanceLevel::Manual => "manual",
}
)
}
}
fn trim_sysfs_line(line: &str) -> &str {
line.trim_matches(char::from(0)).trim()
}