diff --git a/com.steampowered.SteamOSManager1.xml b/com.steampowered.SteamOSManager1.xml index 226b3f7..09391a3 100644 --- a/com.steampowered.SteamOSManager1.xml +++ b/com.steampowered.SteamOSManager1.xml @@ -306,6 +306,27 @@ Version available: 9 --> + + + + + + diff --git a/src/manager/root.rs b/src/manager/root.rs index 8987228..cc99190 100644 --- a/src/manager/root.rs +++ b/src/manager/root.rs @@ -20,8 +20,8 @@ use crate::daemon::DaemonCommand; use crate::error::{to_zbus_error, to_zbus_fdo_error}; use crate::hardware::{variant, FanControl, FanControlState, HardwareVariant}; use crate::power::{ - set_gpu_clocks, set_gpu_performance_level, set_gpu_power_profile, set_tdp_limit, - GPUPerformanceLevel, GPUPowerProfile, + set_cpu_governor, set_gpu_clocks, set_gpu_performance_level, set_gpu_power_profile, + set_tdp_limit, CPUGovernor, GPUPerformanceLevel, GPUPowerProfile, }; use crate::process::{run_script, script_output, ProcessManager}; use crate::wifi::{ @@ -191,6 +191,14 @@ impl SteamOSManager { .map_err(to_zbus_fdo_error) } + async fn set_cpu_governor(&self, governor: u32) -> fdo::Result<()> { + let governor = CPUGovernor::try_from(governor).map_err(to_zbus_fdo_error)?; + set_cpu_governor(governor) + .await + .inspect_err(|message| error!("Error setting CPU governor: {message}")) + .map_err(to_zbus_fdo_error) + } + async fn set_gpu_performance_level(&self, level: u32) -> fdo::Result<()> { let level = match GPUPerformanceLevel::try_from(level) { Ok(level) => level, diff --git a/src/manager/user.rs b/src/manager/user.rs index ec0b1ee..1fd7863 100644 --- a/src/manager/user.rs +++ b/src/manager/user.rs @@ -20,8 +20,8 @@ use crate::daemon::DaemonCommand; use crate::error::{to_zbus_error, to_zbus_fdo_error, zbus_to_zbus_fdo}; use crate::hardware::check_support; use crate::power::{ - get_gpu_clocks, get_gpu_performance_level, get_gpu_power_profile, get_gpu_power_profiles, - get_tdp_limit, + get_cpu_governor, get_cpu_governors, get_gpu_clocks, get_gpu_performance_level, + get_gpu_power_profile, get_gpu_power_profiles, get_tdp_limit, }; use crate::wifi::{get_wifi_backend, get_wifi_power_management_state}; use crate::API_VERSION; @@ -191,6 +191,21 @@ impl SteamOSManager { method!(self, "FormatDevice", device, label, validate) } + #[zbus(property(emits_changed_signal = "false"))] + async fn cpu_governors(&self) -> fdo::Result> { + get_cpu_governors().await.map_err(to_zbus_fdo_error) + } + + #[zbus(property(emits_changed_signal = "false"))] + async fn cpu_governor(&self) -> fdo::Result { + get_cpu_governor().await.map_err(to_zbus_fdo_error) + } + + #[zbus(property)] + async fn set_cpu_governor(&self, governor: u32) -> zbus::Result<()> { + setter!(self, "SetCpuGovernor", &(governor)) + } + #[zbus(property(emits_changed_signal = "false"))] async fn gpu_power_profiles(&self) -> fdo::Result> { get_gpu_power_profiles().await.map_err(to_zbus_fdo_error) diff --git a/src/power.rs b/src/power.rs index 1e13c51..598fbd5 100644 --- a/src/power.rs +++ b/src/power.rs @@ -21,10 +21,16 @@ const GPU_HWMON_PREFIX: &str = "/sys/class/hwmon"; const GPU_HWMON_NAME: &str = "amdgpu"; const GPU_DRM_PREFIX: &str = "/sys/class/drm"; const GPU_VENDOR: &str = "0x1002"; +const CPU_GOVERNOR_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 CPU_GOVERNOR_SUFFIX: &str = "scaling_governor"; +const CPU_GOVERNORS_SUFFIX: &str = "scaling_available_governors"; const TDP_LIMIT1: &str = "power1_cap"; const TDP_LIMIT2: &str = "power2_cap"; @@ -143,6 +149,60 @@ impl fmt::Display for GPUPerformanceLevel { } } +#[derive(Hash, Eq, PartialEq, Debug, Copy, Clone)] +#[repr(u32)] +pub enum CPUGovernor { + Conservative, + OnDemand, + UserSpace, + PowerSave, + Performance, + SchedUtil, +} + +impl TryFrom for CPUGovernor { + type Error = &'static str; + fn try_from(v: u32) -> Result { + match v { + x if x == CPUGovernor::Conservative as u32 => Ok(CPUGovernor::Conservative), + x if x == CPUGovernor::OnDemand as u32 => Ok(CPUGovernor::OnDemand), + x if x == CPUGovernor::UserSpace as u32 => Ok(CPUGovernor::UserSpace), + x if x == CPUGovernor::PowerSave as u32 => Ok(CPUGovernor::PowerSave), + x if x == CPUGovernor::Performance as u32 => Ok(CPUGovernor::Performance), + x if x == CPUGovernor::SchedUtil as u32 => Ok(CPUGovernor::SchedUtil), + _ => Err("No enum match for value {v}"), + } + } +} + +impl FromStr for CPUGovernor { + type Err = Error; + fn from_str(input: &str) -> Result { + Ok(match input { + "conservative" => CPUGovernor::Conservative, + "ondemand" => CPUGovernor::OnDemand, + "userspace" => CPUGovernor::UserSpace, + "powersave" => CPUGovernor::PowerSave, + "performance" => CPUGovernor::Performance, + "schedutil" => CPUGovernor::SchedUtil, + v => bail!("No enum match for value {v}"), + }) + } +} + +impl fmt::Display for CPUGovernor { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CPUGovernor::Conservative => write!(f, "conservative"), + CPUGovernor::OnDemand => write!(f, "ondemand"), + CPUGovernor::UserSpace => write!(f, "userspace"), + CPUGovernor::PowerSave => write!(f, "powersave"), + CPUGovernor::Performance => write!(f, "performance"), + CPUGovernor::SchedUtil => write!(f, "schedutil"), + } + } +} + async fn read_gpu_sysfs_contents() -> Result { // check which profile is current and return if possible let base = find_gpu_prefix().await?; @@ -151,6 +211,64 @@ async fn read_gpu_sysfs_contents() -> Result { .map_err(|message| anyhow!("Error opening sysfs file for reading {message}")) } +async fn read_cpu_governor_sysfs_available_contents() -> Result { + let base = path(CPU_GOVERNOR_PREFIX); + fs::read_to_string(base.join(CPU0_NAME).join(CPU_GOVERNORS_SUFFIX)) + .await + .map_err(|message| anyhow!("Error opening sysfs file for reading {message}")) +} + +async fn read_cpu_governor_sysfs_contents() -> Result { + // Read contents of policy0 path + let base = path(CPU_GOVERNOR_PREFIX); + let full_path = base.join(CPU0_NAME).join(CPU_GOVERNOR_SUFFIX); + fs::read_to_string(full_path) + .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 cpuX paths + let mut dir = fs::read_dir(path(CPU_GOVERNOR_PREFIX)).await?; + let mut wrote_stuff = false; + loop { + let base = match dir.next_entry().await? { + Some(entry) => { + 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) { + entry.path() + } else { + // Not a policy path, so move on + continue; + } + } + None => { + if wrote_stuff { + return Ok(()); + } else { + bail!("No data written, unable to find any policyX sysfs paths") + } + } + }; + let file_name = base.join(CPU_GOVERNOR_SUFFIX); + println!( + "Trying to write to file at path: {:?}", + file_name.as_os_str() + ); + let mut myfile = File::create(file_name) + .await + .inspect_err(|message| error!("Error opening sysfs file for writing: {message}"))?; + + // Write contents to each one + wrote_stuff = true; + myfile.write_all(contents.as_bytes()).await?; + myfile.sync_data().await?; + } +} + pub(crate) async fn get_gpu_power_profile() -> Result { // check which profile is current and return if possible let contents = read_gpu_sysfs_contents().await?; @@ -242,6 +360,43 @@ pub(crate) async fn set_gpu_performance_level(level: GPUPerformanceLevel) -> Res .inspect_err(|message| error!("Error writing to sysfs file: {message}")) } +pub(crate) async fn get_cpu_governors() -> Result> { + let contents = read_cpu_governor_sysfs_available_contents().await?; + // Get the list of supported governors from cpu0 + let mut result = HashMap::new(); + + let words = contents.split_whitespace(); + for word in words { + match CPUGovernor::from_str(word) { + Ok(v) => { + result.insert(v as u32, word.to_string()); + } + Err(message) => bail!("Error parsing governor {message}"), + }; + } + + Ok(result) +} + +pub(crate) async fn get_cpu_governor() -> Result { + // get the current governor from cpu0 (assume all others are the same) + let contents = read_cpu_governor_sysfs_contents().await?; + + let governor = match CPUGovernor::from_str(contents.trim()) { + Ok(v) => v, + Err(_) => bail!("Error converting CPU governor sysfs file contents to enumeration"), + }; + + Ok(governor as u32) +} + +pub(crate) async fn set_cpu_governor(governor: CPUGovernor) -> Result<()> { + // Set the given governor on all cpus + let name = governor.to_string(); + write_cpu_governor_sysfs_contents(name).await?; + Ok(()) +} + pub(crate) 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. @@ -646,6 +801,26 @@ CCLK_RANGE in Core0: assert!(GPUPowerProfile::from_str("fullscreen").is_err()); } + #[test] + fn cpu_governor_roundtrip() { + enum_roundtrip!(CPUGovernor { + 0: u32 = Conservative, + 1: u32 = OnDemand, + 2: u32 = UserSpace, + 3: u32 = PowerSave, + 4: u32 = Performance, + 5: u32 = SchedUtil, + "conservative": str = Conservative, + "ondemand": str = OnDemand, + "userspace": str = UserSpace, + "powersave": str = PowerSave, + "performance": str = Performance, + "schedutil": str = SchedUtil, + }); + assert!(CPUGovernor::try_from(6).is_err()); + assert!(CPUGovernor::from_str("usersave").is_err()); + } + #[test] fn gpu_performance_level_roundtrip() { enum_roundtrip!(GPUPerformanceLevel {