feat: introduce SettingRow and refactor powerFrame with it (#1238)

* migrate setting_row

* migrate power_frame

* add exceptions

* refactor init

* rename performance_frame to power_profile_mode

* simplify performance_frame into amd_power_profile_mode

* small refactoring

* reset intel's power-cap when no value

* settingChanged now is triggered on value_changed (focus loss)

* fix heuristics comp init

* rework oc-page serialization

* comment

* get rid of deref

* remove redundant push

* simplfiy how we handle p-level

* replace tuples with struct for some interfaces

* remove settingValue trait

* merge *_value into *_control

* fix

* remove unneeded shutdown hook
This commit is contained in:
Roman Makarov authored and GitHub committed 2026-11-01 21:38:26 +02:00
1 parent 398f3736fb
commit 6c0e38597c
12 files changed
+1377 -649

No files matched your search

+12 -5
View File
@@ -473,6 +473,11 @@ impl IntelGpuController {
.map(|value| value as f64)
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn set_power_cap(&self, cap: f64) -> anyhow::Result<()> {
self.write_hwmon_file(POWER_CAP_NAMES, &((cap * 1_000_000.0) as u64).to_string())
}
#[allow(clippy::cast_precision_loss)]
fn get_power_cap_max(&self) -> Option<f64> {
self.read_hwmon_file(MAX_POWER_CAP_NAMES, true)
@@ -799,14 +804,16 @@ impl GpuController for IntelGpuController {
bail!("Specified cap {cap} is greated than maximum allowed {max_cap}");
}
self.write_hwmon_file(
POWER_CAP_NAMES,
&((cap * 1_000_000.0) as u64).to_string(),
)
.context("Could not set power cap")?;
self.set_power_cap(cap).context("Could not set power cap")?;
} else {
error!("could not apply power cap, no power limit functionality available");
}
} else if let Some(default_cap) = self.initial_power_cap
&& self.get_power_cap() != Some(default_cap)
{
self.set_power_cap(default_cap).with_context(|| {
format!("Failed to set power cap to default cap: {default_cap}")
})?;
}
if config.fan_control_enabled {
+14 -42
View File
@@ -1240,56 +1240,28 @@ impl AppModel {
) -> anyhow::Result<()> {
debug!("applying settings on gpu {gpu_id}");
let mut gpu_config = self
let stored_config = self
.daemon_client
.get_gpu_config(&gpu_id)
.await
.context("Could not get gpu config")?
.unwrap_or_else(GpuConfig::default);
let oc = self.oc_page.model().serialize(&stored_config);
let mut gpu_config = GpuConfig {
power_cap: oc.power.power_cap,
performance_level: oc.power.performance_level,
power_mizer_mode: oc.power.power_mizer_mode,
power_profile_mode_index: oc.power.power_profile_mode_index,
custom_power_profile_mode_hueristics: oc.power.custom_power_profile_mode_hueristics,
clocks_configuration: oc.clocks_configuration,
power_states: oc.power_states,
uma_carveout: oc.uma_carveout,
..stored_config
};
let cap = self.oc_page.model().get_power_cap();
if let Some(cap) = cap {
gpu_config.power_cap = Some(cap);
}
let performance_level = self.oc_page.model().get_performance_level();
if let Some(level) = performance_level {
gpu_config.performance_level = Some(level);
gpu_config.power_profile_mode_index = self.oc_page.model().get_power_profile_mode();
gpu_config.custom_power_profile_mode_hueristics = self
.oc_page
.model()
.get_power_profile_mode_custom_heuristics();
}
if let Some(mode) = self.oc_page.model().get_active_power_mizer_mode() {
gpu_config.power_mizer_mode = Some(mode);
}
// fixme: switch to serialize()
self.thermals_page.model().apply_config(&mut gpu_config);
self.oc_page
.model()
.apply_clocks_config(&mut gpu_config.clocks_configuration);
if self.device_driver == "nvidia" && !CONFIG.read().experimental_nvidia_pstate_offsets {
let clocks = &mut gpu_config.clocks_configuration;
clocks.gpu_clock_offsets.retain(|pstate, _| *pstate == 0);
clocks.mem_clock_offsets.retain(|pstate, _| *pstate == 0);
}
let enabled_power_states = self.oc_page.model().get_enabled_power_states();
gpu_config.power_states = enabled_power_states;
// Avoids applying the setting if it's the default but doesn't wipe it if it's unchanged in the UI
gpu_config.uma_carveout = self
.oc_page
.model()
.get_uma_carveout()
.map(|value| value as usize)
.or(gpu_config.uma_carveout);
let delay = self
.daemon_client
.set_gpu_config(&gpu_id, gpu_config)
+1
View File
@@ -8,3 +8,4 @@ pub(crate) mod info_row_level;
pub(crate) mod loader;
pub(crate) mod page_section;
pub(crate) mod page_section_expander;
pub(crate) mod setting_row;
+354
View File
@@ -0,0 +1,354 @@
mod adjustment_control;
mod boolean_control;
mod select_control;
use self::{
adjustment_control::AdjustmentControl, boolean_control::BooleanControl,
select_control::SelectControl,
};
#[expect(unused_imports, reason = "setting-row-migration")]
pub use self::{
adjustment_control::{AdjustmentControlMsg, AdjustmentInit},
boolean_control::BooleanInit,
select_control::{SelectInit, SelectOption},
};
use adw::prelude::*;
use relm4::{
Component, ComponentController, Controller, FactorySender, RelmWidgetExt, WidgetTemplate, css,
factory::FactoryComponent,
};
use std::{fmt::Debug, marker::PhantomData};
pub struct SettingRow<Key> {
pub control: SettingControl,
_key: PhantomData<Key>,
}
#[derive(Default)]
pub struct SettingRowInit {
pub title: String,
pub title_tooltip: String,
pub info_text: String,
pub label_group: Option<gtk::SizeGroup>,
pub input_group: Option<gtk::SizeGroup>,
pub control: SettingControlInit,
}
pub enum SettingControlInit {
Adjustment(AdjustmentInit),
Boolean(BooleanInit),
Select(SelectInit),
}
impl Default for SettingControlInit {
fn default() -> Self {
Self::Adjustment(AdjustmentInit::default())
}
}
impl From<AdjustmentInit> for SettingControlInit {
fn from(init: AdjustmentInit) -> Self {
Self::Adjustment(init)
}
}
impl From<BooleanInit> for SettingControlInit {
fn from(init: BooleanInit) -> Self {
Self::Boolean(init)
}
}
impl From<SelectInit> for SettingControlInit {
fn from(init: SelectInit) -> Self {
Self::Select(init)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SettingRowValue {
Numeric(f64),
Boolean(bool),
Selected(u32),
}
impl SettingRowValue {
pub fn as_numeric(self) -> Option<f64> {
match self {
Self::Numeric(value) => Some(value),
_ => None,
}
}
#[expect(dead_code, reason = "setting-row-migration")]
pub fn as_boolean(self) -> Option<bool> {
match self {
Self::Boolean(value) => Some(value),
_ => None,
}
}
pub fn as_selected(self) -> Option<u32> {
match self {
Self::Selected(value) => Some(value),
_ => None,
}
}
}
impl<Key> SettingRow<Key> {
/// Serialize the current value, omitting unset values and defaults.
pub fn serialize(&self) -> Option<SettingRowValue> {
match &self.control {
SettingControl::Adjustment(control) => {
control.model().serialize().map(SettingRowValue::Numeric)
}
SettingControl::Boolean(control) => {
control.model().serialize().map(SettingRowValue::Boolean)
}
SettingControl::Select(control) => {
control.model().serialize().map(SettingRowValue::Selected)
}
}
}
fn set_value(&self, value: SettingRowValue) {
match (&self.control, value) {
(SettingControl::Adjustment(control), SettingRowValue::Numeric(value)) => {
control.model().set_value(value)
}
(SettingControl::Boolean(control), SettingRowValue::Boolean(value)) => {
control.model().set_value(value)
}
(SettingControl::Select(control), SettingRowValue::Selected(value)) => {
control.model().set_value(value)
}
_ => panic!("setting value does not match the row's control"),
}
}
pub fn get_value(&self) -> SettingRowValue {
match &self.control {
SettingControl::Adjustment(control) => {
SettingRowValue::Numeric(control.model().get_value())
}
SettingControl::Boolean(control) => {
SettingRowValue::Boolean(control.model().get_value())
}
SettingControl::Select(control) => {
SettingRowValue::Selected(control.model().get_value())
}
}
}
}
pub struct ControlInit<T> {
pub init: T,
pub title: gtk::Box,
pub label_group: Option<gtk::SizeGroup>,
pub input_group: Option<gtk::SizeGroup>,
}
pub enum SettingControl {
Adjustment(Controller<AdjustmentControl>),
Boolean(Controller<BooleanControl>),
Select(Controller<SelectControl>),
}
impl SettingControl {
fn new<Key: Clone + 'static>(
init: SettingRowInit,
key: Key,
output: &relm4::Sender<(Key, SettingRowValue)>,
) -> Self {
let title = SettingTitle::init(SettingTitleInit {
title: init.title,
tooltip: init.title_tooltip,
info_text: init.info_text,
})
.root;
let label_group = init.label_group;
let input_group = init.input_group;
match init.control {
SettingControlInit::Adjustment(init) => Self::Adjustment(
AdjustmentControl::builder()
.launch(ControlInit {
init,
title,
label_group,
input_group,
})
.forward(output, move |value| {
(key.clone(), SettingRowValue::Numeric(value))
}),
),
SettingControlInit::Boolean(init) => Self::Boolean(
BooleanControl::builder()
.launch(ControlInit {
init,
title,
label_group,
input_group,
})
.forward(output, move |value| {
(key.clone(), SettingRowValue::Boolean(value))
}),
),
SettingControlInit::Select(init) => Self::Select(
SelectControl::builder()
.launch(ControlInit {
init,
title,
label_group,
input_group,
})
.forward(output, move |value| {
(key.clone(), SettingRowValue::Selected(value))
}),
),
}
}
fn widget(&self) -> &gtk::Box {
match self {
Self::Adjustment(control) => control.widget(),
Self::Boolean(control) => control.widget(),
Self::Select(control) => control.widget(),
}
}
/// Size groups also measure widgets inside hidden rows, so hidden rows leave their groups.
fn set_size_groups_enabled(&self, enabled: bool) {
let toggle = |size_group_widgets: &[(gtk::SizeGroup, gtk::Widget)]| {
for (group, widget) in size_group_widgets {
if enabled {
group.add_widget(widget);
} else {
group.remove_widget(widget);
}
}
};
match self {
Self::Adjustment(control) => toggle(&control.model().size_group_widgets),
Self::Boolean(control) => toggle(&control.model().size_group_widgets),
Self::Select(control) => toggle(&control.model().size_group_widgets),
}
}
fn reset(&self) {
match self {
Self::Adjustment(control) => control.model().reset(),
Self::Boolean(control) => control.model().reset(),
Self::Select(control) => control.model().reset(),
}
}
}
#[derive(Debug)]
pub enum SettingRowMsg {
Reset,
SetValue(SettingRowValue),
#[expect(dead_code, reason = "setting-row-migration")]
SetVisible(bool),
}
#[relm4::factory(pub)]
impl<Key: Clone + Debug + 'static> FactoryComponent for SettingRow<Key> {
type ParentWidget = gtk::ListBox;
type Index = Key;
type Init = SettingRowInit;
type Input = SettingRowMsg;
type Output = (Key, SettingRowValue);
type CommandOutput = ();
view! {
#[name = "root_row"]
gtk::ListBoxRow {
set_activatable: false,
set_selectable: false,
add_css_class: "setting-row",
add_css_class: match &self.control {
SettingControl::Adjustment(_) => "adjustment-row",
SettingControl::Boolean(_) => "boolean-row",
SettingControl::Select(_) => "select-row",
},
set_child: Some(self.control.widget()),
},
}
fn init_model(init: Self::Init, index: &Self::Index, sender: FactorySender<Self>) -> Self {
Self {
control: SettingControl::new(init, index.clone(), sender.output_sender()),
_key: PhantomData,
}
}
fn update_with_view(
&mut self,
widgets: &mut Self::Widgets,
msg: Self::Input,
_sender: FactorySender<Self>,
) {
match msg {
SettingRowMsg::Reset => self.control.reset(),
SettingRowMsg::SetValue(value) => self.set_value(value),
SettingRowMsg::SetVisible(visible) => {
if widgets.root_row.get_visible() != visible {
self.control.set_size_groups_enabled(visible);
widgets.root_row.set_visible(visible);
}
}
}
}
}
struct SettingTitleInit {
title: String,
tooltip: String,
info_text: String,
}
#[relm4::widget_template]
impl WidgetTemplate for SettingTitle {
type Init = SettingTitleInit;
view! {
#[name = "root"]
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_valign: gtk::Align::Center,
set_hexpand: true,
set_spacing: 5,
gtk::Label {
set_valign: gtk::Align::Center,
set_xalign: 0.0,
set_markup: &init.title,
set_tooltip_text: (!init.tooltip.is_empty()).then_some(init.tooltip.as_str()),
},
gtk::Box {
add_css_class: css::CAPTION,
set_valign: gtk::Align::End,
gtk::MenuButton {
set_visible: !init.info_text.is_empty(),
add_css_class: css::FLAT,
#[wrap(Some)]
set_popover = &gtk::Popover {
gtk::Label {
set_label: &init.info_text,
set_margin_all: 5,
set_wrap: true,
set_wrap_mode: gtk::pango::WrapMode::Word,
set_max_width_chars: 55,
},
},
#[wrap(Some)]
set_child = &gtk::Image {
set_icon_name: Some("info-outline-symbolic"),
set_pixel_size: 12,
},
},
},
},
}
}
@@ -0,0 +1,206 @@
use super::ControlInit;
use crate::app::utils::{ext::make_event_controller_no_scroll, formatting::fmt_value_with_unit};
use adw::prelude::*;
use relm4::{Component, ComponentParts, ComponentSender, css};
pub struct AdjustmentInit {
pub unit: String,
pub value: f64,
pub default_value: Option<f64>,
pub lower: f64,
pub upper: f64,
pub step_increment: f64,
pub page_increment: f64,
pub lower_label_group: Option<gtk::SizeGroup>,
pub upper_label_group: Option<gtk::SizeGroup>,
}
impl Default for AdjustmentInit {
fn default() -> Self {
Self {
unit: String::new(),
value: 0.0,
default_value: None,
lower: 0.0,
upper: 0.0,
step_increment: 1.0,
page_increment: 10.0,
lower_label_group: None,
upper_label_group: None,
}
}
}
pub struct AdjustmentControl {
adjustment: gtk::Adjustment,
default_value: Option<f64>,
value_ratio: f64,
title: gtk::Box,
unit: String,
pub(super) size_group_widgets: Vec<(gtk::SizeGroup, gtk::Widget)>,
}
#[derive(Debug)]
pub enum AdjustmentControlMsg {
Changed,
#[expect(dead_code, reason = "setting-row-migration")]
SetValueRatio(f64),
}
#[relm4::component(pub)]
impl Component for AdjustmentControl {
type Init = ControlInit<AdjustmentInit>;
type Input = AdjustmentControlMsg;
type Output = f64;
type CommandOutput = ();
view! {
#[root]
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
gtk::Box {
set_spacing: 12,
append: &model.title,
#[name = "spinbutton"]
gtk::SpinButton {
set_adjustment: &model.adjustment,
set_valign: gtk::Align::Center,
add_controller = make_event_controller_no_scroll(),
},
},
gtk::Box {
set_spacing: 12,
#[name = "lower_label"]
gtk::Label {
set_label: &fmt_value_with_unit(model.adjustment.lower(), &model.unit),
add_css_class: css::CAPTION,
add_css_class: css::DIM_LABEL,
},
#[name = "scale"]
gtk::Scale {
set_adjustment: &model.adjustment,
set_orientation: gtk::Orientation::Horizontal,
set_hexpand: true,
set_digits: 0,
set_round_digits: 0,
set_value_pos: gtk::PositionType::Right,
set_width_request: 100,
add_controller = make_event_controller_no_scroll(),
},
#[name = "upper_label"]
gtk::Label {
set_label: &fmt_value_with_unit(model.adjustment.upper(), &model.unit),
add_css_class: css::CAPTION,
add_css_class: css::DIM_LABEL,
},
},
},
#[local_ref]
adjustment -> gtk::Adjustment {
connect_value_changed => AdjustmentControlMsg::Changed @ value_change_signal,
},
}
fn init(
ControlInit {
init,
title,
label_group,
input_group,
}: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let mut model = Self {
adjustment: gtk::Adjustment::new(
init.value,
init.lower,
init.upper,
init.step_increment,
init.page_increment,
0.0,
),
default_value: init.default_value,
value_ratio: 1.0,
title,
unit: init.unit,
size_group_widgets: Vec::new(),
};
let adjustment = &model.adjustment;
let widgets = view_output!();
for (group, widget) in [
(label_group, model.title.clone().upcast::<gtk::Widget>()),
(input_group, widgets.spinbutton.clone().upcast()),
(init.lower_label_group, widgets.lower_label.clone().upcast()),
(init.upper_label_group, widgets.upper_label.clone().upcast()),
] {
if let Some(group) = group {
group.add_widget(&widget);
model.size_group_widgets.push((group, widget));
}
}
ComponentParts { model, widgets }
}
fn update_with_view(
&mut self,
widgets: &mut Self::Widgets,
msg: Self::Input,
sender: ComponentSender<Self>,
_root: &Self::Root,
) {
match msg {
AdjustmentControlMsg::Changed => {
let _ = sender.output(self.get_value());
}
AdjustmentControlMsg::SetValueRatio(ratio) => {
self.adjustment.block_signal(&widgets.value_change_signal);
self.set_value_ratio(ratio);
widgets
.lower_label
.set_label(&fmt_value_with_unit(self.adjustment.lower(), &self.unit));
widgets
.upper_label
.set_label(&fmt_value_with_unit(self.adjustment.upper(), &self.unit));
self.adjustment.unblock_signal(&widgets.value_change_signal);
}
}
}
}
impl AdjustmentControl {
pub(super) fn get_value(&self) -> f64 {
self.adjustment.value() / self.value_ratio
}
pub(super) fn set_value(&self, value: f64) {
self.adjustment.set_value(value * self.value_ratio);
}
pub(super) fn reset(&self) {
if let Some(value) = self.default_value {
self.set_value(value);
}
}
pub(super) fn serialize(&self) -> Option<f64> {
let value = self.get_value();
(Some(value) != self.default_value).then_some(value)
}
/// Changes display units, retaining the raw value.
fn set_value_ratio(&mut self, ratio: f64) {
let value = self.get_value();
let lower = self.adjustment.lower() / self.value_ratio;
let upper = self.adjustment.upper() / self.value_ratio;
self.value_ratio = ratio;
self.adjustment.set_lower(lower * ratio);
self.adjustment.set_upper(upper * ratio);
self.set_value(value);
}
}
@@ -0,0 +1,109 @@
use super::ControlInit;
use adw::prelude::*;
use relm4::{
Component, ComponentParts, ComponentSender,
binding::{Binding, BoolBinding, ConnectBinding},
};
#[derive(Default)]
pub struct BooleanInit {
pub value: bool,
pub default_value: Option<bool>,
}
pub struct BooleanControl {
value: BoolBinding,
default_value: Option<bool>,
title: gtk::Box,
pub(super) size_group_widgets: Vec<(gtk::SizeGroup, gtk::Widget)>,
}
#[derive(Debug)]
pub enum BooleanControlMsg {
Changed,
}
#[relm4::component(pub)]
impl Component for BooleanControl {
type Init = ControlInit<BooleanInit>;
type Input = BooleanControlMsg;
type Output = bool;
type CommandOutput = ();
view! {
gtk::Box {
set_spacing: 12,
append: &model.title,
#[name = "switch"]
gtk::Switch {
set_valign: gtk::Align::Center,
set_halign: gtk::Align::End,
bind: &model.value,
},
},
}
fn init(
ControlInit {
init,
title,
label_group,
input_group,
}: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let mut model = Self {
value: BoolBinding::new(init.value),
default_value: init.default_value,
title,
size_group_widgets: Vec::new(),
};
let widgets = view_output!();
for (group, widget) in [
(label_group, model.title.clone().upcast::<gtk::Widget>()),
(input_group, widgets.switch.clone().upcast()),
] {
if let Some(group) = group {
group.add_widget(&widget);
model.size_group_widgets.push((group, widget));
}
}
model.value.connect_value_notify(move |_| {
sender.input(BooleanControlMsg::Changed);
});
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>, _root: &Self::Root) {
match msg {
BooleanControlMsg::Changed => {
let _ = sender.output(self.get_value());
}
}
}
}
impl BooleanControl {
pub(super) fn get_value(&self) -> bool {
self.value.get()
}
pub(super) fn set_value(&self, value: bool) {
if self.get_value() != value {
self.value.set(value);
}
}
pub(super) fn reset(&self) {
if let Some(value) = self.default_value {
self.set_value(value);
}
}
pub(super) fn serialize(&self) -> Option<bool> {
let value = self.get_value();
(Some(value) != self.default_value).then_some(value)
}
}
@@ -0,0 +1,158 @@
use super::ControlInit;
use adw::prelude::*;
use relm4::{
Component, ComponentParts, ComponentSender, RelmObjectExt,
binding::{Binding, U32Binding},
css,
};
#[derive(Default)]
pub struct SelectOption {
pub label: String,
pub description: String,
}
impl From<String> for SelectOption {
fn from(label: String) -> Self {
Self {
label,
..Default::default()
}
}
}
#[derive(Default)]
pub struct SelectInit {
pub options: Vec<SelectOption>,
/// Index into `options`. Missing or invalid indices select the first option.
pub selected: Option<u32>,
pub default_value: Option<u32>,
}
pub struct SelectControl {
value: U32Binding,
default_value: Option<u32>,
options: Vec<SelectOption>,
title: gtk::Box,
pub(super) size_group_widgets: Vec<(gtk::SizeGroup, gtk::Widget)>,
}
#[derive(Debug)]
pub enum SelectControlMsg {
Changed,
}
#[relm4::component(pub)]
impl Component for SelectControl {
type Init = ControlInit<SelectInit>;
type Input = SelectControlMsg;
type Output = u32;
type CommandOutput = ();
view! {
gtk::Box {
set_spacing: 12,
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
set_valign: gtk::Align::Center,
append: &model.title,
#[name = "description_label"]
gtk::Label {
#[watch]
set_label: model.description(),
#[watch]
set_visible: !model.description().is_empty(),
set_xalign: 0.0,
set_wrap: true,
add_css_class: css::DIM_LABEL,
add_css_class: css::CAPTION,
},
},
#[name = "dropdown"]
gtk::DropDown {
set_valign: gtk::Align::Center,
set_model: Some(&model.options.iter().map(|option| option.label.clone()).collect::<gtk::StringList>()),
add_binding: (&model.value, "selected"),
},
},
}
fn init(
ControlInit {
init,
title,
label_group,
input_group,
}: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let mut model = Self {
value: U32Binding::new(
init.selected
.filter(|selected| *selected < init.options.len() as u32)
.unwrap_or(0),
),
default_value: init.default_value,
options: init.options,
title,
size_group_widgets: Vec::new(),
};
let widgets = view_output!();
for (group, widget) in [
(label_group, model.title.clone().upcast::<gtk::Widget>()),
(input_group, widgets.dropdown.clone().upcast()),
] {
if let Some(group) = group {
group.add_widget(&widget);
model.size_group_widgets.push((group, widget));
}
}
model.value.connect_value_notify(move |_| {
sender.input(SelectControlMsg::Changed);
});
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>, _root: &Self::Root) {
match msg {
SelectControlMsg::Changed => {
let _ = sender.output(self.get_value());
}
}
}
}
impl SelectControl {
pub(super) fn get_value(&self) -> u32 {
self.value.get()
}
pub(super) fn set_value(&self, value: u32) {
if self.get_value() != value {
self.value.set(value);
}
}
pub(super) fn reset(&self) {
if let Some(value) = self.default_value {
self.set_value(value);
}
}
pub(super) fn serialize(&self) -> Option<u32> {
let value = self.get_value();
(Some(value) != self.default_value).then_some(value)
}
fn description(&self) -> &str {
self.options
.get(self.get_value() as usize)
.map(|option| option.description.as_str())
.unwrap_or_default()
}
}
+46 -49
View File
@@ -1,12 +1,12 @@
mod card_layout;
mod clocks_frame;
mod igpu_frame;
mod performance_frame;
mod power_frame;
mod power_profile_mode;
mod power_states;
mod vf_curve;
use crate::APP_BROKER;
use crate::CONFIG;
use crate::app::components::gpu_stats_section::{
GpuStat, GpuStatsSection, GpuStatsSectionConfig, GpuStatsSectionMsg,
};
@@ -23,9 +23,7 @@ use clocks_frame::{ClockDomain, ClocksFrame, ClocksFrameInit, ClocksFrameMsg};
use indexmap::IndexMap;
use lact_schema::config;
use lact_schema::{ClocksTable, DeviceInfo, PowerStates};
use nvml_wrapper::enums::device::PowerMizerMode;
use performance_frame::PerformanceFrameMsg;
use power_frame::{PowerFrame, PowerFrameMsg};
use power_frame::{PowerFrame, PowerFrameMsg, PowerFrameSerialized};
use power_states::power_states_dialog::{PowerStatesDialog, PowerStatesDialogMsg};
use relm4::binding::BoolBinding;
use relm4::{ComponentController, ComponentParts, ComponentSender, RelmWidgetExt};
@@ -47,6 +45,13 @@ pub struct OcPage {
vf_curve_editor: relm4::Controller<VfCurveEditor>,
}
pub struct OcPageSerialized {
pub power: PowerFrameSerialized,
pub clocks_configuration: config::ClocksConfiguration,
pub power_states: IndexMap<PowerLevelKind, Vec<u8>>,
pub uma_carveout: Option<usize>,
}
#[derive(Debug)]
pub enum OcPageMsg {
Update {
@@ -199,17 +204,7 @@ impl relm4::Component for OcPage {
.emit(VfCurveEditorMsg::Stats(stats.clone()));
if initial {
self.power_frame
.emit(PowerFrameMsg::PowerStats(stats.power.clone()));
self.power_frame.emit(PowerFrameMsg::Performance(
PerformanceFrameMsg::PerformanceLevel(stats.performance_level),
));
self.power_frame.emit(PowerFrameMsg::Performance(
PerformanceFrameMsg::PowerMizerInfo {
active: stats.active_power_mizer_mode,
supported: stats.supported_power_mizer_modes.clone(),
},
));
self.power_frame.emit(PowerFrameMsg::Stats(stats.clone()));
}
}
PageUpdate::Info(info) => {
@@ -249,9 +244,8 @@ impl relm4::Component for OcPage {
.emit(VfCurveEditorMsg::Clocks(table.clone()));
}
OcPageMsg::ProfileModesTable(modes_table) => {
self.power_frame.emit(PowerFrameMsg::Performance(
PerformanceFrameMsg::PowerProfileModes(modes_table),
));
self.power_frame
.emit(PowerFrameMsg::PowerProfileModes(modes_table));
}
OcPageMsg::PowerStates {
pstates,
@@ -288,10 +282,8 @@ impl relm4::Component for OcPage {
));
}
OcPageMsg::SetPerformanceLevel(level) => {
self.power_frame.emit(PowerFrameMsg::Performance(
PerformanceFrameMsg::PerformanceLevel(Some(level)),
));
APP_BROKER.send(AppMsg::SettingsChanged);
self.power_frame
.emit(PowerFrameMsg::SetPerformanceLevel(level));
}
OcPageMsg::EnablePstateConfig => {
self.power_states_dialog
@@ -325,25 +317,30 @@ impl OcPage {
self.power_frame.model().performance_level()
}
pub fn get_active_power_mizer_mode(&self) -> Option<PowerMizerMode> {
self.power_frame.model().active_power_mizer_mode()
// stored_config is temporary here, long term serialization should be derevied from SettingRow values
pub fn serialize(&self, stored_config: &config::GpuConfig) -> OcPageSerialized {
OcPageSerialized {
power: self.power_frame.model().serialize(),
clocks_configuration: self.serialize_clocks(stored_config.clocks_configuration.clone()),
power_states: if self.get_performance_level() == Some(PerformanceLevel::Manual) {
self.power_states_dialog.model().get_enabled_power_states()
} else {
IndexMap::new()
},
// Avoids applying the setting if it's the default but doesn't wipe it if it's unchanged in the UI
uma_carveout: self
.igpu_frame
.model()
.get_uma_carveout()
.map(|value| value as usize)
.or(stored_config.uma_carveout),
}
}
pub fn get_power_profile_mode(&self) -> Option<u16> {
self.power_frame.model().power_profile_mode()
}
pub fn get_power_profile_mode_custom_heuristics(&self) -> Vec<Vec<Option<i32>>> {
self.power_frame
.model()
.power_profile_mode_custom_heuristics()
}
pub fn get_power_cap(&self) -> Option<f64> {
self.power_frame.model().get_user_cap()
}
pub fn apply_clocks_config(&self, config: &mut config::ClocksConfiguration) {
fn serialize_clocks(
&self,
mut config: config::ClocksConfiguration,
) -> config::ClocksConfiguration {
let mut commands = self.gpu_clocks_frame.model().get_commands();
commands.extend(self.vram_clocks_frame.model().get_commands());
@@ -356,17 +353,17 @@ impl OcPage {
if !self.vf_curve_editor.model().is_empty() {
config.nvidia_gpu_vf_curve = self.vf_curve_editor.model().get_configured_curve();
}
}
pub fn get_enabled_power_states(&self) -> IndexMap<PowerLevelKind, Vec<u8>> {
if self.get_performance_level() == Some(PerformanceLevel::Manual) {
self.power_states_dialog.model().get_enabled_power_states()
} else {
IndexMap::new()
if self
.device_info
.as_ref()
.is_some_and(|info| info.driver == "nvidia")
&& !CONFIG.read().experimental_nvidia_pstate_offsets
{
config.gpu_clock_offsets.retain(|pstate, _| *pstate == 0);
config.mem_clock_offsets.retain(|pstate, _| *pstate == 0);
}
}
pub fn get_uma_carveout(&self) -> Option<u32> {
self.igpu_frame.model().get_uma_carveout()
config
}
}
@@ -1,465 +0,0 @@
mod heuristics_list;
use crate::{
APP_BROKER, I18N,
app::{msg::AppMsg, pages::oc_page::OcPageMsg},
};
use adw::prelude::*;
use amdgpu_sysfs::gpu_handle::{PerformanceLevel, power_profile_mode::PowerProfileModesTable};
use gtk::StringObject;
use heuristics_list::PowerProfileHeuristicsList;
use i18n_embed_fl::fl;
use nvml_wrapper::enums::device::PowerMizerMode;
use relm4::{Component, ComponentController, ComponentParts, ComponentSender, RelmWidgetExt, css};
const PERFORMANCE_LEVELS: [PerformanceLevel; 8] = [
PerformanceLevel::Auto,
PerformanceLevel::High,
PerformanceLevel::Low,
PerformanceLevel::Manual,
PerformanceLevel::ProfileStandard,
PerformanceLevel::ProfileMinSclk,
PerformanceLevel::ProfileMinMclk,
PerformanceLevel::ProfilePeak,
];
pub struct PerformanceFrame {
performance_level: Option<PerformanceLevel>,
power_profile_modes_table: Option<PowerProfileModesTable>,
power_profile_modes: gtk::StringList,
active_power_mizer_mode: Option<PowerMizerMode>,
supported_power_mizer_modes: Vec<PowerMizerMode>,
power_mizer_modes: gtk::StringList,
heuristics_components: Vec<relm4::Controller<PowerProfileHeuristicsList>>,
}
#[derive(Debug, Clone, Default)]
pub struct PerformanceRows {
pub performance_level: gtk::ListBoxRow,
pub power_profile: gtk::ListBoxRow,
pub power_mizer: gtk::ListBoxRow,
}
impl PerformanceRows {
pub fn rows(&self) -> [&gtk::ListBoxRow; 3] {
[
&self.performance_level,
&self.power_profile,
&self.power_mizer,
]
}
}
#[derive(Debug)]
pub enum PerformanceFrameMsg {
PerformanceLevel(Option<PerformanceLevel>),
PowerProfileModes(Option<PowerProfileModesTable>),
PowerProfileSelected(u16),
PowerMizerInfo {
active: Option<PowerMizerMode>,
supported: Option<Vec<PowerMizerMode>>,
},
PowerMizerSelected(u32),
}
#[relm4::component(pub)]
impl relm4::Component for PerformanceFrame {
type Init = ();
type Input = PerformanceFrameMsg;
type Output = OcPageMsg;
type CommandOutput = ();
view! {
#[root]
PerformanceRows::default() {},
#[local]
performance_level_row -> gtk::ListBoxRow {
set_activatable: false,
set_selectable: false,
#[watch]
set_visible: model.performance_level.is_some(),
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 10,
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
set_valign: gtk::Align::Center,
gtk::Label {
set_label: &fl!(I18N, "performance-level"),
set_halign: gtk::Align::Start,
},
gtk::Label {
#[watch]
set_label: &match model.performance_level {
Some(PerformanceLevel::Auto) => fl!(I18N, "performance-level-auto-description"),
Some(PerformanceLevel::High) => fl!(I18N, "performance-level-high-description"),
Some(PerformanceLevel::Low) => fl!(I18N, "performance-level-low-description"),
Some(PerformanceLevel::Manual) => fl!(I18N, "performance-level-manual-description"),
Some(PerformanceLevel::ProfileStandard) => fl!(I18N, "performance-level-profile-standard-description"),
Some(PerformanceLevel::ProfileMinSclk) => fl!(I18N, "performance-level-profile-min-sclk-description"),
Some(PerformanceLevel::ProfileMinMclk) => fl!(I18N, "performance-level-profile-min-mclk-description"),
Some(PerformanceLevel::ProfilePeak) => fl!(I18N, "performance-level-profile-peak-description"),
_ => String::new(),
},
set_halign: gtk::Align::Start,
set_xalign: 0.0,
set_wrap: true,
add_css_class: css::DIM_LABEL,
add_css_class: css::CAPTION,
},
},
gtk::DropDown::from_strings(&level_names_ref) {
set_valign: gtk::Align::Center,
#[watch]
#[block_signal(level_select_handler)]
set_selected: PERFORMANCE_LEVELS.iter().position(|level| model.performance_level == Some(*level)).unwrap_or(0) as u32,
connect_selected_notify[sender] => move |dropdown| {
let idx = dropdown.selected();
if let Some(level) = PERFORMANCE_LEVELS.get(idx as usize) {
sender.input(PerformanceFrameMsg::PerformanceLevel(Some(*level)));
APP_BROKER.send(AppMsg::SettingsChanged);
}
} @ level_select_handler,
},
},
},
#[local]
power_profile_row -> gtk::ListBoxRow {
set_activatable: false,
set_selectable: false,
#[watch]
set_visible: model.power_profile_modes_table.is_some(),
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 10,
gtk::Label {
set_label: &fl!(I18N, "power-profile-mode"),
set_halign: gtk::Align::Start,
},
gtk::MenuButton {
#[watch]
set_visible: model.performance_level != Some(PerformanceLevel::Manual),
#[wrap(Some)]
set_popover = &gtk::Popover {
gtk::Label {
set_label: &fl!(I18N, "manual-level-needed"),
}
},
add_css_class: css::FLAT,
#[wrap(Some)]
set_child = &gtk::Image {
set_icon_name: Some("info-outline-symbolic"),
set_pixel_size: 12,
},
},
gtk::MenuButton {
set_hexpand: true,
set_halign: gtk::Align::End,
set_always_show_arrow: false,
#[watch]
set_label: model.power_profile_modes_table.as_ref().and_then(|table| table.modes.get(&table.active)).map(|profile| profile.name.as_str()).unwrap_or_default(),
#[wrap(Some)]
set_popover = &gtk::Popover {
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_margin_all: 5,
#[name = "modes_listbox"]
gtk::ListBox {
set_selection_mode: gtk::SelectionMode::Single,
#[watch]
set_sensitive: model.performance_level.is_some_and(|level| level == PerformanceLevel::Manual),
bind_model: (Some(&model.power_profile_modes), |obj| {
let string = obj.downcast_ref::<StringObject>().unwrap();
gtk::Label::builder().label(string.string()).build().into()
}),
connect_row_selected[sender] => move |_, row| {
if let Some(row) = row
&& let Ok(idx) = u16::try_from(row.index()) {
sender.input(PerformanceFrameMsg::PowerProfileSelected(idx));
}
} @ power_profile_selected_handler,
#[watch]
#[block_signal(power_profile_selected_handler)]
select_row: model.power_profile_modes_table.as_ref().and_then(|table| modes_listbox.row_at_index(table.active.into())).as_ref(),
},
#[name = "heuristics_notebook"]
gtk::Notebook {
#[watch]
set_show_tabs: model.heuristics_components.len() > 1,
},
}
},
},
},
},
#[local]
power_mizer_row -> gtk::ListBoxRow {
set_activatable: false,
set_selectable: false,
#[watch]
set_visible: model.active_power_mizer_mode.is_some(),
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 10,
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
set_valign: gtk::Align::Center,
gtk::Label {
set_label: &fl!(I18N, "power-mizer-mode"),
set_halign: gtk::Align::Start,
},
gtk::Label {
#[watch]
set_label: &match model.active_power_mizer_mode {
Some(PowerMizerMode::Auto) => fl!(I18N, "power-mizer-mode-auto-description"),
Some(PowerMizerMode::Adaptive) => fl!(I18N, "power-mizer-mode-adaptive-description"),
Some(PowerMizerMode::PreferMaximumPerformance) => fl!(I18N, "power-mizer-mode-prefer-maximum-performance-description"),
Some(PowerMizerMode::PreferConsistentPerformance) => fl!(I18N, "power-mizer-mode-prefer-consistent-performance-description"),
None => String::new(),
},
set_halign: gtk::Align::Start,
set_xalign: 0.0,
set_wrap: true,
add_css_class: css::DIM_LABEL,
add_css_class: css::CAPTION,
},
},
#[name = "power_mizer_dropdown"]
gtk::DropDown {
set_valign: gtk::Align::Center,
#[watch]
#[block_signal(power_mizer_select_handler)]
set_model: Some(&model.power_mizer_modes),
#[watch]
#[block_signal(power_mizer_select_handler)]
set_selected: model.active_power_mizer_mode
.and_then(|active| model.supported_power_mizer_modes.iter().position(|mode| *mode == active))
.unwrap_or(0) as u32,
connect_selected_notify[sender] => move |dropdown| {
sender.input(PerformanceFrameMsg::PowerMizerSelected(dropdown.selected()));
} @ power_mizer_select_handler,
},
},
}
}
fn init(
_init: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let performance_level_row = root.performance_level.clone();
let power_profile_row = root.power_profile.clone();
let power_mizer_row = root.power_mizer.clone();
let level_names = PERFORMANCE_LEVELS.map(level_friendly_name);
let level_names_ref = level_names
.iter()
.map(|level| level.as_str())
.collect::<Vec<&str>>();
let model = Self {
performance_level: None,
power_profile_modes_table: None,
power_profile_modes: gtk::StringList::new(&[]),
active_power_mizer_mode: None,
supported_power_mizer_modes: vec![],
power_mizer_modes: gtk::StringList::new(&[]),
heuristics_components: vec![],
};
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update_with_view(
&mut self,
widgets: &mut Self::Widgets,
msg: Self::Input,
sender: ComponentSender<Self>,
_root: &Self::Root,
) {
match msg {
PerformanceFrameMsg::PerformanceLevel(level) => {
self.performance_level = level;
sender.output(OcPageMsg::PerformanceLevelChanged).unwrap();
}
PerformanceFrameMsg::PowerProfileModes(table) => {
while self.power_profile_modes.n_items() != 0 {
self.power_profile_modes.remove(0);
}
if let Some(table) = &table {
for mode in table.modes.values() {
self.power_profile_modes.append(&mode.name);
}
}
self.power_profile_modes_table = table;
self.update_heuristic_components(widgets);
}
PerformanceFrameMsg::PowerProfileSelected(idx) => {
if let Some(table) = &mut self.power_profile_modes_table
&& table.active != idx
{
table.active = idx;
APP_BROKER.send(AppMsg::SettingsChanged);
self.update_heuristic_components(widgets);
}
}
PerformanceFrameMsg::PowerMizerInfo { active, supported } => {
let modes = gtk::StringList::new(&[]);
let supported = supported.unwrap_or_default();
for mode in &supported {
modes.append(&power_mizer_mode_friendly_name(*mode));
}
self.power_mizer_modes = modes;
self.active_power_mizer_mode = active;
self.supported_power_mizer_modes = supported;
}
PerformanceFrameMsg::PowerMizerSelected(idx) => {
if let Some(mode) = self.supported_power_mizer_modes.get(idx as usize)
&& self.active_power_mizer_mode != Some(*mode)
{
self.active_power_mizer_mode = Some(*mode);
APP_BROKER.send(AppMsg::SettingsChanged);
}
}
}
if let Some(table) = &self.power_profile_modes_table
&& let Some(active) = table.modes.get(&table.active)
{
for heuristics_component in &self.heuristics_components {
heuristics_component
.widget()
.set_sensitive(active.is_custom());
}
}
self.update_view(widgets, sender);
}
}
impl PerformanceFrame {
fn update_heuristic_components(&mut self, widgets: &PerformanceFrameWidgets) {
while let Some(component) = self.heuristics_components.pop() {
let page = widgets.heuristics_notebook.page(component.widget());
widgets
.heuristics_notebook
.remove_page(Some(page.position() as u32));
}
if let Some(table) = &self.power_profile_modes_table
&& let Some(active_profile) = table.modes.get(&table.active)
{
for component in &active_profile.components {
let title = component.clock_type.as_deref().unwrap_or("All");
let heuristics_component = PowerProfileHeuristicsList::builder()
.launch((component.values.clone(), table.value_names.clone()))
.detach();
widgets.heuristics_notebook.append_page(
heuristics_component.widget(),
Some(&gtk::Label::new(Some(title))),
);
self.heuristics_components.push(heuristics_component);
}
}
}
pub fn performance_level(&self) -> Option<PerformanceLevel> {
self.performance_level
}
pub fn active_power_mizer_mode(&self) -> Option<PowerMizerMode> {
self.active_power_mizer_mode
}
pub fn power_profile_mode(&self) -> Option<u16> {
if self.performance_level == Some(PerformanceLevel::Manual) {
self.power_profile_modes_table
.as_ref()
.map(|table| table.active)
} else {
None
}
}
pub fn power_profile_mode_custom_heuristics(&self) -> Vec<Vec<Option<i32>>> {
if let Some(table) = &self.power_profile_modes_table
&& let Some(mode) = table.modes.get(&table.active)
&& mode.is_custom()
{
return self
.heuristics_components
.iter()
.map(|list| list.model().get_values())
.collect();
}
vec![]
}
}
fn level_friendly_name(level: PerformanceLevel) -> String {
match level {
PerformanceLevel::Auto => fl!(I18N, "performance-level-auto"),
PerformanceLevel::Low => fl!(I18N, "performance-level-low"),
PerformanceLevel::High => fl!(I18N, "performance-level-high"),
PerformanceLevel::Manual => fl!(I18N, "performance-level-manual"),
PerformanceLevel::ProfileStandard => fl!(I18N, "performance-level-profile-standard"),
PerformanceLevel::ProfileMinSclk => fl!(I18N, "performance-level-profile-min-sclk"),
PerformanceLevel::ProfileMinMclk => fl!(I18N, "performance-level-profile-min-mclk"),
PerformanceLevel::ProfilePeak => fl!(I18N, "performance-level-profile-peak"),
PerformanceLevel::PerfDeterminism | PerformanceLevel::ProfileExit => {
unreachable!("unsupported performance level in selector")
}
}
}
fn power_mizer_mode_friendly_name(mode: PowerMizerMode) -> String {
match mode {
PowerMizerMode::Auto => fl!(I18N, "power-mizer-mode-auto"),
PowerMizerMode::Adaptive => fl!(I18N, "power-mizer-mode-adaptive"),
PowerMizerMode::PreferMaximumPerformance => {
fl!(I18N, "power-mizer-mode-prefer-maximum-performance")
}
PowerMizerMode::PreferConsistentPerformance => {
fl!(I18N, "power-mizer-mode-prefer-consistent-performance")
}
}
}
+271 -88
View File
@@ -3,37 +3,68 @@ use crate::{
app::{
components::{
adjustment_card::AdjustmentCard,
adjustment_row::{AdjustmentRow, AdjustmentRowInit, AdjustmentRowMsg},
page_section::PageSection,
setting_row::{
AdjustmentInit, SelectInit, SelectOption, SettingRow, SettingRowInit,
SettingRowMsg, SettingRowValue,
},
},
msg::AppMsg,
pages::oc_page::{
OcPageMsg,
performance_frame::{PerformanceFrame, PerformanceFrameMsg},
power_profile_mode::{AmdPowerProfileMode, AmdPowerProfileModeMsg},
},
utils::ext::RelmDefaultLauchable,
},
};
use adw::prelude::*;
use amdgpu_sysfs::gpu_handle::PerformanceLevel;
use amdgpu_sysfs::gpu_handle::{PerformanceLevel, power_profile_mode::PowerProfileModesTable};
use i18n_embed_fl::fl;
use lact_schema::PowerStats;
use lact_schema::DeviceStats;
use nvml_wrapper::enums::device::PowerMizerMode;
use relm4::{
ComponentController, ComponentParts, ComponentSender, WidgetTemplate, factory::FactoryHashMap,
ComponentController, ComponentParts, ComponentSender, Controller, WidgetTemplate,
factory::FactoryHashMap,
};
use std::sync::Arc;
const PERFORMANCE_LEVELS: [PerformanceLevel; 8] = [
PerformanceLevel::Auto,
PerformanceLevel::High,
PerformanceLevel::Low,
PerformanceLevel::Manual,
PerformanceLevel::ProfileStandard,
PerformanceLevel::ProfileMinSclk,
PerformanceLevel::ProfileMinMclk,
PerformanceLevel::ProfilePeak,
];
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum PowerSetting {
PowerCap,
PerformanceLevel,
PowerMizer,
}
pub struct PowerFrameSerialized {
pub power_cap: Option<f64>,
pub performance_level: Option<PerformanceLevel>,
pub power_mizer_mode: Option<PowerMizerMode>,
pub power_profile_mode_index: Option<u16>,
pub custom_power_profile_mode_hueristics: Vec<Vec<Option<i32>>>,
}
pub struct PowerFrame {
power: PowerStats,
power_row: FactoryHashMap<(), AdjustmentRow<()>>,
performance_frame: relm4::Controller<PerformanceFrame>,
rows: FactoryHashMap<PowerSetting, SettingRow<PowerSetting>>,
supported_power_mizer_modes: Vec<PowerMizerMode>,
power_profile_mode: Option<Controller<AmdPowerProfileMode>>,
}
#[derive(Debug)]
pub enum PowerFrameMsg {
PowerStats(PowerStats),
Performance(PerformanceFrameMsg),
RefreshVisibility,
Stats(Arc<DeviceStats>),
SetPerformanceLevel(PerformanceLevel),
PowerProfileModes(Option<PowerProfileModesTable>),
SettingChanged(PowerSetting),
Reset,
}
@@ -50,20 +81,17 @@ impl relm4::Component for PowerFrame {
set_name: fl!(I18N, "power-section"),
set_hide_visible_container: true,
#[watch]
set_visible: model.is_available(),
set_visible: !model.rows.is_empty() || model.power_profile_mode.is_some(),
append_header = &gtk::Button {
set_label: &fl!(I18N, "default-button"),
connect_clicked => PowerFrameMsg::Reset,
set_halign: gtk::Align::End,
set_hexpand: true,
#[watch]
set_visible: !model.power_row.is_empty(),
set_visible: !model.rows.is_empty(),
},
#[template]
#[local]
append_child = &card -> AdjustmentCard {},
append_child = model.rows.widget(),
},
}
@@ -72,31 +100,21 @@ impl relm4::Component for PowerFrame {
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let card = AdjustmentCard::init(());
let model = Self {
power: PowerStats::default(),
power_row: FactoryHashMap::builder()
.launch(card.content.clone())
.forward(APP_BROKER.sender(), |()| AppMsg::SettingsChanged),
performance_frame: PerformanceFrame::launch_default()
.forward(sender.output_sender(), |msg| msg),
rows: FactoryHashMap::builder()
.launch(AdjustmentCard::init(()).content.clone())
.forward(sender.input_sender(), |(setting, _)| {
PowerFrameMsg::SettingChanged(setting)
}),
supported_power_mizer_modes: Vec::new(),
power_profile_mode: None,
};
for row in model.performance_frame.widget().rows() {
card.content.append(row);
let visibility_sender = sender.clone();
row.connect_visible_notify(move |_| {
visibility_sender.input(PowerFrameMsg::RefreshVisibility);
});
}
let widgets = view_output!();
let rows = model.performance_frame.widget().rows().map(Clone::clone);
// FIXME: performance rows should be factory items like AdjustmentRow; until then
// sort to keep the power limit before them when the factory recreates it.
model.power_row.widget().set_sort_func(move |left, right| {
rows.iter()
.position(|row| row == left)
.cmp(&rows.iter().position(|row| row == right))
model.rows.widget().set_sort_func(|left, right| {
// Keep the power profile mode row after dynamically inserted setting rows.
left.has_css_class("power-profile-mode")
.cmp(&right.has_css_class("power-profile-mode"))
.into()
});
@@ -111,36 +129,125 @@ impl relm4::Component for PowerFrame {
_root: &Self::Root,
) {
match msg {
PowerFrameMsg::PowerStats(power) => {
self.power_row.clear();
if let Some(value) = power.cap_current {
self.power_row.insert(
(),
AdjustmentRowInit {
PowerFrameMsg::Stats(stats) => {
self.rows.clear();
let power = &stats.power;
if let Some(value) = power.cap_current
&& let Some(lower) = power.cap_min
&& let Some(upper) = power.cap_max
{
self.rows.insert(
PowerSetting::PowerCap,
SettingRowInit {
title: fl!(I18N, "power-cap"),
unit: fl!(I18N, "watt"),
value,
lower: power.cap_min.unwrap_or_default(),
upper: power.cap_max.unwrap_or_default(),
control: AdjustmentInit {
unit: fl!(I18N, "watt"),
value,
default_value: power.cap_default,
lower,
upper,
..Default::default()
}
.into(),
..Default::default()
},
);
}
self.power = power;
}
PowerFrameMsg::Performance(msg) => {
self.performance_frame.emit(msg);
}
PowerFrameMsg::RefreshVisibility => (),
PowerFrameMsg::Reset => {
if !self.power_row.is_empty() {
self.power_row.send(
&(),
AdjustmentRowMsg::SetValue(self.power.cap_default.unwrap_or_default()),
if let Some(level) = stats.performance_level {
self.rows.insert(
PowerSetting::PerformanceLevel,
SettingRowInit {
title: fl!(I18N, "performance-level"),
control: SelectInit {
default_value: PERFORMANCE_LEVELS
.iter()
.position(|level| *level == PerformanceLevel::Auto)
.map(|index| index as u32),
options: PERFORMANCE_LEVELS
.iter()
.copied()
.map(performance_option)
.collect(),
selected: PERFORMANCE_LEVELS
.iter()
.position(|candidate| *candidate == level)
.map(|index| index as u32),
}
.into(),
..Default::default()
},
);
}
self.supported_power_mizer_modes = stats
.supported_power_mizer_modes
.clone()
.unwrap_or_default();
if let Some(active) = stats.active_power_mizer_mode {
self.rows.insert(
PowerSetting::PowerMizer,
SettingRowInit {
title: fl!(I18N, "power-mizer-mode"),
control: SelectInit {
default_value: self
.supported_power_mizer_modes
.iter()
.position(|mode| *mode == PowerMizerMode::Auto)
.map(|index| index as u32),
options: self
.supported_power_mizer_modes
.iter()
.copied()
.map(power_mizer_option)
.collect(),
selected: self
.supported_power_mizer_modes
.iter()
.position(|mode| *mode == active)
.map(|index| index as u32),
}
.into(),
..Default::default()
},
);
}
self.performance_level_changed(&sender);
}
PowerFrameMsg::SetPerformanceLevel(level) => {
if self.rows.get(&PowerSetting::PerformanceLevel).is_some()
&& let Some(index) = PERFORMANCE_LEVELS
.iter()
.position(|candidate| *candidate == level)
{
self.rows.send(
&PowerSetting::PerformanceLevel,
SettingRowMsg::SetValue(SettingRowValue::Selected(index as u32)),
);
}
}
PowerFrameMsg::PowerProfileModes(table) => {
if let Some(power_profile_mode) = self.power_profile_mode.take() {
self.rows.widget().remove(power_profile_mode.widget());
}
self.power_profile_mode = table.map(|table| {
AmdPowerProfileMode::builder()
.attach_to(self.rows.widget())
.launch((table, self.performance_level()))
.forward(APP_BROKER.sender(), |()| AppMsg::SettingsChanged)
});
}
PowerFrameMsg::SettingChanged(setting) => {
APP_BROKER.send(AppMsg::SettingsChanged);
if setting == PowerSetting::PerformanceLevel {
self.performance_level_changed(&sender);
}
}
PowerFrameMsg::Reset => {
for setting in self.rows.keys() {
self.rows.send(setting, SettingRowMsg::Reset);
}
}
}
@@ -149,38 +256,114 @@ impl relm4::Component for PowerFrame {
}
impl PowerFrame {
fn is_available(&self) -> bool {
!self.power_row.is_empty()
|| self
.performance_frame
.widget()
.rows()
.iter()
.any(|row| row.get_visible())
pub fn serialize(&self) -> PowerFrameSerialized {
let value = |setting| self.rows.get(&setting).and_then(SettingRow::serialize);
let selected = |setting| value(setting).and_then(SettingRowValue::as_selected);
let power_profile_mode = self
.power_profile_mode
.as_ref()
.and_then(|power_profile_mode| power_profile_mode.model().serialize());
PowerFrameSerialized {
power_cap: value(PowerSetting::PowerCap).and_then(SettingRowValue::as_numeric),
performance_level: selected(PowerSetting::PerformanceLevel)
.and_then(|index| PERFORMANCE_LEVELS.get(index as usize).copied()),
power_mizer_mode: selected(PowerSetting::PowerMizer).and_then(|index| {
self.supported_power_mizer_modes
.get(index as usize)
.copied()
}),
power_profile_mode_index: power_profile_mode.as_ref().map(|mode| mode.index),
custom_power_profile_mode_hueristics: power_profile_mode
.map(|mode| mode.custom_heuristics)
.unwrap_or_default(),
}
}
pub fn get_user_cap(&self) -> Option<f64> {
self.power_row
.get(&())?
.get_changed_value()
.filter(|value| *value != 0.0)
fn selected(&self, setting: PowerSetting) -> Option<u32> {
self.rows.get(&setting)?.get_value().as_selected()
}
pub fn performance_level(&self) -> Option<PerformanceLevel> {
self.performance_frame.model().performance_level()
PERFORMANCE_LEVELS
.get(self.selected(PowerSetting::PerformanceLevel)? as usize)
.copied()
}
pub fn active_power_mizer_mode(&self) -> Option<PowerMizerMode> {
self.performance_frame.model().active_power_mizer_mode()
}
pub fn power_profile_mode(&self) -> Option<u16> {
self.performance_frame.model().power_profile_mode()
}
pub fn power_profile_mode_custom_heuristics(&self) -> Vec<Vec<Option<i32>>> {
self.performance_frame
.model()
.power_profile_mode_custom_heuristics()
fn performance_level_changed(&self, sender: &ComponentSender<Self>) {
if let Some(power_profile_mode) = &self.power_profile_mode {
power_profile_mode.emit(AmdPowerProfileModeMsg::PerformanceLevel(
self.performance_level(),
));
}
sender.output(OcPageMsg::PerformanceLevelChanged).unwrap();
}
}
fn performance_option(level: PerformanceLevel) -> SelectOption {
let (label, description) = match level {
PerformanceLevel::Auto => (
fl!(I18N, "performance-level-auto"),
fl!(I18N, "performance-level-auto-description"),
),
PerformanceLevel::High => (
fl!(I18N, "performance-level-high"),
fl!(I18N, "performance-level-high-description"),
),
PerformanceLevel::Low => (
fl!(I18N, "performance-level-low"),
fl!(I18N, "performance-level-low-description"),
),
PerformanceLevel::Manual => (
fl!(I18N, "performance-level-manual"),
fl!(I18N, "performance-level-manual-description"),
),
PerformanceLevel::ProfileStandard => (
fl!(I18N, "performance-level-profile-standard"),
fl!(I18N, "performance-level-profile-standard-description"),
),
PerformanceLevel::ProfileMinSclk => (
fl!(I18N, "performance-level-profile-min-sclk"),
fl!(I18N, "performance-level-profile-min-sclk-description"),
),
PerformanceLevel::ProfileMinMclk => (
fl!(I18N, "performance-level-profile-min-mclk"),
fl!(I18N, "performance-level-profile-min-mclk-description"),
),
PerformanceLevel::ProfilePeak => (
fl!(I18N, "performance-level-profile-peak"),
fl!(I18N, "performance-level-profile-peak-description"),
),
PerformanceLevel::PerfDeterminism | PerformanceLevel::ProfileExit => {
(level.to_string(), String::new())
}
};
SelectOption { label, description }
}
fn power_mizer_option(mode: PowerMizerMode) -> SelectOption {
let (label, description) = match mode {
PowerMizerMode::Auto => (
fl!(I18N, "power-mizer-mode-auto"),
fl!(I18N, "power-mizer-mode-auto-description"),
),
PowerMizerMode::Adaptive => (
fl!(I18N, "power-mizer-mode-adaptive"),
fl!(I18N, "power-mizer-mode-adaptive-description"),
),
PowerMizerMode::PreferMaximumPerformance => (
fl!(I18N, "power-mizer-mode-prefer-maximum-performance"),
fl!(
I18N,
"power-mizer-mode-prefer-maximum-performance-description"
),
),
PowerMizerMode::PreferConsistentPerformance => (
fl!(I18N, "power-mizer-mode-prefer-consistent-performance"),
fl!(
I18N,
"power-mizer-mode-prefer-consistent-performance-description"
),
),
};
SelectOption { label, description }
}
@@ -0,0 +1,206 @@
mod heuristics_list;
use crate::I18N;
use adw::prelude::*;
use amdgpu_sysfs::gpu_handle::{PerformanceLevel, power_profile_mode::PowerProfileModesTable};
use gtk::StringObject;
use heuristics_list::PowerProfileHeuristicsList;
use i18n_embed_fl::fl;
use relm4::{Component, ComponentController, ComponentParts, ComponentSender, RelmWidgetExt, css};
pub struct AmdPowerProfileMode {
performance_level: Option<PerformanceLevel>,
table: PowerProfileModesTable,
heuristics_components: Vec<relm4::Controller<PowerProfileHeuristicsList>>,
}
pub struct AmdPowerProfileModeSerialized {
pub index: u16,
pub custom_heuristics: Vec<Vec<Option<i32>>>,
}
#[derive(Debug)]
pub enum AmdPowerProfileModeMsg {
PerformanceLevel(Option<PerformanceLevel>),
Selected(u16),
}
#[relm4::component(pub)]
impl Component for AmdPowerProfileMode {
type Init = (PowerProfileModesTable, Option<PerformanceLevel>);
type Input = AmdPowerProfileModeMsg;
type Output = ();
type CommandOutput = ();
view! {
gtk::ListBoxRow {
set_activatable: false,
set_selectable: false,
add_css_class: "power-profile-mode",
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 10,
gtk::Label {
set_label: &fl!(I18N, "power-profile-mode"),
set_halign: gtk::Align::Start,
},
gtk::MenuButton {
#[watch]
set_visible: model.performance_level != Some(PerformanceLevel::Manual),
#[wrap(Some)]
set_popover = &gtk::Popover {
gtk::Label {
set_label: &fl!(I18N, "manual-level-needed"),
}
},
add_css_class: css::FLAT,
#[wrap(Some)]
set_child = &gtk::Image {
set_icon_name: Some("info-outline-symbolic"),
set_pixel_size: 12,
},
},
gtk::MenuButton {
set_hexpand: true,
set_halign: gtk::Align::End,
set_always_show_arrow: false,
#[watch]
set_label: model.table.modes.get(&model.table.active).map(|profile| profile.name.as_str()).unwrap_or_default(),
#[wrap(Some)]
set_popover = &gtk::Popover {
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_margin_all: 5,
#[name = "modes_listbox"]
gtk::ListBox {
set_selection_mode: gtk::SelectionMode::Single,
#[watch]
set_sensitive: model.performance_level == Some(PerformanceLevel::Manual),
bind_model: (
Some(&model.table.modes.values().map(|mode| mode.name.clone()).collect::<gtk::StringList>()),
|obj| {
let string = obj.downcast_ref::<StringObject>().unwrap();
gtk::Label::builder().label(string.string()).build().into()
},
),
connect_row_selected[sender] => move |_, row| {
if let Some(row) = row
&& let Ok(idx) = u16::try_from(row.index()) {
sender.input(AmdPowerProfileModeMsg::Selected(idx));
}
} @ selected_handler,
#[watch]
#[block_signal(selected_handler)]
select_row: modes_listbox.row_at_index(model.table.active.into()).as_ref(),
},
#[name = "heuristics_notebook"]
gtk::Notebook {
#[watch]
set_show_tabs: model.table.modes.get(&model.table.active).is_some_and(|mode| mode.components.len() > 1),
},
}
},
},
},
},
}
fn init(
(table, performance_level): Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let mut model = Self {
performance_level,
table,
heuristics_components: vec![],
};
let widgets = view_output!();
model.update_heuristic_components(&widgets);
ComponentParts { model, widgets }
}
fn update_with_view(
&mut self,
widgets: &mut Self::Widgets,
msg: Self::Input,
sender: ComponentSender<Self>,
_root: &Self::Root,
) {
match msg {
AmdPowerProfileModeMsg::PerformanceLevel(level) => {
self.performance_level = level;
}
AmdPowerProfileModeMsg::Selected(idx) => {
if self.table.active != idx {
self.table.active = idx;
let _ = sender.output(());
self.update_heuristic_components(widgets);
}
}
}
self.update_view(widgets, sender);
}
}
impl AmdPowerProfileMode {
fn update_heuristic_components(&mut self, widgets: &AmdPowerProfileModeWidgets) {
while let Some(component) = self.heuristics_components.pop() {
let page = widgets.heuristics_notebook.page(component.widget());
widgets
.heuristics_notebook
.remove_page(Some(page.position() as u32));
}
if let Some(active_profile) = self.table.modes.get(&self.table.active) {
for component in &active_profile.components {
let title = component.clock_type.as_deref().unwrap_or("All");
let heuristics_component = PowerProfileHeuristicsList::builder()
.launch((component.values.clone(), self.table.value_names.clone()))
.detach();
heuristics_component
.widget()
.set_sensitive(active_profile.is_custom());
widgets.heuristics_notebook.append_page(
heuristics_component.widget(),
Some(&gtk::Label::new(Some(title))),
);
self.heuristics_components.push(heuristics_component);
}
}
}
/// Serialize the active mode with its custom heuristics; modes only apply at the manual level.
pub fn serialize(&self) -> Option<AmdPowerProfileModeSerialized> {
if self.performance_level != Some(PerformanceLevel::Manual) {
return None;
}
let custom_heuristics = match self.table.modes.get(&self.table.active) {
Some(mode) if mode.is_custom() => self
.heuristics_components
.iter()
.map(|list| list.model().get_values())
.collect(),
_ => Vec::new(),
};
Some(AmdPowerProfileModeSerialized {
index: self.table.active,
custom_heuristics,
})
}
}