diff --git a/docs/CONFIG.md b/docs/CONFIG.md index ede51268..52d253cb 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -197,6 +197,10 @@ gpus: 7: clockspeed: 1590 voltage: 1200 + nvidia_gpu_vf_curve: + 22: + clockspeed_offset: 90 + voltage: 750 # VRAM V/F curve with voltage and frequency points per each power state. # Applicable only to AMD GCN GPUs. Overrides the values of min/max memory clock fields. mem_vf_curve: diff --git a/lact-daemon/src/config.rs b/lact-daemon/src/config.rs index fd325d36..eb7b4d17 100644 --- a/lact-daemon/src/config.rs +++ b/lact-daemon/src/config.rs @@ -1,7 +1,10 @@ use crate::server::gpu_controller::{GpuController, VENDOR_NVIDIA}; use anyhow::Context; use indexmap::IndexMap; -use lact_schema::config::{GpuConfig, Profile, ProfileHooks}; +use lact_schema::{ + ClocksInfo, ClocksTable, NvidiaVfPoint, + config::{CurvePoint, GpuConfig, NvidiaCurvePoint, Profile, ProfileHooks}, +}; use nix::unistd::{Group, getuid}; use notify::{RecommendedWatcher, Watcher}; use serde::{Deserialize, Serialize}; @@ -9,13 +12,13 @@ use serde_with::skip_serializing_none; use std::{ cell::Cell, collections::BTreeMap, - env, fs, iter, + env, fs, iter, mem, path::PathBuf, rc::Rc, time::{Duration, Instant}, }; use tokio::{sync::mpsc, time}; -use tracing::{debug, error, info}; +use tracing::{debug, error, info, warn}; const FILE_NAME: &str = "config.yaml"; const DEFAULT_ADMIN_GROUPS: [&str; 2] = ["wheel", "sudo"]; @@ -50,7 +53,7 @@ impl Default for Config { profiles: IndexMap::new(), current_profile: None, auto_switch_profiles: false, - version: 6, + version: 7, } } } @@ -237,6 +240,38 @@ impl Config { } } } + // convert the old Nvidia V/F curve points from abs to offsets + 7 => { + for (id, gpu) in gpu_configs { + if !id.starts_with(VENDOR_NVIDIA) + || gpu.clocks_configuration.gpu_vf_curve.is_empty() + { + continue; + } + + let old_curve = mem::take(&mut gpu.clocks_configuration.gpu_vf_curve); + let base_curve = match gpu_controllers + .get(id) + .map(|controller| controller.get_clocks_info(None)) + { + Some(Ok(ClocksInfo { + table: Some(ClocksTable::Nvidia(table)), + .. + })) => table.gpu_vf_curve, + _ => Vec::new(), + }; + + if let Some(new_curve) = nvidia_vf_curve_to_offsets(&old_curve, &base_curve) + { + info!("converted the V/F curve of '{id}' to clockspeed offsets"); + gpu.clocks_configuration.nvidia_gpu_vf_curve = new_curve; + } else { + warn!( + "could not convert the V/F curve of '{id}' to clockspeed offsets, it was removed and has to be reconfigured" + ); + } + } + } _ => break, } info!("migrated config version {} to {next_version}", self.version); @@ -302,6 +337,38 @@ pub struct Metrics { pub interval: u64, } +fn nvidia_vf_curve_to_offsets( + configured_curve: &IndexMap, + base_curve: &[NvidiaVfPoint], +) -> Option> { + let mut offsets = IndexMap::with_capacity(configured_curve.len()); + + for (index, configured_point) in configured_curve { + let base_point = base_curve.iter().find(|point| point.index == *index)?; + + let Some(clockspeed) = configured_point.clockspeed else { + continue; + }; + + if configured_point + .voltage + .is_some_and(|voltage| voltage != base_point.voltage.cast_signed()) + { + return None; + } + + offsets.insert( + *index, + NvidiaCurvePoint { + clockspeed_offset: clockspeed - base_point.base_freq.cast_signed(), + voltage: Some(base_point.voltage), + }, + ); + } + + (!offsets.is_empty()).then_some(offsets) +} + fn default_metrics_interval() -> u64 { 30 } @@ -424,15 +491,67 @@ fn find_existing_group(groups: &[impl AsRef]) -> Option { #[cfg(test)] mod tests { - use crate::config::{Config, Daemon}; + use crate::config::{Config, Daemon, nvidia_vf_curve_to_offsets}; use indexmap::IndexMap; use insta::assert_yaml_snapshot; use lact_schema::{ - FanControlMode, NvidiaThermalOptions, PmfwOptions, - config::{ClocksConfiguration, FanControlSettings, FanCurve, GpuConfig}, + FanControlMode, NvidiaThermalOptions, NvidiaVfPoint, PmfwOptions, + config::{ + ClocksConfiguration, CurvePoint, FanControlSettings, FanCurve, GpuConfig, + NvidiaCurvePoint, + }, }; use std::collections::BTreeMap; + #[test] + fn vf_curve_migrated_to_offsets() { + fn base_curve() -> Vec { + [(0, 450, 270), (1, 800, 1500), (2, 1050, 2500)] + .map(|(index, voltage, base_freq)| NvidiaVfPoint { + index, + voltage, + base_freq, + freq: base_freq, + base_voltage: voltage, + freq_offset: 0, + }) + .to_vec() + } + fn configured_point(voltage: i32, clockspeed: i32) -> CurvePoint { + CurvePoint { + voltage: Some(voltage), + clockspeed: Some(clockspeed), + } + } + let configured = IndexMap::from([ + (0, configured_point(450, 360)), + (2, configured_point(1050, 2400)), + ]); + + let offsets = + nvidia_vf_curve_to_offsets(&configured, &base_curve()).expect("curve was dropped"); + + assert_eq!( + IndexMap::from([ + ( + 0, + NvidiaCurvePoint { + clockspeed_offset: 90, + voltage: Some(450), + } + ), + ( + 2, + NvidiaCurvePoint { + clockspeed_offset: -100, + voltage: Some(1050), + } + ), + ]), + offsets + ); + } + #[test] fn serde_de_full() { let config = Config { diff --git a/lact-daemon/src/server/gpu_controller/nvidia.rs b/lact-daemon/src/server/gpu_controller/nvidia.rs index 09c2a45d..bc7882a9 100644 --- a/lact-daemon/src/server/gpu_controller/nvidia.rs +++ b/lact-daemon/src/server/gpu_controller/nvidia.rs @@ -7,7 +7,9 @@ use crate::{ server::gpu_controller::{ common::{fan_control::FanCurveExt, resolve_process_name}, nvidia::nvapi::{ - CLOCK_CLIENT_CLK_VF_POINT_TYPE_PROG, ClockClientClkVfPointInfoV1, NvGpuClockDomainId, + CLOCK_CLIENT_CLK_VF_POINT_TYPE_PROG, ClockClientClkVfPointInfoV1, + ClockClientClkVfPointsControlV1, ClockClientClkVfPointsInfoV1, + ClockClientClkVfPointsStatusV3, NvGpuClockDomainId, }, }, }; @@ -25,7 +27,7 @@ use lact_schema::{ FanStats, IntelDrmInfo, LinkInfo, NvidiaClockOffset, NvidiaClocksTable, NvidiaThermalInfo, NvidiaVfPoint, NvidiaVoltageBoost, PmfwInfo, PowerState, PowerStates, PowerStats, ProcessInfo, ProcessList, ProcessType, ProcessUtilizationType, TemperatureEntry, VoltageStats, VramStats, - config::{CurvePoint, FanControlSettings, FanCurve, GpuConfig}, + config::{FanControlSettings, FanCurve, GpuConfig, NvidiaCurvePoint}, }; use nvapi::NvApi; use nvml_wrapper::{ @@ -74,8 +76,6 @@ pub struct NvidiaGpuController { // Check if reset is needed to avoid unnecessarily going to nvapi vf_curve_written: Cell, voltage_boost_written: Cell, - // Used as the initial value on cards which do not report base VF points themselves (Turing) - base_vf_curve: RefCell>>, } impl NvidiaGpuController { @@ -151,7 +151,6 @@ impl NvidiaGpuController { last_applied_vram_locked_clocks: RefCell::new(None), vf_curve_written: Cell::new(false), voltage_boost_written: Cell::new(false), - base_vf_curve: RefCell::new(None), }) } @@ -396,138 +395,54 @@ impl NvidiaGpuController { } fn get_vf_curve(&self) -> anyhow::Result> { - if let Some((nvapi, handle)) = self.nvapi.as_ref() { - let info; - let status; + let (nvapi, handle) = self.nvapi.as_ref().context("NvAPI not available")?; - unsafe { - info = nvapi.clock_client_clk_vf_points_get_info(*handle)?; - status = - nvapi.clock_client_clk_vf_points_get_status(*handle, info.vf_points_mask)?; - } + let info; + let status; + let control; - let base_vf_curve = self.base_vf_curve.borrow(); - let point_count = point_count_from_mask(info.vf_points_mask); - let mut curve = Vec::with_capacity(point_count); - - for i in 0..point_count { - let point = status.vf_points[i]; - let point_info = info.vf_points[i]; - - // Only report configurable and voltage-based points - if !vf_curve_point_is_editable(point_info) { - continue; - } - - let (base_freq, base_voltage) = if status.b_vf_tuple_base_supported == 0 { - // If vf_tuple_base is not supported, base_vf_curve must be populated before any write - // Otherwise, current values are base - if let Some(base_curve) = &*base_vf_curve { - let base_point = base_curve - .iter() - .find(|point| point.index as usize == i) - .expect("Mismatched base point"); - (base_point.base_freq, base_point.base_voltage) - } else { - (point.freq_khz / 1000, point.voltage_uv / 1000) - } - } else { - ( - point.vf_tuple_base.freq_khz / 1000, - point.vf_tuple_base.voltage_uv / 1000, - ) - }; - - curve.push(NvidiaVfPoint { - index: u8::try_from(i).expect("max 255 points"), - freq: point.freq_khz / 1000, - voltage: point.voltage_uv / 1000, - base_freq, - base_voltage, - }); - } - - Ok(curve) - } else { - Err(anyhow!("NvAPI not available")) + unsafe { + info = nvapi.clock_client_clk_vf_points_get_info(*handle)?; + status = nvapi.clock_client_clk_vf_points_get_status(*handle, info.vf_points_mask)?; + control = nvapi.clock_client_clk_vf_get_control(*handle, info.vf_points_mask)?; } + + Ok(build_vf_curve(&info, &status, &control)) } - #[expect(clippy::similar_names)] - fn apply_vf_curve(&self, curve: &IndexMap) -> anyhow::Result<()> { + fn apply_vf_curve(&self, curve: &IndexMap) -> anyhow::Result<()> { let (nvapi, handle) = self.nvapi.as_ref().context("NvAPI not available")?; debug!("applying curve with {} points", curve.len()); - let info = unsafe { nvapi.clock_client_clk_vf_points_get_info(*handle)? }; - let current_vf_curve = - unsafe { nvapi.clock_client_clk_vf_points_get_status(*handle, info.vf_points_mask)? }; - let mut curve_control = - unsafe { nvapi.clock_client_clk_vf_get_control(*handle, info.vf_points_mask)? }; - let offset_info = self .device() .clock_offset(Clock::Graphics, PerformanceState::Zero) .context("Could not get offset info")?; - // If a base curve is needed, we should get a clean unmodified version - if current_vf_curve.b_vf_tuple_base_supported == 0 && self.base_vf_curve.borrow().is_none() - { - self.reset_vf_curve().context("Could not reset curve")?; - self.base_vf_curve.replace(Some(self.get_vf_curve()?)); - - return self.apply_vf_curve(curve); - } - - let base_vf_curve = self.base_vf_curve.borrow(); - - for (i, configured_point) in curve { - let i = *i as usize; - - let point_info = info.vf_points[i]; - let current_point = current_vf_curve.vf_points[i]; - let point_control = &mut curve_control.vf_points[i]; - - if !vf_curve_point_is_editable(point_info) { - bail!("Trying to edit non-configurable point {i}"); - } - - if let Some(configured_mv) = configured_point.voltage { - let current_mv = (current_point.voltage_uv / 1000).cast_signed(); - if configured_mv != current_mv { - bail!( - "Voltage is immutable - point {i} was attempted to be changed from {current_mv}mV to {configured_mv}mV" - ); - } - } - - if let Some(configured_mhz) = configured_point.clockspeed { - let base_freq: i32 = if let Some(base_points) = &*base_vf_curve { - let base_point = base_points - .iter() - .find(|point| point.index as usize == i) - .expect("Mismatched base point"); - base_point.base_freq.cast_signed() * 1000 - } else { - current_point.vf_tuple_base.freq_khz.try_into()? - }; - let offset_khz = configured_mhz * 1000 - base_freq; - let offset_mhz = offset_khz / 1000; - - let min_offset = cmp::min(offset_info.min_clock_offset_mhz, -(base_freq / 1000)); - - if !(min_offset..=offset_info.max_clock_offset_mhz).contains(&offset_mhz) { - bail!("Configured offset {offset_mhz}MHz is outside of the allowed range"); - } - - debug!("writing offset {offset_khz}KHz to point {i}"); - - point_control.data.prog.freq_offset_khz = offset_khz; - } - } + let info; + let status; + let control; unsafe { - nvapi.clock_client_clk_vf_set_control(*handle, curve_control)?; + info = nvapi.clock_client_clk_vf_points_get_info(*handle)?; + status = nvapi.clock_client_clk_vf_points_get_status(*handle, info.vf_points_mask)?; + control = nvapi.clock_client_clk_vf_get_control(*handle, info.vf_points_mask)?; + } + + let control = build_curve_control( + curve, + &info, + &status, + control, + ( + offset_info.min_clock_offset_mhz, + offset_info.max_clock_offset_mhz, + ), + )?; + + unsafe { + nvapi.clock_client_clk_vf_set_control(*handle, control)?; } self.vf_curve_written.set(true); @@ -629,6 +544,100 @@ fn vf_curve_point_is_editable(point: ClockClientClkVfPointInfoV1) -> bool { point.b_voltage_based == 1 && point.type_ == CLOCK_CLIENT_CLK_VF_POINT_TYPE_PROG } +/// The base curve for a point. Changes with GPU state(load,temps, e.tc), so it may only be used for display and validation +fn vf_curve_point_base_freq_khz( + status: &ClockClientClkVfPointsStatusV3, + control: &ClockClientClkVfPointsControlV1, + i: usize, +) -> i32 { + if status.b_vf_tuple_base_supported == 0 { + // Cards without the base tuple (Turing) only expose the resulting curve + status.vf_points[i].freq_khz.cast_signed() + - unsafe { control.vf_points[i].data.prog.freq_offset_khz } + } else { + status.vf_points[i].vf_tuple_base.freq_khz.cast_signed() + } +} + +fn build_vf_curve( + info: &ClockClientClkVfPointsInfoV1, + status: &ClockClientClkVfPointsStatusV3, + control: &ClockClientClkVfPointsControlV1, +) -> Vec { + let point_count = point_count_from_mask(info.vf_points_mask); + let mut curve = Vec::with_capacity(point_count); + + for i in 0..point_count { + // Only report configurable and voltage-based points + if !vf_curve_point_is_editable(info.vf_points[i]) { + continue; + } + + let point = status.vf_points[i]; + let offset_khz = unsafe { control.vf_points[i].data.prog.freq_offset_khz }; + + let base_freq_khz = vf_curve_point_base_freq_khz(status, control, i); + let base_voltage_uv = if status.b_vf_tuple_base_supported == 0 { + point.voltage_uv + } else { + point.vf_tuple_base.voltage_uv + }; + + curve.push(NvidiaVfPoint { + index: u8::try_from(i).expect("max 255 points"), + freq: point.freq_khz / 1000, + voltage: point.voltage_uv / 1000, + base_freq: base_freq_khz.max(0).cast_unsigned() / 1000, + base_voltage: base_voltage_uv / 1000, + freq_offset: offset_khz / 1000, + }); + } + + curve +} + +fn build_curve_control( + curve: &IndexMap, + info: &ClockClientClkVfPointsInfoV1, + status: &ClockClientClkVfPointsStatusV3, + mut control: ClockClientClkVfPointsControlV1, + (min_offset_mhz, max_offset_mhz): (i32, i32), +) -> anyhow::Result { + let point_count = point_count_from_mask(info.vf_points_mask); + + for (index, configured_point) in curve { + let i = usize::from(*index); + + if i >= point_count || !vf_curve_point_is_editable(info.vf_points[i]) { + bail!("Point {i} is not configurable on this device"); + } + + if let Some(configured_mv) = configured_point.voltage { + let current_mv = status.vf_points[i].voltage_uv / 1000; + ensure!( + configured_mv == current_mv, + "Voltage is immutable - point {i} is at {current_mv}mV but was configured as {configured_mv}mV" + ); + } + + let offset_mhz = configured_point.clockspeed_offset; + let base_freq_mhz = vf_curve_point_base_freq_khz(status, &control, i) / 1000; + + let point_min_offset_mhz = cmp::min(min_offset_mhz, -base_freq_mhz); + ensure!( + (point_min_offset_mhz..=max_offset_mhz).contains(&offset_mhz), + "Configured offset {offset_mhz}MHz for point {i} is outside of the allowed range {point_min_offset_mhz}..={max_offset_mhz}" + ); + let offset_mhz = offset_mhz.max(1 - base_freq_mhz); + + trace!("writing offset {offset_mhz}MHz to point {i}"); + + control.vf_points[i].data.prog.freq_offset_khz = offset_mhz * 1000; + } + + Ok(control) +} + fn point_count_from_mask(mask: [u32; 8]) -> usize { let count: usize = mask.iter().map(|i| i.count_ones() as usize).sum(); assert!(u8::try_from(count).is_ok()); @@ -1265,8 +1274,8 @@ impl GpuController for NvidiaGpuController { .insert(pstate, *offset); } - if !clocks.gpu_vf_curve.is_empty() { - self.apply_vf_curve(&clocks.gpu_vf_curve) + if !clocks.nvidia_gpu_vf_curve.is_empty() { + self.apply_vf_curve(&clocks.nvidia_gpu_vf_curve) .context("Could not apply VF curve")?; } @@ -1550,3 +1559,228 @@ impl GpuController for NvidiaGpuController { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::{ + CLOCK_CLIENT_CLK_VF_POINT_TYPE_PROG, ClockClientClkVfPointInfoV1, + ClockClientClkVfPointsControlV1, ClockClientClkVfPointsInfoV1, + ClockClientClkVfPointsStatusV3, build_curve_control, build_vf_curve, + }; + use indexmap::IndexMap; + use lact_schema::config::NvidiaCurvePoint; + + const OFFSET_RANGE: (i32, i32) = (-1000, 1000); + + #[derive(Clone, Copy)] + struct TestPoint { + voltage_mv: u32, + base_freq_mhz: u32, + offset_mhz: i32, + /// Difference between the materialized frequency and `base + offset` + correction_mhz: i32, + } + + impl TestPoint { + fn new(voltage_mv: u32, base_freq_mhz: u32, offset_mhz: i32) -> Self { + Self { + voltage_mv, + base_freq_mhz, + offset_mhz, + correction_mhz: 0, + } + } + + fn with_correction(mut self, correction_mhz: i32) -> Self { + self.correction_mhz = correction_mhz; + self + } + } + + fn tables( + points: &[TestPoint], + base_tuple_supported: bool, + ) -> ( + ClockClientClkVfPointsInfoV1, + ClockClientClkVfPointsStatusV3, + ClockClientClkVfPointsControlV1, + ) { + let mut info = ClockClientClkVfPointsInfoV1::default(); + let mut status = ClockClientClkVfPointsStatusV3::default(); + let mut control = ClockClientClkVfPointsControlV1::default(); + + status.b_vf_tuple_base_supported = u8::from(base_tuple_supported); + + for (i, point) in points.iter().enumerate() { + info.vf_points_mask[i / 32] |= 1 << (i % 32); + + info.vf_points[i] = ClockClientClkVfPointInfoV1 { + type_: CLOCK_CLIENT_CLK_VF_POINT_TYPE_PROG, + b_voltage_based: 1, + rsvd: [0; 16], + }; + + let materialized_mhz = + point.base_freq_mhz.cast_signed() + point.offset_mhz + point.correction_mhz; + + status.vf_points[i].voltage_uv = point.voltage_mv * 1000; + status.vf_points[i].freq_khz = (materialized_mhz * 1000).cast_unsigned(); + status.vf_points[i].vf_tuple_base.voltage_uv = point.voltage_mv * 1000; + status.vf_points[i].vf_tuple_base.freq_khz = point.base_freq_mhz * 1000; + + control.vf_points[i].data.prog.freq_offset_khz = point.offset_mhz * 1000; + } + + (info, status, control) + } + + fn written_offsets(control: &ClockClientClkVfPointsControlV1, count: usize) -> Vec { + (0..count) + .map(|i| unsafe { control.vf_points[i].data.prog.freq_offset_khz }) + .collect() + } + + fn offset_curve(points: &[(u8, i32)]) -> IndexMap { + points + .iter() + .map(|(index, offset)| { + ( + *index, + NvidiaCurvePoint { + clockspeed_offset: *offset, + voltage: None, + }, + ) + }) + .collect() + } + + #[test] + fn reports_offset_and_base_from_the_tables() { + let points = [ + TestPoint::new(450, 270, 90), + TestPoint::new(1195, 2880, -60).with_correction(15), + ]; + let (info, status, control) = tables(&points, true); + + let curve = build_vf_curve(&info, &status, &control); + + assert_eq!(2, curve.len()); + + assert_eq!(0, curve[0].index); + assert_eq!(450, curve[0].voltage); + assert_eq!(270, curve[0].base_freq); + assert_eq!(90, curve[0].freq_offset); + assert_eq!(360, curve[0].freq); + + assert_eq!(2880, curve[1].base_freq); + assert_eq!(-60, curve[1].freq_offset); + assert_eq!(2835, curve[1].freq); + } + + #[test] + fn derives_base_from_the_offset_without_the_base_tuple() { + let points = [TestPoint::new(450, 270, 90), TestPoint::new(600, 1000, -45)]; + let (info, status, control) = tables(&points, false); + + let curve = build_vf_curve(&info, &status, &control); + + assert_eq!(270, curve[0].base_freq); + assert_eq!(90, curve[0].freq_offset); + assert_eq!(1000, curve[1].base_freq); + assert_eq!(-45, curve[1].freq_offset); + } + + #[test] + fn skips_non_editable_points() { + let points = [TestPoint::new(450, 270, 90), TestPoint::new(600, 1000, 0)]; + let (mut info, status, control) = tables(&points, true); + info.vf_points[0].b_voltage_based = 0; + + let curve = build_vf_curve(&info, &status, &control); + + assert_eq!(1, curve.len()); + assert_eq!(1, curve[0].index); + } + + #[test] + fn keeps_the_offsets_of_unconfigured_points() { + let points = [TestPoint::new(450, 270, 90), TestPoint::new(600, 1000, 45)]; + let (info, status, control) = tables(&points, true); + + let result = build_curve_control( + &offset_curve(&[(0, -15)]), + &info, + &status, + control, + OFFSET_RANGE, + ) + .expect("apply failed"); + + assert_eq!(vec![-15_000, 45_000], written_offsets(&result, 2)); + } + + #[test] + fn rejects_an_offset_outside_of_the_range() { + let points = [TestPoint::new(450, 270, 0)]; + let (info, status, control) = tables(&points, true); + + let err = build_curve_control( + &offset_curve(&[(0, 1500)]), + &info, + &status, + control, + OFFSET_RANGE, + ) + .map(|_| ()) + .expect_err("out of range offset was accepted"); + assert!( + err.to_string().contains("outside of the allowed range"), + "{err}" + ); + } + + #[test] + fn clamps_an_offset_that_would_reduce_the_frequency_to_zero() { + let points = [TestPoint::new(450, 270, 0)]; + let (info, status, control) = tables(&points, true); + + // Below the reported minimum and equal to `-base`, so clamp to a 1 MHz frequency + let result = build_curve_control( + &offset_curve(&[(0, -270)]), + &info, + &status, + control, + (-100, 1000), + ) + .expect("apply failed"); + assert_eq!(vec![-269_000], written_offsets(&result, 1)); + } + + #[test] + fn rejects_points_that_do_not_exist_or_do_not_match() { + let points = [TestPoint::new(450, 270, 0)]; + + let invalid_curves = [ + IndexMap::from([( + 0, + NvidiaCurvePoint { + clockspeed_offset: 90, + // Stale voltage, the point layout changed under the configuration + voltage: Some(500), + }, + )]), + offset_curve(&[(7, 45)]), + // The point arrays hold 255 entries, so this index cannot be used to index them + offset_curve(&[(u8::MAX, 30)]), + ]; + + for curve in invalid_curves { + let (info, status, control) = tables(&points, true); + + build_curve_control(&curve, &info, &status, control, OFFSET_RANGE) + .map(|_| ()) + .expect_err("an invalid point was accepted"); + } + } +} diff --git a/lact-daemon/src/snapshots/lact_daemon__config__tests__parse_doc.snap b/lact-daemon/src/snapshots/lact_daemon__config__tests__parse_doc.snap index d259be16..852f0c3d 100644 --- a/lact-daemon/src/snapshots/lact_daemon__config__tests__parse_doc.snap +++ b/lact-daemon/src/snapshots/lact_daemon__config__tests__parse_doc.snap @@ -60,6 +60,10 @@ gpus: mem_vf_curve: 3: clockspeed: 920 + nvidia_gpu_vf_curve: + 22: + clockspeed_offset: 90 + voltage: 750 voltage_offset: 0 voltage_boost: 0 power_profile_mode_index: 0 diff --git a/lact-gui/i18n/en/lact_gui.ftl b/lact-gui/i18n/en/lact_gui.ftl index 5425ab92..d16e7499 100644 --- a/lact-gui/i18n/en/lact_gui.ftl +++ b/lact-gui/i18n/en/lact_gui.ftl @@ -183,14 +183,13 @@ oc-warning = Changing these values may lead to system instability and can potent 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 pstate-list-description = The following values are clock offsets for each P-State, going from highest to lowest. no-clocks-data = No clocks data available reset-oc-tooltip = Warning: this resets all clock settings to defaults! vf-curve-editor = VF Curve Editor nvidia-vf-curve-warning = The voltage-frequency curve editor relies on undocumented driver functionality. - There are no guarantees regarding its behaviour, safety or availability. Use at your own risk. -vf-curve-enable-editing = Enable Editing voltage = Voltage frequency = Frequency vf-active-curve = Active Curve @@ -199,6 +198,7 @@ vf-curve-visible-range = Visible Range (%): vf-curve-visible-range-to = to 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) diff --git a/lact-gui/src/app.rs b/lact-gui/src/app.rs index 9dded515..b27327ce 100644 --- a/lact-gui/src/app.rs +++ b/lact-gui/src/app.rs @@ -1141,8 +1141,12 @@ impl AppModel { None } }; - self.oc_page - .emit(OcPageMsg::ClocksTable(maybe_clocks_table)); + self.oc_page.emit(OcPageMsg::ClocksTable { + table: maybe_clocks_table, + vf_curve_is_configured: gpu_config + .as_ref() + .is_some_and(|config| !config.clocks_configuration.nvidia_gpu_vf_curve.is_empty()), + }); let maybe_modes_table = match self .daemon_client diff --git a/lact-gui/src/app/pages/oc_page.rs b/lact-gui/src/app/pages/oc_page.rs index f6741ae1..47229bde 100644 --- a/lact-gui/src/app/pages/oc_page.rs +++ b/lact-gui/src/app/pages/oc_page.rs @@ -13,7 +13,7 @@ use crate::app::{msg::AppMsg, utils::ext::RelmDefaultLauchable}; use amdgpu_sysfs::gpu_handle::{ PerformanceLevel, PowerLevelKind, power_profile_mode::PowerProfileModesTable, }; -use clocks_frame::{ClocksFrame, ClocksFrameMsg}; +use clocks_frame::{ClocksFrame, ClocksFrameInit, ClocksFrameMsg}; use gtk::prelude::{BoxExt, OrientableExt, WidgetExt}; use indexmap::IndexMap; use lact_schema::config; @@ -27,7 +27,7 @@ use relm4::{ComponentController, ComponentParts, ComponentSender, RelmWidgetExt} use std::collections::HashSet; use std::sync::Arc; use tracing::debug; -use vf_curve::{VfCurveEditor, VfCurveEditorMsg}; +use vf_curve::{VfCurveEditor, VfCurveEditorInit, VfCurveEditorMsg}; pub struct OcPage { stats_section: relm4::Controller, @@ -47,7 +47,10 @@ pub enum OcPageMsg { update: PageUpdate, initial: bool, }, - ClocksTable(Option), + ClocksTable { + table: Option, + vf_curve_is_configured: bool, + }, ProfileModesTable(Option), PowerStates { pstates: PowerStates, @@ -55,6 +58,7 @@ pub enum OcPageMsg { }, PerformanceLevelChanged, ShowVfCurveEditor, + VfCurveEditingToggled(bool), } #[relm4::component(pub)] @@ -101,12 +105,19 @@ impl relm4::Component for OcPage { ]), }); let power_cap_section = PowerCapSection::detach_default(); - let clocks_frame = ClocksFrame::launch_default().forward(sender.input_sender(), |msg| msg); + let vf_curve_editing = BoolBinding::new(false); + let clocks_frame = ClocksFrame::launch(ClocksFrameInit { + vf_curve_editing: vf_curve_editing.clone(), + }) + .forward(sender.input_sender(), |msg| msg); let power_states_frame = PowerStatesFrame::detach_default(); let performance_frame = PerformanceFrame::launch_default().forward(sender.input_sender(), |msg| msg); - let vf_curve_editor = VfCurveEditor::detach(settings_changed); + let vf_curve_editor = VfCurveEditor::detach(VfCurveEditorInit { + global_settings_changed: settings_changed, + allow_editing: vf_curve_editing, + }); let model = Self { stats_section, @@ -176,11 +187,16 @@ impl relm4::Component for OcPage { .emit(ClocksFrameMsg::VramRatio(vram_clock_ratio)); } }, - OcPageMsg::ClocksTable(table) => { + OcPageMsg::ClocksTable { + table, + vf_curve_is_configured, + } => { let table = table.map(Arc::new); - self.clocks_frame - .emit(ClocksFrameMsg::Clocks(table.clone())); + self.clocks_frame.emit(ClocksFrameMsg::Clocks { + table: table.clone(), + vf_curve_is_configured, + }); self.vf_curve_editor .emit(VfCurveEditorMsg::Clocks(table.clone())); } @@ -217,6 +233,13 @@ impl relm4::Component for OcPage { OcPageMsg::ShowVfCurveEditor => { self.vf_curve_editor.emit(VfCurveEditorMsg::Show); } + OcPageMsg::VfCurveEditingToggled(enabled) => { + if enabled { + self.clocks_frame.emit(ClocksFrameMsg::ResetGpuClockOffsets); + } else { + self.vf_curve_editor.emit(VfCurveEditorMsg::ResetCurve); + } + } } self.update_view(widgets, sender); @@ -256,7 +279,7 @@ impl OcPage { } if !self.vf_curve_editor.model().is_empty() { - config.gpu_vf_curve = self.vf_curve_editor.model().get_configured_curve(); + config.nvidia_gpu_vf_curve = self.vf_curve_editor.model().get_configured_curve(); } } diff --git a/lact-gui/src/app/pages/oc_page/clocks_frame/adjustment_group.rs b/lact-gui/src/app/pages/oc_page/clocks_frame/adjustment_group.rs index e04d01f2..48b6e2c9 100644 --- a/lact-gui/src/app/pages/oc_page/clocks_frame/adjustment_group.rs +++ b/lact-gui/src/app/pages/oc_page/clocks_frame/adjustment_group.rs @@ -99,7 +99,10 @@ impl AdjustmentGroup { show_nvidia_options: bool, enable_gpu_locked: bool, enable_vram_locked: bool, + vf_curve_editing: bool, ) { + let mut any_visible = false; + for (key, row) in self.adjustments.iter() { let show_current = match key { ClockspeedType::MaxCoreClock | ClockspeedType::MinCoreClock @@ -112,12 +115,20 @@ impl AdjustmentGroup { { enable_vram_locked } + ClockspeedType::GpuClockOffset(_) if show_nvidia_options && vf_curve_editing => { + false + } _ => !row.is_secondary || show_secondary, }; + any_visible |= show_current; + self.adjustments .send(key, ClockAdjustmentRowMsg::SetVisible(show_current)); } + + // removes empty card + self.adjustments.widget().set_visible(any_visible); } pub fn get_commands(&self) -> Vec<(ClockspeedType, Option)> { @@ -127,6 +138,15 @@ impl AdjustmentGroup { .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, ClockAdjustmentRowMsg::SetValue(0)); + } + } + } + pub fn get_raw_value(&self, clock_type: ClockspeedType) -> i32 { self.adjustments .get(&clock_type) diff --git a/lact-gui/src/app/pages/oc_page/clocks_frame/adjustment_row.rs b/lact-gui/src/app/pages/oc_page/clocks_frame/adjustment_row.rs index 66f0d59c..a71b7895 100644 --- a/lact-gui/src/app/pages/oc_page/clocks_frame/adjustment_row.rs +++ b/lact-gui/src/app/pages/oc_page/clocks_frame/adjustment_row.rs @@ -58,6 +58,7 @@ impl ClocksData { #[derive(Debug)] pub enum ClockAdjustmentRowMsg { ValueRatio(f64), + SetValue(i32), SetVisible(bool), AddSizeGroup { label_group: gtk::SizeGroup, @@ -218,6 +219,10 @@ impl FactoryComponent for ClockAdjustmentRow { label_group.add_widget(&widgets.title_box); input_group.add_widget(&widgets.input_button); } + ClockAdjustmentRowMsg::SetValue(value) => { + self.adjustment + .set_value(f64::from(value) * self.value_ratio); + } ClockAdjustmentRowMsg::SetVisible(visible) => { widgets.root_box.set_visible(visible); } diff --git a/lact-gui/src/app/pages/oc_page/clocks_frame/mod.rs b/lact-gui/src/app/pages/oc_page/clocks_frame/mod.rs index 828de531..90b449a0 100644 --- a/lact-gui/src/app/pages/oc_page/clocks_frame/mod.rs +++ b/lact-gui/src/app/pages/oc_page/clocks_frame/mod.rs @@ -33,20 +33,29 @@ pub struct ClocksFrame { 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, } +pub struct ClocksFrameInit { + pub vf_curve_editing: BoolBinding, +} + #[derive(Debug)] pub enum ClocksFrameMsg { - Clocks(Option>), + Clocks { + table: Option>, + vf_curve_is_configured: bool, + }, VramRatio(f64), TogglePStatesVisibility, + ResetGpuClockOffsets, } #[relm4::component(pub)] impl relm4::Component for ClocksFrame { - type Init = (); + type Init = ClocksFrameInit; type Input = ClocksFrameMsg; type Output = OcPageMsg; type CommandOutput = (); @@ -79,7 +88,6 @@ impl relm4::Component for ClocksFrame { append = >k::Button { set_label: &fl!(I18N, "vf-curve-editor"), - add_css_class: css::WARNING, #[watch] set_visible: model.show_nvidia_options && model.vf_curve_available, @@ -139,6 +147,18 @@ impl relm4::Component for ClocksFrame { } @ gpu_locked_clock_signal, }, + append: vf_curve_editing_togglebutton = >k::CheckButton { + #[watch] + set_visible: model.show_nvidia_options && model.vf_curve_available, + set_label: Some(&fl!(I18N, "enable-vf-curve")), + add_css_class: css::WARNING, + add_binding["active"]: &model.vf_curve_editing, + 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 = >k::CheckButton { #[watch] set_visible: model.show_nvidia_options, @@ -214,7 +234,7 @@ impl relm4::Component for ClocksFrame { } fn init( - _init: Self::Init, + ClocksFrameInit { vf_curve_editing }: Self::Init, root: Self::Root, sender: ComponentSender, ) -> ComponentParts { @@ -225,12 +245,14 @@ impl relm4::Component for ClocksFrame { show_nvidia_options: false, vf_curve_available: false, show_all_pstates: BoolBinding::new(false), + vf_curve_editing, enable_gpu_locked_clocks: BoolBinding::new(false), enable_vram_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, ] { @@ -256,7 +278,13 @@ impl relm4::Component for ClocksFrame { _root: &Self::Root, ) { match msg { - ClocksFrameMsg::Clocks(clocks_table) => { + ClocksFrameMsg::Clocks { + table: clocks_table, + vf_curve_is_configured, + } => { + widgets + .vf_curve_editing_togglebutton + .block_signal(&widgets.vf_curve_editing_signal); widgets .gpu_locked_clocks_togglebutton .block_signal(&widgets.gpu_locked_clock_signal); @@ -269,6 +297,7 @@ impl relm4::Component for ClocksFrame { self.enable_gpu_locked_clocks.set_value(false); self.enable_vram_locked_clocks.set_value(false); + self.vf_curve_editing.set_value(vf_curve_is_configured); self.show_nvidia_options = false; if let Some(table) = clocks_table { @@ -286,6 +315,9 @@ impl relm4::Component for ClocksFrame { group.add_size_group(label_size_group.clone(), 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); @@ -296,6 +328,11 @@ impl relm4::Component for ClocksFrame { self.update_vram_clock_ratio(); sender.input(ClocksFrameMsg::TogglePStatesVisibility); } + ClocksFrameMsg::ResetGpuClockOffsets => { + for group in self.core_groups.values() { + group.reset_gpu_clock_offsets(); + } + } ClocksFrameMsg::VramRatio(vram_ratio) => { self.vram_clock_ratio = vram_ratio; self.update_vram_clock_ratio(); @@ -307,6 +344,7 @@ impl relm4::Component for ClocksFrame { self.show_nvidia_options, self.enable_gpu_locked_clocks.value(), self.enable_vram_locked_clocks.value(), + self.vf_curve_editing.value(), ); } } diff --git a/lact-gui/src/app/pages/oc_page/vf_curve.rs b/lact-gui/src/app/pages/oc_page/vf_curve.rs index be3db380..68c25e15 100644 --- a/lact-gui/src/app/pages/oc_page/vf_curve.rs +++ b/lact-gui/src/app/pages/oc_page/vf_curve.rs @@ -3,15 +3,7 @@ use crate::{ app::{APP_BROKER, graphs_window::plot::PlotColorScheme, msg::AppMsg}, }; use amdgpu_sysfs::gpu_handle::PowerLevelId; -use gtk::{ - gdk, - glib::object::ObjectExt, - prelude::{ - AdjustmentExt, BoxExt as _, ButtonExt as _, CheckButtonExt as _, DrawingAreaExtManual as _, - EventControllerExt as _, GestureSingleExt as _, GtkWindowExt as _, OrientableExt as _, - PopoverExt, RangeExt as _, ScaleExt as _, WidgetExt as _, - }, -}; +use gtk::{gdk, prelude::*}; use i18n_embed_fl::fl; use indexmap::IndexMap; use lact_schema::{ClocksTable, DeviceStats, NvidiaVfPoint, config}; @@ -33,10 +25,14 @@ use std::{cmp, fmt::Write as _}; // In percentage const POINT_VOLTAGE_HOVER_MARGIN: f32 = 0.01; const POINT_FREQ_HOVER_MARGIN: f32 = 0.03; -const MIN_VISIBLE_FREQ_RANGE_PADDING: u32 = 200; + +// In MHz +const VISIBLE_FREQ_RANGE_PADDING_MIN: u32 = 100; +const VISIBLE_FREQ_RANGE_PADDING_MAX: u32 = 300; #[derive(Clone)] pub struct VfCurveEditor { + /// `freq` is drawn and dragged, `freq_offset` is what gets exported - see [`set_point_freq`] points: Rc>>, stats: Rc>>, allow_editing: BoolBinding, @@ -61,6 +57,11 @@ pub struct VfCurveEditor { selected_range_end: Rc>>, } +pub struct VfCurveEditorInit { + pub global_settings_changed: BoolBinding, + pub allow_editing: BoolBinding, +} + #[derive(Debug)] pub enum VfCurveEditorMsg { Show, @@ -80,7 +81,7 @@ pub enum VfCurveEditorMsg { #[relm4::component(pub)] impl relm4::Component for VfCurveEditor { - type Init = BoolBinding; + type Init = VfCurveEditorInit; type Input = VfCurveEditorMsg; type Output = (); type CommandOutput = (); @@ -98,7 +99,7 @@ impl relm4::Component for VfCurveEditor { #[wrap(Some)] set_content = >k::Box { set_orientation: gtk::Orientation::Vertical, - set_margin_all: 5, + set_margin_all: 15, set_spacing: 10, gtk::Box { @@ -108,8 +109,8 @@ impl relm4::Component for VfCurveEditor { gtk::Label { set_markup: &fl!(I18N, "nvidia-vf-curve-warning"), - add_css_class: "error", - add_css_class: "heading", + add_css_class: css::WARNING, + add_css_class: css::HEADING, }, }, @@ -118,7 +119,6 @@ impl relm4::Component for VfCurveEditor { #[name = "drawing_area"] gtk::DrawingArea { set_expand: true, - set_margin_all: 10, set_draw_func[model] => move |_, ctx, width, height| { model.draw_chart(ctx, width, height, PlotColorScheme::current()); @@ -170,11 +170,10 @@ impl relm4::Component for VfCurveEditor { gtk::Box { set_orientation: gtk::Orientation::Horizontal, set_spacing: 5, - set_margin_horizontal: 5, gtk::Label { set_label: &fl!(I18N, "vf-curve-visible-range"), - add_css_class: "heading", + add_css_class: css::HEADING, }, gtk::Scale { @@ -188,7 +187,7 @@ impl relm4::Component for VfCurveEditor { gtk::Label { set_label: &fl!(I18N, "vf-curve-visible-range-to"), - add_css_class: "heading", + add_css_class: css::HEADING, }, gtk::Scale { @@ -199,46 +198,45 @@ impl relm4::Component for VfCurveEditor { set_digits: 0, }, - #[name = "enable_editing_button"] - gtk::CheckButton { - set_label: Some(&fl!(I18N, "vf-curve-enable-editing")), - set_halign: gtk::Align::End, - set_valign: gtk::Align::Center, + #[name = "editing_disabled_label"] + gtk::Label { + set_label: &fl!(I18N, "vf-curve-editing-disabled"), + set_halign: gtk::Align::Center, set_hexpand: true, - add_css_class: "warning", - add_binding: (&model.allow_editing, "active"), - - connect_toggled[drawing_area] => move |_| { - APP_BROKER.send(AppMsg::SettingsChanged); - drawing_area.queue_draw(); - } @ enable_editing_signal, + set_valign: gtk::Align::Center, + add_css_class: css::DIM_LABEL, }, - gtk::Button { - set_label: &fl!(I18N, "default-button"), + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 5, set_halign: gtk::Align::End, - add_css_class: css::DESTRUCTIVE_ACTION, - set_valign: gtk::Align::Center, + set_hexpand: true, + add_write_only_binding: (&model.allow_editing, "visible"), - #[watch] - set_sensitive: curve_is_configured(&model.points.borrow()), + gtk::Button { + set_label: &fl!(I18N, "default-button"), + add_css_class: css::DESTRUCTIVE_ACTION, + set_valign: gtk::Align::Center, - connect_clicked => VfCurveEditorMsg::ResetCurve, - connect_clicked => move |_| { - APP_BROKER.send(AppMsg::SettingsChanged); - } - }, + #[watch] + set_sensitive: curve_has_offsets(&model.points.borrow()), + connect_clicked => VfCurveEditorMsg::ResetCurve, + connect_clicked => move |_| { + APP_BROKER.send(AppMsg::SettingsChanged); + } + }, - gtk::Button { - set_label: &fl!(I18N, "apply-button"), - set_halign: gtk::Align::End, - add_binding: (&model.global_settings_changed, "sensitive"), - add_css_class: css::SUGGESTED_ACTION, - set_valign: gtk::Align::Center, + gtk::Button { + set_label: &fl!(I18N, "apply-button"), + add_binding: (&model.global_settings_changed, "sensitive"), + add_css_class: css::SUGGESTED_ACTION, + set_valign: gtk::Align::Center, - connect_clicked => move |_| { - APP_BROKER.send(AppMsg::ApplyChanges); + connect_clicked => move |_| { + APP_BROKER.send(AppMsg::ApplyChanges); + }, }, }, }, @@ -248,14 +246,14 @@ impl relm4::Component for VfCurveEditor { #[name = "point_menu"] gtk::Popover { - add_css_class: "menu", + add_css_class: css::MENU, connect_closed => |popover| { popover.unparent(); }, gtk::Box { - add_css_class: "flat", + add_css_class: css::FLAT, set_orientation: gtk::Orientation::Vertical, gtk::Button { @@ -264,7 +262,7 @@ impl relm4::Component for VfCurveEditor { connect_clicked[point_menu] => move |_| { point_menu.popdown(); }, - add_css_class: "flat", + add_css_class: css::FLAT, #[watch] set_visible: model.selected_range_start.get().is_some() && model.selected_range_end.get().is_some(), }, @@ -275,14 +273,17 @@ impl relm4::Component for VfCurveEditor { connect_clicked[point_menu] => move |_| { point_menu.popdown(); }, - add_css_class: "flat", + add_css_class: css::FLAT, }, }, } } fn init( - global_settings_changed: Self::Init, + VfCurveEditorInit { + global_settings_changed, + allow_editing, + }: Self::Init, root: Self::Root, sender: relm4::ComponentSender, ) -> relm4::ComponentParts { @@ -292,7 +293,7 @@ impl relm4::Component for VfCurveEditor { global_settings_changed, locked_clocks_range: Rc::default(), freq_range: Rc::default(), - allow_editing: BoolBinding::new(false), + allow_editing, cursor_position: Rc::new(Cell::new(None)), visible_range_start: gtk::Adjustment::new(30.0, 0.0, 100.0, 1.0, 10.0, 0.0), visible_range_end: gtk::Adjustment::new(100.0, 0.0, 100.0, 1.0, 10.0, 0.0), @@ -307,6 +308,18 @@ impl relm4::Component for VfCurveEditor { let widgets = view_output!(); + let drawing_area = widgets.drawing_area.clone(); + model + .allow_editing + .connect_value_notify(move |_| drawing_area.queue_draw()); + + model + .allow_editing + .bind_property("value", &widgets.editing_disabled_label, "visible") + .invert_boolean() + .sync_create() + .build(); + ComponentParts { model, widgets } } @@ -338,14 +351,6 @@ impl relm4::Component for VfCurveEditor { .set(Self::freq_limits_range(&points, offset_range)); } - widgets - .enable_editing_button - .block_signal(&widgets.enable_editing_signal); - self.allow_editing.set_value(curve_is_configured(&points)); - widgets - .enable_editing_button - .unblock_signal(&widgets.enable_editing_signal); - if points.is_empty() { root.set_visible(false); } @@ -403,7 +408,7 @@ impl relm4::Component for VfCurveEditor { let target_freq = points[base_point_idx].freq; for point in points.iter_mut().skip(base_point_idx) { - point.freq = target_freq; + set_point_freq(point, target_freq); } APP_BROKER.send(AppMsg::SettingsChanged); @@ -425,7 +430,7 @@ impl relm4::Component for VfCurveEditor { if (selected_volt_start..=selected_volt_end) .contains(&(point.voltage as usize)) { - point.freq = target_freq; + set_point_freq(point, target_freq); } } @@ -436,6 +441,7 @@ impl relm4::Component for VfCurveEditor { let mut points = self.points.borrow_mut(); for point in points.iter_mut() { point.freq = point.base_freq; + point.freq_offset = 0; } } } @@ -519,7 +525,7 @@ impl VfCurveEditor { Rectangle::new([(x - 15, y + 2), (x, y - 1)], colors.success.filled()) }); - if curve_is_configured(points) { + if curve_has_offsets(points) { let base_line_style = colors.success.mix(0.3); chart .draw_series(LineSeries::new( @@ -770,21 +776,23 @@ impl VfCurveEditor { .contains(gdk::ModifierType::SHIFT_MASK) { for point in points.iter_mut() { - point.freq = (point.freq as i32 + drag_delta) + let freq = (point.freq as i32 + drag_delta) .clamp(freq_range.0 as i32, freq_range.1 as i32) as u32; + set_point_freq(point, freq); } } else if let Some((selected_start, selected_end)) = self.get_selected_voltage_range() { for point in points.iter_mut() { let voltage = point.voltage as usize; if selected_start < voltage && voltage < selected_end { - point.freq = (point.freq as i32 + drag_delta) + let freq = (point.freq as i32 + drag_delta) .clamp(freq_range.0 as i32, freq_range.1 as i32) as u32; + set_point_freq(point, freq); } } } else { - points[point_idx].freq = new_freq; + set_point_freq(&mut points[point_idx], new_freq); } } @@ -855,10 +863,10 @@ impl VfCurveEditor { .max()?; let y_start = min_freq - .saturating_sub(MIN_VISIBLE_FREQ_RANGE_PADDING) + .saturating_sub(VISIBLE_FREQ_RANGE_PADDING_MIN) .max(freq_range.0); let mut y_end = max_freq - .saturating_add(MIN_VISIBLE_FREQ_RANGE_PADDING) + .saturating_add(VISIBLE_FREQ_RANGE_PADDING_MAX) .min(freq_range.1); if y_start >= y_end { y_end = y_start.saturating_add(1).min(freq_range.1); @@ -871,18 +879,19 @@ impl VfCurveEditor { self.points.borrow().is_empty() } - pub fn get_configured_curve(&self) -> IndexMap { + pub fn get_configured_curve(&self) -> IndexMap { + let points = self.points.borrow(); + if !self.allow_editing.value() { return IndexMap::new(); } - self.points - .borrow() + points .iter() .map(|point| { - let vf_point = config::CurvePoint { - voltage: Some(point.voltage as i32), - clockspeed: Some(point.freq as i32), + let vf_point = config::NvidiaCurvePoint { + clockspeed_offset: point.freq_offset, + voltage: Some(point.voltage), }; (point.index, vf_point) }) @@ -898,6 +907,11 @@ fn vf_point_base_coords(point: &NvidiaVfPoint) -> (u32, u32) { (point.base_voltage, point.base_freq) } +fn set_point_freq(point: &mut NvidiaVfPoint, freq: u32) { + point.freq = freq; + point.freq_offset = freq as i32 - point.base_freq as i32; +} + fn offset_freq(base_freq: u32, offset: i32) -> u32 { if offset.is_negative() { base_freq.saturating_sub(offset.unsigned_abs()) @@ -906,6 +920,6 @@ fn offset_freq(base_freq: u32, offset: i32) -> u32 { } } -fn curve_is_configured(points: &[NvidiaVfPoint]) -> bool { - points.iter().any(|point| point.freq != point.base_freq) +fn curve_has_offsets(points: &[NvidiaVfPoint]) -> bool { + points.iter().any(|point| point.freq_offset != 0) } diff --git a/lact-schema/src/config.rs b/lact-schema/src/config.rs index 8538f50e..8c3859ae 100644 --- a/lact-schema/src/config.rs +++ b/lact-schema/src/config.rs @@ -88,6 +88,12 @@ pub struct ClocksConfiguration { deserialize_with = "int_map::deserialize" )] pub mem_vf_curve: IndexMap, + #[serde( + default, + skip_serializing_if = "IndexMap::is_empty", + deserialize_with = "int_map::deserialize" + )] + pub nvidia_gpu_vf_curve: IndexMap, pub voltage_offset: Option, pub voltage_boost: Option, } @@ -146,6 +152,13 @@ pub struct CurvePoint { pub clockspeed: Option, } +#[skip_serializing_none] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)] +pub struct NvidiaCurvePoint { + pub clockspeed_offset: i32, + pub voltage: Option, +} + mod int_map { use indexmap::IndexMap; use serde::{Deserialize, Deserializer, de::Error}; diff --git a/lact-schema/src/lib.rs b/lact-schema/src/lib.rs index ce90b660..ea677dac 100644 --- a/lact-schema/src/lib.rs +++ b/lact-schema/src/lib.rs @@ -524,6 +524,8 @@ pub struct NvidiaVfPoint { pub voltage: u32, pub base_freq: u32, pub base_voltage: u32, + #[serde(default)] + pub freq_offset: i32, } /// Doc from `xe_gt_freq.c`