mirror of
https://gitlab.steamos.cloud/holo/steamos-manager.git
synced 2025-07-12 09:22:26 -04:00
429 lines
13 KiB
Rust
429 lines
13 KiB
Rust
/*
|
|
* Copyright © 2023 Collabora Ltd.
|
|
* Copyright © 2024 Valve Software
|
|
*
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
|
|
use anyhow::{anyhow, bail, ensure, Error, Result};
|
|
use std::path::PathBuf;
|
|
use std::str::FromStr;
|
|
use tokio::fs::{self, File};
|
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
|
use tracing::error;
|
|
|
|
use crate::{path, write_synced};
|
|
|
|
const GPU_HWMON_PREFIX: &str = "/sys/class/drm/card0/device/hwmon";
|
|
|
|
const GPU_PERFORMANCE_LEVEL_PATH: &str =
|
|
"/sys/class/drm/card0/device/power_dpm_force_performance_level";
|
|
const GPU_CLOCKS_PATH: &str = "/sys/class/drm/card0/device/pp_od_clk_voltage";
|
|
|
|
const TDP_LIMIT1: &str = "power1_cap";
|
|
const TDP_LIMIT2: &str = "power2_cap";
|
|
|
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
|
#[repr(u32)]
|
|
pub enum GPUPerformanceLevel {
|
|
Auto = 0,
|
|
Low = 1,
|
|
High = 2,
|
|
Manual = 3,
|
|
ProfilePeak = 4,
|
|
}
|
|
|
|
impl TryFrom<u32> for GPUPerformanceLevel {
|
|
type Error = &'static str;
|
|
fn try_from(v: u32) -> Result<Self, Self::Error> {
|
|
match v {
|
|
x if x == GPUPerformanceLevel::Auto as u32 => Ok(GPUPerformanceLevel::Auto),
|
|
x if x == GPUPerformanceLevel::Low as u32 => Ok(GPUPerformanceLevel::Low),
|
|
x if x == GPUPerformanceLevel::High as u32 => Ok(GPUPerformanceLevel::High),
|
|
x if x == GPUPerformanceLevel::Manual as u32 => Ok(GPUPerformanceLevel::Manual),
|
|
x if x == GPUPerformanceLevel::ProfilePeak as u32 => {
|
|
Ok(GPUPerformanceLevel::ProfilePeak)
|
|
}
|
|
_ => Err("No enum match for value {v}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FromStr for GPUPerformanceLevel {
|
|
type Err = Error;
|
|
fn from_str(input: &str) -> Result<GPUPerformanceLevel, Self::Err> {
|
|
match input {
|
|
"auto" => Ok(GPUPerformanceLevel::Auto),
|
|
"low" => Ok(GPUPerformanceLevel::Low),
|
|
"high" => Ok(GPUPerformanceLevel::High),
|
|
"manual" => Ok(GPUPerformanceLevel::Manual),
|
|
"peak_performance" => Ok(GPUPerformanceLevel::ProfilePeak),
|
|
v => Err(anyhow!("No enum match for value {v}")),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ToString for GPUPerformanceLevel {
|
|
fn to_string(&self) -> String {
|
|
String::from(match self {
|
|
GPUPerformanceLevel::Auto => "auto",
|
|
GPUPerformanceLevel::Low => "low",
|
|
GPUPerformanceLevel::High => "high",
|
|
GPUPerformanceLevel::Manual => "manual",
|
|
GPUPerformanceLevel::ProfilePeak => "peak_performance",
|
|
})
|
|
}
|
|
}
|
|
|
|
pub async fn get_gpu_performance_level() -> Result<GPUPerformanceLevel> {
|
|
let level = fs::read_to_string(path(GPU_PERFORMANCE_LEVEL_PATH))
|
|
.await
|
|
.inspect_err(|message| error!("Error opening sysfs file for reading: {message}"))?;
|
|
|
|
GPUPerformanceLevel::from_str(level.trim())
|
|
}
|
|
|
|
pub async fn set_gpu_performance_level(level: GPUPerformanceLevel) -> Result<()> {
|
|
let level: String = level.to_string();
|
|
write_synced(path(GPU_PERFORMANCE_LEVEL_PATH), level.as_bytes())
|
|
.await
|
|
.inspect_err(|message| error!("Error writing to sysfs file: {message}"))
|
|
}
|
|
|
|
pub async fn set_gpu_clocks(clocks: u32) -> Result<()> {
|
|
// Set GPU clocks to given value valid between 200 - 1600
|
|
// Only used when GPU Performance Level is manual, but write whenever called.
|
|
ensure!((200..=1600).contains(&clocks), "Invalid clocks");
|
|
|
|
let mut myfile = File::create(path(GPU_CLOCKS_PATH))
|
|
.await
|
|
.inspect_err(|message| error!("Error opening sysfs file for writing: {message}"))?;
|
|
|
|
let data = format!("s 0 {clocks}\n");
|
|
myfile
|
|
.write(data.as_bytes())
|
|
.await
|
|
.inspect_err(|message| error!("Error writing to sysfs file: {message}"))?;
|
|
myfile.flush().await?;
|
|
|
|
let data = format!("s 1 {clocks}\n");
|
|
myfile
|
|
.write(data.as_bytes())
|
|
.await
|
|
.inspect_err(|message| error!("Error writing to sysfs file: {message}"))?;
|
|
myfile.flush().await?;
|
|
|
|
myfile
|
|
.write("c\n".as_bytes())
|
|
.await
|
|
.inspect_err(|message| error!("Error writing to sysfs file: {message}"))?;
|
|
myfile.flush().await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn get_gpu_clocks() -> Result<u32> {
|
|
let clocks_file = File::open(path(GPU_CLOCKS_PATH)).await?;
|
|
let mut reader = BufReader::new(clocks_file);
|
|
loop {
|
|
let mut line = String::new();
|
|
if reader.read_line(&mut line).await? == 0 {
|
|
break;
|
|
}
|
|
if line != "OD_SCLK:\n" {
|
|
continue;
|
|
}
|
|
|
|
let mut line = String::new();
|
|
if reader.read_line(&mut line).await? == 0 {
|
|
break;
|
|
}
|
|
let mhz = match line.split_whitespace().nth(1) {
|
|
Some(mhz) if mhz.ends_with("Mhz") => mhz.trim_end_matches("Mhz"),
|
|
_ => break,
|
|
};
|
|
|
|
match mhz.parse() {
|
|
Ok(mhz) => return Ok(mhz),
|
|
Err(e) => return Err(e.into()),
|
|
}
|
|
}
|
|
Err(anyhow!("Couldn't find GPU clocks"))
|
|
}
|
|
|
|
async fn find_hwmon() -> Result<PathBuf> {
|
|
let mut dir = fs::read_dir(path(GPU_HWMON_PREFIX)).await?;
|
|
loop {
|
|
let base = match dir.next_entry().await? {
|
|
Some(entry) => entry.path(),
|
|
None => bail!("hwmon not found"),
|
|
};
|
|
if fs::try_exists(base.join(TDP_LIMIT1)).await? {
|
|
return Ok(base);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_tdp_limit() -> Result<u32> {
|
|
let base = find_hwmon().await?;
|
|
let power1cap = fs::read_to_string(base.join(TDP_LIMIT1)).await?;
|
|
let power1cap: u32 = power1cap.trim_end().parse()?;
|
|
Ok(power1cap / 1000000)
|
|
}
|
|
|
|
pub async fn set_tdp_limit(limit: u32) -> Result<()> {
|
|
// Set TDP limit given if within range (3-15)
|
|
// Returns false on error or out of range
|
|
ensure!((3..=15).contains(&limit), "Invalid limit");
|
|
let data = format!("{limit}000000");
|
|
|
|
let base = find_hwmon().await?;
|
|
write_synced(base.join(TDP_LIMIT1), data.as_bytes())
|
|
.await
|
|
.inspect_err(|message| {
|
|
error!("Error opening sysfs power1_cap file for writing TDP limits {message}")
|
|
})?;
|
|
|
|
if let Ok(mut power2file) = File::create(base.join(TDP_LIMIT2)).await {
|
|
power2file
|
|
.write(data.as_bytes())
|
|
.await
|
|
.inspect_err(|message| error!("Error writing to power2_cap file: {message}"))?;
|
|
power2file.flush().await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub mod test {
|
|
use super::*;
|
|
use crate::testing;
|
|
use anyhow::anyhow;
|
|
use tokio::fs::{create_dir_all, read_to_string, remove_dir, write};
|
|
|
|
pub async fn setup() {
|
|
let filename = path(GPU_PERFORMANCE_LEVEL_PATH);
|
|
create_dir_all(filename.parent().unwrap())
|
|
.await
|
|
.expect("create_dir_all");
|
|
}
|
|
|
|
pub async fn write_clocks(mhz: u32) {
|
|
let filename = path(GPU_CLOCKS_PATH);
|
|
create_dir_all(filename.parent().unwrap())
|
|
.await
|
|
.expect("create_dir_all");
|
|
|
|
let contents = format!(
|
|
"OD_SCLK:
|
|
0: {mhz}Mhz
|
|
1: {mhz}Mhz
|
|
OD_RANGE:
|
|
SCLK: 200Mhz 1600Mhz
|
|
CCLK: 1400Mhz 3500Mhz
|
|
CCLK_RANGE in Core0:
|
|
0: 1400Mhz
|
|
1: 3500Mhz\n"
|
|
);
|
|
|
|
write(filename.as_path(), contents).await.expect("write");
|
|
}
|
|
|
|
pub async fn expect_clocks(mhz: u32) {
|
|
let clocks = read_to_string(path(GPU_CLOCKS_PATH)).await.expect("read");
|
|
assert_eq!(clocks, format!("s 0 {mhz}\ns 1 {mhz}\nc\n"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_gpu_performance_level() {
|
|
let _h = testing::start();
|
|
|
|
let filename = path(GPU_PERFORMANCE_LEVEL_PATH);
|
|
setup().await;
|
|
assert!(get_gpu_performance_level().await.is_err());
|
|
|
|
write(filename.as_path(), "auto\n").await.expect("write");
|
|
assert_eq!(
|
|
get_gpu_performance_level().await.unwrap(),
|
|
GPUPerformanceLevel::Auto
|
|
);
|
|
|
|
write(filename.as_path(), "low\n").await.expect("write");
|
|
assert_eq!(
|
|
get_gpu_performance_level().await.unwrap(),
|
|
GPUPerformanceLevel::Low
|
|
);
|
|
|
|
write(filename.as_path(), "high\n").await.expect("write");
|
|
assert_eq!(
|
|
get_gpu_performance_level().await.unwrap(),
|
|
GPUPerformanceLevel::High
|
|
);
|
|
|
|
write(filename.as_path(), "manual\n").await.expect("write");
|
|
assert_eq!(
|
|
get_gpu_performance_level().await.unwrap(),
|
|
GPUPerformanceLevel::Manual
|
|
);
|
|
|
|
write(filename.as_path(), "peak_performance\n")
|
|
.await
|
|
.expect("write");
|
|
assert_eq!(
|
|
get_gpu_performance_level().await.unwrap(),
|
|
GPUPerformanceLevel::ProfilePeak
|
|
);
|
|
|
|
write(filename.as_path(), "nothing\n").await.expect("write");
|
|
assert!(get_gpu_performance_level().await.is_err());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_set_gpu_performance_level() {
|
|
let _h = testing::start();
|
|
|
|
let filename = path(GPU_PERFORMANCE_LEVEL_PATH);
|
|
setup().await;
|
|
|
|
set_gpu_performance_level(GPUPerformanceLevel::Auto)
|
|
.await
|
|
.expect("set");
|
|
assert_eq!(
|
|
read_to_string(filename.as_path()).await.unwrap().trim(),
|
|
"auto"
|
|
);
|
|
set_gpu_performance_level(GPUPerformanceLevel::Low)
|
|
.await
|
|
.expect("set");
|
|
assert_eq!(
|
|
read_to_string(filename.as_path()).await.unwrap().trim(),
|
|
"low"
|
|
);
|
|
set_gpu_performance_level(GPUPerformanceLevel::High)
|
|
.await
|
|
.expect("set");
|
|
assert_eq!(
|
|
read_to_string(filename.as_path()).await.unwrap().trim(),
|
|
"high"
|
|
);
|
|
set_gpu_performance_level(GPUPerformanceLevel::Manual)
|
|
.await
|
|
.expect("set");
|
|
assert_eq!(
|
|
read_to_string(filename.as_path()).await.unwrap().trim(),
|
|
"manual"
|
|
);
|
|
set_gpu_performance_level(GPUPerformanceLevel::ProfilePeak)
|
|
.await
|
|
.expect("set");
|
|
assert_eq!(
|
|
read_to_string(filename.as_path()).await.unwrap().trim(),
|
|
"peak_performance"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_tdp_limit() {
|
|
let _h = testing::start();
|
|
|
|
let hwmon = path(GPU_HWMON_PREFIX);
|
|
create_dir_all(hwmon.join("hwmon5").as_path())
|
|
.await
|
|
.expect("create_dir_all");
|
|
|
|
assert!(get_tdp_limit().await.is_err());
|
|
|
|
write(hwmon.join("hwmon5").join(TDP_LIMIT1), "15000000\n")
|
|
.await
|
|
.expect("write");
|
|
assert_eq!(get_tdp_limit().await.unwrap(), 15);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_set_tdp_limit() {
|
|
let _h = testing::start();
|
|
|
|
assert_eq!(
|
|
set_tdp_limit(2).await.unwrap_err().to_string(),
|
|
anyhow!("Invalid limit").to_string()
|
|
);
|
|
assert_eq!(
|
|
set_tdp_limit(20).await.unwrap_err().to_string(),
|
|
anyhow!("Invalid limit").to_string()
|
|
);
|
|
assert!(set_tdp_limit(10).await.is_err());
|
|
|
|
let hwmon = path(GPU_HWMON_PREFIX);
|
|
create_dir_all(hwmon.as_path())
|
|
.await
|
|
.expect("create_dir_all");
|
|
assert_eq!(
|
|
set_tdp_limit(10).await.unwrap_err().to_string(),
|
|
anyhow!("hwmon not found").to_string()
|
|
);
|
|
|
|
let hwmon = hwmon.join("hwmon5");
|
|
create_dir_all(hwmon.join(TDP_LIMIT1))
|
|
.await
|
|
.expect("create_dir_all");
|
|
create_dir_all(hwmon.join(TDP_LIMIT2))
|
|
.await
|
|
.expect("create_dir_all");
|
|
assert_eq!(
|
|
set_tdp_limit(10).await.unwrap_err().to_string(),
|
|
anyhow!("Is a directory (os error 21)").to_string()
|
|
);
|
|
|
|
remove_dir(hwmon.join(TDP_LIMIT1))
|
|
.await
|
|
.expect("remove_dir");
|
|
write(hwmon.join(TDP_LIMIT1), "0").await.expect("write");
|
|
assert!(set_tdp_limit(10).await.is_ok());
|
|
let power1_cap = read_to_string(hwmon.join(TDP_LIMIT1))
|
|
.await
|
|
.expect("power1_cap");
|
|
assert_eq!(power1_cap, "10000000");
|
|
|
|
remove_dir(hwmon.join(TDP_LIMIT2))
|
|
.await
|
|
.expect("remove_dir");
|
|
write(hwmon.join(TDP_LIMIT2), "0").await.expect("write");
|
|
assert!(set_tdp_limit(15).await.is_ok());
|
|
let power1_cap = read_to_string(hwmon.join(TDP_LIMIT1))
|
|
.await
|
|
.expect("power1_cap");
|
|
assert_eq!(power1_cap, "15000000");
|
|
let power2_cap = read_to_string(hwmon.join(TDP_LIMIT2))
|
|
.await
|
|
.expect("power2_cap");
|
|
assert_eq!(power2_cap, "15000000");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_gpu_clocks() {
|
|
let _h = testing::start();
|
|
setup().await;
|
|
|
|
assert!(get_gpu_clocks().await.is_err());
|
|
write_clocks(1600).await;
|
|
|
|
assert_eq!(get_gpu_clocks().await.unwrap(), 1600);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_set_gpu_clocks() {
|
|
let _h = testing::start();
|
|
|
|
assert!(set_gpu_clocks(1600).await.is_err());
|
|
setup().await;
|
|
|
|
assert!(set_gpu_clocks(100).await.is_err());
|
|
assert!(set_gpu_clocks(2000).await.is_err());
|
|
|
|
assert!(set_gpu_clocks(200).await.is_ok());
|
|
expect_clocks(200).await;
|
|
|
|
assert!(set_gpu_clocks(1600).await.is_ok());
|
|
expect_clocks(1600).await;
|
|
}
|
|
}
|