refactor: make OcPage a relm component (#456)

* wip

* fix: avoid showing apply button

* fix: reconnect signals after setting power states
This commit is contained in:
Ilya Zlobintsev 2025-01-29 23:37:12 +02:00 committed by GitHub
parent 0eea61cc7d
commit d6c8329b80
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 306 additions and 273 deletions

View File

@ -25,7 +25,6 @@ use header::{
profile_rule_window::ProfileRuleWindowMsg, Header, HeaderMsg, PROFILE_RULE_WINDOW_BROKER,
};
use lact_client::{ConnectionStatusMsg, DaemonClient};
use lact_daemon::MODULE_CONF_PATH;
use lact_schema::{
args::GuiArgs,
request::{ConfirmCommand, SetClocksCommand},
@ -33,8 +32,11 @@ use lact_schema::{
};
use msg::AppMsg;
use pages::{
info_page::InformationPage, oc_page::OcPage, software_page::SoftwarePage,
thermals_page::ThermalsPage, PageUpdate,
info_page::InformationPage,
oc_page::{OcPage, OcPageMsg},
software_page::SoftwarePage,
thermals_page::ThermalsPage,
PageUpdate,
};
use relm4::{
actions::{RelmAction, RelmActionGroup},
@ -62,7 +64,7 @@ pub struct AppModel {
graphs_window: relm4::Controller<GraphsWindow>,
info_page: relm4::Controller<InformationPage>,
oc_page: OcPage,
oc_page: relm4::Controller<OcPage>,
thermals_page: ThermalsPage,
software_page: relm4::Controller<SoftwarePage>,
@ -101,7 +103,7 @@ impl AsyncComponent for AppModel {
set_margin_end: 30,
add_titled[Some("info_page"), "Information"] = model.info_page.widget(),
add_titled[Some("oc_page"), "OC"] = &model.oc_page.container.clone(),
add_titled[Some("oc_page"), "OC"] = model.oc_page.widget(),
add_titled[Some("thermals_page"), "Thermals"] = &model.thermals_page.container.clone(),
add_titled[Some("software_page"), "Software"] = model.software_page.widget(),
},
@ -165,6 +167,7 @@ impl AsyncComponent for AppModel {
.get_system_info()
.await
.expect("Could not fetch system info");
let system_info = Rc::new(system_info);
let devices = daemon_client
.list_devices()
@ -178,7 +181,9 @@ impl AsyncComponent for AppModel {
let info_page = InformationPage::builder().launch(()).detach();
let oc_page = OcPage::new(&system_info);
let oc_page = OcPage::builder()
.launch(system_info.clone())
.forward(sender.input_sender(), |msg| msg);
let thermals_page = ThermalsPage::new(&system_info);
let software_page = SoftwarePage::builder()
@ -193,13 +198,6 @@ impl AsyncComponent for AppModel {
.launch(())
.forward(sender.input_sender(), |msg| msg);
oc_page.clocks_frame.connect_clocks_reset(clone!(
#[strong]
sender,
move || {
sender.input(AppMsg::ResetClocks);
}
));
thermals_page.connect_reset_pmfw(clone!(
#[strong]
sender,
@ -208,21 +206,6 @@ impl AsyncComponent for AppModel {
}
));
if let Some(ref button) = oc_page.enable_overclocking_button {
button.connect_clicked(clone!(
#[strong]
sender,
move |_| {
sender.input(AppMsg::ask_confirmation(
AppMsg::EnableOverdrive,
"Enable Overclocking",
format!("This will enable the overdrive feature of the amdgpu driver by creating a file at <b>{MODULE_CONF_PATH}</b> and updating the initramfs. Are you sure you want to do this?"),
gtk::ButtonsType::OkCancel,
));
}
));
}
let graphs_window = GraphsWindow::builder().launch(()).detach();
let model = AppModel {
@ -281,6 +264,9 @@ impl AppModel {
) -> Result<(), Arc<anyhow::Error>> {
match msg {
AppMsg::Error(err) => return Err(err),
AppMsg::SettingsChanged => {
self.apply_revealer.emit(ApplyRevealerMsg::Show);
}
AppMsg::ReloadProfiles { include_state } => {
self.reload_profiles(include_state).await?;
sender.input(AppMsg::ReloadData { full: false });
@ -329,10 +315,14 @@ impl AppModel {
});
}
AppMsg::Stats(stats) => {
self.info_page.emit(PageUpdate::Stats(stats.clone()));
let update = PageUpdate::Stats(stats.clone());
self.info_page.emit(update.clone());
self.oc_page.emit(OcPageMsg::Update {
update,
initial: false,
});
self.thermals_page.set_stats(&stats, false);
self.oc_page.set_stats(&stats, false);
self.graphs_window.emit(GraphsWindowMsg::Stats(stats));
}
@ -454,9 +444,12 @@ impl AppModel {
sender.input(AppMsg::Error(Arc::new(anyhow!("Nvidia driver detected, but the management library could not be loaded. Check lact service status for more information."))));
}
self.info_page.emit(PageUpdate::Info(info.clone()));
self.oc_page.set_info(&info);
let update = PageUpdate::Info(info.clone());
self.info_page.emit(update.clone());
self.oc_page.emit(OcPageMsg::Update {
update,
initial: true,
});
let vram_clock_ratio = info
.drm_info
@ -493,10 +486,14 @@ impl AppModel {
.context("Could not fetch stats")?;
let stats = Arc::new(stats);
self.oc_page.set_stats(&stats, true);
self.thermals_page.set_stats(&stats, true);
self.info_page.emit(PageUpdate::Stats(stats));
let update = PageUpdate::Stats(stats.clone());
self.info_page.emit(update.clone());
self.oc_page.emit(OcPageMsg::Update {
update,
initial: true,
});
let maybe_clocks_table = match self.daemon_client.get_device_clocks_info(&gpu_id).await {
Ok(info) => info.table,
@ -505,7 +502,8 @@ impl AppModel {
None
}
};
self.oc_page.set_clocks_table(maybe_clocks_table);
self.oc_page
.emit(OcPageMsg::ClocksTable(maybe_clocks_table));
let maybe_modes_table = match self
.daemon_client
@ -519,14 +517,11 @@ impl AppModel {
}
};
self.oc_page
.performance_frame
.set_power_profile_modes(maybe_modes_table);
.emit(OcPageMsg::ProfileModesTable(maybe_modes_table));
match self.daemon_client.get_power_states(&gpu_id).await {
Ok(power_states) => {
self.oc_page
.power_states_frame
.set_power_states(power_states);
self.oc_page.emit(OcPageMsg::PowerStates(power_states));
}
Err(err) => warn!("could not get power states: {err:?}"),
}
@ -544,8 +539,6 @@ impl AppModel {
self.thermals_page
.connect_settings_changed(show_revealer.clone());
self.oc_page.connect_settings_changed(show_revealer);
self.apply_revealer
.sender()
.send(ApplyRevealerMsg::Hide)
@ -571,7 +564,8 @@ impl AppModel {
debug!("applying settings on gpu {gpu_id}");
if let Some(cap) = self.oc_page.get_power_cap() {
let cap = self.oc_page.model().get_power_cap();
if let Some(cap) = cap {
self.daemon_client
.set_power_cap(&gpu_id, Some(cap))
.await
@ -593,7 +587,8 @@ impl AppModel {
.await
.context("Could not commit config")?;
if let Some(level) = self.oc_page.get_performance_level() {
let performance_level = self.oc_page.model().get_performance_level();
if let Some(level) = performance_level {
self.daemon_client
.set_performance_level(&gpu_id, level)
.await
@ -605,10 +600,12 @@ impl AppModel {
let mode_index = self
.oc_page
.model()
.performance_frame
.get_selected_power_profile_mode();
let custom_heuristics = self
.oc_page
.model()
.performance_frame
.get_power_profile_mode_custom_heuristics();
@ -645,11 +642,11 @@ impl AppModel {
.context("Could not commit config")?;
}
let clocks_commands = self.oc_page.clocks_frame.get_commands();
let clocks_commands = self.oc_page.model().get_clocks_commands();
debug!("applying clocks commands {clocks_commands:#?}");
let enabled_power_states = self.oc_page.get_enabled_power_states();
let enabled_power_states = self.oc_page.model().get_enabled_power_states();
for (kind, states) in enabled_power_states {
if !states.is_empty() {

View File

@ -12,6 +12,7 @@ pub enum AppMsg {
Stats(Arc<DeviceStats>),
ApplyChanges,
RevertChanges,
SettingsChanged,
ResetClocks,
ResetPmfw,
ShowGraphsWindow,

View File

@ -8,7 +8,7 @@ use gtk::{prelude::*, *};
use lact_schema::{DeviceInfo, DeviceStats};
use std::sync::Arc;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum PageUpdate {
Info(Arc<DeviceInfo>),
Stats(Arc<DeviceStats>),

View File

@ -0,0 +1,257 @@
mod clocks_frame;
mod gpu_stats_section;
mod performance_frame;
mod power_cap_section;
mod power_profile;
mod power_states;
use super::PageUpdate;
use crate::app::msg::AppMsg;
use amdgpu_sysfs::gpu_handle::{
power_profile_mode::PowerProfileModesTable, PerformanceLevel, PowerLevelKind,
};
use clocks_frame::ClocksFrame;
use gpu_stats_section::GpuStatsSection;
use gtk::{
glib::object::ObjectExt,
pango,
prelude::{BoxExt, ButtonExt, FrameExt, OrientableExt, WidgetExt},
};
use lact_daemon::MODULE_CONF_PATH;
use lact_schema::{request::SetClocksCommand, ClocksTable, PowerStates, SystemInfo};
use performance_frame::PerformanceFrame;
use power_cap_section::PowerCapSection;
use power_states::power_states_frame::PowerStatesFrame;
use relm4::{ComponentParts, ComponentSender, RelmWidgetExt};
use std::{cell::Cell, collections::HashMap, rc::Rc};
use tracing::warn;
const OVERCLOCKING_DISABLED_TEXT: &str = "Overclocking support is not enabled! \
You can still change basic settings, but the more advanced clocks and voltage control will not be available.";
pub struct OcPage {
stats_section: GpuStatsSection,
pub performance_frame: PerformanceFrame,
power_cap_section: PowerCapSection,
power_states_frame: PowerStatesFrame,
clocks_frame: ClocksFrame,
// TODO: refactor this out when child components use senders
signals_blocked: Rc<Cell<bool>>,
}
#[derive(Debug)]
pub enum OcPageMsg {
Update { update: PageUpdate, initial: bool },
ClocksTable(Option<ClocksTable>),
ProfileModesTable(Option<PowerProfileModesTable>),
PowerStates(PowerStates),
}
#[relm4::component(pub)]
impl relm4::Component for OcPage {
type Init = Rc<SystemInfo>;
type Input = OcPageMsg;
type Output = AppMsg;
type CommandOutput = ();
view! {
gtk::ScrolledWindow {
set_hscrollbar_policy: gtk::PolicyType::Never,
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 15,
set_margin_horizontal: 20,
gtk::Frame {
set_visible: system_info.amdgpu_overdrive_enabled == Some(false),
set_label_align: 0.3,
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 2,
set_margin_all: 10,
gtk::Label {
set_markup: OVERCLOCKING_DISABLED_TEXT,
set_wrap: true,
set_wrap_mode: pango::WrapMode::Word,
},
gtk::Button {
set_label: "Enable Overclocking",
set_halign: gtk::Align::End,
connect_clicked[sender] => move |_| {
sender.output(AppMsg::ask_confirmation(
AppMsg::EnableOverdrive,
"Enable Overclocking",
format!("This will enable the overdrive feature of the amdgpu driver by creating a file at <b>{MODULE_CONF_PATH}</b> and updating the initramfs. Are you sure you want to do this?"),
gtk::ButtonsType::OkCancel,
)).expect("Channel closed");
}
},
},
},
model.stats_section.clone(),
model.power_cap_section.clone() {
connect_current_value_notify[sender] => move |_| {
sender.output(AppMsg::SettingsChanged).unwrap();
} @power_cap_notify,
},
model.performance_frame.container.clone(),
model.power_states_frame.clone(),
model.clocks_frame.container.clone(),
}
}
}
fn init(
system_info: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self {
stats_section: GpuStatsSection::new(),
performance_frame: PerformanceFrame::new(),
power_cap_section: PowerCapSection::new(),
power_states_frame: PowerStatesFrame::new(),
clocks_frame: ClocksFrame::new(),
signals_blocked: Rc::new(Cell::new(false)),
};
let widgets = view_output!();
model.clocks_frame.connect_clocks_reset(move || {
sender.output(AppMsg::ResetClocks).expect("Channel closed")
});
ComponentParts { model, widgets }
}
fn update_with_view(
&mut self,
widgets: &mut Self::Widgets,
msg: Self::Input,
sender: ComponentSender<Self>,
_root: &Self::Root,
) {
self.signals_blocked.set(true);
match msg {
OcPageMsg::Update { update, initial } => match update {
PageUpdate::Stats(stats) => {
self.stats_section.set_stats(&stats);
self.power_states_frame.set_stats(&stats);
if initial {
self.power_cap_section
.block_signal(&widgets.power_cap_notify);
self.power_cap_section
.set_max_value(stats.power.cap_max.unwrap_or_default());
self.power_cap_section
.set_min_value(stats.power.cap_min.unwrap_or_default());
self.power_cap_section
.set_default_value(stats.power.cap_default.unwrap_or_default());
if let Some(current_cap) = stats.power.cap_current {
self.power_cap_section.set_initial_value(current_cap);
self.power_cap_section.set_visible(true);
} else {
self.power_cap_section.set_visible(false);
}
match stats.performance_level {
Some(profile) => {
self.performance_frame.show();
self.performance_frame.set_active_level(profile);
}
None => self.performance_frame.hide(),
}
self.power_cap_section
.unblock_signal(&widgets.power_cap_notify);
}
}
PageUpdate::Info(info) => {
let vram_clock_ratio = info
.drm_info
.as_ref()
.map(|info| info.vram_clock_ratio)
.unwrap_or(1.0);
self.power_states_frame
.set_vram_clock_ratio(vram_clock_ratio);
self.stats_section.set_vram_clock_ratio(vram_clock_ratio);
self.clocks_frame.set_vram_clock_ratio(vram_clock_ratio);
}
},
OcPageMsg::ClocksTable(table) => match table {
Some(table) => match self.clocks_frame.set_table(table) {
Ok(()) => {
self.clocks_frame.show();
}
Err(err) => {
warn!("got invalid clocks table: {err:?}");
self.clocks_frame.hide();
}
},
None => {
self.clocks_frame.hide();
}
},
OcPageMsg::ProfileModesTable(modes_table) => {
self.performance_frame.set_power_profile_modes(modes_table);
}
OcPageMsg::PowerStates(states) => {
self.power_states_frame.set_power_states(states);
}
}
self.signals_blocked.set(false);
let signals_blocked = self.signals_blocked.clone();
let f = move || {
if !signals_blocked.get() {
sender
.output(AppMsg::SettingsChanged)
.expect("Channel closed")
}
};
self.performance_frame.connect_settings_changed(f.clone());
self.clocks_frame.connect_clocks_changed(f.clone());
self.power_states_frame.connect_values_changed(f);
}
}
impl OcPage {
pub fn get_performance_level(&self) -> Option<PerformanceLevel> {
if self.performance_frame.get_visibility() {
let level = self.performance_frame.get_selected_performance_level();
Some(level)
} else {
None
}
}
pub fn get_power_cap(&self) -> Option<f64> {
self.power_cap_section.get_user_cap()
}
pub fn get_clocks_commands(&self) -> Vec<SetClocksCommand> {
self.clocks_frame.get_commands()
}
pub fn get_enabled_power_states(&self) -> HashMap<PowerLevelKind, Vec<u8>> {
if self.performance_frame.get_selected_performance_level() == PerformanceLevel::Manual {
self.power_states_frame.get_enabled_power_states()
} else {
HashMap::new()
}
}
}

View File

@ -1,222 +0,0 @@
mod clocks_frame;
mod gpu_stats_section;
mod performance_frame;
mod power_cap_section;
mod power_profile;
mod power_states;
use self::power_cap_section::PowerCapSection;
use self::power_states::power_states_frame::PowerStatesFrame;
use amdgpu_sysfs::gpu_handle::{PerformanceLevel, PowerLevelKind};
use clocks_frame::ClocksFrame;
use gpu_stats_section::GpuStatsSection;
use gtk::*;
use gtk::{glib::clone, prelude::*};
use lact_client::schema::{DeviceInfo, DeviceStats, SystemInfo};
use lact_schema::ClocksTable;
use performance_frame::PerformanceFrame;
// use power_cap_frame::PowerCapFrame;
use std::collections::HashMap;
use tracing::warn;
const OVERCLOCKING_DISABLED_TEXT: &str = "Overclocking support is not enabled! \
You can still change basic settings, but the more advanced clocks and voltage control will not be available.";
#[derive(Clone)]
pub struct OcPage {
pub container: ScrolledWindow,
stats_section: GpuStatsSection,
pub performance_frame: PerformanceFrame,
// power_cap_frame: PowerCapFrame,
power_cap_section: PowerCapSection,
pub power_states_frame: PowerStatesFrame,
pub clocks_frame: ClocksFrame,
pub enable_overclocking_button: Option<Button>,
}
impl OcPage {
pub fn new(system_info: &SystemInfo) -> Self {
let container = ScrolledWindow::builder()
.hscrollbar_policy(PolicyType::Never)
.build();
let vbox = Box::builder()
.orientation(Orientation::Vertical)
.spacing(15)
.margin_start(20)
.margin_end(20)
.build();
let mut enable_overclocking_button = None;
if system_info.amdgpu_overdrive_enabled == Some(false) {
let (warning_frame, button) = oc_warning_frame();
enable_overclocking_button = Some(button);
vbox.append(&warning_frame);
}
let stats_section = GpuStatsSection::new();
vbox.append(&stats_section);
let power_cap_section = PowerCapSection::new();
let performance_level_frame = PerformanceFrame::new();
let clocks_frame = ClocksFrame::new();
let power_states_frame = PowerStatesFrame::new();
performance_level_frame.connect_settings_changed(clone!(
#[strong]
performance_level_frame,
#[strong]
power_states_frame,
move || {
let level = performance_level_frame.get_selected_performance_level();
power_states_frame.set_configurable(level == PerformanceLevel::Manual);
}
));
vbox.append(&power_cap_section);
vbox.append(&performance_level_frame.container);
vbox.append(&power_states_frame);
vbox.append(&clocks_frame.container);
container.set_child(Some(&vbox));
Self {
container,
stats_section,
performance_frame: performance_level_frame,
clocks_frame,
power_cap_section,
enable_overclocking_button,
power_states_frame,
}
}
pub fn set_stats(&self, stats: &DeviceStats, initial: bool) {
self.stats_section.set_stats(stats);
self.power_states_frame.set_stats(stats);
if initial {
self.power_cap_section
.set_max_value(stats.power.cap_max.unwrap_or_default());
self.power_cap_section
.set_min_value(stats.power.cap_min.unwrap_or_default());
self.power_cap_section
.set_default_value(stats.power.cap_default.unwrap_or_default());
if let Some(current_cap) = stats.power.cap_current {
self.power_cap_section.set_initial_value(current_cap);
self.power_cap_section.set_visible(true);
} else {
self.power_cap_section.set_visible(false);
}
self.set_performance_level(stats.performance_level);
}
}
pub fn set_info(&self, info: &DeviceInfo) {
let vram_clock_ratio = info
.drm_info
.as_ref()
.map(|info| info.vram_clock_ratio)
.unwrap_or(1.0);
self.power_states_frame
.set_vram_clock_ratio(vram_clock_ratio);
self.stats_section.set_vram_clock_ratio(vram_clock_ratio);
self.clocks_frame.set_vram_clock_ratio(vram_clock_ratio);
}
pub fn set_clocks_table(&self, table: Option<ClocksTable>) {
match table {
Some(table) => match self.clocks_frame.set_table(table) {
Ok(()) => {
self.clocks_frame.show();
}
Err(err) => {
warn!("got invalid clocks table: {err:?}");
self.clocks_frame.hide();
}
},
None => {
self.clocks_frame.hide();
}
}
}
pub fn connect_settings_changed<F: Fn() + 'static + Clone>(&self, f: F) {
self.performance_frame.connect_settings_changed(f.clone());
self.power_cap_section.connect_current_value_notify(clone!(
#[strong]
f,
move |_| f()
));
self.clocks_frame.connect_clocks_changed(f.clone());
self.power_states_frame.connect_values_changed(f);
}
pub fn set_performance_level(&self, profile: Option<PerformanceLevel>) {
match profile {
Some(profile) => {
self.performance_frame.show();
self.performance_frame.set_active_level(profile);
}
None => self.performance_frame.hide(),
}
}
pub fn get_performance_level(&self) -> Option<PerformanceLevel> {
if self.performance_frame.get_visibility() {
let level = self.performance_frame.get_selected_performance_level();
Some(level)
} else {
None
}
}
pub fn get_power_cap(&self) -> Option<f64> {
self.power_cap_section.get_user_cap()
}
pub fn get_enabled_power_states(&self) -> HashMap<PowerLevelKind, Vec<u8>> {
if self.performance_frame.get_selected_performance_level() == PerformanceLevel::Manual {
self.power_states_frame.get_enabled_power_states()
} else {
HashMap::new()
}
}
}
fn oc_warning_frame() -> (Frame, Button) {
let container = Frame::new(Some("Overclocking information"));
container.set_label_align(0.3);
let vbox = Box::builder()
.orientation(Orientation::Vertical)
.spacing(5)
.margin_top(10)
.margin_bottom(10)
.margin_start(10)
.margin_end(10)
.build();
let warning_label = Label::builder()
.use_markup(true)
.label(OVERCLOCKING_DISABLED_TEXT)
.wrap(true)
.wrap_mode(pango::WrapMode::Word)
.build();
let enable_button = Button::builder()
.label("Enable Overclocking")
.halign(Align::End)
.build();
vbox.append(&warning_label);
vbox.append(&enable_button);
container.set_child(Some(&vbox));
(container, enable_button)
}

View File

@ -2,13 +2,13 @@ use crate::{app::info_row::InfoRow, GUI_VERSION};
use gtk::prelude::*;
use lact_client::schema::{SystemInfo, GIT_COMMIT};
use relm4::{ComponentParts, ComponentSender, SimpleComponent};
use std::fmt::Write;
use std::{fmt::Write, rc::Rc};
pub struct SoftwarePage {}
#[relm4::component(pub)]
impl SimpleComponent for SoftwarePage {
type Init = (SystemInfo, bool);
type Init = (Rc<SystemInfo>, bool);
type Input = ();
type Output = ();
@ -38,7 +38,7 @@ impl SimpleComponent for SoftwarePage {
if embedded {
daemon_version.push_str("-embedded");
}
if let Some(commit) = system_info.commit {
if let Some(commit) = &system_info.commit {
write!(daemon_version, " (commit {commit})").unwrap();
}