Merge remote-tracking branch 'origin/master' into uma-carveout

This commit is contained in:
Ilya Zlobintsev committed 2026-10-04 12:58:21 +03:00
commit ae0934195a
24 files changed
+553 -259

No files matched your search

+1 -1
View File
@@ -15,6 +15,6 @@ runs:
with:
cache-targets: false
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.9
uses: mozilla-actions/sccache-action@v0.0.11
with:
version: "v0.10.0"
+13 -13
View File
@@ -20,10 +20,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Docker Cache
uses: AndreKurait/docker-cache@eb0683e3972ed3066587b7199d3a0e9bd114bd3c #0.6.0
uses: AndreKurait/docker-cache@7a3887908bdb97935395833df69b060cfcca0f7f # 0.7.0
with:
key: docker-${{ matrix.target-os }}-${{ hashFiles('pkg/images/') }}
@@ -51,7 +51,7 @@ jobs:
chmod +x /usr/local/bin/pkger
# A GitHub Action that implements smart caching for rust/cargo projects with sensible defaults.
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- name: Build packages (with signing)
if: ${{ (contains(matrix.target-os, 'fedora') || contains(matrix.target-os, 'opensuse') || matrix.target-os == 'rhel-9') && env.GPG_KEY_PASSWORD != '' }}
@@ -91,7 +91,7 @@ jobs:
gpg --armor --export > $PWD/release-artifacts/lact.pubkey
- name: Upload artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ matrix.target-os }}-${{ matrix.recipe }}
path: release-artifacts/*
@@ -102,14 +102,14 @@ jobs:
image: bilelmoussaoui/flatpak-github-actions:gnome-47
options: --privileged
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: flatpak/flatpak-github-actions/flatpak-builder@92ae9851ad316786193b1fd3f40c4b51eb5cb101 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: flatpak/flatpak-github-actions/flatpak-builder@79327416609af08178ad73b352877e51450790b3 # v6
with:
bundle: io.github.ilya_zlobintsev.LACT.flatpak
manifest-path: flatpak/io.github.ilya_zlobintsev.LACT.yaml
cache-key: flatpak-builder-${{ github.sha }}
- name: Upload artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: flatpak
path: io.github.ilya_zlobintsev.LACT.flatpak
@@ -119,7 +119,7 @@ jobs:
steps:
- name: Log into GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -127,15 +127,15 @@ jobs:
- name: Extract Docker image metadata
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
with:
images: ghcr.io/${{ github.repository }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
- name: Build and push
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
with:
file: pkg/docker/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
@@ -151,7 +151,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Download artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
@@ -184,7 +184,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Download artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
+2 -2
View File
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Install yq
run: |
sudo curl -L https://github.com/mikefarah/yq/releases/download/v4.34.1/yq_linux_amd64 -o /usr/local/bin/yq
@@ -40,7 +40,7 @@ jobs:
git commit -m "chore: Update spec files for release ${{ github.event.release.tag_name }}"
git push origin $branch_name
- name: Create Pull Request
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const { repo, owner } = context.repo;
+2 -2
View File
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Generate token
uses: actions/create-github-app-token@v3
@@ -28,7 +28,7 @@ jobs:
private-key: ${{ secrets.RENOVATE_PRIVATE_KEY }}
- name: Self-hosted Renovate
uses: renovatebot/github-action@v46.1.5
uses: renovatebot/github-action@v46.3.6
with:
token: ${{ steps.app-token.outputs.token }}
env:
+9 -9
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-24.04
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.draft }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: ./.github/actions/setup-environment
- name: Build
run: cargo build
@@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-24.04
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.draft }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Install rustfmt
run: rustup component add rustfmt
- name: Check formatting
@@ -38,7 +38,7 @@ jobs:
runs-on: ubuntu-24.04
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.draft }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: ./.github/actions/setup-environment
- name: Install clippy
run: rustup component add clippy
@@ -49,7 +49,7 @@ jobs:
runs-on: ubuntu-24.04
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.draft }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: ./.github/actions/setup-environment
- name: Install cargo-audit from crates.io
uses: baptiste0928/cargo-install@v3
@@ -63,7 +63,7 @@ jobs:
runs-on: ubuntu-24.04
if: ${{ github.event_name == 'push' || !github.event.pull_request.draft }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: ./.github/actions/setup-environment
- name: Install clang
uses: awalsh128/cache-apt-pkgs-action@latest
@@ -83,7 +83,7 @@ jobs:
cargo llvm-cov --no-fail-fast --include-ffi --workspace --lcov --output-path lcov.info
- name: Upload to Codecov
uses: codecov/codecov-action@v5.5.3
uses: codecov/codecov-action@v7.1.1
continue-on-error: true
with:
files: lcov.info
@@ -95,7 +95,7 @@ jobs:
runs-on: ubuntu-24.04
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.draft }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: ./.github/actions/setup-environment
- name: Install Nightly Rust toolchain
uses: actions-rs/toolchain@v1
@@ -122,7 +122,7 @@ jobs:
LSAN_OPTIONS: fast_unwind_on_malloc=0:max_leak_stack_depth=50:print_suppressions=1
ASAN_OPTIONS: detect_leaks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: ./.github/actions/setup-environment
- name: Install Nightly Rust toolchain
uses: actions-rs/toolchain@v1
@@ -146,7 +146,7 @@ jobs:
- check --feature-powerset --no-dev-deps --all
- test --feature-powerset --all
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: ./.github/actions/setup-environment
- uses: taiki-e/install-action@cargo-hack
- name: Run cargo hack ${{ matrix.command }}
+28 -14
View File
@@ -9,7 +9,7 @@ use crate::{
nvidia::nvapi::{
CLOCK_CLIENT_CLK_VF_POINT_TYPE_PROG, ClockClientClkVfPointInfoV1,
ClockClientClkVfPointsControlV1, ClockClientClkVfPointsInfoV1,
ClockClientClkVfPointsStatusV3, NvGpuClockDomainId,
ClockClientClkVfPointsStatusV3, NvApiThermChannelInfo, NvGpuClockDomainId,
},
},
};
@@ -66,7 +66,7 @@ pub struct NvidiaGpuController {
nvapi: Option<(Rc<NvApi>, NvPhysicalGpuHandle)>,
driver_handle: Option<DriverHandle>,
nvapi_therm_channel_mask: Option<i32>,
nvapi_thermals_info: Option<NvApiThermChannelInfo>,
last_util_timestamp: Cell<Option<u64>>,
// Store last applied offsets as a workaround when the driver doesn't tell us the current offset
@@ -93,7 +93,7 @@ impl NvidiaGpuController {
)
})?;
let (nvapi_handle, nvapi_therm_channel_mask) = match nvapi.as_ref() {
let (nvapi_handle, nvapi_thermals_info) = match nvapi.as_ref() {
Some(nvapi) => {
let bus_id = common.get_slot_info()?.bus;
let gpu_handle = nvapi
@@ -102,19 +102,24 @@ impl NvidiaGpuController {
.ok()
.flatten();
let therm_channel_mask = gpu_handle.and_then(|handle| unsafe {
let thermals_info = gpu_handle.and_then(|handle| unsafe {
nvapi
.calculate_therm_channel_mask(handle)
.inspect(|mask| {
debug!("calculated NvAPI therm channel mask {mask:x}");
.therm_channel_get_info(handle)
.inspect(|info| {
debug!(
"NvAPI therm channel mask {:x}, hotspot channel {:?}, vram channel {:?}",
info.mask(),
info.hotspot_channel(),
info.vram_channel()
);
})
.inspect_err(|err| {
error!("could not calculate NvAPI therm channel mask: {err:#}");
error!("could not get NvAPI therm channel info: {err:#}");
})
.ok()
});
(gpu_handle, therm_channel_mask)
(gpu_handle, thermals_info)
}
None => (None, None),
};
@@ -142,7 +147,7 @@ impl NvidiaGpuController {
nvapi: nvapi.zip(nvapi_handle),
common,
driver_handle,
nvapi_therm_channel_mask,
nvapi_thermals_info,
initial_target_temp: target_temp,
last_util_timestamp: Cell::new(None),
fan_control_handle: RefCell::new(None),
@@ -912,10 +917,16 @@ impl GpuController for NvidiaGpuController {
let arch = device.architecture().ok();
unsafe {
if let Some(mask) = self.nvapi_therm_channel_mask
&& let Ok(thermals) = nvapi.therm_channel_get_status(*handle, mask)
if let Some(thermals_info) = &self.nvapi_thermals_info
&& let Ok(thermals) =
nvapi.therm_channel_get_status(*handle, thermals_info.mask())
{
if let Some(hotspot) = nvapi.read_hotspot(&thermals, *handle, arch.as_ref()) {
if let Some(hotspot) = nvapi.read_hotspot(
&thermals,
thermals_info.hotspot_channel(),
*handle,
arch.as_ref(),
) {
temps.insert(
"GPU Hotspot".to_owned(),
TemperatureEntry {
@@ -935,7 +946,10 @@ impl GpuController for NvidiaGpuController {
.as_ref()
.and_then(|driver| driver.get_ram_type().ok());
if let Some(vram) = thermals.vram(vram_type) {
if let Some(vram) = thermals_info
.vram_channel()
.and_then(|channel| thermals.get_value(channel))
{
temps.insert(
"VRAM".to_owned(),
TemperatureEntry {
@@ -27,6 +27,7 @@ const QUERY_NVAPI_ENUM_PHYSICAL_GPUS: u32 = 0xe5ac921f;
const QUERY_NVAPI_GPU_GET_BUS_ID: u32 = 0x1be0b8e5;
const QUERY_NVAPI_GET_ERROR_MESSAGE: u32 = 0x6c2d048c;
// Undocumented calls
const QUERY_NVAPI_GPU_THERM_CHANNEL_GET_INFO: u32 = 0x0bc8163d;
const QUERY_NVAPI_GPU_THERM_CHANNEL_GET_STATUS: u32 = 0x65fe3aad;
const QUERY_NVAPI_GPU_CLIENT_VOLT_RAILS_GET_STATUS: u32 = 0x465f9bcf;
const QUERY_NVAPI_GPU_CLIENT_VOLT_RAILS_GET_CONTROL: u32 = 0x9df23ca1;
@@ -89,26 +90,34 @@ impl NvApi {
Ok(None)
}
pub unsafe fn therm_channel_get_info(
&self,
handle: NvPhysicalGpuHandle,
) -> anyhow::Result<NvApiThermChannelInfo> {
let mut thermals_info = NvApiThermChannelInfo::default();
self.physical_gpu_query(
handle,
&mut thermals_info,
QUERY_NVAPI_GPU_THERM_CHANNEL_GET_INFO,
)?;
Ok(thermals_info)
}
pub unsafe fn therm_channel_get_status(
&self,
handle: NvPhysicalGpuHandle,
mask: i32,
) -> anyhow::Result<NvApiThermals> {
let f = self.query_interface(QUERY_NVAPI_GPU_THERM_CHANNEL_GET_STATUS)?;
let f: unsafe extern "C" fn(
handle: NvPhysicalGpuHandle,
sensors: &mut NvApiThermals,
) -> NvAPI_Status = transmute(f);
let mut sensors = NvApiThermals {
#[allow(clippy::cast_possible_truncation)]
version: make_version::<NvApiThermals>(2),
mask,
values: [0; 40],
..Default::default()
};
let status = f(handle, &mut sensors);
self.handle_status(status)?;
self.physical_gpu_query(
handle,
&mut sensors,
QUERY_NVAPI_GPU_THERM_CHANNEL_GET_STATUS,
)?;
Ok(sensors)
}
@@ -236,37 +245,6 @@ impl NvApi {
Ok(())
}
pub unsafe fn calculate_therm_channel_mask(
&self,
handle: NvPhysicalGpuHandle,
) -> anyhow::Result<i32> {
let f = self.query_interface(QUERY_NVAPI_GPU_THERM_CHANNEL_GET_STATUS)?;
let f: unsafe extern "C" fn(
handle: NvPhysicalGpuHandle,
sensors: &mut NvApiThermals,
) -> NvAPI_Status = transmute(f);
let mut sensors = NvApiThermals {
#[allow(clippy::cast_possible_truncation)]
version: (mem::size_of::<NvApiThermals>() | (2 << 16)) as u32,
mask: 1,
values: [0; 40],
};
let initial_status = f(handle, &mut sensors);
self.handle_status(initial_status)?;
for bit in 0..32 {
sensors.mask = 1 << bit;
let status = f(handle, &mut sensors);
if status != 0 {
return Ok(sensors.mask - 1);
}
}
bail!("Could not find suitable mask");
}
unsafe fn enum_physical_gpus(&self) -> anyhow::Result<Vec<NvPhysicalGpuHandle>> {
let f = self.query_interface(QUERY_NVAPI_ENUM_PHYSICAL_GPUS)?;
let f: unsafe extern "C" fn(
@@ -480,6 +458,7 @@ impl NvApi {
pub fn read_hotspot(
&self,
thermals: &NvApiThermals,
channel: Option<usize>,
handle: NvPhysicalGpuHandle,
arch: Option<&DeviceArchitecture>,
) -> Option<i32> {
@@ -490,7 +469,7 @@ impl NvApi {
.and_then(|value| value.try_into().ok())
}
} else {
thermals.get_value(9)
channel.and_then(|channel| thermals.get_value(channel))
}
}
}
@@ -513,21 +492,90 @@ impl Drop for NvApi {
pub struct NvApiThermals {
version: u32,
mask: i32,
values: [i32; 40],
rsvd: [NvU8; 32],
temps: [i32; 32],
}
impl Default for NvApiThermals {
fn default() -> Self {
Self {
version: make_version::<Self>(2),
mask: 0,
rsvd: [0; 32],
temps: [0; 32],
}
}
}
impl NvApiThermals {
fn get_value(&self, index: usize) -> Option<i32> {
self.values
.get(index)
pub fn get_value(&self, channel: usize) -> Option<i32> {
self.temps
.get(channel)
.map(|&value| value / 256)
.filter(|&value| value > 0 && value < 255)
}
}
pub fn vram(&self, vram_type: Option<&str>) -> Option<i32> {
match vram_type {
Some("GDDR7") => self.get_value(10),
_ => self.get_value(15),
const THERM_CHANNEL_TYPE_HOTSPOT: usize = 1;
const THERM_CHANNEL_TYPE_MEMORY: usize = 3;
#[repr(C)]
#[derive(Debug, Copy, Clone, Default)]
struct NvApiThermChannel {
class: NvU32,
channel_type: NvU32,
rel_loc: NvU32,
target_gpu: NvU32,
scaling: NvS32,
offset_sw: NvS32,
min_temp: NvS32,
max_temp: NvS32,
is_temp_sim_supported: NvU8,
flags: NvU8,
offset_hw: NvS32,
rsvd: [NvU8; 28],
data: [NvU8; 16],
}
#[repr(C)]
#[derive(Debug)]
pub struct NvApiThermChannelInfo {
version: NvU32,
mask: i32,
rsvd: [NvU8; 32],
channels: [NvApiThermChannel; 32],
primary_channel_idx: [NvU8; 5],
}
impl NvApiThermChannelInfo {
pub fn mask(&self) -> i32 {
self.mask
}
pub fn hotspot_channel(&self) -> Option<usize> {
self.primary_channel(THERM_CHANNEL_TYPE_HOTSPOT)
}
pub fn vram_channel(&self) -> Option<usize> {
self.primary_channel(THERM_CHANNEL_TYPE_MEMORY)
}
fn primary_channel(&self, channel_type: usize) -> Option<usize> {
self.primary_channel_idx
.get(channel_type)
.map(|&channel| usize::from(channel))
.filter(|&channel| channel < 32 && self.mask & (1 << channel) != 0)
}
}
impl Default for NvApiThermChannelInfo {
fn default() -> Self {
Self {
version: make_version::<Self>(2),
mask: 0,
rsvd: [0; 32],
channels: [NvApiThermChannel::default(); 32],
primary_channel_idx: [u8::MAX; 5],
}
}
}
+5
View File
@@ -2,6 +2,10 @@ language = Language
language-system-default = System Default
language-restart-notice = Restart the LACT GUI to apply language changes.
experimental-features = Experimental Features
nvidia-pstate-offsets = NVIDIA per-P-state offsets
nvidia-pstate-offsets-description = Expose offsets for non-P0 states. Disabled by default because no known device/driver version supports it.
info-page = Hardware Info
oc-page = Overclocking
thermals-page = Thermals
@@ -217,6 +221,7 @@ pstate-clock-offset = P-State {$pstate} Clock Offset
pstate-clock = P-State {$pstate} Clock
pstate-clock-voltage = P-State {$pstate} Voltage
pstate = Power State
pstates = Power States
gpu-pstates = GPU Power States
vram-pstates = VRAM Power States
+18 -4
View File
@@ -670,6 +670,9 @@ impl AppModel {
AppMsg::ShowPreferencesDialog => {
self.preferences_dialog.emit(PreferencesDialogMsg::Show);
}
AppMsg::ExperimentalFeaturesChanged => {
sender.input(AppMsg::ReloadData { full: false });
}
AppMsg::ShowAboutDialog => {
self.about_dialog.emit(AboutDialogMsg::Show);
}
@@ -1268,6 +1271,12 @@ impl AppModel {
.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;
@@ -1284,19 +1293,24 @@ impl AppModel {
.set_gpu_config(&gpu_id, gpu_config)
.await
.context("Could not apply settings")?;
self.ask_settings_confirmation(delay, root, sender);
self.ask_settings_confirmation(
delay,
&self
.application
.active_window()
.unwrap_or_else(|| root.clone().upcast()),
sender,
);
sender.input(AppMsg::ReloadData { full: false });
root.present();
Ok(())
}
fn ask_settings_confirmation(
&self,
mut delay: u64,
window: &adw::ApplicationWindow,
window: &gtk::Window,
sender: &AsyncComponentSender<AppModel>,
) {
let dialog = adw::AlertDialog::builder()
@@ -1,15 +1,29 @@
.adjustment-list > row {
padding: 10px;
}
.adjustment-list > row.adjustment-row {
padding: 5px 10px;
}
.adjustment-list > row:first-child {
.adjustment-list > row.adjustment-row:first-child {
padding-top: 10px;
}
.adjustment-list > row:last-child {
.adjustment-list > row.adjustment-row:last-child {
padding-bottom: 10px;
}
/* The card owns row spacing, including Adwaita action rows. */
.adjustment-list > row > box.header {
margin: 0;
min-height: 0;
}
.adjustment-list > row > box.header > box.title {
margin: 0;
}
.adjustment-row scale {
padding: 10px 0;
}
@@ -75,11 +75,10 @@ impl<Key: 'static> FactoryComponent for AdjustmentRow<Key> {
gtk::ListBoxRow {
set_activatable: false,
set_selectable: false,
add_css_class: "adjustment-row",
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
add_css_class: "adjustment-row",
gtk::Box {
set_spacing: 12,
+1
View File
@@ -27,6 +27,7 @@ pub enum AppMsg {
DumpVBios,
DebugSnapshot,
ShowPreferencesDialog,
ExperimentalFeaturesChanged,
ShowAboutDialog,
ShowOverdriveDialog,
ShowServiceSetupDialog,
+11
View File
@@ -2,3 +2,14 @@
min-width: 400px;
padding: 0;
}
.power-states-dialog-card list:not(.boxed-list) {
/* Embedded lists use the surrounding card's background, not the view background. */
background: none;
border-radius: 12px;
}
.power-states-dialog-card .page-section-children-box,
.power-states-dialog-card .adjustment-card-advanced-features {
margin: 0;
}
+38 -24
View File
@@ -26,7 +26,7 @@ use lact_schema::{ClocksTable, DeviceInfo, PowerStates};
use nvml_wrapper::enums::device::PowerMizerMode;
use performance_frame::PerformanceFrameMsg;
use power_frame::{PowerFrame, PowerFrameMsg};
use power_states::power_states_frame::{PowerStatesFrame, PowerStatesFrameMsg};
use power_states::power_states_dialog::{PowerStatesDialog, PowerStatesDialogMsg};
use relm4::binding::BoolBinding;
use relm4::{ComponentController, ComponentParts, ComponentSender, RelmWidgetExt};
use std::collections::HashSet;
@@ -39,7 +39,7 @@ pub struct OcPage {
device_info: Option<Arc<DeviceInfo>>,
power_frame: relm4::Controller<PowerFrame>,
power_states_frame: relm4::Controller<PowerStatesFrame>,
power_states_dialog: relm4::Controller<PowerStatesDialog>,
gpu_clocks_frame: relm4::Controller<ClocksFrame>,
vram_clocks_frame: relm4::Controller<ClocksFrame>,
igpu_frame: relm4::Controller<IGpuFrame>,
@@ -65,6 +65,7 @@ pub enum OcPageMsg {
PerformanceLevelChanged,
SetPerformanceLevel(PerformanceLevel),
EnablePstateConfig,
ShowPowerStates(ClockDomain),
ShowVfCurveEditor,
VfCurveEditingToggled(bool),
}
@@ -90,7 +91,6 @@ impl relm4::Component for OcPage {
ColumnBias::Left, // Core
ColumnBias::Right, // VRAM
ColumnBias::Left, // Power
ColumnBias::Right, // Power States
ColumnBias::Right, // Integrated Graphics
])),
@@ -108,10 +108,6 @@ impl relm4::Component for OcPage {
add_css_class: "oc-page-section",
},
model.power_states_frame.widget() {
add_css_class: "oc-page-section",
},
model.igpu_frame.widget() {
add_css_class: "oc-page-section",
},
@@ -153,8 +149,8 @@ impl relm4::Component for OcPage {
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 power_states_dialog =
PowerStatesDialog::launch_default().forward(sender.input_sender(), |msg| msg);
let power_frame = PowerFrame::launch_default().forward(sender.input_sender(), |msg| msg);
let igpu_frame = IGpuFrame::launch_default().forward(sender.input_sender(), |msg| msg);
@@ -167,7 +163,7 @@ impl relm4::Component for OcPage {
stats_section,
device_info: None,
power_frame,
power_states_frame,
power_states_dialog,
gpu_clocks_frame,
vram_clocks_frame,
igpu_frame,
@@ -184,13 +180,17 @@ impl relm4::Component for OcPage {
widgets: &mut Self::Widgets,
msg: Self::Input,
sender: ComponentSender<Self>,
_root: &Self::Root,
root: &Self::Root,
) {
match msg {
OcPageMsg::Update { update, initial } => match &update {
PageUpdate::Stats(stats) => {
self.power_states_frame
.emit(PowerStatesFrameMsg::Stats(stats.clone()));
self.power_states_dialog
.emit(PowerStatesDialogMsg::Stats(stats.clone()));
self.gpu_clocks_frame
.emit(ClocksFrameMsg::Stats(stats.clone()));
self.vram_clocks_frame
.emit(ClocksFrameMsg::Stats(stats.clone()));
self.stats_section
.emit(GpuStatsSectionMsg::Stats(stats.clone()));
@@ -218,8 +218,8 @@ impl relm4::Component for OcPage {
self.device_info = Some(info.clone());
self.stats_section
.emit(GpuStatsSectionMsg::Info(info.clone()));
self.power_states_frame
.emit(PowerStatesFrameMsg::VramClockRatio(vram_clock_ratio));
self.power_states_dialog
.emit(PowerStatesDialogMsg::VramClockRatio(vram_clock_ratio));
self.vram_clocks_frame
.emit(ClocksFrameMsg::VramRatio(vram_clock_ratio));
@@ -257,11 +257,19 @@ impl relm4::Component for OcPage {
pstates,
configured,
} => {
self.power_states_frame
.emit(PowerStatesFrameMsg::PowerStates {
self.power_states_dialog
.emit(PowerStatesDialogMsg::PowerStates {
pstates: pstates.clone(),
configured,
});
self.gpu_clocks_frame
.emit(ClocksFrameMsg::PowerStatesAvailable(
!pstates.core.is_empty(),
));
self.vram_clocks_frame
.emit(ClocksFrameMsg::PowerStatesAvailable(
!pstates.vram.is_empty(),
));
self.stats_section
.emit(GpuStatsSectionMsg::PowerStates(Arc::new(pstates)));
sender.input(OcPageMsg::PerformanceLevelChanged);
@@ -269,13 +277,13 @@ impl relm4::Component for OcPage {
OcPageMsg::PerformanceLevelChanged => {
let custom_pstates_configurable =
self.get_performance_level() == Some(PerformanceLevel::Manual);
self.power_states_frame
.emit(PowerStatesFrameMsg::Configurable(
self.power_states_dialog
.emit(PowerStatesDialogMsg::Configurable(
custom_pstates_configurable,
));
self.power_states_frame
.emit(PowerStatesFrameMsg::PerformanceLevel(
self.power_states_dialog
.emit(PowerStatesDialogMsg::PerformanceLevel(
self.get_performance_level(),
));
}
@@ -286,8 +294,14 @@ impl relm4::Component for OcPage {
APP_BROKER.send(AppMsg::SettingsChanged);
}
OcPageMsg::EnablePstateConfig => {
self.power_states_frame
.emit(PowerStatesFrameMsg::EnableWithManualPerformanceLevel);
self.power_states_dialog
.emit(PowerStatesDialogMsg::EnableWithManualPerformanceLevel);
}
OcPageMsg::ShowPowerStates(domain) => {
self.power_states_dialog.emit(PowerStatesDialogMsg::Show {
domain,
parent: root.clone().upcast(),
});
}
OcPageMsg::ShowVfCurveEditor => {
self.vf_curve_editor.emit(VfCurveEditorMsg::Show);
@@ -346,7 +360,7 @@ impl OcPage {
pub fn get_enabled_power_states(&self) -> IndexMap<PowerLevelKind, Vec<u8>> {
if self.get_performance_level() == Some(PerformanceLevel::Manual) {
self.power_states_frame.model().get_enabled_power_states()
self.power_states_dialog.model().get_enabled_power_states()
} else {
IndexMap::new()
}
@@ -0,0 +1,4 @@
.adjustment-list > row.pstates {
padding-top: 15px;
padding-bottom: 15px;
}
@@ -1,5 +1,5 @@
use crate::{
APP_BROKER, I18N,
APP_BROKER, CONFIG, I18N,
app::{
components::{
adjustment_card::AdjustmentCard,
@@ -11,10 +11,10 @@ use crate::{
},
};
use adw::prelude::*;
use amdgpu_sysfs::gpu_handle::overdrive::ClocksTableGen as AmdClocksTable;
use amdgpu_sysfs::gpu_handle::{PowerLevelId, overdrive::ClocksTableGen as AmdClocksTable};
use i18n_embed_fl::fl;
use lact_schema::{
ClocksTable, IntelClocksTable, NvidiaClockOffset, NvidiaClocksTable,
ClocksTable, DeviceStats, IntelClocksTable, NvidiaClockOffset, NvidiaClocksTable,
request::{ClockspeedType, SetClocksCommand},
};
use relm4::{
@@ -35,6 +35,9 @@ pub struct ClocksFrame {
show_all_pstates: BoolBinding,
vf_curve_editing: BoolBinding,
enable_locked_clocks: BoolBinding,
has_power_states: bool,
active_power_state: Option<PowerLevelId>,
current_clock: Option<u64>,
}
#[derive(Default)]
@@ -99,6 +102,8 @@ pub enum ClocksFrameMsg {
vf_curve_is_configured: bool,
},
VramRatio(f64),
PowerStatesAvailable(bool),
Stats(Arc<DeviceStats>),
TogglePStatesVisibility,
ResetGpuClockOffsets,
}
@@ -119,7 +124,9 @@ impl relm4::Component for ClocksFrame {
ClockDomain::Vram => fl!(I18N, "vram-section"),
},
#[watch]
set_visible: model.domain == ClockDomain::Gpu || model.has_any_clocks(),
set_visible: model.domain == ClockDomain::Gpu
|| model.has_any_clocks()
|| model.has_power_states,
append_header = &gtk::Box {
set_spacing: 10,
@@ -167,7 +174,8 @@ impl relm4::Component for ClocksFrame {
controls {
append = &gtk::ToggleButton {
#[watch]
set_visible: model.has_secondary_p_states(),
set_visible: model.has_secondary_p_states()
&& model.secondary_p_states_enabled(),
add_css_class: "adjustment-card-option-toggle",
add_binding["active"]: &model.show_all_pstates,
@@ -244,6 +252,43 @@ impl relm4::Component for ClocksFrame {
set_halign: gtk::Align::Start,
},
},
adw::ActionRow {
set_title: &fl!(I18N, "pstate"),
set_activatable: true,
add_css_class: "pstates",
#[watch]
set_visible: model.has_power_states,
connect_activated[sender, domain = model.domain] => move |_| {
sender.output(OcPageMsg::ShowPowerStates(domain)).unwrap();
},
add_suffix = &gtk::Box {
set_spacing: 4,
set_valign: gtk::Align::Center,
gtk::Label {
add_css_class: css::HEADING,
#[watch]
set_label: &match model.active_power_state {
Some(PowerLevelId::Index(index)) => format!("P{index}"),
Some(PowerLevelId::Sleep) => "S".to_owned(),
None => "N/A".to_owned(),
},
},
gtk::Label {
add_css_class: css::DIM_LABEL,
#[watch]
set_label: &model.power_state_clock_label(),
},
},
add_suffix = &gtk::Image {
set_icon_name: Some("go-next-symbolic"),
add_css_class: css::DIM_LABEL,
},
},
},
},
}
@@ -271,6 +316,9 @@ impl relm4::Component for ClocksFrame {
show_all_pstates,
vf_curve_editing,
enable_locked_clocks: BoolBinding::new(false),
has_power_states: false,
active_power_state: None,
current_clock: None,
};
for binding in [
@@ -286,6 +334,13 @@ impl relm4::Component for ClocksFrame {
let widgets = view_output!();
model.adjustments.widget().set_sort_func(|left, right| {
// Keep the pstates row after dynamically inserted clock controls.
left.has_css_class("pstates")
.cmp(&right.has_css_class("pstates"))
.into()
});
ComponentParts { model, widgets }
}
@@ -362,7 +417,20 @@ impl relm4::Component for ClocksFrame {
self.vram_clock_ratio = vram_ratio;
self.update_vram_clock_ratio();
}
ClocksFrameMsg::PowerStatesAvailable(available) => {
self.has_power_states = available;
}
ClocksFrameMsg::Stats(stats) => {
let active_states = stats.active_power_states.unwrap_or_default();
(self.active_power_state, self.current_clock) = match self.domain {
ClockDomain::Gpu => (active_states.core, stats.clockspeed.gpu_clockspeed),
ClockDomain::Vram => (active_states.memory, stats.clockspeed.vram_clockspeed),
};
}
ClocksFrameMsg::TogglePStatesVisibility => {
if !self.secondary_p_states_enabled() && self.show_all_pstates.value() {
self.show_all_pstates.set_value(false);
}
for clock_type in self.adjustments.keys() {
let visible = match clock_type {
ClockspeedType::MaxCoreClock
@@ -380,7 +448,8 @@ impl relm4::Component for ClocksFrame {
}
_ => {
!self.secondary_p_state_clocks.contains(clock_type)
|| self.show_all_pstates.value()
|| (self.secondary_p_states_enabled()
&& self.show_all_pstates.value())
}
};
self.adjustments
@@ -394,6 +463,23 @@ impl relm4::Component for ClocksFrame {
}
impl ClocksFrame {
fn power_state_clock_label(&self) -> String {
match self.current_clock {
Some(clock) => {
let ratio = match self.domain {
ClockDomain::Gpu => 1.0,
ClockDomain::Vram => self.vram_clock_ratio,
};
format!("{:.0} {}", clock as f64 * ratio, fl!(I18N, "mhz"))
}
None => "N/A".to_owned(),
}
}
fn secondary_p_states_enabled(&self) -> bool {
!self.show_nvidia_options || CONFIG.read().experimental_nvidia_pstate_offsets
}
fn set_clock(&mut self, clock_type: ClockspeedType, data: ClocksData) {
if !self.domain.matches(clock_type) {
return;
@@ -712,6 +798,11 @@ impl ClocksFrame {
self.adjustments
.iter()
.filter_map(|(clock_type, row)| {
if !self.secondary_p_states_enabled()
&& self.secondary_p_state_clocks.contains(clock_type)
{
return None;
}
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 {
@@ -1,3 +1,3 @@
pub mod power_states_frame;
pub mod power_states_dialog;
mod power_states_list;
mod power_states_row;
@@ -5,7 +5,7 @@ use crate::{
app::{
components::{adjustment_card::AdjustmentCard, page_section::PageSection},
msg::AppMsg,
pages::oc_page::OcPageMsg,
pages::oc_page::{OcPageMsg, clocks_frame::ClockDomain},
utils::ext::RelmLaunchable as _,
},
};
@@ -16,12 +16,13 @@ use i18n_embed_fl::fl;
use indexmap::IndexMap;
use lact_schema::{DeviceStats, PowerStates};
use relm4::{
ComponentController, ComponentParts, ComponentSender,
ComponentController, ComponentParts, ComponentSender, RelmWidgetExt,
binding::{Binding, BoolBinding},
};
use std::sync::Arc;
pub struct PowerStatesFrame {
pub struct PowerStatesDialog {
domain: ClockDomain,
core_states_list: relm4::Controller<PowerStatesList>,
vram_states_list: relm4::Controller<PowerStatesList>,
states_configurable: BoolBinding,
@@ -34,7 +35,11 @@ pub struct PowerStatesFrame {
}
#[derive(Debug)]
pub enum PowerStatesFrameMsg {
pub enum PowerStatesDialogMsg {
Show {
domain: ClockDomain,
parent: gtk::Widget,
},
PowerStates {
pstates: PowerStates,
configured: bool,
@@ -51,69 +56,83 @@ pub enum PowerStatesFrameMsg {
}
#[relm4::component(pub)]
impl relm4::SimpleComponent for PowerStatesFrame {
impl relm4::Component for PowerStatesDialog {
type Init = ();
type Input = PowerStatesFrameMsg;
type Input = PowerStatesDialogMsg;
type Output = OcPageMsg;
type CommandOutput = ();
view! {
PageSection {
set_name: fl!(I18N, "pstates"),
set_hide_visible_container: true,
#[template]
append_child = &AdjustmentCard {
#[template_child]
advanced_features {
#[watch]
set_visible: model.performance_level.is_some(),
},
adw::Dialog {
set_content_width: 420,
set_follows_content_size: true,
#[watch]
set_title: &match model.domain {
ClockDomain::Gpu => fl!(I18N, "gpu-pstates"),
ClockDomain::Vram => fl!(I18N, "vram-pstates"),
},
#[template_child]
controls {
gtk::ToggleButton {
set_halign: gtk::Align::Start,
add_css_class: "adjustment-card-option-toggle",
#[wrap(Some)]
set_child = &adw::ToolbarView {
add_top_bar = &adw::HeaderBar {},
#[watch]
#[block_signal(configured_toggled_handler)]
set_active: model.states_configuration_enabled.value(),
#[wrap(Some)]
set_content = &gtk::ScrolledWindow {
set_hscrollbar_policy: gtk::PolicyType::Never,
set_propagate_natural_height: true,
set_max_content_height: 500,
connect_toggled[sender] => move |button| {
sender.input(PowerStatesFrameMsg::ConfiguredToggled {
configured: button.is_active(),
});
} @ configured_toggled_handler,
PageSection {
set_hide_visible_container: true,
set_margin_all: 15,
add_css_class: "power-states-dialog-card",
#[wrap(Some)]
set_child = &gtk::Box {
gtk::Label {
set_label: &fl!(I18N, "enable-pstate-config"),
},
},
},
},
#[template_child]
content {
gtk::ListBoxRow {
set_activatable: false,
set_selectable: false,
gtk::Box {
set_spacing: 10,
set_orientation: gtk::Orientation::Horizontal,
set_homogeneous: true,
gtk::Box {
#[template]
append_child = &AdjustmentCard {
#[template_child]
advanced_features {
#[watch]
set_visible: model.has_core_states,
append = model.core_states_list.widget(),
set_visible: model.performance_level.is_some(),
},
gtk::Box {
#[watch]
set_visible: model.has_vram_states,
append = model.vram_states_list.widget(),
#[template_child]
controls {
gtk::ToggleButton {
set_halign: gtk::Align::Start,
add_css_class: "adjustment-card-option-toggle",
set_label: &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(PowerStatesDialogMsg::ConfiguredToggled {
configured: button.is_active(),
});
} @ configured_toggled_handler,
},
},
#[template_child]
content {
gtk::ListBoxRow {
set_activatable: false,
set_selectable: false,
#[watch]
set_visible: model.domain == ClockDomain::Gpu && model.has_core_states,
#[wrap(Some)]
set_child = model.core_states_list.widget(),
},
gtk::ListBoxRow {
set_activatable: false,
set_selectable: false,
#[watch]
set_visible: model.domain == ClockDomain::Vram && model.has_vram_states,
#[wrap(Some)]
set_child = model.vram_states_list.widget(),
},
},
},
},
@@ -128,11 +147,9 @@ impl relm4::SimpleComponent for PowerStatesFrame {
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let core_states_list = PowerStatesList::detach(PowerStatesListOptions {
title: fl!(I18N, "gpu-pstates"),
value_suffix: fl!(I18N, "mhz"),
});
let vram_states_list = PowerStatesList::detach(PowerStatesListOptions {
title: fl!(I18N, "vram-pstates"),
value_suffix: fl!(I18N, "mhz"),
});
@@ -141,13 +158,14 @@ impl relm4::SimpleComponent for PowerStatesFrame {
let configured_sender = sender.clone();
let configured_signal =
states_configuration_enabled.connect_value_notify(move |states_configured| {
configured_sender.input(PowerStatesFrameMsg::InternalConfigurableChanged(
configured_sender.input(PowerStatesDialogMsg::InternalConfigurableChanged(
states_configured.get(),
));
APP_BROKER.send(AppMsg::SettingsChanged);
});
let model = Self {
domain: ClockDomain::Gpu,
core_states_list,
vram_states_list,
states_configurable: BoolBinding::new(false),
@@ -164,9 +182,20 @@ impl relm4::SimpleComponent for PowerStatesFrame {
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
fn update_with_view(
&mut self,
widgets: &mut Self::Widgets,
msg: Self::Input,
sender: ComponentSender<Self>,
root: &Self::Root,
) {
match msg {
PowerStatesFrameMsg::PowerStates {
PowerStatesDialogMsg::Show { domain, parent } => {
self.domain = domain;
self.update_view(widgets, sender.clone());
root.present(Some(&parent));
}
PowerStatesDialogMsg::PowerStates {
pstates,
configured,
} => {
@@ -186,7 +215,7 @@ impl relm4::SimpleComponent for PowerStatesFrame {
self.vram_clock_ratio,
));
}
PowerStatesFrameMsg::Stats(stats) => {
PowerStatesDialogMsg::Stats(stats) => {
self.core_states_list.emit(PowerStatesListMsg::ActiveState(
stats.active_power_states.and_then(|states| states.core),
));
@@ -194,10 +223,12 @@ impl relm4::SimpleComponent for PowerStatesFrame {
stats.active_power_states.and_then(|states| states.memory),
));
}
PowerStatesFrameMsg::VramClockRatio(ratio) => {
PowerStatesDialogMsg::VramClockRatio(ratio) => {
self.vram_clock_ratio = ratio;
self.vram_states_list
.emit(PowerStatesListMsg::ValueRatio(ratio));
}
PowerStatesFrameMsg::Configurable(is_plvl_manual) => {
PowerStatesDialogMsg::Configurable(is_plvl_manual) => {
let configurable = is_plvl_manual && (self.has_core_states || self.has_vram_states);
self.states_configurable.set_value(configurable);
@@ -216,33 +247,34 @@ impl relm4::SimpleComponent for PowerStatesFrame {
configurable && self.states_configuration_enabled.value(),
));
}
PowerStatesFrameMsg::PerformanceLevel(level) => {
PowerStatesDialogMsg::PerformanceLevel(level) => {
self.performance_level = level;
}
PowerStatesFrameMsg::ConfiguredToggled { configured } => {
PowerStatesDialogMsg::ConfiguredToggled { configured } => {
if !configured || self.performance_level == Some(PerformanceLevel::Manual) {
self.states_configuration_enabled.set_value(configured);
} else {
APP_BROKER.send(AppMsg::EnablePstateConfig);
}
}
PowerStatesFrameMsg::EnableWithManualPerformanceLevel => {
PowerStatesDialogMsg::EnableWithManualPerformanceLevel => {
sender
.output(OcPageMsg::SetPerformanceLevel(PerformanceLevel::Manual))
.unwrap();
self.states_configuration_enabled.set_value(true);
}
PowerStatesFrameMsg::InternalConfigurableChanged(configurable) => {
PowerStatesDialogMsg::InternalConfigurableChanged(configurable) => {
self.core_states_list
.emit(PowerStatesListMsg::Configurable(configurable));
self.vram_states_list
.emit(PowerStatesListMsg::Configurable(configurable));
}
}
self.update_view(widgets, sender);
}
}
impl PowerStatesFrame {
impl PowerStatesDialog {
pub fn get_enabled_power_states(&self) -> IndexMap<PowerLevelKind, Vec<u8>> {
if self.states_configuration_enabled.value() {
let state_types = [
@@ -279,7 +311,7 @@ mod tests {
adw::init().unwrap();
let context = gtk::glib::MainContext::default();
let _guard = context.acquire().unwrap();
let frame = PowerStatesFrame::detach(());
let dialog = PowerStatesDialog::detach(());
let states = PowerStates {
core: vec![],
vram: (0..4)
@@ -293,30 +325,30 @@ mod tests {
};
// Queue the startup messages together, before child components can update
frame.emit(PowerStatesFrameMsg::PowerStates {
dialog.emit(PowerStatesDialogMsg::PowerStates {
pstates: states,
configured: true,
});
frame.emit(PowerStatesFrameMsg::Configurable(true));
dialog.emit(PowerStatesDialogMsg::Configurable(true));
while context.pending() {
context.iteration(false);
}
assert!(frame.model().states_configuration_enabled.value());
assert!(dialog.model().states_configuration_enabled.value());
assert_eq!(
frame.model().get_enabled_power_states()[&PowerLevelKind::MemoryClock],
dialog.model().get_enabled_power_states()[&PowerLevelKind::MemoryClock],
vec![3],
);
// Changing to a GPU without power states must clear the previous capability
frame.emit(PowerStatesFrameMsg::PowerStates {
dialog.emit(PowerStatesDialogMsg::PowerStates {
pstates: PowerStates::default(),
configured: false,
});
frame.emit(PowerStatesFrameMsg::Configurable(true));
dialog.emit(PowerStatesDialogMsg::Configurable(true));
while context.pending() {
context.iteration(false);
}
assert!(!frame.model().states_configurable.value());
assert!(frame.model().get_enabled_power_states().is_empty());
assert!(!dialog.model().states_configurable.value());
assert!(dialog.model().get_enabled_power_states().is_empty());
}
}
@@ -1,21 +1,18 @@
use super::power_states_row::{PowerStateRow, PowerStateRowMsg, PowerStateRowOptions};
use amdgpu_sysfs::gpu_handle::PowerLevelId;
use gtk::prelude::{FrameExt, WidgetExt};
use gtk::prelude::WidgetExt;
use lact_schema::PowerState;
use relm4::{
ComponentParts, ComponentSender, RelmWidgetExt, binding::BoolBinding, css,
prelude::FactoryVecDeque,
};
use relm4::{ComponentParts, ComponentSender, binding::BoolBinding, prelude::FactoryVecDeque};
pub struct PowerStatesList {
states: FactoryVecDeque<PowerStateRow>,
value_suffix: String,
is_active_indicator_visible: BoolBinding,
configurable: BoolBinding,
active_state: Option<PowerLevelId>,
}
pub struct PowerStatesListOptions {
pub title: String,
pub value_suffix: String,
}
@@ -24,6 +21,7 @@ pub enum PowerStatesListMsg {
PowerStates(Vec<PowerState>, f64),
ActiveState(Option<PowerLevelId>),
Configurable(bool),
ValueRatio(f64),
}
#[relm4::component(pub)]
@@ -33,16 +31,11 @@ impl relm4::SimpleComponent for PowerStatesList {
type Output = ();
view! {
gtk::Frame {
gtk::Box {
set_hexpand: true,
#[wrap(Some)]
set_label_widget = &gtk::Label {
set_label: &opts.title,
set_margin_horizontal: 5,
add_css_class: css::CAPTION_HEADING,
},
#[local_ref]
states_widget -> gtk::ListBox {
set_hexpand: true,
set_selection_mode: gtk::SelectionMode::None,
},
}
@@ -60,6 +53,7 @@ impl relm4::SimpleComponent for PowerStatesList {
value_suffix: opts.value_suffix,
is_active_indicator_visible: BoolBinding::new(false),
configurable: BoolBinding::new(true),
active_state: None,
};
let states_widget = model.states.widget();
@@ -75,12 +69,12 @@ impl relm4::SimpleComponent for PowerStatesList {
let mut states = self.states.guard();
states.clear();
for mut power_state in new_pstates {
power_state.value = (power_state.value as f64 * value_ratio) as u64;
for power_state in new_pstates {
let opts = PowerStateRowOptions {
power_state,
value_suffix: self.value_suffix.clone(),
active: false,
value_ratio,
active: self.active_state.is_some() && power_state.id == self.active_state,
show_active_indicator: self.is_active_indicator_visible.clone(),
configurable: self.configurable.clone(),
};
@@ -88,10 +82,11 @@ impl relm4::SimpleComponent for PowerStatesList {
}
}
PowerStatesListMsg::ActiveState(active_idx) => {
self.active_state = active_idx;
self.is_active_indicator_visible
.set_value(active_idx.is_some());
for (i, row) in self.states.iter().enumerate() {
let is_active = row.power_state.id == active_idx;
let is_active = active_idx.is_some() && row.power_state.id == active_idx;
self.states.send(i, PowerStateRowMsg::Active(is_active));
}
@@ -99,6 +94,9 @@ impl relm4::SimpleComponent for PowerStatesList {
PowerStatesListMsg::Configurable(configurable) => {
self.configurable.set_value(configurable);
}
PowerStatesListMsg::ValueRatio(ratio) => {
self.states.broadcast(PowerStateRowMsg::ValueRatio(ratio));
}
}
}
}
@@ -1,6 +1,6 @@
use crate::{APP_BROKER, app::msg::AppMsg};
use amdgpu_sysfs::gpu_handle::PowerLevelId;
use gtk::prelude::{BoxExt, OrientableExt, WidgetExt};
use gtk::prelude::{BoxExt, ListBoxRowExt, OrientableExt, WidgetExt};
use lact_schema::PowerState;
use relm4::{RelmObjectExt, RelmWidgetExt, binding::BoolBinding, css};
@@ -9,6 +9,7 @@ pub struct PowerStateRow {
pub(super) enabled: BoolBinding,
pub(super) power_state: PowerState,
value_suffix: String,
value_ratio: f64,
configurable: BoolBinding,
show_active_indicator: BoolBinding,
}
@@ -16,6 +17,7 @@ pub struct PowerStateRow {
pub struct PowerStateRowOptions {
pub power_state: PowerState,
pub value_suffix: String,
pub value_ratio: f64,
pub active: bool,
pub show_active_indicator: BoolBinding,
pub configurable: BoolBinding,
@@ -24,6 +26,7 @@ pub struct PowerStateRowOptions {
#[derive(Clone, Debug)]
pub enum PowerStateRowMsg {
Active(bool),
ValueRatio(f64),
}
#[relm4::factory(pub)]
@@ -35,40 +38,60 @@ impl relm4::factory::FactoryComponent for PowerStateRow {
type Init = PowerStateRowOptions;
view! {
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 5,
set_margin_vertical: 2,
set_margin_horizontal: 5,
gtk::ListBoxRow {
set_selectable: false,
set_activatable: false,
set_focusable: false,
append: image = &gtk::Image {
set_icon_name: Some("pan-end-symbolic"),
add_binding: (&self.show_active_indicator, "visible"),
#[watch]
set_opacity: if self.active.value() { 1.0 } else { 0.0 },
},
#[wrap(Some)]
set_child = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 5,
set_margin_vertical: 2,
set_margin_horizontal: 5,
append = &gtk::CheckButton {
add_binding: (&self.enabled, "active"),
add_binding: (&self.configurable, "visible"),
set_sensitive: matches!(self.power_state.id, Some(PowerLevelId::Index(_))),
},
append: image = &gtk::Image {
set_icon_name: Some("pan-end-symbolic"),
add_binding: (&self.show_active_indicator, "visible"),
#[watch]
set_opacity: if self.active.value() { 1.0 } else { 0.0 },
},
append = &gtk::Label {
add_css_class: css::MONOSPACE,
#[watch]
set_class_active: (css::DIM_LABEL, !self.active.value()),
set_label: &{
let index_text = match self.power_state.id {
Some(PowerLevelId::Index(index)) => index.to_string(),
Some(PowerLevelId::Sleep) => "S".to_owned(),
append = &gtk::CheckButton {
add_binding: (&self.enabled, "active"),
add_binding: (&self.configurable, "visible"),
set_sensitive: matches!(self.power_state.id, Some(PowerLevelId::Index(_))),
},
append = &gtk::Label {
add_css_class: css::MONOSPACE,
#[watch]
set_class_active: (css::DIM_LABEL, !self.active.value()),
set_label: &match self.power_state.id {
Some(PowerLevelId::Index(index)) => format!("{index}:"),
Some(PowerLevelId::Sleep) => "S:".to_owned(),
None => String::new(),
};
let value_text = match self.power_state.min_value {
Some(min) if min != self.power_state.value => format!("{min}-{}", self.power_state.value),
_ => self.power_state.value.to_string(),
};
format!("{index_text}: {value_text} {}", self.value_suffix)
},
},
append = &gtk::Label {
add_css_class: css::MONOSPACE,
set_hexpand: true,
set_xalign: 1.0,
#[watch]
set_class_active: (css::DIM_LABEL, !self.active.value()),
#[watch]
set_label: &{
let value = (self.power_state.value as f64 * self.value_ratio) as u64;
let value_text = match self.power_state.min_value {
Some(min) if min != self.power_state.value => {
let min = (min as f64 * self.value_ratio) as u64;
format!("{min}-{value}")
}
_ => value.to_string(),
};
format!("{value_text} {}", self.value_suffix)
},
},
},
}
@@ -87,13 +110,16 @@ impl relm4::factory::FactoryComponent for PowerStateRow {
active: BoolBinding::new(opts.active),
power_state: opts.power_state,
value_suffix: opts.value_suffix,
value_ratio: opts.value_ratio,
configurable: opts.configurable,
show_active_indicator: opts.show_active_indicator,
}
}
fn update(&mut self, msg: Self::Input, _: relm4::FactorySender<Self>) {
let PowerStateRowMsg::Active(active) = msg;
self.active.set_value(active);
match msg {
PowerStateRowMsg::Active(active) => self.active.set_value(active),
PowerStateRowMsg::ValueRatio(ratio) => self.value_ratio = ratio,
}
}
}
@@ -92,6 +92,7 @@ impl relm4::Component for VfCurveEditor {
view! {
#[root]
adw::Window {
set_application: Some(&relm4::main_application()),
set_hide_on_close: true,
set_default_size: (1100, 700),
set_title: Some(&fl!(I18N, "vf-curve-editor")),
+16
View File
@@ -172,6 +172,22 @@ impl relm4::Component for PreferencesDialog {
},
},
add = &adw::PreferencesGroup {
set_title: &fl!(I18N, "experimental-features"),
adw::SwitchRow {
set_title: &fl!(I18N, "nvidia-pstate-offsets"),
set_subtitle: &fl!(I18N, "nvidia-pstate-offsets-description"),
set_active: CONFIG.read().experimental_nvidia_pstate_offsets,
connect_active_notify => move |row| {
CONFIG.write().edit(|config| {
config.experimental_nvidia_pstate_offsets = row.is_active();
});
APP_BROKER.send(AppMsg::ExperimentalFeaturesChanged);
},
},
},
add = &adw::PreferencesGroup {
set_title: &fl!(I18N, "daemon"),
+3
View File
@@ -30,6 +30,8 @@ pub struct UiConfig {
pub theme: AppTheme,
#[serde(default)]
pub color_scheme: AppColorScheme,
#[serde(default)]
pub experimental_nvidia_pstate_offsets: bool,
pub language: Option<String>,
pub window_size: Option<WindowSize>,
}
@@ -45,6 +47,7 @@ impl Default for UiConfig {
gpus: HashMap::new(),
theme: AppTheme::Automatic,
color_scheme: AppColorScheme::default(),
experimental_nvidia_pstate_offsets: false,
language: None,
window_size: None,
}
+3
View File
@@ -218,6 +218,9 @@
"1458": {
"2409": "GIGABYTE RX 6950 XT Gaming OC"
},
"148c": {
"2420": "PowerColor Red Devil RX 6950 XT"
},
"1849": {
"5230": "ASRock RX 6950 XT OC Formula"
}