feat: oc cards layout (#1178)

* feat: change OC layout to cards

* feat: switch from checkbox to toggle-buttons for OC optional features

* put into size-group, some small fixes

* make p-states more compact

* remove core offset p-states label

* add dedigated features block for sections

* hacky fix for padding

* update p-state section toggle

* fix adv block bottom border

* use info-dialog to guard manual p-lvl for p-states conf

* upd p-level description to fit in split layout

* independent all p-state toggle for core/vram

* p-level/powermizer: use more compact description

* rollback description changes due to better fix

* use OcAdjustment for performance limit

* shorten description a bit

* remove nvidia oc description

* rename power_section to power_frame for consistency

* fix signal blocking and simplify power frame visibility

* feat: move warning to sidebar and make it more specific

* phrasing

* remove wrapper

* remove OC-adjustment group, make adjustment components shared.

* rename css for consistency

* make adjustment row control it's state

* remove oc_adjustment_row wrapper

* simplify power_frames adjustment wiring

* fix css priority to avoid overrides

* breeze theme should be between adwaita and app css

* rename secondary_clocks

* set border radius to features buttons

* implement MasonryLayout via LayoutManager

* remember children's column

* Revert "remember children's column"

This reverts commit ed82e94eff.

* use 2 columns and flowbox instead of masonry

* cleanup

* redo layout with single column order control

* hide reset button on vram frame

* fix specificity

* upd spacing

* shorten labels

* chore: better label, disable all pstates button when it's not usable

---------

Co-authored-by: Ilya Zlobintsev <ilya.zl@protonmail.com>
This commit is contained in:
Roman MakarovandIlya Zlobintsev authored and GitHub committed 2026-09-13 14:04:47 +03:00
1 parent 7145befe5e
commit 4db0e87b66
18 files changed
+828 -788

No files matched your search

+18 -31
View File
@@ -112,7 +112,8 @@ confirm-settings = Confirm Settings
revert-button = Revert
settings-confirmation = Do you want to keep the new settings? (Reverting in {$seconds_left} seconds)
power-cap = Power Usage Limit
power-cap = Power Limit
power-section = Power
watt = W
ghz = GHz
@@ -124,12 +125,14 @@ gibibyte = GiB
stats-section = Statistics
gpu-clock = GPU Core Clock
core-section = Core
gpu-clock-avg = GPU Core Clock (Average)
gpu-clock-target = GPU Core Clock (Target)
gpu-voltage = GPU Voltage
gpu-temp = Temperature
gpu-usage = GPU Usage
vram-clock = VRAM Clock
vram-section = VRAM
power-usage = Power Usage
extra-clocks = Extra Clocks
no-throttling = No
@@ -169,21 +172,10 @@ power-mizer-mode-prefer-maximum-performance-description = Favor maximum performa
power-mizer-mode-prefer-consistent-performance-description = Lock to GPU base clocks.
overclock-section = Clockspeed and Voltage
nvidia-oc-info = Overclocking Information
nvidia-oc-description =
Overclocking functionality on Nvidia includes setting offsets for GPU/VRAM clockspeeds and limiting the potential range of clockspeeds using the "locked clocks" feature.
On many cards, the VRAM clockpeed offset will only affect the actual memory clockspeed by half of the offset value.
For example, a +1000MHz VRAM offset may only increase the measured VRAM speed by 500MHz.
This is normal, and is how Nvidia handles GDDR data rates. Adjust your overclock accordingly.
It is possible to achieve a pseudo-undervolt by combining the locked clocks option with a positive clockspeed offset.
This will force the GPU to run at a voltage that's constrained by the locked clocks, while achieving a higher clockspeed due to the offset.
This can cause system instability if pushed too high.
show-all-pstates = Show all P-States
enable-gpu-locked-clocks = Enable GPU Locked Clocks
enable-vram-locked-clocks = Enable VRAM Locked Clocks
enable-vf-curve = Enable VF Curve Edit
advanced-features = Advanced features
show-all-pstates = All P-States
enable-locked-clocks = Locked Clocks
enable-vf-curve = Custom VF Curve
pstate-list-description = <b>The following values are clock offsets for each P-State, going from highest to lowest.</b>
no-clocks-data = No clocks data available
reset-oc-tooltip = Warning: this resets all clock settings to defaults!
@@ -200,22 +192,17 @@ vf-curve-flatten-right = Flatten curve to the right
vf-curve-flatten-selection = Flatten selection
vf-curve-editing-disabled = VF curve editing is disabled on the OC page
gpu-clock-offset = GPU Clock Offset (MHz)
max-gpu-clock = Maximum GPU Clock (MHz)
max-vram-clock = Maximum VRAM Clock (MHz)
max-gpu-voltage = Maximum GPU Voltage (mV)
min-gpu-clock = Minimum GPU Clock (MHz)
min-vram-clock = Minimum VRAM Clock (MHz)
min-gpu-voltage = Minimum GPU Voltage (mV)
gpu-voltage-offset = GPU voltage offset (mV)
gpu-voltage-boost = GPU Voltage Boost (%)
gpu-clock-offset = Clock Offset (MHz)
max-clock = Maximum Clock (MHz)
max-gpu-voltage = Maximum Voltage (mV)
min-clock = Minimum Clock (MHz)
min-gpu-voltage = Minimum Voltage (mV)
gpu-voltage-offset = Voltage Offset (mV)
gpu-voltage-boost = Voltage Boost (%)
gpu-voltage-boost-tooltip = Controls how much of the additional voltage headroom defined by the driver is available. 100% means all of this headroom, not 100% of total GPU voltage. More headroom may sustain higher clockspeeds but increases power draw and heat.
gpu-pstate-clock-offset = GPU P-State {$pstate} Clock Offset (MHz)
vram-pstate-clock-offset = VRAM P-State {$pstate} Clock Offset (MHz)
gpu-pstate-clock = GPU P-State {$pstate} Clock (MHz)
mem-pstate-clock = VRAM P-State {$pstate} Clock (MHz)
gpu-pstate-clock-voltage = GPU P-State {$pstate} Voltage (mV)
mem-pstate-clock-voltage = VRAM P-State {$pstate} Voltage (mV)
pstate-clock-offset = P-State {$pstate} Clock Offset (MHz)
pstate-clock = P-State {$pstate} Clock (MHz)
pstate-clock-voltage = P-State {$pstate} Voltage (mV)
pstates = Power States
gpu-pstates = GPU Power States
-9
View File
@@ -13,12 +13,3 @@
.main-sidebar-container .sidebar-section {
margin: 4px 8px 8px 8px;
}
.oc-warning-button > button {
color: @warning_color;
background: alpha(@warning_color, 0.15);
}
.oc-warning-button > button:hover {
background: alpha(@warning_color, 0.25);
}
+2 -8
View File
@@ -39,10 +39,7 @@ use crate::{
use adw::prelude::*;
use anyhow::{Context, anyhow};
use graphs_window::{GraphsWindow, GraphsWindowMsg};
use gtk::{
STYLE_PROVIDER_PRIORITY_APPLICATION,
glib::{self, ControlFlow, clone},
};
use gtk::glib::{self, ControlFlow, clone};
use i18n_embed_fl::fl;
use lact_client::{ConnectionStatusMsg, DaemonClient};
use lact_schema::{
@@ -368,10 +365,7 @@ impl AsyncComponent for AppModel {
.application()
.expect("Failed to get application from root window");
relm4::set_global_css_with_priority(
styles::COMBINED_CSS,
STYLE_PROVIDER_PRIORITY_APPLICATION,
);
relm4::set_global_css_with_priority(styles::COMBINED_CSS, styles::APPLICATION_CSS_PRIORITY);
if let Err(err) = styles::apply_theme(CONFIG.read().theme) {
error!("could not apply theme: {err:#}");
@@ -0,0 +1,15 @@
.adjustment-card-advanced-features {
background-color: alpha(@window_fg_color, 0.05);
border-radius: 9px 9px 0 0;
margin: -10px -10px 0; /* nulify parent's padding */
padding: 10px;
}
.adjustment-card-advanced-features .adjustment-card-option-toggle {
border-radius: 25px;
}
.adjustment-card-advanced-features .adjustment-card-option-toggle:checked {
background-color: @accent_bg_color;
color: @accent_fg_color;
}
@@ -0,0 +1,44 @@
use crate::I18N;
use gtk::prelude::*;
use i18n_embed_fl::fl;
use relm4::css;
#[relm4::widget_template(pub)]
impl relm4::WidgetTemplate for AdjustmentCard {
view! {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 10,
set_hexpand: true,
set_valign: gtk::Align::Start,
#[name = "advanced_features"]
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
set_visible: false,
add_css_class: "adjustment-card-advanced-features",
gtk::Label {
set_label: &fl!(I18N, "advanced-features"),
set_halign: gtk::Align::Start,
add_css_class: css::DIM_LABEL,
add_css_class: css::CAPTION,
},
#[name = "controls"]
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 10,
set_halign: gtk::Align::Start,
},
},
#[name = "content"]
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 10,
},
}
}
}
+1
View File
@@ -1,3 +1,4 @@
pub(crate) mod adjustment_card;
pub(crate) mod adjustment_row;
pub(crate) mod adjustment_value;
pub(crate) mod gpu_stats_section;
+4
View File
@@ -0,0 +1,4 @@
.oc-page-section {
min-width: 400px;
padding: 0;
}
+82 -51
View File
@@ -1,6 +1,7 @@
mod card_layout;
mod clocks_frame;
mod performance_frame;
mod power_cap_section;
mod power_frame;
mod power_states;
mod vf_curve;
@@ -11,17 +12,18 @@ use crate::app::components::gpu_stats_section::{
use crate::app::pages::PageUpdate;
use crate::app::utils::ext::RelmLaunchable as _;
use crate::app::{msg::AppMsg, utils::ext::RelmDefaultLauchable};
use adw::prelude::*;
use amdgpu_sysfs::gpu_handle::{
PerformanceLevel, PowerLevelKind, power_profile_mode::PowerProfileModesTable,
};
use clocks_frame::{ClocksFrame, ClocksFrameInit, ClocksFrameMsg};
use gtk::prelude::{BoxExt, OrientableExt, WidgetExt};
use card_layout::{CardLayout, ColumnBias};
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::{PerformanceFrame, PerformanceFrameMsg};
use power_cap_section::{PowerCapMsg, PowerCapSection};
use performance_frame::PerformanceFrameMsg;
use power_frame::{PowerFrame, PowerFrameMsg};
use power_states::power_states_frame::{PowerStatesFrame, PowerStatesFrameMsg};
use relm4::binding::BoolBinding;
use relm4::{ComponentController, ComponentParts, ComponentSender, RelmWidgetExt};
@@ -34,10 +36,10 @@ pub struct OcPage {
stats_section: relm4::Controller<GpuStatsSection>,
device_info: Option<Arc<DeviceInfo>>,
performance_frame: relm4::Controller<PerformanceFrame>,
power_cap_section: relm4::Controller<PowerCapSection>,
power_frame: relm4::Controller<PowerFrame>,
power_states_frame: relm4::Controller<PowerStatesFrame>,
clocks_frame: relm4::Controller<ClocksFrame>,
gpu_clocks_frame: relm4::Controller<ClocksFrame>,
vram_clocks_frame: relm4::Controller<ClocksFrame>,
vf_curve_editor: relm4::Controller<VfCurveEditor>,
}
@@ -74,15 +76,38 @@ impl relm4::Component for OcPage {
view! {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 10,
set_spacing: 15,
set_margin_all: 15,
set_margin_top: 20, // align with gpu picker
model.stats_section.widget(),
model.power_cap_section.widget(),
model.performance_frame.widget(),
model.power_states_frame.widget(),
model.clocks_frame.widget(),
gtk::Box {
set_layout_manager: Some(CardLayout::new([
ColumnBias::Left, // Core
ColumnBias::Right, // VRAM
ColumnBias::Left, // Power
ColumnBias::Right, // Power States
])),
set_valign: gtk::Align::Start,
model.gpu_clocks_frame.widget() {
add_css_class: "oc-page-section",
},
model.vram_clocks_frame.widget() {
add_css_class: "oc-page-section",
},
model.power_frame.widget() {
add_css_class: "oc-page-section",
},
model.power_states_frame.widget() {
add_css_class: "oc-page-section",
},
},
},
}
@@ -107,16 +132,22 @@ impl relm4::Component for OcPage {
GpuStat::ExtraClocks,
]),
});
let power_cap_section = PowerCapSection::detach_default();
let vf_curve_editing = BoolBinding::new(false);
let clocks_frame = ClocksFrame::launch(ClocksFrameInit {
let gpu_clocks_frame = ClocksFrame::launch(ClocksFrameInit {
domain: ClockDomain::Gpu,
vf_curve_editing: vf_curve_editing.clone(),
show_all_pstates: BoolBinding::new(false),
})
.forward(sender.input_sender(), |msg| msg);
let vram_clocks_frame = ClocksFrame::launch(ClocksFrameInit {
domain: ClockDomain::Vram,
vf_curve_editing: BoolBinding::new(false),
show_all_pstates: BoolBinding::new(false),
})
.forward(sender.input_sender(), |msg| msg);
let power_states_frame =
PowerStatesFrame::launch_default().forward(sender.input_sender(), |msg| msg);
let performance_frame =
PerformanceFrame::launch_default().forward(sender.input_sender(), |msg| msg);
let power_frame = PowerFrame::launch_default().forward(sender.input_sender(), |msg| msg);
let vf_curve_editor = VfCurveEditor::detach(VfCurveEditorInit {
global_settings_changed: settings_changed,
@@ -126,10 +157,10 @@ impl relm4::Component for OcPage {
let model = Self {
stats_section,
device_info: None,
performance_frame,
power_cap_section,
power_frame,
power_states_frame,
clocks_frame,
gpu_clocks_frame,
vram_clocks_frame,
vf_curve_editor,
};
@@ -158,25 +189,17 @@ impl relm4::Component for OcPage {
.emit(VfCurveEditorMsg::Stats(stats.clone()));
if initial {
self.power_cap_section
.emit(PowerCapMsg::Update(update.clone()));
if stats.power.cap_current.is_some() {
self.power_cap_section.widget().set_visible(true);
} else {
self.power_cap_section.widget().set_visible(false);
}
self.performance_frame
.emit(PerformanceFrameMsg::PerformanceLevel(
stats.performance_level,
));
self.performance_frame
.emit(PerformanceFrameMsg::PowerMizerInfo {
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(),
});
sender.input(OcPageMsg::PerformanceLevelChanged);
},
));
}
}
PageUpdate::Info(info) => {
@@ -187,7 +210,7 @@ impl relm4::Component for OcPage {
.emit(GpuStatsSectionMsg::Info(info.clone()));
self.power_states_frame
.emit(PowerStatesFrameMsg::VramClockRatio(vram_clock_ratio));
self.clocks_frame
self.vram_clocks_frame
.emit(ClocksFrameMsg::VramRatio(vram_clock_ratio));
}
},
@@ -197,7 +220,11 @@ impl relm4::Component for OcPage {
} => {
let table = table.map(Arc::new);
self.clocks_frame.emit(ClocksFrameMsg::Clocks {
self.gpu_clocks_frame.emit(ClocksFrameMsg::Clocks {
table: table.clone(),
vf_curve_is_configured,
});
self.vram_clocks_frame.emit(ClocksFrameMsg::Clocks {
table: table.clone(),
vf_curve_is_configured,
});
@@ -205,8 +232,9 @@ impl relm4::Component for OcPage {
.emit(VfCurveEditorMsg::Clocks(table.clone()));
}
OcPageMsg::ProfileModesTable(modes_table) => {
self.performance_frame
.emit(PerformanceFrameMsg::PowerProfileModes(modes_table));
self.power_frame.emit(PowerFrameMsg::Performance(
PerformanceFrameMsg::PowerProfileModes(modes_table),
));
}
OcPageMsg::PowerStates {
pstates,
@@ -235,8 +263,9 @@ impl relm4::Component for OcPage {
));
}
OcPageMsg::SetPerformanceLevel(level) => {
self.performance_frame
.emit(PerformanceFrameMsg::PerformanceLevel(Some(level)));
self.power_frame.emit(PowerFrameMsg::Performance(
PerformanceFrameMsg::PerformanceLevel(Some(level)),
));
APP_BROKER.send(AppMsg::SettingsChanged);
}
OcPageMsg::EnablePstateConfig => {
@@ -248,7 +277,8 @@ impl relm4::Component for OcPage {
}
OcPageMsg::VfCurveEditingToggled(enabled) => {
if enabled {
self.clocks_frame.emit(ClocksFrameMsg::ResetGpuClockOffsets);
self.gpu_clocks_frame
.emit(ClocksFrameMsg::ResetGpuClockOffsets);
} else {
self.vf_curve_editor.emit(VfCurveEditorMsg::ResetCurve);
}
@@ -261,29 +291,30 @@ impl relm4::Component for OcPage {
impl OcPage {
pub fn get_performance_level(&self) -> Option<PerformanceLevel> {
self.performance_frame.model().performance_level()
self.power_frame.model().performance_level()
}
pub fn get_active_power_mizer_mode(&self) -> Option<PowerMizerMode> {
self.performance_frame.model().active_power_mizer_mode()
self.power_frame.model().active_power_mizer_mode()
}
pub fn get_power_profile_mode(&self) -> Option<u16> {
self.performance_frame.model().power_profile_mode()
self.power_frame.model().power_profile_mode()
}
pub fn get_power_profile_mode_custom_heuristics(&self) -> Vec<Vec<Option<i32>>> {
self.performance_frame
self.power_frame
.model()
.power_profile_mode_custom_heuristics()
}
pub fn get_power_cap(&self) -> Option<f64> {
self.power_cap_section.model().get_user_cap()
self.power_frame.model().get_user_cap()
}
pub fn apply_clocks_config(&self, config: &mut config::ClocksConfiguration) {
let commands = self.clocks_frame.model().get_commands();
let mut commands = self.gpu_clocks_frame.model().get_commands();
commands.extend(self.vram_clocks_frame.model().get_commands());
debug!("applying clocks commands {commands:#?}");
@@ -297,7 +328,7 @@ impl OcPage {
}
pub fn get_enabled_power_states(&self) -> IndexMap<PowerLevelKind, Vec<u8>> {
if self.performance_frame.model().performance_level() == Some(PerformanceLevel::Manual) {
if self.get_performance_level() == Some(PerformanceLevel::Manual) {
self.power_states_frame.model().get_enabled_power_states()
} else {
IndexMap::new()
@@ -0,0 +1,127 @@
use gtk::{glib, prelude::*, subclass::prelude::*};
use std::cell::OnceCell;
#[derive(Clone, Copy, Debug)]
pub enum ColumnBias {
Left,
Right,
}
glib::wrapper! {
pub struct CardLayout(ObjectSubclass<imp::CardLayout>)
@extends gtk::LayoutManager;
}
impl CardLayout {
pub fn new(column_bias: impl Into<Vec<ColumnBias>>) -> Self {
let layout: Self = glib::Object::new();
layout.imp().column_bias.set(column_bias.into()).unwrap();
layout
}
}
mod imp {
use super::*;
#[derive(Default)]
pub struct CardLayout {
pub column_bias: OnceCell<Vec<ColumnBias>>,
}
#[glib::object_subclass]
impl ObjectSubclass for CardLayout {
const NAME: &'static str = "CardLayout";
type Type = super::CardLayout;
type ParentType = gtk::LayoutManager;
}
impl ObjectImpl for CardLayout {}
impl LayoutManagerImpl for CardLayout {
fn request_mode(&self, _widget: &gtk::Widget) -> gtk::SizeRequestMode {
gtk::SizeRequestMode::HeightForWidth
}
fn measure(
&self,
widget: &gtk::Widget,
orientation: gtk::Orientation,
for_size: i32,
) -> (i32, i32, i32, i32) {
let (minimum, natural) = measure_width(widget);
if orientation == gtk::Orientation::Horizontal {
(minimum, natural, -1, -1)
} else {
let width = if for_size < 0 { natural } else { for_size };
let height = layout(widget, width, self.column_bias.get().unwrap(), |_, _| {});
(height, height, -1, -1)
}
}
fn allocate(&self, widget: &gtk::Widget, width: i32, _height: i32, _baseline: i32) {
layout(
widget,
width,
self.column_bias.get().unwrap(),
|child, allocation| {
child.size_allocate(allocation, -1);
},
);
}
}
}
const SPACING: i32 = 15;
fn children(widget: &gtk::Widget) -> impl Iterator<Item = (usize, gtk::Widget)> {
std::iter::successors(widget.first_child(), |child| child.next_sibling())
.enumerate()
.filter(|(_, child)| child.should_layout())
}
fn measure_width(widget: &gtk::Widget) -> (i32, i32) {
let mut minimum = 0;
let mut natural = 0;
let mut count = 0;
for (_, child) in children(widget) {
let (child_minimum, child_natural, _, _) = child.measure(gtk::Orientation::Horizontal, -1);
minimum = minimum.max(child_minimum);
natural = natural.max(child_natural);
count += 1;
}
if count > 1 {
natural = natural * 2 + SPACING;
}
(minimum, natural)
}
// Measurement and allocation share placement so the scroller gets the full height.
fn layout(
widget: &gtk::Widget,
width: i32,
column_bias: &[ColumnBias],
mut place: impl FnMut(&gtk::Widget, &gtk::Allocation),
) -> i32 {
let (minimum, _) = measure_width(widget);
let columns = if children(widget).count() > 1 && width >= minimum * 2 + SPACING {
2
} else {
1
};
let column_width = ((width - SPACING * (columns as i32 - 1)) / columns as i32).max(0);
let mut heights = [0, 0];
for (index, child) in children(widget) {
let column = usize::from(columns == 2 && matches!(column_bias[index], ColumnBias::Right));
let x = column as i32 * (column_width + SPACING);
let child_width = if column == columns - 1 {
width - x
} else {
column_width
};
let (_, height, _, _) = child.measure(gtk::Orientation::Vertical, child_width);
let y = heights[column];
place(&child, &gtk::Allocation::new(x, y, child_width, height));
heights[column] = y + height + SPACING;
}
(heights[0].max(heights[1]) - SPACING).max(0)
}
@@ -1,11 +0,0 @@
.clocks-frame-group {
padding: 0;
}
.clocks-frame-group .card {
padding: 10px;
}
.clocks-frame-group .adjustment-row > scale {
min-width: 100px;
}
@@ -1,231 +0,0 @@
use super::{ClocksData, clock_title};
use crate::{
APP_BROKER, I18N,
app::{
components::adjustment_row::{AdjustmentRow, AdjustmentRowInit, AdjustmentRowMsg},
msg::AppMsg,
},
};
use adw::prelude::*;
use i18n_embed_fl::fl;
use lact_schema::request::ClockspeedType;
use relm4::{css, factory::FactoryHashMap, prelude::FactoryComponent};
use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ClockCategory {
CoreClock,
CoreVoltage,
VramClock,
CoreCurveClock,
VramCurveClock,
CoreCurveVoltage,
VramCurveVoltage,
}
impl ClockCategory {
pub fn from_type(clock_type: ClockspeedType) -> Self {
match clock_type {
ClockspeedType::MaxCoreClock
| ClockspeedType::MinCoreClock
| ClockspeedType::GpuClockOffset(_) => ClockCategory::CoreClock,
ClockspeedType::MinVoltage
| ClockspeedType::MaxVoltage
| ClockspeedType::VoltageOffset
| ClockspeedType::VoltageBoost => ClockCategory::CoreVoltage,
ClockspeedType::MaxMemoryClock
| ClockspeedType::MinMemoryClock
| ClockspeedType::MemClockOffset(_) => ClockCategory::VramClock,
ClockspeedType::GpuVfCurveClock(_) => ClockCategory::CoreCurveClock,
ClockspeedType::MemVfCurveClock(_) => ClockCategory::VramCurveClock,
ClockspeedType::GpuVfCurveVoltage(_) => ClockCategory::CoreCurveVoltage,
ClockspeedType::MemVfCurveVoltage(_) => ClockCategory::VramCurveVoltage,
ClockspeedType::Reset => unreachable!(),
}
}
pub fn is_core(&self) -> bool {
Self::CORE.contains(self)
}
pub fn is_vram(&self) -> bool {
Self::VRAM.contains(self)
}
pub const CORE: [ClockCategory; 4] = [
ClockCategory::CoreClock,
ClockCategory::CoreVoltage,
ClockCategory::CoreCurveClock,
ClockCategory::CoreCurveVoltage,
];
pub const VRAM: [ClockCategory; 3] = [
ClockCategory::VramClock,
ClockCategory::VramCurveClock,
ClockCategory::VramCurveVoltage,
];
}
pub struct AdjustmentGroup {
adjustments: FactoryHashMap<ClockspeedType, AdjustmentRow<ClockspeedType>>,
secondary_clocks: HashSet<ClockspeedType>,
}
impl AdjustmentGroup {
pub fn is_empty(&self) -> bool {
self.adjustments.is_empty()
}
pub fn has_secondary(&self) -> bool {
!self.secondary_clocks.is_empty()
}
pub fn set_clock(&mut self, clock_type: ClockspeedType, data: ClocksData) {
self.adjustments.insert(
clock_type,
AdjustmentRowInit {
title: data.custom_title.unwrap_or_else(|| clock_title(clock_type)),
info_text: if clock_type == ClockspeedType::VoltageBoost {
fl!(I18N, "gpu-voltage-boost-tooltip")
} else {
String::new()
},
value: f64::from(data.current),
lower: f64::from(data.min),
upper: f64::from(data.max),
step_increment: get_row_step(clock_type),
..Default::default()
},
);
if data.is_secondary {
self.secondary_clocks.insert(clock_type);
} else {
self.secondary_clocks.remove(&clock_type);
}
}
pub fn add_size_group(&self, label_group: gtk::SizeGroup, input_group: gtk::SizeGroup) {
for clock_type in self.adjustments.keys() {
self.adjustments.send(
clock_type,
AdjustmentRowMsg::AddSizeGroup {
label_group: label_group.clone(),
input_group: input_group.clone(),
},
);
}
}
pub fn set_value_ratio(&self, ratio: f64) {
for clock_type in self.adjustments.keys() {
self.adjustments
.send(clock_type, AdjustmentRowMsg::ValueRatio(ratio));
}
}
pub fn toggle_secondary_visibility(
&self,
show_secondary: bool,
show_nvidia_options: bool,
enable_gpu_locked: bool,
enable_vram_locked: bool,
vf_curve_editing: bool,
) {
let mut any_visible = false;
for key in self.adjustments.keys() {
let show_current = match key {
ClockspeedType::MaxCoreClock | ClockspeedType::MinCoreClock
if show_nvidia_options =>
{
enable_gpu_locked
}
ClockspeedType::MaxMemoryClock | ClockspeedType::MinMemoryClock
if show_nvidia_options =>
{
enable_vram_locked
}
ClockspeedType::GpuClockOffset(_) if show_nvidia_options && vf_curve_editing => {
false
}
_ => !self.secondary_clocks.contains(key) || show_secondary,
};
any_visible |= show_current;
self.adjustments
.send(key, AdjustmentRowMsg::SetVisible(show_current));
}
// removes empty card
self.adjustments.widget().set_visible(any_visible);
}
pub fn get_commands(&self) -> Vec<(ClockspeedType, Option<i32>)> {
self.adjustments
.iter()
.map(|(clock_type, row)| {
(
*clock_type,
row.get_changed_value().map(|value| value as i32),
)
})
.collect()
}
pub fn reset_gpu_clock_offsets(&self) {
for clock_type in self.adjustments.keys() {
if matches!(clock_type, ClockspeedType::GpuClockOffset(_)) {
self.adjustments
.send(clock_type, AdjustmentRowMsg::SetValue(0.0));
}
}
}
pub fn get_raw_value(&self, clock_type: ClockspeedType) -> i32 {
self.adjustments
.get(&clock_type)
.map(|row| row.get_value() as i32)
.unwrap_or(0)
}
}
fn get_row_step(clock_type: ClockspeedType) -> f64 {
match ClockCategory::from_type(clock_type) {
ClockCategory::CoreClock
| ClockCategory::VramClock
| ClockCategory::CoreCurveClock
| ClockCategory::VramCurveClock => 5.0,
ClockCategory::CoreVoltage
| ClockCategory::CoreCurveVoltage
| ClockCategory::VramCurveVoltage => 1.0,
}
}
#[relm4::factory(pub)]
impl FactoryComponent for AdjustmentGroup {
type Init = ();
type Input = ();
type Output = ();
type CommandOutput = ();
type ParentWidget = gtk::Box;
type Index = ClockCategory;
view! {
self.adjustments.widget().clone() -> gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
set_valign: gtk::Align::Start,
add_css_class: css::CARD,
}
}
fn init_model(_: Self::Init, _: &Self::Index, _: relm4::FactorySender<Self>) -> Self {
Self {
adjustments: FactoryHashMap::builder()
.launch_default()
.forward(APP_BROKER.sender(), |()| AppMsg::SettingsChanged),
secondary_clocks: HashSet::new(),
}
}
}
+287 -255
View File
@@ -1,16 +1,17 @@
mod adjustment_group;
use crate::{
APP_BROKER, I18N,
app::{components::page_section::PageSection, msg::AppMsg, pages::oc_page::OcPageMsg},
app::{
components::{
adjustment_card::AdjustmentCard,
adjustment_row::{AdjustmentRow, AdjustmentRowInit, AdjustmentRowMsg},
page_section::PageSection,
},
msg::AppMsg,
pages::oc_page::OcPageMsg,
},
};
use adjustment_group::{AdjustmentGroup, ClockCategory};
use adw::prelude::*;
use amdgpu_sysfs::gpu_handle::overdrive::ClocksTableGen as AmdClocksTable;
use gtk::{
glib::object::ObjectExt,
pango,
prelude::{BoxExt, ButtonExt, CheckButtonExt, OrientableExt, WidgetExt},
};
use i18n_embed_fl::fl;
use lact_schema::{
ClocksTable, IntelClocksTable, NvidiaClockOffset, NvidiaClocksTable,
@@ -20,20 +21,20 @@ use relm4::{
ComponentParts, ComponentSender, RelmObjectExt, RelmWidgetExt, binding::BoolBinding, css,
factory::FactoryHashMap,
};
use std::sync::Arc;
use std::{collections::HashSet, sync::Arc};
const DEFAULT_VOLTAGE_OFFSET_RANGE: i32 = 250;
pub struct ClocksFrame {
core_groups: FactoryHashMap<ClockCategory, AdjustmentGroup>,
vram_groups: FactoryHashMap<ClockCategory, AdjustmentGroup>,
adjustments: FactoryHashMap<ClockspeedType, AdjustmentRow<ClockspeedType>>,
secondary_p_state_clocks: HashSet<ClockspeedType>,
domain: ClockDomain,
vram_clock_ratio: f64,
show_nvidia_options: bool,
vf_curve_available: bool,
show_all_pstates: BoolBinding,
vf_curve_editing: BoolBinding,
enable_gpu_locked_clocks: BoolBinding,
enable_vram_locked_clocks: BoolBinding,
enable_locked_clocks: BoolBinding,
}
#[derive(Default)]
@@ -42,7 +43,7 @@ struct ClocksData {
min: i32,
max: i32,
custom_title: Option<String>,
is_secondary: bool,
is_secondary_p_state: bool,
}
impl ClocksData {
@@ -57,7 +58,38 @@ impl ClocksData {
}
pub struct ClocksFrameInit {
pub domain: ClockDomain,
pub vf_curve_editing: BoolBinding,
pub show_all_pstates: BoolBinding,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClockDomain {
Gpu,
Vram,
}
impl ClockDomain {
fn matches(self, clock_type: ClockspeedType) -> bool {
let domain = match clock_type {
ClockspeedType::MaxCoreClock
| ClockspeedType::MinCoreClock
| ClockspeedType::GpuClockOffset(_)
| ClockspeedType::MinVoltage
| ClockspeedType::MaxVoltage
| ClockspeedType::VoltageOffset
| ClockspeedType::VoltageBoost
| ClockspeedType::GpuVfCurveClock(_)
| ClockspeedType::GpuVfCurveVoltage(_) => Self::Gpu,
ClockspeedType::MaxMemoryClock
| ClockspeedType::MinMemoryClock
| ClockspeedType::MemClockOffset(_)
| ClockspeedType::MemVfCurveClock(_)
| ClockspeedType::MemVfCurveVoltage(_) => Self::Vram,
ClockspeedType::Reset => unreachable!(),
};
self == domain
}
}
#[derive(Debug)]
@@ -79,36 +111,27 @@ impl relm4::Component for ClocksFrame {
type CommandOutput = ();
view! {
PageSection::new(&fl!(I18N, "overclock-section")) {
set_hide_visible_container: true,
add_css_class: "clocks-frame",
PageSection::new("") {
#[watch]
set_name: match model.domain {
ClockDomain::Gpu => fl!(I18N, "core-section"),
ClockDomain::Vram => fl!(I18N, "vram-section"),
},
#[watch]
set_visible: model.domain == ClockDomain::Gpu || model.has_any_clocks(),
append_header = &gtk::Box {
set_spacing: 10,
set_hexpand: true,
set_halign: gtk::Align::End,
append = &gtk::MenuButton {
#[watch]
set_visible: model.show_nvidia_options,
set_label: &fl!(I18N, "nvidia-oc-info"),
#[wrap(Some)]
set_popover = &gtk::Popover {
gtk::Label {
set_margin_all: 5,
set_markup: &fl!(I18N, "nvidia-oc-description"),
set_wrap: true,
set_max_width_chars: 75,
}
}
},
append = &gtk::Button {
set_label: &fl!(I18N, "vf-curve-editor"),
#[watch]
set_visible: model.show_nvidia_options && model.vf_curve_available,
set_visible: model.domain == ClockDomain::Gpu
&& model.show_nvidia_options
&& model.vf_curve_available,
connect_clicked[sender] => move |_| {
sender.output(OcPageMsg::ShowVfCurveEditor).unwrap();
@@ -122,7 +145,7 @@ impl relm4::Component for ClocksFrame {
add_css_class: css::DESTRUCTIVE_ACTION,
#[watch]
set_visible: model.has_any_clocks(),
set_visible: model.domain == ClockDomain::Gpu && model.has_any_clocks(),
connect_clicked => move |_| {
APP_BROKER.send(AppMsg::ResetClocks);
@@ -130,141 +153,129 @@ impl relm4::Component for ClocksFrame {
},
},
append_child = &gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
#[template]
append_child = &AdjustmentCard {
#[template_child]
advanced_features {
#[watch]
set_visible: model.has_secondary_p_states() || model.show_nvidia_options,
},
append = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_homogeneous: true,
set_hexpand: true,
append = &gtk::CheckButton {
#[template_child]
controls {
append = &gtk::ToggleButton {
#[watch]
set_visible: model.any_is_secondary(),
set_visible: model.has_secondary_p_states(),
set_label: Some(&fl!(I18N, "show-all-pstates")),
add_css_class: "adjustment-card-option-toggle",
add_binding["active"]: &model.show_all_pstates,
#[watch]
set_sensitive: {
if model.domain == ClockDomain::Gpu
&& model.show_nvidia_options
&& model.vf_curve_available {
!model.vf_curve_editing.value()
} else {
true
}
},
#[wrap(Some)]
set_child = &gtk::Box {
append = &gtk::Label {
set_label: &fl!(I18N, "show-all-pstates"),
},
},
},
append: gpu_locked_clocks_togglebutton = &gtk::CheckButton {
append: locked_clocks_togglebutton = &gtk::ToggleButton {
#[watch]
set_visible: model.show_nvidia_options,
set_label: Some(&fl!(I18N, "enable-gpu-locked-clocks")),
add_binding["active"]: &model.enable_gpu_locked_clocks,
add_css_class: "adjustment-card-option-toggle",
add_binding["active"]: &model.enable_locked_clocks,
#[wrap(Some)]
set_child = &gtk::Box {
append = &gtk::Label {
set_label: &fl!(I18N, "enable-locked-clocks"),
},
},
connect_toggled => move |_| {
APP_BROKER.send(AppMsg::SettingsChanged);
} @ gpu_locked_clock_signal,
} @ locked_clock_signal,
},
append: vf_curve_editing_togglebutton = &gtk::CheckButton {
append: vf_curve_editing_togglebutton = &gtk::ToggleButton {
#[watch]
set_visible: model.show_nvidia_options && model.vf_curve_available,
set_label: Some(&fl!(I18N, "enable-vf-curve")),
set_visible: model.domain == ClockDomain::Gpu
&& model.show_nvidia_options
&& model.vf_curve_available,
add_css_class: "adjustment-card-option-toggle",
add_css_class: css::WARNING,
add_binding["active"]: &model.vf_curve_editing,
#[wrap(Some)]
set_child = &gtk::Box {
append = &gtk::Label {
set_label: &fl!(I18N, "enable-vf-curve"),
},
},
connect_toggled[sender] => move |button| {
sender.output(OcPageMsg::VfCurveEditingToggled(button.is_active())).unwrap();
APP_BROKER.send(AppMsg::SettingsChanged);
} @ vf_curve_editing_signal,
},
},
append: vram_locked_clocks_togglebutton = &gtk::CheckButton {
#[template_child]
content {
#[local_ref]
adjustments_widget -> gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
},
gtk::Label {
set_label: &fl!(I18N, "no-clocks-data"),
set_margin_horizontal: 10,
set_halign: gtk::Align::Start,
#[watch]
set_visible: model.show_nvidia_options,
set_label: Some(&fl!(I18N, "enable-vram-locked-clocks")),
add_binding["active"]: &model.enable_vram_locked_clocks,
connect_toggled => move |_| {
APP_BROKER.send(AppMsg::SettingsChanged);
} @ vram_locked_clock_signal,
set_visible: !model.has_any_clocks(),
},
},
append = &gtk::Label {
#[watch]
set_visible: model.show_all_pstates.value() && model.show_nvidia_options,
set_margin_horizontal: 5,
set_markup: &fl!(I18N, "pstate-list-description"),
set_wrap_mode: pango::WrapMode::Word,
set_halign: gtk::Align::Start,
},
},
append_child = &gtk::FlowBox {
set_orientation: gtk::Orientation::Horizontal,
set_selection_mode: gtk::SelectionMode::None,
#[watch]
set_max_children_per_line: if model.core_any_visible() && model.vram_any_visible() { 2 } else { 1 },
set_column_spacing: 10,
set_row_spacing: 10,
set_homogeneous: false,
set_valign: gtk::Align::Start,
set_hexpand: true,
append = &gtk::FlowBoxChild {
add_css_class: "clocks-frame-group",
set_valign: gtk::Align::Start,
#[watch]
set_visible: model.core_any_visible(),
#[local_ref]
core_groups_widget -> gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_valign: gtk::Align::Start,
set_spacing: 10,
set_hexpand: true,
},
},
append = &gtk::FlowBoxChild {
add_css_class: "clocks-frame-group",
set_valign: gtk::Align::Start,
#[watch]
set_visible: model.vram_any_visible(),
#[local_ref]
vram_groups_widget -> gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_valign: gtk::Align::Start,
set_spacing: 10,
set_hexpand: true,
},
},
},
append_child = &gtk::Label {
set_label: &fl!(I18N, "no-clocks-data"),
set_margin_horizontal: 10,
set_halign: gtk::Align::Start,
#[watch]
set_visible: !model.has_any_clocks(),
},
}
}
fn init(
ClocksFrameInit { vf_curve_editing }: Self::Init,
ClocksFrameInit {
domain,
vf_curve_editing,
show_all_pstates,
}: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self {
core_groups: FactoryHashMap::builder().launch_default().detach(),
vram_groups: FactoryHashMap::builder().launch_default().detach(),
adjustments: FactoryHashMap::builder()
.launch_default()
.forward(APP_BROKER.sender(), |()| AppMsg::SettingsChanged),
secondary_p_state_clocks: HashSet::new(),
domain,
vram_clock_ratio: 1.0,
show_nvidia_options: false,
vf_curve_available: false,
show_all_pstates: BoolBinding::new(false),
show_all_pstates,
vf_curve_editing,
enable_gpu_locked_clocks: BoolBinding::new(false),
enable_vram_locked_clocks: BoolBinding::new(false),
enable_locked_clocks: BoolBinding::new(false),
};
for binding in [
&model.show_all_pstates,
&model.vf_curve_editing,
&model.enable_gpu_locked_clocks,
&model.enable_vram_locked_clocks,
&model.enable_locked_clocks,
] {
let sender = sender.clone();
binding.connect_value_notify(move |_| {
@@ -272,9 +283,7 @@ impl relm4::Component for ClocksFrame {
});
}
let core_groups_widget = model.core_groups.widget();
let vram_groups_widget = model.vram_groups.widget();
let adjustments_widget = model.adjustments.widget();
let widgets = view_output!();
ComponentParts { model, widgets }
@@ -296,17 +305,13 @@ impl relm4::Component for ClocksFrame {
.vf_curve_editing_togglebutton
.block_signal(&widgets.vf_curve_editing_signal);
widgets
.gpu_locked_clocks_togglebutton
.block_signal(&widgets.gpu_locked_clock_signal);
widgets
.vram_locked_clocks_togglebutton
.block_signal(&widgets.vram_locked_clock_signal);
.locked_clocks_togglebutton
.block_signal(&widgets.locked_clock_signal);
self.core_groups.clear();
self.vram_groups.clear();
self.adjustments.clear();
self.secondary_p_state_clocks.clear();
self.enable_gpu_locked_clocks.set_value(false);
self.enable_vram_locked_clocks.set_value(false);
self.enable_locked_clocks.set_value(false);
self.vf_curve_editing.set_value(vf_curve_is_configured);
self.show_nvidia_options = false;
@@ -321,26 +326,32 @@ impl relm4::Component for ClocksFrame {
let label_size_group = gtk::SizeGroup::new(gtk::SizeGroupMode::Horizontal);
let input_size_group = gtk::SizeGroup::new(gtk::SizeGroupMode::Horizontal);
for group in self.all_groups() {
group.add_size_group(label_size_group.clone(), input_size_group.clone());
for clock_type in self.adjustments.keys() {
self.adjustments.send(
clock_type,
AdjustmentRowMsg::AddSizeGroup {
label_group: label_size_group.clone(),
input_group: input_size_group.clone(),
},
);
}
widgets
.vf_curve_editing_togglebutton
.unblock_signal(&widgets.vf_curve_editing_signal);
widgets
.gpu_locked_clocks_togglebutton
.unblock_signal(&widgets.gpu_locked_clock_signal);
widgets
.vram_locked_clocks_togglebutton
.unblock_signal(&widgets.vram_locked_clock_signal);
.locked_clocks_togglebutton
.unblock_signal(&widgets.locked_clock_signal);
self.update_vram_clock_ratio();
sender.input(ClocksFrameMsg::TogglePStatesVisibility);
}
ClocksFrameMsg::ResetGpuClockOffsets => {
for group in self.core_groups.values() {
group.reset_gpu_clock_offsets();
for clock_type in self.adjustments.keys() {
if matches!(clock_type, ClockspeedType::GpuClockOffset(_)) {
self.adjustments
.send(clock_type, AdjustmentRowMsg::SetValue(0.0));
}
}
}
ClocksFrameMsg::VramRatio(vram_ratio) => {
@@ -348,14 +359,28 @@ impl relm4::Component for ClocksFrame {
self.update_vram_clock_ratio();
}
ClocksFrameMsg::TogglePStatesVisibility => {
for group in self.all_groups() {
group.toggle_secondary_visibility(
self.show_all_pstates.value(),
self.show_nvidia_options,
self.enable_gpu_locked_clocks.value(),
self.enable_vram_locked_clocks.value(),
self.vf_curve_editing.value(),
);
for clock_type in self.adjustments.keys() {
let visible = match clock_type {
ClockspeedType::MaxCoreClock
| ClockspeedType::MinCoreClock
| ClockspeedType::MaxMemoryClock
| ClockspeedType::MinMemoryClock
if self.show_nvidia_options =>
{
self.enable_locked_clocks.value()
}
ClockspeedType::GpuClockOffset(_)
if self.show_nvidia_options && self.vf_curve_editing.value() =>
{
false
}
_ => {
!self.secondary_p_state_clocks.contains(clock_type)
|| self.show_all_pstates.value()
}
};
self.adjustments
.send(clock_type, AdjustmentRowMsg::SetVisible(visible));
}
}
}
@@ -366,49 +391,54 @@ impl relm4::Component for ClocksFrame {
impl ClocksFrame {
fn set_clock(&mut self, clock_type: ClockspeedType, data: ClocksData) {
let category = ClockCategory::from_type(clock_type);
if !self.domain.matches(clock_type) {
return;
}
let groups = if category.is_core() {
&mut self.core_groups
} else if category.is_vram() {
&mut self.vram_groups
self.adjustments.insert(
clock_type,
AdjustmentRowInit {
title: data.custom_title.unwrap_or_else(|| clock_title(clock_type)),
info_text: if clock_type == ClockspeedType::VoltageBoost {
fl!(I18N, "gpu-voltage-boost-tooltip")
} else {
String::new()
},
value: f64::from(data.current),
lower: f64::from(data.min),
upper: f64::from(data.max),
step_increment: get_row_step(clock_type),
..Default::default()
},
);
if data.is_secondary_p_state {
self.secondary_p_state_clocks.insert(clock_type);
} else {
unreachable!()
};
let mut group = if let Some(group) = groups.get_mut(&category) {
group
} else {
groups.insert(category, ());
groups.get_mut(&category).unwrap()
};
group.set_clock(clock_type, data);
}
fn all_groups(&self) -> impl Iterator<Item = &AdjustmentGroup> {
self.core_groups.values().chain(self.vram_groups.values())
self.secondary_p_state_clocks.remove(&clock_type);
}
}
fn has_any_clocks(&self) -> bool {
self.core_groups.values().any(|group| !group.is_empty())
!self.adjustments.is_empty()
}
fn any_is_secondary(&self) -> bool {
self.all_groups().any(|group| group.has_secondary())
}
fn core_any_visible(&self) -> bool {
self.core_groups.values().any(|group| !group.is_empty())
}
fn vram_any_visible(&self) -> bool {
self.vram_groups.values().any(|group| !group.is_empty())
fn has_secondary_p_states(&self) -> bool {
!self.secondary_p_state_clocks.is_empty()
}
fn update_vram_clock_ratio(&self) {
if let Some(vram_group) = self.vram_groups.get(&ClockCategory::VramClock) {
vram_group.set_value_ratio(self.vram_clock_ratio);
for clock_type in self.adjustments.keys() {
if matches!(
clock_type,
ClockspeedType::MaxMemoryClock
| ClockspeedType::MinMemoryClock
| ClockspeedType::MemClockOffset(_)
) {
self.adjustments.send(
clock_type,
AdjustmentRowMsg::ValueRatio(self.vram_clock_ratio),
);
}
}
}
@@ -577,7 +607,7 @@ impl ClocksFrame {
) {
let values_len = values.len();
for (i, value) in values.enumerate().rev() {
let is_secondary = i > 0 && i < values_len - 1;
let is_secondary_p_state = i > 0 && i < values_len - 1;
self.set_clock(
clock_type(i as u8),
@@ -585,7 +615,7 @@ impl ClocksFrame {
current: value,
min,
max,
is_secondary,
is_secondary_p_state,
..Default::default()
},
);
@@ -596,42 +626,38 @@ impl ClocksFrame {
self.show_nvidia_options = true;
self.vf_curve_available = !table.gpu_vf_curve.is_empty();
let locked_clocks = [
(
let (clock_range, locked_clocks, min_type, max_type) = match self.domain {
ClockDomain::Gpu => (
table.gpu_clock_range,
table.gpu_locked_clocks,
ClockspeedType::MinCoreClock,
ClockspeedType::MaxCoreClock,
self.enable_gpu_locked_clocks.clone(),
),
(
ClockDomain::Vram => (
table.vram_clock_range,
table.vram_locked_clocks,
ClockspeedType::MinMemoryClock,
ClockspeedType::MaxMemoryClock,
self.enable_vram_locked_clocks.clone(),
),
];
};
for (clock_range, locked_clocks, min_type, max_type, enable_binding) in locked_clocks {
if let Some((gpu_min, gpu_max)) = clock_range {
let (current_min, current_max) = match locked_clocks {
Some(locked_range) => {
enable_binding.set_value(true);
locked_range
}
None => (gpu_min, gpu_max),
};
if let Some((gpu_min, gpu_max)) = clock_range {
let (current_min, current_max) = match locked_clocks {
Some(locked_range) => {
self.enable_locked_clocks.set_value(true);
locked_range
}
None => (gpu_min, gpu_max),
};
self.set_clock(
min_type,
ClocksData::new(current_min as i32, gpu_min as i32, gpu_max as i32),
);
self.set_clock(
max_type,
ClocksData::new(current_max as i32, gpu_min as i32, gpu_max as i32),
);
}
self.set_clock(
min_type,
ClocksData::new(current_min as i32, gpu_min as i32, gpu_max as i32),
);
self.set_clock(
max_type,
ClocksData::new(current_max as i32, gpu_min as i32, gpu_max as i32),
);
}
for (pstate, offset) in &table.gpu_offsets {
@@ -678,30 +704,20 @@ impl ClocksFrame {
}
pub fn get_commands(&self) -> Vec<SetClocksCommand> {
self.all_groups()
.flat_map(|group| group.get_commands())
.filter_map(|(clock_type, configured_value)| {
self.adjustments
.iter()
.filter_map(|(clock_type, row)| {
let configured_value = row.get_changed_value().map(|value| value as i32);
// If nvidia options are enabled, we always set locked clocks to None or Some
let value = if self.show_nvidia_options {
match clock_type {
ClockspeedType::MinCoreClock | ClockspeedType::MaxCoreClock => self
.enable_gpu_locked_clocks
ClockspeedType::MinCoreClock
| ClockspeedType::MaxCoreClock
| ClockspeedType::MinMemoryClock
| ClockspeedType::MaxMemoryClock => self
.enable_locked_clocks
.value()
.then(|| {
self.core_groups
.get(&ClockCategory::from_type(clock_type))
.map(|group| group.get_raw_value(clock_type))
})
.flatten(),
ClockspeedType::MinMemoryClock | ClockspeedType::MaxMemoryClock => self
.enable_vram_locked_clocks
.value()
.then(|| {
self.vram_groups
.get(&ClockCategory::from_type(clock_type))
.map(|group| group.get_raw_value(clock_type))
})
.flatten(),
.then(|| row.get_value() as i32),
_ => Some(configured_value?),
}
} else {
@@ -709,7 +725,7 @@ impl ClocksFrame {
};
Some(SetClocksCommand {
r#type: clock_type,
r#type: *clock_type,
value,
})
})
@@ -717,40 +733,56 @@ impl ClocksFrame {
}
}
fn nvidia_clock_offset_to_data(clock_info: &NvidiaClockOffset, is_secondary: bool) -> ClocksData {
fn nvidia_clock_offset_to_data(
clock_info: &NvidiaClockOffset,
is_secondary_p_state: bool,
) -> ClocksData {
ClocksData {
current: clock_info.current,
min: clock_info.min,
max: clock_info.max,
is_secondary,
is_secondary_p_state,
..Default::default()
}
}
fn clock_title(clock_type: ClockspeedType) -> String {
match clock_type {
ClockspeedType::MaxCoreClock => fl!(I18N, "max-gpu-clock"),
ClockspeedType::MaxMemoryClock => fl!(I18N, "max-vram-clock"),
ClockspeedType::MaxCoreClock | ClockspeedType::MaxMemoryClock => fl!(I18N, "max-clock"),
ClockspeedType::MaxVoltage => fl!(I18N, "max-gpu-voltage"),
ClockspeedType::MinCoreClock => fl!(I18N, "min-gpu-clock"),
ClockspeedType::MinMemoryClock => fl!(I18N, "min-vram-clock"),
ClockspeedType::MinCoreClock | ClockspeedType::MinMemoryClock => fl!(I18N, "min-clock"),
ClockspeedType::MinVoltage => fl!(I18N, "min-gpu-voltage"),
ClockspeedType::VoltageOffset => fl!(I18N, "gpu-voltage-offset"),
ClockspeedType::VoltageBoost => fl!(I18N, "gpu-voltage-boost"),
ClockspeedType::GpuClockOffset(pstate) => {
fl!(I18N, "gpu-pstate-clock-offset", pstate = pstate)
ClockspeedType::GpuClockOffset(pstate) | ClockspeedType::MemClockOffset(pstate) => {
fl!(I18N, "pstate-clock-offset", pstate = pstate)
}
ClockspeedType::MemClockOffset(pstate) => {
fl!(I18N, "vram-pstate-clock-offset", pstate = pstate)
ClockspeedType::GpuVfCurveClock(pstate) | ClockspeedType::MemVfCurveClock(pstate) => {
fl!(I18N, "pstate-clock", pstate = pstate)
}
ClockspeedType::GpuVfCurveClock(pstate) => fl!(I18N, "gpu-pstate-clock", pstate = pstate),
ClockspeedType::MemVfCurveClock(pstate) => fl!(I18N, "mem-pstate-clock", pstate = pstate),
ClockspeedType::GpuVfCurveVoltage(pstate) => {
fl!(I18N, "gpu-pstate-clock-voltage", pstate = pstate)
}
ClockspeedType::MemVfCurveVoltage(pstate) => {
fl!(I18N, "mem-pstate-clock-voltage", pstate = pstate)
ClockspeedType::GpuVfCurveVoltage(pstate) | ClockspeedType::MemVfCurveVoltage(pstate) => {
fl!(I18N, "pstate-clock-voltage", pstate = pstate)
}
ClockspeedType::Reset => unreachable!(),
}
}
fn get_row_step(clock_type: ClockspeedType) -> f64 {
match clock_type {
ClockspeedType::MaxCoreClock
| ClockspeedType::MinCoreClock
| ClockspeedType::GpuClockOffset(_)
| ClockspeedType::MaxMemoryClock
| ClockspeedType::MinMemoryClock
| ClockspeedType::MemClockOffset(_)
| ClockspeedType::GpuVfCurveClock(_)
| ClockspeedType::MemVfCurveClock(_) => 5.0,
ClockspeedType::MinVoltage
| ClockspeedType::MaxVoltage
| ClockspeedType::VoltageOffset
| ClockspeedType::VoltageBoost
| ClockspeedType::GpuVfCurveVoltage(_)
| ClockspeedType::MemVfCurveVoltage(_) => 1.0,
ClockspeedType::Reset => unreachable!(),
}
}
@@ -2,15 +2,11 @@ mod heuristics_list;
use crate::{
APP_BROKER, I18N,
app::{components::page_section::PageSection, msg::AppMsg, pages::oc_page::OcPageMsg},
app::{msg::AppMsg, pages::oc_page::OcPageMsg},
};
use adw::prelude::*;
use amdgpu_sysfs::gpu_handle::{PerformanceLevel, power_profile_mode::PowerProfileModesTable};
use gtk::{
StringObject,
gio::prelude::ListModelExt,
glib::object::Cast,
prelude::{BoxExt, ListBoxRowExt, OrientableExt, WidgetExt},
};
use gtk::StringObject;
use heuristics_list::PowerProfileHeuristicsList;
use i18n_embed_fl::fl;
use nvml_wrapper::enums::device::PowerMizerMode;
@@ -57,11 +53,15 @@ impl relm4::Component for PerformanceFrame {
type CommandOutput = ();
view! {
PageSection::new("Performance") {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 10,
#[watch]
set_visible: model.performance_level.is_some() || model.active_power_mizer_mode.is_some(),
set_visible: model.performance_level.is_some()
|| model.power_profile_modes_table.is_some()
|| model.active_power_mizer_mode.is_some(),
append_child = &gtk::Box {
append = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 10,
#[watch]
@@ -114,7 +114,7 @@ impl relm4::Component for PerformanceFrame {
},
},
append_child = &gtk::Box {
append = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 10,
#[watch]
@@ -183,7 +183,7 @@ impl relm4::Component for PerformanceFrame {
},
},
append_child = &gtk::Box {
append = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 10,
#[watch]
@@ -1,141 +0,0 @@
use crate::{
APP_BROKER, I18N,
app::{
components::{adjustment_value::AdjustmentValue, page_section::PageSection},
msg::AppMsg,
pages::PageUpdate,
},
};
use gtk::{
glib::object::ObjectExt,
prelude::{AdjustmentExt, BoxExt, ButtonExt, OrientableExt, RangeExt, ScaleExt, WidgetExt},
};
use i18n_embed_fl::fl;
use lact_schema::PowerStats;
use relm4::{ComponentParts, ComponentSender, RelmWidgetExt};
use std::fmt::Write;
#[derive(Default)]
pub struct PowerCapSection {
power: PowerStats,
adjustment: AdjustmentValue,
value_text: String,
}
#[derive(Debug)]
pub enum PowerCapMsg {
Update(PageUpdate),
RefreshText,
Reset,
}
#[relm4::component(pub)]
impl relm4::Component for PowerCapSection {
type Init = ();
type Input = PowerCapMsg;
type Output = ();
type CommandOutput = ();
view! {
#[root]
PageSection::new(&fl!(I18N, "power-cap")) {
append_header = &gtk::Button {
set_label: &fl!(I18N, "default-button"),
connect_clicked => PowerCapMsg::Reset,
set_halign: gtk::Align::End,
set_hexpand: true,
},
append_child = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 10,
gtk::Label {
#[watch]
set_label: &model.value_text,
},
gtk::Scale {
set_orientation: gtk::Orientation::Horizontal,
set_hexpand: true,
set_round_digits: 0,
set_margin_horizontal: 5,
set_draw_value: false,
set_adjustment: adjustment,
},
}
},
#[local_ref]
adjustment -> AdjustmentValue {
connect_value_notify => move |_| {
APP_BROKER.send(AppMsg::SettingsChanged);
} @ value_notify,
connect_value_notify => PowerCapMsg::RefreshText,
connect_upper_notify => PowerCapMsg::RefreshText,
},
}
fn init(
_init: Self::Init,
root: Self::Root,
_sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self::default();
let adjustment = &model.adjustment;
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 {
PowerCapMsg::Update(PageUpdate::Stats(stats)) => {
// The signal blocking has to be manual,
// because relm's signal block macro feature doesn't seem to work with non-widget objects
self.adjustment.block_signal(&widgets.value_notify);
let power = stats.power.clone();
self.adjustment.set_upper(power.cap_max.unwrap_or_default());
self.adjustment.set_lower(power.cap_min.unwrap_or_default());
self.adjustment
.set_initial_value(power.cap_current.unwrap_or_default());
self.adjustment.unblock_signal(&widgets.value_notify);
self.power = power;
}
PowerCapMsg::Update(PageUpdate::Info(_)) => (),
PowerCapMsg::RefreshText => {
self.value_text.clear();
write!(
self.value_text,
"{}/{} {}",
self.adjustment.value(),
self.adjustment.upper(),
fl!(I18N, "watt")
)
.unwrap();
}
PowerCapMsg::Reset => {
self.adjustment
.set_value(self.power.cap_default.unwrap_or_default());
}
}
self.update_view(widgets, sender);
}
}
impl PowerCapSection {
pub fn get_user_cap(&self) -> Option<f64> {
self.adjustment.get_changed_value(true)
}
}
@@ -0,0 +1,177 @@
use crate::{
APP_BROKER, I18N,
app::{
components::{
adjustment_card::AdjustmentCard,
adjustment_row::{AdjustmentRow, AdjustmentRowInit, AdjustmentRowMsg},
page_section::PageSection,
},
msg::AppMsg,
pages::oc_page::{
OcPageMsg,
performance_frame::{PerformanceFrame, PerformanceFrameMsg},
},
utils::ext::RelmDefaultLauchable,
},
};
use adw::prelude::*;
use amdgpu_sysfs::gpu_handle::PerformanceLevel;
use i18n_embed_fl::fl;
use lact_schema::PowerStats;
use nvml_wrapper::enums::device::PowerMizerMode;
use relm4::{ComponentController, ComponentParts, ComponentSender, factory::FactoryHashMap};
pub struct PowerFrame {
power: PowerStats,
power_row: FactoryHashMap<(), AdjustmentRow<()>>,
performance_frame: relm4::Controller<PerformanceFrame>,
}
#[derive(Debug)]
pub enum PowerFrameMsg {
PowerStats(PowerStats),
Performance(PerformanceFrameMsg),
RefreshVisibility,
Reset,
}
#[relm4::component(pub)]
impl relm4::Component for PowerFrame {
type Init = ();
type Input = PowerFrameMsg;
type Output = OcPageMsg;
type CommandOutput = ();
view! {
#[root]
PageSection::new(&fl!(I18N, "power-section")) {
#[watch]
set_visible: model.is_available(),
append_header = &gtk::Button {
set_label: &fl!(I18N, "default-button"),
connect_clicked => PowerFrameMsg::Reset,
set_halign: gtk::Align::End,
set_hexpand: true,
#[watch]
set_visible: !model.power_row.is_empty(),
},
#[template]
append_child = &AdjustmentCard {
#[template_child]
content {
#[local_ref]
power_row_widget -> gtk::Box {
set_orientation: gtk::Orientation::Vertical,
#[watch]
set_visible: !model.power_row.is_empty(),
},
append: model.performance_frame.widget(),
},
},
},
}
fn init(
_init: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self {
power: PowerStats::default(),
power_row: FactoryHashMap::builder()
.launch_default()
.forward(APP_BROKER.sender(), |()| AppMsg::SettingsChanged),
performance_frame: PerformanceFrame::launch_default()
.forward(sender.output_sender(), |msg| msg),
};
let visibility_sender = sender.clone();
model
.performance_frame
.widget()
.connect_visible_notify(move |_| {
visibility_sender.input(PowerFrameMsg::RefreshVisibility);
});
let power_row_widget = model.power_row.widget();
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 {
PowerFrameMsg::PowerStats(power) => {
self.power_row.clear();
if let Some(value) = power.cap_current {
self.power_row.insert(
(),
AdjustmentRowInit {
title: format!("{} ({})", fl!(I18N, "power-cap"), fl!(I18N, "watt")),
value,
lower: power.cap_min.unwrap_or_default(),
upper: power.cap_max.unwrap_or_default(),
..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()),
);
}
}
}
self.update_view(widgets, sender);
}
}
impl PowerFrame {
fn is_available(&self) -> bool {
!self.power_row.is_empty() || self.performance_frame.widget().get_visible()
}
pub fn get_user_cap(&self) -> Option<f64> {
self.power_row
.get(&())?
.get_changed_value()
.filter(|value| *value != 0.0)
}
pub fn performance_level(&self) -> Option<PerformanceLevel> {
self.performance_frame.model().performance_level()
}
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()
}
}
@@ -3,15 +3,15 @@ use super::power_states_list::{PowerStatesListMsg, PowerStatesListOptions};
use crate::{
APP_BROKER, I18N,
app::{
components::page_section_expander::PageSectionExpander, msg::AppMsg,
pages::oc_page::OcPageMsg, utils::ext::RelmLaunchable as _,
components::{adjustment_card::AdjustmentCard, page_section::PageSection},
msg::AppMsg,
pages::oc_page::OcPageMsg,
utils::ext::RelmLaunchable as _,
},
};
use adw::prelude::*;
use amdgpu_sysfs::gpu_handle::{PerformanceLevel, PowerLevelKind};
use gtk::{
glib::{SignalHandlerId, object::ObjectExt},
prelude::{BoxExt, CheckButtonExt, OrientableExt, WidgetExt},
};
use gtk::glib::{SignalHandlerId, object::ObjectExt};
use i18n_embed_fl::fl;
use indexmap::IndexMap;
use lact_schema::{DeviceStats, PowerStates};
@@ -57,44 +57,61 @@ impl relm4::SimpleComponent for PowerStatesFrame {
type Output = OcPageMsg;
view! {
PageSectionExpander::new(&fl!(I18N, "pstates")) {
append_expandable = &gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
gtk::CheckButton {
set_label: Some(&fl!(I18N, "enable-pstate-config")),
#[watch]
#[block_signal(configured_toggled_handler)]
set_active: model.states_configuration_enabled.value(),
connect_toggled[sender] => move |button| {
sender.input(PowerStatesFrameMsg::ConfiguredToggled {
configured: button.is_active(),
});
} @ configured_toggled_handler,
PageSection::new(&fl!(I18N, "pstates")) {
#[template]
append_child = &AdjustmentCard {
#[template_child]
advanced_features {
#[watch]
set_visible: model.performance_level.is_some(),
},
gtk::Box {
set_spacing: 10,
set_orientation: gtk::Orientation::Horizontal,
#[template_child]
controls {
gtk::ToggleButton {
set_halign: gtk::Align::Start,
add_css_class: "adjustment-card-option-toggle",
gtk::Box {
#[watch]
set_visible: model.has_core_states,
append = model.core_states_list.widget(),
},
#[block_signal(configured_toggled_handler)]
set_active: model.states_configuration_enabled.value(),
gtk::Box {
#[watch]
set_visible: model.has_vram_states,
append = model.vram_states_list.widget(),
connect_toggled[sender] => move |button| {
sender.input(PowerStatesFrameMsg::ConfiguredToggled {
configured: button.is_active(),
});
} @ configured_toggled_handler,
#[wrap(Some)]
set_child = &gtk::Box {
gtk::Label {
set_label: &fl!(I18N, "enable-pstate-config"),
},
},
},
}
}
},
#[template_child]
content {
gtk::Box {
set_spacing: 10,
set_orientation: gtk::Orientation::Horizontal,
set_homogeneous: true,
gtk::Box {
#[watch]
set_visible: model.has_core_states,
append = model.core_states_list.widget(),
},
gtk::Box {
#[watch]
set_visible: model.has_vram_states,
append = model.vram_states_list.widget(),
},
},
},
},
}
}
+2 -2
View File
@@ -542,14 +542,14 @@ impl VfCurveEditor {
if let Some((min_freq, max_freq)) = self.locked_clocks_range.get() {
let mut curves = vec![(
[(x_spec.start, max_freq), (x_spec.end, max_freq)],
fl!(I18N, "max-gpu-clock"),
fl!(I18N, "max-clock"),
colors.error,
)];
if min_freq >= y_spec.start {
curves.push((
[(x_spec.start, min_freq), (x_spec.end, min_freq)],
fl!(I18N, "min-gpu-clock"),
fl!(I18N, "min-clock"),
colors.warning,
));
}
+4 -1
View File
@@ -8,6 +8,9 @@ use std::cell::RefCell;
use crate::{CONFIG, app::utils::color_scheme::AppColorScheme};
pub const COMBINED_CSS: &str = include_str!(concat!(env!("OUT_DIR"), "/combined.css"));
// cosmic gtk4 override has 800 prio, so we have to beat it
const BREEZE_CSS_PRIORITY: u32 = 801;
pub const APPLICATION_CSS_PRIORITY: u32 = 802;
macro_rules! include_theme_str {
($file:literal) => {
@@ -64,7 +67,7 @@ pub fn apply_theme(theme: AppTheme) -> anyhow::Result<()> {
#[allow(deprecated)]
provider.load_from_data(css);
style_context_add_provider_for_display(&display, &provider, 900);
style_context_add_provider_for_display(&display, &provider, BREEZE_CSS_PRIORITY);
EXISTING_STYLE_PROVIDER.set(Some(provider));
}