Implemented Power Profile management

This commit is contained in:
Ilya Zlobintsev 2020-11-20 10:54:24 +02:00
parent cdb977fd54
commit e58663f7a9
7 changed files with 430 additions and 217 deletions

View File

@ -48,6 +48,7 @@ fn main() {
println!("GPU Model: {}", gpu_info.card_model);
println!("Driver in use: {}", gpu_info.driver);
print!("VBIOS Version: {}", gpu_info.vbios_version);
println!("{:?}", gpu_info);
},
Opt::StartFanControl { gpu_id } => {
println!("{:?}", d.start_fan_control(gpu_id));

View File

@ -1,6 +1,8 @@
use serde::{Deserialize, Serialize};
use std::{collections::{BTreeMap, HashMap}, fs, io, path::PathBuf};
use crate::gpu_controller::PowerProfile;
#[derive(Debug)]
pub enum ConfigError {
IoError(io::Error),
@ -38,6 +40,7 @@ pub struct GpuConfig {
pub fan_control_enabled: bool,
pub fan_curve: BTreeMap<i32, f64>,
pub power_cap: i32,
pub power_profile: PowerProfile,
}
impl GpuConfig {
@ -53,6 +56,7 @@ impl GpuConfig {
fan_curve,
fan_control_enabled: false,
power_cap: -1,
power_profile: PowerProfile::Auto,
}
}
}

View File

@ -1,4 +1,4 @@
use crate::{Action, DaemonError, DaemonResponse, SOCK_PATH, gpu_controller::GpuInfo, gpu_controller::{FanControlInfo, GpuStats}};
use crate::{Action, DaemonError, DaemonResponse, gpu_controller::PowerProfile, SOCK_PATH, gpu_controller::GpuInfo, gpu_controller::{FanControlInfo, GpuStats}};
use std::{collections::HashMap, os::unix::net::UnixStream}; use std::{ collections::BTreeMap, io::{Read, Write}, };
#[derive(Clone, Copy)]
@ -181,6 +181,22 @@ impl DaemonConnection {
}
}
pub fn set_power_profile(&self, gpu_id: u32, profile: PowerProfile) -> Result<(), DaemonError> {
let mut s = UnixStream::connect(SOCK_PATH).unwrap();
s.write_all(&bincode::serialize(&Action::SetPowerProfile(gpu_id, profile)).unwrap())
.unwrap();
s.shutdown(std::net::Shutdown::Write).expect("Could not shut down");
let mut buffer = Vec::<u8>::new();
s.read_to_end(&mut buffer).unwrap();
let result: Result<DaemonResponse, DaemonError> = bincode::deserialize(&buffer).unwrap();
match result {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
}
pub fn get_gpus(&self) -> Result<HashMap<u32, String>, DaemonError> {
log::trace!("sending request");
let mut s = UnixStream::connect(SOCK_PATH).unwrap();

View File

@ -5,6 +5,42 @@ use std::path::{Path, PathBuf};
use std::{collections::BTreeMap, fs};
use vulkano::instance::{Instance, InstanceExtensions, PhysicalDevice};
#[derive(Serialize, Deserialize, Debug)]
pub enum GpuControllerError {
NotSupported,
ParseError,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum PowerProfile {
Auto,
Low,
High,
}
impl Default for PowerProfile {
fn default() -> Self { PowerProfile::Auto }
}
impl PowerProfile {
pub fn from_str(profile: &str) -> Result<Self, GpuControllerError> {
match profile {
"auto" | "Automatic" => Ok(PowerProfile::Auto),
"high" | "Highest Clocks" => Ok(PowerProfile::High),
"low" | "Lowest Clocks" => Ok(PowerProfile::Low),
_ => Err(GpuControllerError::ParseError),
}
}
pub fn to_string(&self) -> String {
match self {
PowerProfile::Auto => "auto".to_string(),
PowerProfile::High => "high".to_string(),
PowerProfile::Low => "low".to_string(),
}
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GpuStats {
pub mem_used: u64,
@ -29,7 +65,7 @@ pub struct FanControlInfo {
pub struct GpuController {
pub hw_path: PathBuf,
hw_mon: Option<HWMon>,
pub gpu_info: GpuInfo,
//pub gpu_info: GpuInfo,
config: GpuConfig,
}
@ -55,32 +91,19 @@ pub struct GpuInfo {
pub link_width: u8,
pub vulkan_info: VulkanInfo,
pub pci_slot: String,
pub power_profile: PowerProfile,
}
impl GpuController {
pub fn new(hw_path: PathBuf, config: GpuConfig) -> Self {
let hw_mon = match fs::read_dir(&hw_path.join("hwmon")) {
Ok(mut path) => {
let path = path.next().unwrap().unwrap().path();
let hw_mon = HWMon::new(
&path,
config.fan_control_enabled,
config.fan_curve.clone(),
config.power_cap,
);
Some(hw_mon)
},
_ => None,
};
let mut controller = GpuController {
hw_path: hw_path.clone(),
hw_mon,
gpu_info: Default::default(),
config,
hw_mon: None,
config: GpuConfig::new(),
};
controller.gpu_info = controller.get_info();
log::trace!("{:?}", controller.gpu_info);
controller.load_config(config);
controller
}
@ -99,6 +122,7 @@ impl GpuController {
_ => None,
};
self.set_power_profile(config.power_profile).unwrap();
}
pub fn get_config(&self) -> GpuConfig {
@ -106,14 +130,15 @@ impl GpuController {
}
pub fn get_identifier(&self) -> GpuIdentifier {
GpuIdentifier { pci_id: self.gpu_info.pci_slot.clone(),
card_model: self.gpu_info.card_model.clone(),
gpu_model: self.gpu_info.gpu_model.clone(),
let gpu_info = self.get_info();
GpuIdentifier { pci_id: gpu_info.pci_slot.clone(),
card_model: gpu_info.card_model.clone(),
gpu_model: gpu_info.gpu_model.clone(),
path: self.hw_path.clone() }
}
fn get_info(&self) -> GpuInfo {
pub fn get_info(&self) -> GpuInfo {
let uevent =
fs::read_to_string(self.hw_path.join("uevent")).expect("Failed to read uevent");
@ -213,6 +238,11 @@ impl GpuController {
let vulkan_info = GpuController::get_vulkan_info(&model_id);
let power_profile = match self.get_power_profile() {
Ok(p) => p,
Err(_) => PowerProfile::Auto,
};
GpuInfo {
gpu_vendor: vendor,
gpu_model: model,
@ -227,6 +257,7 @@ impl GpuController {
link_width,
vulkan_info,
pci_slot,
power_profile,
}
}
@ -336,6 +367,25 @@ impl GpuController {
}
}
fn get_power_profile(&self) -> Result<PowerProfile, GpuControllerError> {
match fs::read_to_string(self.hw_path.join("power_dpm_force_performance_level")) {
Ok(s) => {
Ok(PowerProfile::from_str(&s.trim()).unwrap())
},
Err(_) => Err(GpuControllerError::NotSupported),
}
}
pub fn set_power_profile(&mut self, profile: PowerProfile) -> Result<(), GpuControllerError> {
match fs::write(self.hw_path.join("power_dpm_force_performance_level"), profile.to_string()) {
Ok(_) => {
self.config.power_profile = profile;
Ok(())
},
Err(_) => Err(GpuControllerError::NotSupported),
}
}
fn get_vulkan_info(pci_id: &str) -> VulkanInfo {
let mut device_name = String::from("Not supported");
let mut api_version = String::new();

View File

@ -4,6 +4,7 @@ pub mod gpu_controller;
pub mod hw_mon;
use config::{Config, GpuConfig};
use gpu_controller::PowerProfile;
use serde::{Deserialize, Serialize};
use std::{collections::{BTreeMap, HashMap}, fs};
use std::os::unix::net::{UnixListener, UnixStream};
@ -37,6 +38,7 @@ pub enum Action {
SetFanCurve(u32, BTreeMap<i32, f64>),
SetPowerCap(u32, i32),
GetPowerCap(u32),
SetPowerProfile(u32, PowerProfile),
Shutdown,
}
@ -87,9 +89,10 @@ impl Daemon {
log::info!("Initializing {:?}", entry.path());
let mut controller = GpuController::new(entry.path().join("device"), GpuConfig::new());
let gpu_info = controller.get_info();
for (id, (gpu_identifier, gpu_config)) in &config.gpu_configs {
if controller.gpu_info.pci_slot == gpu_identifier.pci_id && controller.gpu_info.card_model == gpu_identifier.card_model && controller.gpu_info.gpu_model == gpu_identifier.gpu_model {
for (id, (gpu_identifier, gpu_config)) in &config.gpu_configs {
if gpu_info.pci_slot == gpu_identifier.pci_id && gpu_info.card_model == gpu_identifier.card_model && gpu_info.gpu_model == gpu_identifier.gpu_model {
controller.load_config(gpu_config.clone());
gpu_controllers.insert(id.clone(), controller);
log::info!("already known");
@ -106,7 +109,9 @@ impl Daemon {
}
}
}
config.save().unwrap();
if !unprivileged {
config.save().unwrap();
}
Daemon {
listener,
@ -149,8 +154,8 @@ impl Daemon {
Action::CheckAlive => Ok(DaemonResponse::OK),
Action::GetGpus => {
let mut gpus: HashMap<u32, String> = HashMap::new();
for controller in &self.gpu_controllers {
gpus.insert(*controller.0, controller.1.gpu_info.gpu_model.clone());
for (id, controller) in &self.gpu_controllers {
gpus.insert(*id, controller.get_info().gpu_model.clone());
}
Ok(DaemonResponse::Gpus(gpus))
},
@ -159,7 +164,7 @@ impl Daemon {
None => Err(DaemonError::InvalidID),
},
Action::GetInfo(i) => match self.gpu_controllers.get(&i) {
Some(controller) => Ok(DaemonResponse::GpuInfo(controller.gpu_info.clone())),
Some(controller) => Ok(DaemonResponse::GpuInfo(controller.get_info())),
None => Err(DaemonError::InvalidID),
},
Action::StartFanControl(i) => match self.gpu_controllers.get_mut(&i) {
@ -226,6 +231,19 @@ impl Daemon {
}
None => Err(DaemonError::InvalidID),
}
Action::SetPowerProfile(i, profile) => match self.gpu_controllers.get_mut(&i) {
Some(controller) => {
match controller.set_power_profile(profile) {
Ok(_) => {
self.config.gpu_configs.insert(i, (controller.get_identifier(), controller.get_config()));
self.config.save().unwrap();
Ok(DaemonResponse::OK)
},
Err(_) => Err(DaemonError::ControllerError)
}
},
None => Err(DaemonError::InvalidID),
}
Action::Shutdown => {
for (_, controller) in &mut self.gpu_controllers {
controller.stop_fan_control().expect("Failed to stop fan control");
@ -264,4 +282,5 @@ pub enum DaemonError {
ConnectionFailed,
InvalidID,
HWMonError,
ControllerError,
}

View File

@ -2,14 +2,24 @@ extern crate gdk;
extern crate gio;
extern crate gtk;
use daemon::{Daemon, daemon_connection::DaemonConnection};
use daemon::{daemon_connection::DaemonConnection, gpu_controller::PowerProfile, Daemon};
use gio::prelude::*;
use gtk::{Adjustment, Button, ButtonsType, ComboBoxText, DialogFlags, Frame, Label, LevelBar, MessageType, Switch, prelude::*};
use gtk::{
prelude::*, Adjustment, Button, ButtonsType, ComboBoxText, DialogFlags, Frame, Label, LevelBar,
MessageType, Switch,
};
use gtk::{Builder, MessageDialog, TextBuffer, Window};
use pango::EllipsizeMode;
use std::{collections::BTreeMap, env::args, fs, sync::{Arc, RwLock}, thread, time::Duration};
use std::{
collections::BTreeMap,
env::args,
fs,
sync::{Arc, RwLock},
thread,
time::Duration,
};
fn build_ui(application: &gtk::Application) {
let glade_src = include_str!("main_window.glade");
@ -28,7 +38,8 @@ fn build_ui(application: &gtk::Application) {
.get_object("vram_usage_label")
.expect("Couldn't get label");
let gpu_select_comboboxtext: ComboBoxText = builder.get_object("gpu_select_comboboxtext").unwrap();
let gpu_select_comboboxtext: ComboBoxText =
builder.get_object("gpu_select_comboboxtext").unwrap();
let gpu_clock_text_buffer: TextBuffer = builder.get_object("gpu_clock_text_buffer").unwrap();
@ -43,14 +54,19 @@ fn build_ui(application: &gtk::Application) {
let power_cap_label: Label = builder.get_object("power_cap_label").unwrap();
let apply_button: Button = builder.get_object("apply_button").unwrap();
let automatic_fan_control_switch: Switch =
builder.get_object("automatic_fan_control_switch").unwrap();
let fan_curve_frame: Frame = builder.get_object("fan_curve_frame").unwrap();
let gpu_power_adjustment: Adjustment = builder.get_object("gpu_power_adjustment").unwrap();
let power_profile_select_comboboxtext: ComboBoxText = builder
.get_object("power_profile_select_comboboxtext")
.unwrap();
let power_profile_description_label: Label = builder.get_object("power_profile_description_label").unwrap();
let mut unpriviliged: bool = false;
@ -91,30 +107,103 @@ fn build_ui(application: &gtk::Application) {
cell.set_property("ellipsize", &EllipsizeMode::End).unwrap();
}
let current_gpu_id = Arc::new(RwLock::new(0u32));
let current_gpu_id = Arc::new(RwLock::new(0u32));
let build = builder.clone();
let b = apply_button.clone();
gpu_power_adjustment.connect_value_changed(move |adjustment| {
println!("changed adjustment value to {}/{}", adjustment.get_value(), adjustment.get_upper());
println!(
"changed adjustment value to {}/{}",
adjustment.get_value(),
adjustment.get_upper()
);
b.set_sensitive(true);
power_cap_label.set_text(&format!("{}/{}", adjustment.get_value().floor(), adjustment.get_upper()));
power_cap_label.set_text(&format!(
"{}/{}",
adjustment.get_value().floor(),
adjustment.get_upper()
));
});
let b = apply_button.clone();
let description_label = power_profile_description_label.clone();
power_profile_select_comboboxtext.connect_changed(move |combobox| {
println!("power profile selection changed");
b.set_sensitive(true);
match combobox.get_active().unwrap() {
0 => description_label.set_text("Automatically adjust core and VRAM clocks. (Default)"),
1 => description_label.set_text("Always run the on the highest clocks."),
2 => description_label.set_text("Always run the on the lowest clocks."),
_ => unreachable!(),
}
});
let cur_id = current_gpu_id.clone();
let b = apply_button.clone();
gpu_select_comboboxtext.connect_changed(move |combobox| {
let mut current_gpu_id = cur_id.write().unwrap();
*current_gpu_id = combobox.get_active_id().unwrap().parse::<u32>().expect("invalid id");
*current_gpu_id = combobox
.get_active_id()
.unwrap()
.parse::<u32>()
.expect("invalid id");
println!("Set current gpu id to {}", current_gpu_id);
set_info(&build, d, current_gpu_id.clone());
b.set_sensitive(false);
});
let cur_id = current_gpu_id.clone();
let auto_fan_control_switch = automatic_fan_control_switch.clone();
apply_button.connect_clicked(move |b| {
let gpu_id = *cur_id.read().unwrap();
let mut curve: BTreeMap<i32, f64> = BTreeMap::new();
for i in 1..6 {
let curve_temperature_adjustment: Adjustment = builder
.get_object(&format!("curve_temperature_adjustment_{}", i))
.unwrap();
curve.insert(20 * i, curve_temperature_adjustment.get_value());
}
println!("setting curve to {:?}", curve);
d.set_fan_curve(gpu_id, curve).unwrap();
b.set_sensitive(false);
match auto_fan_control_switch.get_active() {
true => {
d.stop_fan_control(gpu_id).unwrap();
let diag = MessageDialog::new(
None::<&Window>,
DialogFlags::empty(),
MessageType::Error,
ButtonsType::Ok,
"WARNING: Due to a driver bug, the GPU fan may misbehave after switching to automatic control. You may need to reboot your system to avoid issues.",
);
diag.run();
diag.hide();
}
false => {
d.start_fan_control(gpu_id).unwrap();
}
}
let power_cap = gpu_power_adjustment.get_value().floor() as i32;
d.set_power_cap(gpu_id, power_cap).unwrap();
d.set_power_profile(gpu_id, PowerProfile::from_str(&power_profile_select_comboboxtext.get_active_text().unwrap()).unwrap()).unwrap();
set_info(&builder, d, gpu_id);
});
//gpu_select_comboboxtext.set_active_id(Some(&current_gpu_id.to_string()));
@ -159,7 +248,6 @@ fn build_ui(application: &gtk::Application) {
(gpu_stats.fan_speed as f64 / gpu_stats.max_fan_speed as f64 * 100 as f64) as i32
));
glib::Continue(true)
});
@ -179,8 +267,6 @@ fn build_ui(application: &gtk::Application) {
b.set_sensitive(true);
});
main_window.set_application(Some(application));
main_window.show();
@ -190,7 +276,9 @@ fn set_info(builder: &Builder, d: DaemonConnection, gpu_id: u32) {
let gpu_model_text_buffer: TextBuffer = builder
.get_object("gpu_model_text_buffer")
.expect("Couldn't get textbuffer");
let vbios_version_text_buffer: TextBuffer = builder .get_object("vbios_version_text_buffer") .expect("Couldn't get textbuffer");
let vbios_version_text_buffer: TextBuffer = builder
.get_object("vbios_version_text_buffer")
.expect("Couldn't get textbuffer");
let driver_text_buffer: TextBuffer = builder
.get_object("driver_text_buffer")
.expect("Couldn't get textbuffer");
@ -223,19 +311,23 @@ let vbios_version_text_buffer: TextBuffer = builder .get_object("vbios_version_t
builder.get_object("automatic_fan_control_switch").unwrap();
let fan_curve_frame: Frame = builder.get_object("fan_curve_frame").unwrap();
let gpu_power_adjustment: Adjustment = builder.get_object("gpu_power_adjustment").unwrap();
let apply_button: Button = builder.get_object("apply_button").unwrap();
let overclocking_info_frame: Frame = builder.get_object("overclocking_info_frame").unwrap();
let power_profile_select_comboboxtext: ComboBoxText = builder
.get_object("power_profile_select_comboboxtext")
.unwrap();
match fs::read_to_string("/proc/cmdline") {
Ok(cmdline) => {
if cmdline.contains("amdgpu.ppfeaturemask=") {
overclocking_info_frame.set_visible(false);
}
},
}
Err(_) => (),
}
@ -251,7 +343,15 @@ let vbios_version_text_buffer: TextBuffer = builder .get_object("vbios_version_t
&gpu_info.link_speed, &gpu_info.link_width
));
let vulkan_features = gpu_info.vulkan_info.features.replace(',', "\n").replace("Features", "").replace("{", "").replace("}", "").replace(" ", "").replace(":", ": ");
let vulkan_features = gpu_info
.vulkan_info
.features
.replace(',', "\n")
.replace("Features", "")
.replace("{", "")
.replace("}", "")
.replace(" ", "")
.replace(":", ": ");
vulkan_device_name_text_buffer.set_text(&gpu_info.vulkan_info.device_name);
vulkan_version_text_buffer.set_text(&gpu_info.vulkan_info.api_version);
@ -262,9 +362,14 @@ let vbios_version_text_buffer: TextBuffer = builder .get_object("vbios_version_t
gpu_power_adjustment.set_upper(power_cap_max as f64);
gpu_power_adjustment.set_value(power_cap as f64);
power_profile_select_comboboxtext.set_active(match &gpu_info.power_profile {
PowerProfile::Auto => Some(0),
PowerProfile::High => Some(1),
PowerProfile::Low => Some(2),
});
let fan_control = d.get_fan_control(gpu_id);
match fan_control {
Ok(ref fan_control) => {
if fan_control.enabled {
@ -276,73 +381,38 @@ let vbios_version_text_buffer: TextBuffer = builder .get_object("vbios_version_t
automatic_fan_control_switch.set_active(true);
fan_curve_frame.set_visible(false);
}
},
}
Err(_) => {
automatic_fan_control_switch.set_sensitive(false);
automatic_fan_control_switch.set_tooltip_text(Some("Unavailable"));
fan_curve_frame.set_visible(false);
}
}
match fan_control {
Ok(fan_control) => {
let curve: Arc<RwLock<BTreeMap<i32, f64>>> = Arc::new(RwLock::new(fan_control.curve));
//let curve: Arc<RwLock<BTreeMap<i32, f64>>> = Arc::new(RwLock::new(fan_control.curve));
for i in 1..6 {
let curve_temperature_adjustment: Adjustment = builder
.get_object(&format!("curve_temperature_adjustment_{}", i))
.unwrap();
let value = *curve
.read()
.unwrap()
let value = *fan_control.curve
.get(&(i * 20))
.expect("Could not get by index");
println!("Setting value {} on adjustment {}", value, i);
curve_temperature_adjustment.set_value(value);
let c = curve.clone();
let b = apply_button.clone();
curve_temperature_adjustment.connect_value_changed(move |adj| {
c.write().unwrap().insert(20 * i, adj.get_value());
curve_temperature_adjustment.connect_value_changed(move |_| {
b.set_sensitive(true);
});
}
apply_button.connect_clicked(move |b| {
//let current_gpu_id = *current_gpu_id.read().unwrap();
let curve = curve.read().unwrap().clone();
println!("setting curve to {:?}", curve);
d.set_fan_curve(gpu_id, curve).unwrap();
b.set_sensitive(false);
match automatic_fan_control_switch.get_active() {
true => {
d.stop_fan_control(gpu_id).unwrap();
let diag = MessageDialog::new(
None::<&Window>,
DialogFlags::empty(),
MessageType::Error,
ButtonsType::Ok,
"WARNING: Due to a driver bug, the GPU fan may misbehave after switching to automatic control. You may need to reboot your system to avoid issues.",
);
diag.run();
diag.hide();
}
false => {
d.start_fan_control(gpu_id).unwrap();
}
}
let power_cap = gpu_power_adjustment.get_value().floor() as i32;
d.set_power_cap(gpu_id, power_cap).unwrap();
});
},
}
Err(_) => (),
}
}

View File

@ -50,20 +50,6 @@
<object class="GtkTextBuffer" id="gpu_temp_text_buffer">
<property name="text" translatable="yes">100°C</property>
</object>
<object class="GtkListStore" id="gpus_list_store">
<columns>
<!-- column-name name -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0" translatable="yes">test</col>
</row>
<row>
<col id="0" translatable="yes">test1</col>
</row>
</data>
</object>
<object class="GtkTextBuffer" id="link_speed_text_buffer">
<property name="text" translatable="yes">1.0 GT/s PCIe x1</property>
</object>
@ -521,116 +507,6 @@
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="hexpand">True</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="label" translatable="yes">Power Usage</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkTextView">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-top">7</property>
<property name="editable">False</property>
<property name="cursor-visible">False</property>
<property name="buffer">gpu_power_text_buffer</property>
<property name="accepts-tab">False</property>
<property name="monospace">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label-xalign">0.10000000149011612</property>
<property name="shadow-type">none</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<child>
<object class="GtkLabel" id="power_cap_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">50/100</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScale" id="power_cap_scale">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="adjustment">gpu_power_adjustment</property>
<property name="restrict-to-fill-level">False</property>
<property name="fill-level">0</property>
<property name="round-digits">0</property>
<property name="digits">0</property>
<property name="draw-value">False</property>
<property name="value-pos">left</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Power Cap (Watts)</property>
<attributes>
<attribute name="weight" value="semibold"/>
</attributes>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<!-- n-columns=2 n-rows=2 -->
<object class="GtkGrid">
@ -788,12 +664,189 @@
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="hexpand">True</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="label" translatable="yes">Power Usage</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkTextView">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-top">7</property>
<property name="editable">False</property>
<property name="cursor-visible">False</property>
<property name="buffer">gpu_power_text_buffer</property>
<property name="accepts-tab">False</property>
<property name="monospace">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="label-xalign">0.10000000149011612</property>
<property name="shadow-type">none</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<child>
<object class="GtkLabel" id="power_cap_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">50/100</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScale" id="power_cap_scale">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="adjustment">gpu_power_adjustment</property>
<property name="restrict-to-fill-level">False</property>
<property name="fill-level">0</property>
<property name="round-digits">0</property>
<property name="digits">0</property>
<property name="draw-value">False</property>
<property name="value-pos">left</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Power Cap (Watts)</property>
<attributes>
<attribute name="weight" value="semibold"/>
</attributes>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label-xalign">0.10000000149011612</property>
<property name="shadow-type">none</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<child>
<object class="GtkComboBoxText" id="power_profile_select_comboboxtext">
<property name="visible">True</property>
<property name="can-focus">False</property>
<items>
<item translatable="yes">Automatic</item>
<item translatable="yes">Highest Clocks</item>
<item translatable="yes">Lowest Clocks</item>
</items>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="power_profile_description_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="label" translatable="yes">Description of the selected power profile</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Power Profile</property>
<attributes>
<attribute name="weight" value="semibold"/>
</attributes>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>
<property name="position">1</property>