Add gpu power profile(s) properties.

Add profiles property to give back available
profiles on this device.
Add profile property to get and set current gpu
power profile.
Filter possible properties when on deck.
Get current profile based on position of * character.
Get card0 path based on which cardX has vendor 0x1002
Add some basic tests.
TODO:
Possibly cache available properties instead of fetching repeatedly.
This commit is contained in:
Jeremy Whiting 2024-05-31 14:39:34 -06:00
parent 8c1baa152b
commit 0759ff7077
6 changed files with 261 additions and 4 deletions

View file

@ -277,6 +277,26 @@
--> -->
<property name="HdmiCecState" type="u" access="readwrite"/> <property name="HdmiCecState" type="u" access="readwrite"/>
<!--
GpuPowerProfiles:
Enumerate the supported gpu power profiles available on the system.
A list of supported profiles (a dictionary of values to names)
Version available: 9
-->
<property name="GpuPowerProfiles" type="a{us}" access="read"/>
<!--
GpuPowerProfile:
The current gpu power profile. Valid values come from GpuPowerProfiles property.
Version available: 9
-->
<property name="GpuPowerProfile" type="u" access="readwrite"/>
</interface> </interface>
<!-- <!--

View file

@ -115,6 +115,13 @@ pub(crate) async fn variant() -> Result<HardwareVariant> {
HardwareVariant::from_str(board_name.trim_end()) HardwareVariant::from_str(board_name.trim_end())
} }
pub(crate) async fn is_deck() -> Result<bool> {
match variant().await {
Ok(variant) => Ok(variant != HardwareVariant::Unknown),
Err(e) => Err(e),
}
}
pub(crate) async fn check_support() -> Result<HardwareCurrentlySupported> { pub(crate) async fn check_support() -> Result<HardwareCurrentlySupported> {
// Run jupiter-check-support note this script does exit 1 for "Support: No" case // Run jupiter-check-support note this script does exit 1 for "Support: No" case
// so no need to parse output, etc. // so no need to parse output, etc.

View file

@ -31,7 +31,7 @@ pub mod wifi;
#[cfg(test)] #[cfg(test)]
mod testing; mod testing;
const API_VERSION: u32 = 8; const API_VERSION: u32 = 9;
pub trait Service pub trait Service
where where

View file

@ -14,7 +14,10 @@ use zbus::{fdo, interface, Connection, SignalContext};
use crate::error::{to_zbus_error, to_zbus_fdo_error}; use crate::error::{to_zbus_error, to_zbus_fdo_error};
use crate::hardware::{variant, FanControl, FanControlState, HardwareVariant}; use crate::hardware::{variant, FanControl, FanControlState, HardwareVariant};
use crate::power::{set_gpu_clocks, set_gpu_performance_level, set_tdp_limit, GPUPerformanceLevel}; use crate::power::{
set_gpu_clocks, set_gpu_performance_level, set_gpu_power_profile, set_tdp_limit,
GPUPerformanceLevel, GPUPowerProfile,
};
use crate::process::{run_script, script_output, ProcessManager}; use crate::process::{run_script, script_output, ProcessManager};
use crate::wifi::{ use crate::wifi::{
set_wifi_backend, set_wifi_debug_mode, set_wifi_power_management_state, WifiBackend, set_wifi_backend, set_wifi_debug_mode, set_wifi_power_management_state, WifiBackend,
@ -173,6 +176,14 @@ impl SteamOSManager {
.await .await
} }
async fn set_gpu_power_profile(&self, value: u32) -> fdo::Result<()> {
let profile = GPUPowerProfile::try_from(value).map_err(to_zbus_fdo_error)?;
set_gpu_power_profile(profile)
.await
.inspect_err(|message| error!("Error setting GPU power profile: {message}"))
.map_err(to_zbus_fdo_error)
}
async fn set_gpu_performance_level(&self, level: u32) -> fdo::Result<()> { async fn set_gpu_performance_level(&self, level: u32) -> fdo::Result<()> {
let level = match GPUPerformanceLevel::try_from(level) { let level = match GPUPerformanceLevel::try_from(level) {
Ok(level) => level, Ok(level) => level,

View file

@ -7,6 +7,7 @@
*/ */
use anyhow::Result; use anyhow::Result;
use std::collections::HashMap;
use tracing::error; use tracing::error;
use zbus::proxy::Builder; use zbus::proxy::Builder;
use zbus::zvariant::Fd; use zbus::zvariant::Fd;
@ -15,7 +16,10 @@ use zbus::{fdo, interface, Connection, Proxy, SignalContext};
use crate::cec::{HdmiCecControl, HdmiCecState}; use crate::cec::{HdmiCecControl, HdmiCecState};
use crate::error::{to_zbus_error, to_zbus_fdo_error, zbus_to_zbus_fdo}; use crate::error::{to_zbus_error, to_zbus_fdo_error, zbus_to_zbus_fdo};
use crate::hardware::check_support; use crate::hardware::check_support;
use crate::power::{get_gpu_clocks, get_gpu_performance_level, get_tdp_limit}; use crate::power::{
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::wifi::{get_wifi_backend, get_wifi_power_management_state};
use crate::API_VERSION; use crate::API_VERSION;
@ -178,6 +182,30 @@ impl SteamOSManager {
method!(self, "FormatDevice", device, label, validate) method!(self, "FormatDevice", device, label, validate)
} }
#[zbus(property(emits_changed_signal = "false"))]
async fn gpu_power_profiles(&self) -> fdo::Result<HashMap<u32, String>> {
get_gpu_power_profiles().await.map_err(to_zbus_fdo_error)
}
#[zbus(property(emits_changed_signal = "false"))]
async fn gpu_power_profile(&self) -> fdo::Result<u32> {
match get_gpu_power_profile().await {
Ok(profile) => Ok(profile as u32),
Err(e) => {
error!("Error getting GPU power profile: {e}");
Err(to_zbus_fdo_error(e))
}
}
}
#[zbus(property)]
async fn set_gpu_power_profile(&self, profile: u32) -> zbus::Result<()> {
self.proxy
.call("SetGpuPowerProfile", &(profile))
.await
.map_err(to_zbus_error)
}
#[zbus(property(emits_changed_signal = "false"))] #[zbus(property(emits_changed_signal = "false"))]
async fn gpu_performance_level(&self) -> fdo::Result<u32> { async fn gpu_performance_level(&self) -> fdo::Result<u32> {
match get_gpu_performance_level().await { match get_gpu_performance_level().await {

View file

@ -5,7 +5,8 @@
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
use anyhow::{bail, ensure, Error, Result}; use anyhow::{anyhow, bail, ensure, Error, Result};
use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
@ -13,17 +14,83 @@ use tokio::fs::{self, File};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tracing::error; use tracing::error;
use crate::hardware::is_deck;
use crate::{path, write_synced}; use crate::{path, write_synced};
const GPU_HWMON_PREFIX: &str = "/sys/class/hwmon"; const GPU_HWMON_PREFIX: &str = "/sys/class/hwmon";
const GPU_HWMON_NAME: &str = "amdgpu"; const GPU_HWMON_NAME: &str = "amdgpu";
const GPU_DRM_PREFIX: &str = "/sys/class/drm";
const GPU_VENDOR: &str = "0x1002";
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_PERFORMANCE_LEVEL_SUFFIX: &str = "device/power_dpm_force_performance_level";
const GPU_CLOCKS_SUFFIX: &str = "device/pp_od_clk_voltage"; const GPU_CLOCKS_SUFFIX: &str = "device/pp_od_clk_voltage";
const TDP_LIMIT1: &str = "power1_cap"; const TDP_LIMIT1: &str = "power1_cap";
const TDP_LIMIT2: &str = "power2_cap"; const TDP_LIMIT2: &str = "power2_cap";
#[derive(PartialEq, Debug, Copy, Clone)]
#[repr(u32)]
pub enum GPUPowerProfile {
// Currently firmware exposes these values, though
// deck doesn't support them yet
FullScreen = 1, // 3D_FULL_SCREEN
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,
}
impl TryFrom<u32> for GPUPowerProfile {
type Error = &'static str;
fn try_from(v: u32) -> Result<Self, Self::Error> {
match v {
x if x == GPUPowerProfile::FullScreen as u32 => Ok(GPUPowerProfile::FullScreen),
x if x == GPUPowerProfile::Video as u32 => Ok(GPUPowerProfile::Video),
x if x == GPUPowerProfile::VR as u32 => Ok(GPUPowerProfile::VR),
x if x == GPUPowerProfile::Compute as u32 => Ok(GPUPowerProfile::Compute),
x if x == GPUPowerProfile::Custom as u32 => Ok(GPUPowerProfile::Custom),
x if x == GPUPowerProfile::Capped as u32 => Ok(GPUPowerProfile::Capped),
x if x == GPUPowerProfile::Uncapped as u32 => Ok(GPUPowerProfile::Uncapped),
_ => Err("No GPUPowerProfile for value"),
}
}
}
impl FromStr for GPUPowerProfile {
type Err = Error;
fn from_str(input: &str) -> Result<GPUPowerProfile, Self::Err> {
Ok(match input.to_lowercase().as_str() {
"3d_full_screen" => GPUPowerProfile::FullScreen,
"video" => GPUPowerProfile::Video,
"vr" => GPUPowerProfile::VR,
"compute" => GPUPowerProfile::Compute,
"custom" => GPUPowerProfile::Custom,
"capped" => GPUPowerProfile::Capped,
"uncapped" => GPUPowerProfile::Uncapped,
_ => bail!("No match for value {input}"),
})
}
}
impl fmt::Display for GPUPowerProfile {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
GPUPowerProfile::FullScreen => write!(f, "3d_full_screen"),
GPUPowerProfile::Video => write!(f, "video"),
GPUPowerProfile::VR => write!(f, "vr"),
GPUPowerProfile::Compute => write!(f, "compute"),
GPUPowerProfile::Custom => write!(f, "custom"),
GPUPowerProfile::Capped => write!(f, "capped"),
GPUPowerProfile::Uncapped => write!(f, "uncapped"),
}
}
}
#[derive(PartialEq, Debug, Copy, Clone)] #[derive(PartialEq, Debug, Copy, Clone)]
#[repr(u32)] #[repr(u32)]
pub enum GPUPerformanceLevel { pub enum GPUPerformanceLevel {
@ -76,6 +143,88 @@ impl fmt::Display for GPUPerformanceLevel {
} }
} }
async fn read_gpu_sysfs_contents() -> Result<String> {
// check which profile is current and return if possible
let base = find_gpu_prefix().await?;
fs::read_to_string(base.join(GPU_POWER_PROFILE_SUFFIX))
.await
.map_err(|message| anyhow!("Error opening sysfs file for reading {message}"))
}
pub(crate) async fn get_gpu_power_profile() -> Result<GPUPowerProfile> {
// check which profile is current and return if possible
let contents = read_gpu_sysfs_contents().await?;
// NOTE: We don't filter based on is_deck here because the sysfs
// firmware support setting the value to no-op values.
let lines = contents.lines();
for line in lines {
let mut words = line.split_whitespace();
let value: u32 = match words.next() {
Some(v) => v
.parse()
.map_err(|message| anyhow!("Unable to parse value from sysfs {message}"))?,
None => bail!("Unable to get value from sysfs"),
};
let name = match words.next() {
Some(v) => v.to_string(),
None => bail!("Unable to get name from sysfs"),
};
if name.ends_with('*') {
match GPUPowerProfile::try_from(value) {
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_gpu_power_profiles() -> Result<HashMap<u32, String>> {
let contents = read_gpu_sysfs_contents().await?;
let deck = is_deck().await?;
let mut map = HashMap::new();
let lines = contents.lines();
for line in lines {
let mut words = line.split_whitespace();
let value: u32 = match words.next() {
Some(v) => v
.parse()
.map_err(|message| anyhow!("Unable to parse value from sysfs {message}"))?,
None => bail!("Unable to get value from sysfs"),
};
let name = match words.next() {
Some(v) => v.to_string().replace('*', ""),
None => bail!("Unable to get name from sysfs"),
};
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.insert(value, name);
} else {
// Got unsupported value, so don't include it
}
} else {
// Do basic validation to ensure our enum is up to date?
map.insert(value, name);
}
}
Ok(map)
}
pub(crate) async fn set_gpu_power_profile(value: GPUPowerProfile) -> Result<()> {
let profile = (value as u32).to_string();
let base = find_gpu_prefix().await?;
write_synced(base.join(GPU_POWER_PROFILE_SUFFIX), profile.as_bytes())
.await
.inspect_err(|message| error!("Error writing to sysfs file: {message}"))
}
pub(crate) async fn get_gpu_performance_level() -> Result<GPUPerformanceLevel> { pub(crate) async fn get_gpu_performance_level() -> Result<GPUPerformanceLevel> {
let base = find_hwmon().await?; let base = find_hwmon().await?;
let level = fs::read_to_string(base.join(GPU_PERFORMANCE_LEVEL_SUFFIX)) let level = fs::read_to_string(base.join(GPU_PERFORMANCE_LEVEL_SUFFIX))
@ -153,6 +302,24 @@ pub(crate) async fn get_gpu_clocks() -> Result<u32> {
Ok(0) Ok(0)
} }
async fn find_gpu_prefix() -> Result<PathBuf> {
let mut dir = fs::read_dir(path(GPU_DRM_PREFIX)).await?;
loop {
let base = match dir.next_entry().await? {
Some(entry) => entry.path(),
None => bail!("GPU node not found"),
};
let file_name = base.join("device").join("vendor");
let vendor = fs::read_to_string(file_name.as_path())
.await?
.trim()
.to_string();
if vendor == GPU_VENDOR {
return Ok(base);
}
}
}
async fn find_hwmon() -> Result<PathBuf> { async fn find_hwmon() -> Result<PathBuf> {
let mut dir = fs::read_dir(path(GPU_HWMON_PREFIX)).await?; let mut dir = fs::read_dir(path(GPU_HWMON_PREFIX)).await?;
loop { loop {
@ -455,6 +622,30 @@ CCLK_RANGE in Core0:
assert_eq!(read_clocks().await.unwrap(), format_clocks(1600)); assert_eq!(read_clocks().await.unwrap(), format_clocks(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] #[test]
fn gpu_performance_level_roundtrip() { fn gpu_performance_level_roundtrip() {
enum_roundtrip!(GPUPerformanceLevel { enum_roundtrip!(GPUPerformanceLevel {