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 {