/* * Copyright © 2023 Collabora Ltd. * Copyright © 2024 Valve Software * * SPDX-License-Identifier: MIT */ use anyhow::{anyhow, bail, ensure, Result}; use async_trait::async_trait; use lazy_static::lazy_static; use num_enum::TryFromPrimitive; use regex::Regex; use std::ops::RangeInclusive; use std::path::{Path, PathBuf}; use std::str::FromStr; use strum::{Display, EnumString, VariantNames}; use tokio::fs::{self, try_exists, File}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tracing::{error, warn}; use crate::hardware::{device_type, DeviceType}; use crate::platform::platform_config; use crate::{path, write_synced}; const HWMON_PREFIX: &str = "/sys/class/hwmon"; const GPU_HWMON_NAME: &str = "amdgpu"; const CPU_PREFIX: &str = "/sys/devices/system/cpu/cpufreq"; const CPU0_NAME: &str = "policy0"; const CPU_POLICY_NAME: &str = "policy"; const GPU_POWER_PROFILE_SUFFIX: &str = "device/pp_power_profile_mode"; const GPU_PERFORMANCE_LEVEL_SUFFIX: &str = "device/power_dpm_force_performance_level"; const GPU_CLOCKS_SUFFIX: &str = "device/pp_od_clk_voltage"; const GPU_CLOCK_LEVELS_SUFFIX: &str = "device/pp_dpm_sclk"; const CPU_SCALING_GOVERNOR_SUFFIX: &str = "scaling_governor"; const CPU_SCALING_AVAILABLE_GOVERNORS_SUFFIX: &str = "scaling_available_governors"; const PLATFORM_PROFILE_PREFIX: &str = "/sys/class/platform-profile"; const TDP_LIMIT1: &str = "power1_cap"; const TDP_LIMIT2: &str = "power2_cap"; lazy_static! { static ref GPU_POWER_PROFILE_REGEX: Regex = Regex::new(r"^\s*(?[0-9]+)\s+(?[0-9A-Za-z_]+)(?\*)?").unwrap(); static ref GPU_CLOCK_LEVELS_REGEX: Regex = Regex::new(r"^\s*(?[0-9]+): (?[0-9]+)Mhz").unwrap(); } #[derive(Display, EnumString, PartialEq, Debug, Copy, Clone, TryFromPrimitive)] #[strum(serialize_all = "snake_case")] #[repr(u32)] pub enum GPUPowerProfile { // Currently firmware exposes these values, though // deck doesn't support them yet #[strum(serialize = "3d_full_screen")] FullScreen = 1, Video = 3, VR = 4, Compute = 5, Custom = 6, // Currently only capped and uncapped are supported on // deck hardware/firmware. Add more later as needed Capped = 8, Uncapped = 9, } #[derive(Display, EnumString, PartialEq, Debug, Copy, Clone)] #[strum(serialize_all = "snake_case")] pub enum GPUPerformanceLevel { Auto, Low, High, Manual, ProfilePeak, } #[derive(Display, EnumString, Hash, Eq, PartialEq, Debug, Copy, Clone)] #[strum(serialize_all = "lowercase")] pub enum CPUScalingGovernor { Conservative, OnDemand, UserSpace, PowerSave, Performance, SchedUtil, } #[derive(Display, EnumString, VariantNames, PartialEq, Debug, Copy, Clone)] #[strum(serialize_all = "snake_case")] pub enum TdpLimitingMethod { GpuHwmon, LenovoWmi, } #[derive(Debug)] pub(crate) struct GpuHwmonTdpLimitManager {} #[derive(Debug)] pub(crate) struct LenovoWmiTdpLimitManager {} #[async_trait] pub(crate) trait TdpLimitManager: Send + Sync { async fn get_tdp_limit(&self) -> Result; async fn set_tdp_limit(&self, limit: u32) -> Result<()>; async fn get_tdp_limit_range(&self) -> Result>; } pub(crate) async fn tdp_limit_manager() -> Result> { let config = platform_config().await?; let config = config .as_ref() .and_then(|config| config.tdp_limit.as_ref()) .ok_or(anyhow!("No TDP limit configured"))?; Ok(match config.method { TdpLimitingMethod::LenovoWmi => Box::new(LenovoWmiTdpLimitManager {}), TdpLimitingMethod::GpuHwmon => Box::new(GpuHwmonTdpLimitManager {}), }) } async fn read_gpu_sysfs_contents>(suffix: S) -> Result { // Read a given suffix for the GPU let base = find_hwmon(GPU_HWMON_NAME).await?; fs::read_to_string(base.join(suffix.as_ref())) .await .map_err(|message| anyhow!("Error opening sysfs file for reading {message}")) } async fn write_gpu_sysfs_contents>(suffix: S, data: &[u8]) -> Result<()> { let base = find_hwmon(GPU_HWMON_NAME).await?; write_synced(base.join(suffix), data) .await .inspect_err(|message| error!("Error writing to sysfs file: {message}")) } async fn read_cpu_sysfs_contents>(suffix: S) -> Result { let base = path(CPU_PREFIX).join(CPU0_NAME); fs::read_to_string(base.join(suffix.as_ref())) .await .map_err(|message| anyhow!("Error opening sysfs file for reading {message}")) } async fn write_cpu_governor_sysfs_contents(contents: String) -> Result<()> { // Iterate over all policyX paths let mut dir = fs::read_dir(path(CPU_PREFIX)).await?; let mut wrote_stuff = false; loop { let Some(entry) = dir.next_entry().await? else { ensure!( wrote_stuff, "No data written, unable to find any policyX sysfs paths" ); return Ok(()); }; let file_name = entry .file_name() .into_string() .map_err(|_| anyhow!("Unable to convert path to string"))?; if !file_name.starts_with(CPU_POLICY_NAME) { continue; } let base = entry.path(); // Write contents to each one wrote_stuff = true; write_synced(base.join(CPU_SCALING_GOVERNOR_SUFFIX), contents.as_bytes()) .await .inspect_err(|message| error!("Error writing to sysfs file: {message}"))?; } } pub(crate) async fn get_gpu_power_profile() -> Result { // check which profile is current and return if possible let contents = read_gpu_sysfs_contents(GPU_POWER_PROFILE_SUFFIX).await?; // NOTE: We don't filter based on deck here because the sysfs // firmware support setting the value to no-op values. let lines = contents.lines(); for line in lines { let Some(caps) = GPU_POWER_PROFILE_REGEX.captures(line) else { continue; }; let name = &caps["name"].to_lowercase(); if caps.name("active").is_some() { match GPUPowerProfile::from_str(name.as_str()) { Ok(v) => { return Ok(v); } Err(e) => bail!("Unable to parse value for GPU power profile: {e}"), } } } bail!("Unable to determine current GPU power profile"); } pub(crate) async fn get_available_gpu_power_profiles() -> Result> { let contents = read_gpu_sysfs_contents(GPU_POWER_PROFILE_SUFFIX).await?; let deck = device_type().await.unwrap_or_default() == DeviceType::SteamDeck; let mut map = Vec::new(); let lines = contents.lines(); for line in lines { let Some(caps) = GPU_POWER_PROFILE_REGEX.captures(line) else { continue; }; let value: u32 = caps["value"] .parse() .map_err(|message| anyhow!("Unable to parse value for GPU power profile: {message}"))?; let name = &caps["name"]; if deck { // Deck is designed to operate in one of the CAPPED or UNCAPPED power profiles, // the other profiles aren't correctly tuned for the hardware. if value == GPUPowerProfile::Capped as u32 || value == GPUPowerProfile::Uncapped as u32 { map.push((value, name.to_string())); } else { // Got unsupported value, so don't include it } } else { // Do basic validation to ensure our enum is up to date? map.push((value, name.to_string())); } } Ok(map) } pub(crate) async fn set_gpu_power_profile(value: GPUPowerProfile) -> Result<()> { let profile = (value as u32).to_string(); write_gpu_sysfs_contents(GPU_POWER_PROFILE_SUFFIX, profile.as_bytes()).await } pub(crate) async fn get_available_gpu_performance_levels() -> Result> { let base = find_hwmon(GPU_HWMON_NAME).await?; if try_exists(base.join(GPU_PERFORMANCE_LEVEL_SUFFIX)).await? { Ok(vec![ GPUPerformanceLevel::Auto, GPUPerformanceLevel::Low, GPUPerformanceLevel::High, GPUPerformanceLevel::Manual, GPUPerformanceLevel::ProfilePeak, ]) } else { Ok(Vec::new()) } } pub(crate) async fn get_gpu_performance_level() -> Result { let level = read_gpu_sysfs_contents(GPU_PERFORMANCE_LEVEL_SUFFIX).await?; Ok(GPUPerformanceLevel::from_str(level.trim())?) } pub(crate) async fn set_gpu_performance_level(level: GPUPerformanceLevel) -> Result<()> { let level: String = level.to_string(); write_gpu_sysfs_contents(GPU_PERFORMANCE_LEVEL_SUFFIX, level.as_bytes()).await } pub(crate) async fn get_available_cpu_scaling_governors() -> Result> { let contents = read_cpu_sysfs_contents(CPU_SCALING_AVAILABLE_GOVERNORS_SUFFIX).await?; // Get the list of supported governors from cpu0 let mut result = Vec::new(); let words = contents.split_whitespace(); for word in words { match CPUScalingGovernor::from_str(word) { Ok(governor) => result.push(governor), Err(message) => warn!("Error parsing governor {message}"), } } Ok(result) } pub(crate) async fn get_cpu_scaling_governor() -> Result { // get the current governor from cpu0 (assume all others are the same) let contents = read_cpu_sysfs_contents(CPU_SCALING_GOVERNOR_SUFFIX).await?; let contents = contents.trim(); CPUScalingGovernor::from_str(contents).map_err(|message| { anyhow!( "Error converting CPU scaling governor sysfs file contents to enumeration: {message}" ) }) } pub(crate) async fn set_cpu_scaling_governor(governor: CPUScalingGovernor) -> Result<()> { // Set the given governor on all cpus let name = governor.to_string(); write_cpu_governor_sysfs_contents(name).await } pub(crate) async fn get_gpu_clocks_range() -> Result> { if let Some(range) = platform_config() .await? .as_ref() .and_then(|config| config.gpu_clocks) { return Ok(range.min..=range.max); } let contents = read_gpu_sysfs_contents(GPU_CLOCK_LEVELS_SUFFIX).await?; let lines = contents.lines(); let mut min = 1_000_000; let mut max = 0; for line in lines { let Some(caps) = GPU_CLOCK_LEVELS_REGEX.captures(line) else { continue; }; let value: u32 = caps["value"] .parse() .map_err(|message| anyhow!("Unable to parse value for GPU power profile: {message}"))?; if value < min { min = value; } if value > max { max = value; } } ensure!(min <= max, "Could not read any clocks"); Ok(min..=max) } pub(crate) async fn set_gpu_clocks(clocks: u32) -> Result<()> { // Set GPU clocks to given value valid // Only used when GPU Performance Level is manual, but write whenever called. let base = find_hwmon(GPU_HWMON_NAME).await?; let mut myfile = File::create(base.join(GPU_CLOCKS_SUFFIX)) .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(crate) async fn get_gpu_clocks() -> Result { let base = find_hwmon(GPU_HWMON_NAME).await?; let clocks_file = File::open(base.join(GPU_CLOCKS_SUFFIX)).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, }; return Ok(mhz.parse()?); } Ok(0) } async fn find_sysdir(prefix: impl AsRef, expected: &str) -> Result { let mut dir = fs::read_dir(prefix.as_ref()).await?; loop { let base = match dir.next_entry().await? { Some(entry) => entry.path(), None => bail!("prefix not found"), }; let file_name = base.join("name"); let name = fs::read_to_string(file_name.as_path()) .await? .trim() .to_string(); if name == expected { return Ok(base); } } } async fn find_hwmon(hwmon: &str) -> Result { find_sysdir(path(HWMON_PREFIX), hwmon).await } async fn find_platform_profile(name: &str) -> Result { find_sysdir(path(PLATFORM_PROFILE_PREFIX), name).await } #[async_trait] impl TdpLimitManager for GpuHwmonTdpLimitManager { async fn get_tdp_limit(&self) -> Result { let base = find_hwmon(GPU_HWMON_NAME).await?; let power1cap = fs::read_to_string(base.join(TDP_LIMIT1)).await?; let power1cap: u32 = power1cap.trim_end().parse()?; Ok(power1cap / 1_000_000) } async fn set_tdp_limit(&self, limit: u32) -> Result<()> { ensure!( self.get_tdp_limit_range().await?.contains(&limit), "Invalid limit" ); let data = format!("{limit}000000"); let base = find_hwmon(GPU_HWMON_NAME).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(()) } async fn get_tdp_limit_range(&self) -> Result> { let config = platform_config().await?; let config = config .as_ref() .and_then(|config| config.tdp_limit.as_ref()) .ok_or(anyhow!("No TDP limit configured"))?; if let Some(range) = config.range { return Ok(range.min..=range.max); } bail!("No TDP limit range configured"); } } impl LenovoWmiTdpLimitManager { const PREFIX: &str = "/sys/class/firmware-attributes/lenovo-wmi-other-0/attributes"; const SPL_SUFFIX: &str = "ppt_pl1_spl"; const SPPT_SUFFIX: &str = "ppt_pl2_sppt"; const FPPT_SUFFIX: &str = "ppt_pl3_fppt"; } #[async_trait] impl TdpLimitManager for LenovoWmiTdpLimitManager { async fn get_tdp_limit(&self) -> Result { let config = platform_config().await?; if let Some(config) = config .as_ref() .and_then(|config| config.performance_profile.as_ref()) { ensure!( get_platform_profile(&config.platform_profile_name).await? == "custom", "TDP limiting not active" ); } let base = path(Self::PREFIX); fs::read_to_string(base.join(Self::SPL_SUFFIX).join("current_value")) .await .map_err(|message| anyhow!("Error reading sysfs: {message}"))? .trim() .parse() .map_err(|e| anyhow!("Error parsing value: {e}")) } async fn set_tdp_limit(&self, limit: u32) -> Result<()> { ensure!( self.get_tdp_limit_range().await?.contains(&limit), "Invalid limit" ); let limit = limit.to_string(); let base = path(Self::PREFIX); write_synced( base.join(Self::SPL_SUFFIX).join("current_value"), limit.as_bytes(), ) .await .inspect_err(|message| error!("Error writing to sysfs file: {message}"))?; write_synced( base.join(Self::SPPT_SUFFIX).join("current_value"), limit.as_bytes(), ) .await .inspect_err(|message| error!("Error writing to sysfs file: {message}"))?; write_synced( base.join(Self::FPPT_SUFFIX).join("current_value"), limit.as_bytes(), ) .await .inspect_err(|message| error!("Error writing to sysfs file: {message}")) } async fn get_tdp_limit_range(&self) -> Result> { let base = path(Self::PREFIX).join(Self::SPL_SUFFIX); let min: u32 = fs::read_to_string(base.join("min_value")) .await .map_err(|message| anyhow!("Error reading sysfs: {message}"))? .trim() .parse() .map_err(|e| anyhow!("Error parsing value: {e}"))?; let max: u32 = fs::read_to_string(base.join("max_value")) .await .map_err(|message| anyhow!("Error reading sysfs: {message}"))? .trim() .parse() .map_err(|e| anyhow!("Error parsing value: {e}"))?; Ok(min..=max) } } pub(crate) async fn get_max_charge_level() -> Result { let config = platform_config().await?; let config = config .as_ref() .and_then(|config| config.battery_charge_limit.as_ref()) .ok_or(anyhow!("No battery charge limit configured"))?; let base = find_hwmon(config.hwmon_name.as_str()).await?; fs::read_to_string(base.join(config.attribute.as_str())) .await .map_err(|message| anyhow!("Error reading sysfs: {message}"))? .trim() .parse() .map_err(|e| anyhow!("Error parsing value: {e}")) } pub(crate) async fn set_max_charge_level(limit: i32) -> Result<()> { ensure!((0..=100).contains(&limit), "Invalid limit"); let data = limit.to_string(); let config = platform_config().await?; let config = config .as_ref() .and_then(|config| config.battery_charge_limit.as_ref()) .ok_or(anyhow!("No battery charge limit configured"))?; let base = find_hwmon(config.hwmon_name.as_str()).await?; write_synced(base.join(config.attribute.as_str()), data.as_bytes()) .await .inspect_err(|message| error!("Error writing to sysfs file: {message}")) } pub(crate) async fn get_available_platform_profiles(name: &str) -> Result> { let base = find_platform_profile(name).await?; Ok(fs::read_to_string(base.join("choices")) .await .map_err(|message| anyhow!("Error reading sysfs: {message}"))? .trim() .split(' ') .map(ToString::to_string) .collect()) } pub(crate) async fn get_platform_profile(name: &str) -> Result { let base = find_platform_profile(name).await?; Ok(fs::read_to_string(base.join("profile")) .await .map_err(|message| anyhow!("Error reading sysfs: {message}"))? .trim() .to_string()) } pub(crate) async fn set_platform_profile(name: &str, profile: &str) -> Result<()> { let base = find_platform_profile(name).await?; fs::write(base.join("profile"), profile.as_bytes()) .await .map_err(|message| anyhow!("Error writing to sysfs: {message}")) } #[cfg(test)] pub(crate) mod test { use super::*; use crate::hardware::test::fake_model; use crate::hardware::SteamDeckVariant; use crate::platform::{BatteryChargeLimitConfig, PlatformConfig, RangeConfig, TdpLimitConfig}; use crate::{enum_roundtrip, testing}; use anyhow::anyhow; use tokio::fs::{create_dir_all, read_to_string, remove_dir, write}; pub async fn setup() -> Result<()> { // Use hwmon5 just as a test. We needed a subfolder of HWMON_PREFIX // and this is as good as any. let base = path(HWMON_PREFIX).join("hwmon5"); let filename = base.join(GPU_PERFORMANCE_LEVEL_SUFFIX); // Creates hwmon path, including device subpath create_dir_all(filename.parent().unwrap()).await?; // Writes name file as addgpu so find_hwmon() will find it. write_synced(base.join("name"), GPU_HWMON_NAME.as_bytes()).await?; Ok(()) } pub async fn create_nodes() -> Result<()> { setup().await?; let base = find_hwmon(GPU_HWMON_NAME).await?; let filename = base.join(GPU_PERFORMANCE_LEVEL_SUFFIX); write(filename.as_path(), "auto\n").await?; let filename = base.join(GPU_POWER_PROFILE_SUFFIX); let contents = " 1 3D_FULL_SCREEN 3 VIDEO* 4 VR 5 COMPUTE 6 CUSTOM 8 CAPPED 9 UNCAPPED"; write(filename.as_path(), contents).await?; let filename = base.join(TDP_LIMIT1); write(filename.as_path(), "15000000\n").await?; let base = path(HWMON_PREFIX).join("hwmon6"); create_dir_all(&base).await?; write(base.join("name"), "steamdeck_hwmon\n").await?; write(base.join("max_battery_charge_level"), "10\n").await?; let base = path(PLATFORM_PROFILE_PREFIX).join("platform-profile0"); create_dir_all(&base).await?; write_synced(base.join("name"), b"power-driver\n").await?; write_synced(base.join("choices"), b"a b c\n").await?; Ok(()) } pub async fn write_clocks(mhz: u32) { let base = find_hwmon(GPU_HWMON_NAME).await.unwrap(); let filename = base.join(GPU_CLOCKS_SUFFIX); 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 read_clocks() -> Result { let base = find_hwmon(GPU_HWMON_NAME).await.unwrap(); read_to_string(base.join(GPU_CLOCKS_SUFFIX)).await } pub fn format_clocks(mhz: u32) -> String { format!("s 0 {mhz}\ns 1 {mhz}\nc\n") } #[tokio::test] async fn test_get_gpu_performance_level() { let _h = testing::start(); setup().await.expect("setup"); let base = find_hwmon(GPU_HWMON_NAME).await.unwrap(); let filename = base.join(GPU_PERFORMANCE_LEVEL_SUFFIX); 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(), "profile_peak\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(); setup().await.expect("setup"); let base = find_hwmon(GPU_HWMON_NAME).await.unwrap(); let filename = base.join(GPU_PERFORMANCE_LEVEL_SUFFIX); 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(), "profile_peak" ); } #[tokio::test] async fn test_gpu_hwmon_get_tdp_limit() { let handle = testing::start(); let mut platform_config = PlatformConfig::default(); platform_config.tdp_limit = Some(TdpLimitConfig { method: TdpLimitingMethod::GpuHwmon, range: Some(RangeConfig { min: 3, max: 15 }), }); handle.test.platform_config.replace(Some(platform_config)); let manager = tdp_limit_manager().await.unwrap(); setup().await.expect("setup"); let hwmon = path(HWMON_PREFIX); assert!(manager.get_tdp_limit().await.is_err()); write(hwmon.join("hwmon5").join(TDP_LIMIT1), "15000000\n") .await .expect("write"); assert_eq!(manager.get_tdp_limit().await.unwrap(), 15); } #[tokio::test] async fn test_gpu_hwmon_set_tdp_limit() { let handle = testing::start(); let mut platform_config = PlatformConfig::default(); platform_config.tdp_limit = Some(TdpLimitConfig { method: TdpLimitingMethod::GpuHwmon, range: Some(RangeConfig { min: 3, max: 15 }), }); handle.test.platform_config.replace(Some(platform_config)); let manager = tdp_limit_manager().await.unwrap(); assert_eq!( manager.set_tdp_limit(2).await.unwrap_err().to_string(), anyhow!("Invalid limit").to_string() ); assert_eq!( manager.set_tdp_limit(20).await.unwrap_err().to_string(), anyhow!("Invalid limit").to_string() ); assert!(manager.set_tdp_limit(10).await.is_err()); let hwmon = path(HWMON_PREFIX); assert_eq!( manager.set_tdp_limit(10).await.unwrap_err().to_string(), anyhow!("No such file or directory (os error 2)").to_string() ); setup().await.expect("setup"); 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!( manager.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!(manager.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!(manager.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(); assert!(get_gpu_clocks().await.is_err()); setup().await.expect("setup"); let base = find_hwmon(GPU_HWMON_NAME).await.unwrap(); let filename = base.join(GPU_CLOCKS_SUFFIX); create_dir_all(filename.parent().unwrap()) .await .expect("create_dir_all"); write(filename.as_path(), b"").await.expect("write"); assert_eq!(get_gpu_clocks().await.unwrap(), 0); 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.expect("setup"); assert!(set_gpu_clocks(200).await.is_ok()); assert_eq!(read_clocks().await.unwrap(), format_clocks(200)); assert!(set_gpu_clocks(1600).await.is_ok()); assert_eq!(read_clocks().await.unwrap(), format_clocks(1600)); } #[tokio::test] async fn test_get_gpu_clocks_range() { let _h = testing::start(); setup().await.expect("setup"); let base = find_hwmon(GPU_HWMON_NAME).await.unwrap(); let filename = base.join(GPU_CLOCK_LEVELS_SUFFIX); create_dir_all(filename.parent().unwrap()) .await .expect("create_dir_all"); assert!(get_gpu_clocks_range().await.is_err()); write(filename.as_path(), &[] as &[u8; 0]) .await .expect("write"); assert!(get_gpu_clocks_range().await.is_err()); let contents = "0: 200Mhz * 1: 1100Mhz 2: 1600Mhz"; write(filename.as_path(), contents).await.expect("write"); assert_eq!(get_gpu_clocks_range().await.unwrap(), 200..=1600); let contents = "0: 1600Mhz * 1: 200Mhz 2: 1100Mhz"; write(filename.as_path(), contents).await.expect("write"); assert_eq!(get_gpu_clocks_range().await.unwrap(), 200..=1600); } #[test] fn gpu_power_profile_roundtrip() { enum_roundtrip!(GPUPowerProfile { 1: u32 = FullScreen, 3: u32 = Video, 4: u32 = VR, 5: u32 = Compute, 6: u32 = Custom, 8: u32 = Capped, 9: u32 = Uncapped, "3d_full_screen": str = FullScreen, "video": str = Video, "vr": str = VR, "compute": str = Compute, "custom": str = Custom, "capped": str = Capped, "uncapped": str = Uncapped, }); assert!(GPUPowerProfile::try_from(0).is_err()); assert!(GPUPowerProfile::try_from(2).is_err()); assert!(GPUPowerProfile::try_from(10).is_err()); assert!(GPUPowerProfile::from_str("fullscreen").is_err()); } #[test] fn cpu_governor_roundtrip() { enum_roundtrip!(CPUScalingGovernor { "conservative": str = Conservative, "ondemand": str = OnDemand, "userspace": str = UserSpace, "powersave": str = PowerSave, "performance": str = Performance, "schedutil": str = SchedUtil, }); assert!(CPUScalingGovernor::from_str("usersave").is_err()); } #[test] fn gpu_performance_level_roundtrip() { enum_roundtrip!(GPUPerformanceLevel { "auto": str = Auto, "low": str = Low, "high": str = High, "manual": str = Manual, "profile_peak": str = ProfilePeak, }); assert!(GPUPerformanceLevel::from_str("peak_performance").is_err()); } #[tokio::test] async fn read_power_profiles() { let _h = testing::start(); setup().await.expect("setup"); let base = find_hwmon(GPU_HWMON_NAME).await.unwrap(); let filename = base.join(GPU_POWER_PROFILE_SUFFIX); create_dir_all(filename.parent().unwrap()) .await .expect("create_dir_all"); let contents = " 1 3D_FULL_SCREEN 3 VIDEO* 4 VR 5 COMPUTE 6 CUSTOM 8 CAPPED 9 UNCAPPED"; write(filename.as_path(), contents).await.expect("write"); fake_model(SteamDeckVariant::Unknown) .await .expect("fake_model"); let profiles = get_available_gpu_power_profiles().await.expect("get"); assert_eq!( profiles, &[ ( GPUPowerProfile::FullScreen as u32, String::from("3D_FULL_SCREEN") ), (GPUPowerProfile::Video as u32, String::from("VIDEO")), (GPUPowerProfile::VR as u32, String::from("VR")), (GPUPowerProfile::Compute as u32, String::from("COMPUTE")), (GPUPowerProfile::Custom as u32, String::from("CUSTOM")), (GPUPowerProfile::Capped as u32, String::from("CAPPED")), (GPUPowerProfile::Uncapped as u32, String::from("UNCAPPED")) ] ); fake_model(SteamDeckVariant::Jupiter) .await .expect("fake_model"); let profiles = get_available_gpu_power_profiles().await.expect("get"); assert_eq!( profiles, &[ (GPUPowerProfile::Capped as u32, String::from("CAPPED")), (GPUPowerProfile::Uncapped as u32, String::from("UNCAPPED")) ] ); } #[tokio::test] async fn read_unknown_power_profiles() { let _h = testing::start(); setup().await.expect("setup"); let base = find_hwmon(GPU_HWMON_NAME).await.unwrap(); let filename = base.join(GPU_POWER_PROFILE_SUFFIX); create_dir_all(filename.parent().unwrap()) .await .expect("create_dir_all"); let contents = " 1 3D_FULL_SCREEN 2 CGA 3 VIDEO* 4 VR 5 COMPUTE 6 CUSTOM 8 CAPPED 9 UNCAPPED"; write(filename.as_path(), contents).await.expect("write"); fake_model(SteamDeckVariant::Unknown) .await .expect("fake_model"); let profiles = get_available_gpu_power_profiles().await.expect("get"); assert_eq!( profiles, &[ ( GPUPowerProfile::FullScreen as u32, String::from("3D_FULL_SCREEN") ), (2, String::from("CGA")), (GPUPowerProfile::Video as u32, String::from("VIDEO")), (GPUPowerProfile::VR as u32, String::from("VR")), (GPUPowerProfile::Compute as u32, String::from("COMPUTE")), (GPUPowerProfile::Custom as u32, String::from("CUSTOM")), (GPUPowerProfile::Capped as u32, String::from("CAPPED")), (GPUPowerProfile::Uncapped as u32, String::from("UNCAPPED")) ] ); fake_model(SteamDeckVariant::Jupiter) .await .expect("fake_model"); let profiles = get_available_gpu_power_profiles().await.expect("get"); assert_eq!( profiles, &[ (GPUPowerProfile::Capped as u32, String::from("CAPPED")), (GPUPowerProfile::Uncapped as u32, String::from("UNCAPPED")) ] ); } #[tokio::test] async fn read_power_profile() { let _h = testing::start(); setup().await.expect("setup"); let base = find_hwmon(GPU_HWMON_NAME).await.unwrap(); let filename = base.join(GPU_POWER_PROFILE_SUFFIX); create_dir_all(filename.parent().unwrap()) .await .expect("create_dir_all"); let contents = " 1 3D_FULL_SCREEN 3 VIDEO* 4 VR 5 COMPUTE 6 CUSTOM 8 CAPPED 9 UNCAPPED"; write(filename.as_path(), contents).await.expect("write"); fake_model(SteamDeckVariant::Unknown) .await .expect("fake_model"); assert_eq!( get_gpu_power_profile().await.expect("get"), GPUPowerProfile::Video ); fake_model(SteamDeckVariant::Jupiter) .await .expect("fake_model"); assert_eq!( get_gpu_power_profile().await.expect("get"), GPUPowerProfile::Video ); } #[tokio::test] async fn read_no_power_profile() { let _h = testing::start(); setup().await.expect("setup"); let base = find_hwmon(GPU_HWMON_NAME).await.unwrap(); let filename = base.join(GPU_POWER_PROFILE_SUFFIX); create_dir_all(filename.parent().unwrap()) .await .expect("create_dir_all"); let contents = " 1 3D_FULL_SCREEN 3 VIDEO 4 VR 5 COMPUTE 6 CUSTOM 8 CAPPED 9 UNCAPPED"; write(filename.as_path(), contents).await.expect("write"); fake_model(SteamDeckVariant::Unknown) .await .expect("fake_model"); assert!(get_gpu_power_profile().await.is_err()); fake_model(SteamDeckVariant::Jupiter) .await .expect("fake_model"); assert!(get_gpu_power_profile().await.is_err()); } #[tokio::test] async fn read_unknown_power_profile() { let _h = testing::start(); setup().await.expect("setup"); let base = find_hwmon(GPU_HWMON_NAME).await.unwrap(); let filename = base.join(GPU_POWER_PROFILE_SUFFIX); create_dir_all(filename.parent().unwrap()) .await .expect("create_dir_all"); let contents = " 1 3D_FULL_SCREEN 2 CGA* 3 VIDEO 4 VR 5 COMPUTE 6 CUSTOM 8 CAPPED 9 UNCAPPED"; write(filename.as_path(), contents).await.expect("write"); fake_model(SteamDeckVariant::Unknown) .await .expect("fake_model"); assert!(get_gpu_power_profile().await.is_err()); fake_model(SteamDeckVariant::Jupiter) .await .expect("fake_model"); assert!(get_gpu_power_profile().await.is_err()); } #[tokio::test] async fn read_cpu_available_governors() { let _h = testing::start(); let base = path(CPU_PREFIX).join(CPU0_NAME); create_dir_all(&base).await.expect("create_dir_all"); let contents = "conservative ondemand userspace powersave performance schedutil"; write(base.join(CPU_SCALING_AVAILABLE_GOVERNORS_SUFFIX), contents) .await .expect("write"); assert_eq!( get_available_cpu_scaling_governors().await.unwrap(), vec![ CPUScalingGovernor::Conservative, CPUScalingGovernor::OnDemand, CPUScalingGovernor::UserSpace, CPUScalingGovernor::PowerSave, CPUScalingGovernor::Performance, CPUScalingGovernor::SchedUtil ] ); } #[tokio::test] async fn read_invalid_cpu_available_governors() { let _h = testing::start(); let base = path(CPU_PREFIX).join(CPU0_NAME); create_dir_all(&base).await.expect("create_dir_all"); let contents = "conservative ondemand userspace rescascade powersave performance schedutil\n"; write(base.join(CPU_SCALING_AVAILABLE_GOVERNORS_SUFFIX), contents) .await .expect("write"); assert_eq!( get_available_cpu_scaling_governors().await.unwrap(), vec![ CPUScalingGovernor::Conservative, CPUScalingGovernor::OnDemand, CPUScalingGovernor::UserSpace, CPUScalingGovernor::PowerSave, CPUScalingGovernor::Performance, CPUScalingGovernor::SchedUtil ] ); } #[tokio::test] async fn read_cpu_governor() { let _h = testing::start(); let base = path(CPU_PREFIX).join(CPU0_NAME); create_dir_all(&base).await.expect("create_dir_all"); let contents = "ondemand\n"; write(base.join(CPU_SCALING_GOVERNOR_SUFFIX), contents) .await .expect("write"); assert_eq!( get_cpu_scaling_governor().await.unwrap(), CPUScalingGovernor::OnDemand ); } #[tokio::test] async fn read_invalid_cpu_governor() { let _h = testing::start(); let base = path(CPU_PREFIX).join(CPU0_NAME); create_dir_all(&base).await.expect("create_dir_all"); let contents = "rescascade\n"; write(base.join(CPU_SCALING_GOVERNOR_SUFFIX), contents) .await .expect("write"); assert!(get_cpu_scaling_governor().await.is_err()); } #[tokio::test] async fn read_max_charge_level() { let handle = testing::start(); let mut platform_config = PlatformConfig::default(); platform_config.battery_charge_limit = Some(BatteryChargeLimitConfig { suggested_minimum_limit: Some(10), hwmon_name: String::from("steamdeck_hwmon"), attribute: String::from("max_battery_charge_level"), }); handle.test.platform_config.replace(Some(platform_config)); let base = path(HWMON_PREFIX).join("hwmon6"); create_dir_all(&base).await.expect("create_dir_all"); write(base.join("name"), "steamdeck_hwmon\n") .await .expect("write"); assert_eq!( find_hwmon("steamdeck_hwmon").await.unwrap(), path(HWMON_PREFIX).join("hwmon6") ); write(base.join("max_battery_charge_level"), "10\n") .await .expect("write"); assert_eq!(get_max_charge_level().await.unwrap(), 10); set_max_charge_level(99).await.expect("set"); assert_eq!(get_max_charge_level().await.unwrap(), 99); set_max_charge_level(0).await.expect("set"); assert_eq!(get_max_charge_level().await.unwrap(), 0); assert!(set_max_charge_level(101).await.is_err()); assert!(set_max_charge_level(-1).await.is_err()); } #[tokio::test] async fn read_available_performance_profiles() { let _h = testing::start(); assert!(get_available_platform_profiles("power-driver") .await .is_err()); let base = path(PLATFORM_PROFILE_PREFIX).join("platform-profile0"); create_dir_all(&base).await.unwrap(); assert!(get_available_platform_profiles("power-driver") .await .is_err()); write_synced(base.join("name"), b"power-driver\n") .await .unwrap(); assert!(get_available_platform_profiles("power-driver") .await .is_err()); write_synced(base.join("choices"), b"a b c\n") .await .unwrap(); assert_eq!( get_available_platform_profiles("power-driver") .await .unwrap(), &["a", "b", "c"] ); } }