mirror of
https://github.com/ilya-zlobintsev/LACT.git
synced 2026-11-03 12:50:31 -06:00
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:
1 parent
398f3736fb
commit
6c0e38597c
12 files changed
+1377
-649
No files matched your search
@@ -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
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
@@ -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) -> >k::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 = >k::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 = >k::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()
|
||||
}
|
||||
}
|
||||
@@ -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) -> [>k::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 = >k::Popover {
|
||||
gtk::Label {
|
||||
set_label: &fl!(I18N, "manual-level-needed"),
|
||||
}
|
||||
},
|
||||
|
||||
add_css_class: css::FLAT,
|
||||
#[wrap(Some)]
|
||||
set_child = >k::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 = >k::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(>k::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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = >k::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 }
|
||||
}
|
||||
File renamed without changes.
@@ -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 = >k::Popover {
|
||||
gtk::Label {
|
||||
set_label: &fl!(I18N, "manual-level-needed"),
|
||||
}
|
||||
},
|
||||
|
||||
add_css_class: css::FLAT,
|
||||
#[wrap(Some)]
|
||||
set_child = >k::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 = >k::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(>k::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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user