blob: 02a6783e87581a92afff8835835bd5cfb0929638 [file] [log] [blame]
// Copyright 2021 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::{bail, Result};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use common;
#[derive(Debug)]
pub struct DeviceGPUConfigParams {
max_freq_path: PathBuf,
turbo_freq_path: PathBuf,
power_limit_path: PathBuf,
// power_limit_thr contains a mapping of power limits to corresponding frequency ranges.
// Field contains a vector of (power limit, GPU max freq) pairs.
power_limit_thr: Vec<(u32, u32)>,
}
// TODO: Distinguish RO vs R/W.
enum SupportedPaths {
GpuMax,
GpuTurbo,
PowerLimitCurrent,
}
/// Starts a thread that continuously monitors power limits and adjusts gpu frequency accordingly.
///
/// This function will spawn a thread that will periodically poll the power limit and adjust
/// GPU frequency as needed. The thread can be terminated by the caller by setting the game_mode_on
/// atomic bool to false. There can be a delay of up to 1x polling_freq_ms to clean up the thread.
///
/// # Arguments
///
/// * `game_mode_on` - A shared atomic bool with the caller that controls when the
/// newly spawned thread will terminate.
///
/// * `polling_freq_ms` - The polling frequency to use for reading the power limit val.
/// 1000ms can be used as a default.
///
/// # Return
///
/// Result enum indicated successful creation of thread.
pub fn init_gpu_scaling_thread(game_mode_on: Arc<AtomicBool>, polling_freq_ms: u64) -> Result<()> {
let config = init_gpu_params()?;
let mut last_pl_val: u32 = 15000000;
thread::spawn(move || {
let mut consecutive_fails: u32 = 0;
while game_mode_on.load(Ordering::Relaxed) && consecutive_fails < 100 {
println!("Game mode ON");
let last_pl_buf = evaluate_gpu_frequency(&config, last_pl_val);
match last_pl_buf {
Ok(pl) => {
last_pl_val = pl;
consecutive_fails = 0;
}
Err(_) => consecutive_fails += 1,
}
thread::sleep(Duration::from_millis(polling_freq_ms));
}
if consecutive_fails >= 100 {
println!("Gpu scaling failed");
}
cleanup_gpu_scaling();
println!("Game mode is off");
});
Ok(())
}
fn cleanup_gpu_scaling() -> Result<()> {
println!("Cleanup and reset to defaults");
// TODO: Needs implementation and sysfs values for default GPU min, max, turbo.
Ok(())
}
/// Stateless function to check the power limit and adjust GPU frequency range.
///
/// This function is called periodically to check if GPU frequency range needs
/// to be adjusted. Providing a last_pl_val allows it to check for deltas in
/// power limit.
///
/// # Arguments
///
/// * `config` - Device-specifc configuration files to use.
///
/// * `last_pl_val` - Previously buffered power limit value.
///
/// # Return u32 value
///
/// Returns the current power limit. Can be buffered and sent in the subsequent call.
pub fn evaluate_gpu_frequency(config: &DeviceGPUConfigParams, last_pl_val: u32) -> Result<u32> {
let current_pl = get_sysfs_val(&config, SupportedPaths::PowerLimitCurrent)?;
println!("Last PL =\t {}\nCurrent PL =\t {}", last_pl_val, current_pl);
if current_pl == last_pl_val {
// No change in powerlimit since last check, no action needed.
return Ok(current_pl);
}
let mut prev_thr = 0;
let mut requested_gpu_freq = 0;
let mut last_bucket = false;
// This loop will iterate over the power limit threshold to GPU max frequency mappings
// provided in the config param. If the current_pl falls within the range of the any 2
// threshold ranges (prev_thr and pl_thr), attempt to assign the corresponding max GPU freq.
// Additionally checks to see if the previously buffered power_limit value (last_pl_val) was
// in a different bucket compared to the current power limit value (current_pl).
for (i, &(pl_thr, gpu_max)) in config.power_limit_thr.iter().enumerate() {
println!(
"Checking config: pl_thr = {}, gpu_max = {}",
pl_thr, gpu_max
);
if i == 0 {
if current_pl >= pl_thr {
// This case should never happen, but assign Max in case.
requested_gpu_freq = gpu_max;
break;
} else {
prev_thr = pl_thr.clone();
}
continue;
} else if i == (config.power_limit_thr.len() - 1) {
last_bucket = true;
}
// If threshold is within a bucket range or below the min bucket.
if prev_thr >= current_pl && current_pl > pl_thr || (last_bucket && current_pl < pl_thr) {
println!("Current pl thr in bucket = {}", gpu_max);
// This check might be unnecessary. Read guard on write protects unnecessary override already.
if prev_thr > last_pl_val && last_pl_val > pl_thr
|| (last_bucket && last_pl_val < pl_thr)
{
// Still in same bucket, no change in GPU freq.
return Ok(current_pl);
}
// PL is in new threshold bucket, adjust max freq.
requested_gpu_freq = gpu_max;
break;
}
}
// TODO: Compare against GPU_MIN+buffer instead of 0. Need to read GPU min from sysfs.
// This block will assign a new GPU max if needed.
if requested_gpu_freq > 0
&& requested_gpu_freq != get_sysfs_val(&config, SupportedPaths::GpuMax)?
{
println!("Setting GPU max to {}", requested_gpu_freq);
// For the initial version, gpu_max = turbo.
set_sysfs_val(&config, SupportedPaths::GpuMax, requested_gpu_freq)?;
set_sysfs_val(&config, SupportedPaths::GpuTurbo, requested_gpu_freq)?;
}
Ok(current_pl)
}
/// Gathers and bundles device-specific configurations related to GPU frequency and power limit.
pub fn init_gpu_params() -> Result<DeviceGPUConfigParams> {
let config = DeviceGPUConfigParams {
max_freq_path: PathBuf::from("/sys/class/drm/card0/gt_max_freq_mhz"),
turbo_freq_path: PathBuf::from("/sys/class/drm/card0/gt_boost_freq_mhz"),
power_limit_path: PathBuf::from(
"/sys/class/powercap/intel-rapl/intel-rapl:0/constraint_0_power_limit_uw",
),
//TODO: power_limit_thr must be guaranteed pre-sorted
// Need to get TDP max (/sys/class/powercap/intel-rapl/intel-rapl:0/constraint_0_max_power_uw),
// GPU max/min from sysfs; or read tuning from a device config file
power_limit_thr: vec![
(15000000, 1000),
(14500000, 900),
(13500000, 800),
(12500000, 700),
(10000000, 650),
],
};
Ok(config)
}
fn get_supported_path(
config: &DeviceGPUConfigParams,
path_type: SupportedPaths,
) -> Result<PathBuf> {
let path;
match path_type {
SupportedPaths::GpuMax => path = &config.max_freq_path,
SupportedPaths::GpuTurbo => path = &config.turbo_freq_path,
SupportedPaths::PowerLimitCurrent => path = &config.power_limit_path,
_ => bail!("Unsupported path type."),
}
Ok(PathBuf::from(path))
}
// TODO: Move the R/W functions to traits to allow mocking and stubbing for unit testing.
fn get_sysfs_val(config: &DeviceGPUConfigParams, path_type: SupportedPaths) -> Result<u32> {
let path_buf = get_supported_path(&config, path_type)?;
Ok(common::read_file_to_u64(path_buf.as_path())? as u32)
}
fn set_sysfs_val(
config: &DeviceGPUConfigParams,
path_type: SupportedPaths,
val: u32,
) -> Result<()> {
let path_buf = get_supported_path(&config, path_type)?;
let path = path_buf.as_path();
match Path::new(path).exists() {
true => {
fs::write(path, val.to_string())?;
Ok(())
}
false => bail!("Could not write to path: {:?}", path.to_str()),
}
}