fix: swtich nvidia V/F curve to offsets (#1161)

* wip

* clean up

* upd comment

* fix vfcurve flip to enabled

* fmt

* rename old curve_is_configured

* move enable edit checkbox from VFCurve to OC panel

* cleanup tests

* set offset 0 on curve reset

* add defauilt to freq_offset

* bail if vf_curve is invalid or not editable

* hide apply/default buttons on ro curve

* fix curve margins

* vfcurve - show info that editing is disabled

* move top vf curve ceiling a bit

* center vf-curve-editing label

* fix flipping edit vfcurve checkbox

* easen up warning

* add abs to offset migration

* fix point_min_offset_mhz calculation

* don't let offset to be same or bigger than basefreq

* fix test
This commit is contained in:
Roman Makarov authored and GitHub committed 2026-08-23 16:16:21 +03:00
1 parent c9fa4879a2
commit 6f02740b2f
13 files changed
+707 -227

No files matched your search

+4
View File
@@ -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:
+126 -7
View File
@@ -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<u8, CurvePoint>,
base_curve: &[NvidiaVfPoint],
) -> Option<IndexMap<u8, NvidiaCurvePoint>> {
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<str>]) -> Option<String> {
#[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<NvidiaVfPoint> {
[(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 {
+357 -123
View File
@@ -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<bool>,
voltage_boost_written: Cell<bool>,
// Used as the initial value on cards which do not report base VF points themselves (Turing)
base_vf_curve: RefCell<Option<Vec<NvidiaVfPoint>>>,
}
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<Vec<NvidiaVfPoint>> {
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<u8, CurvePoint>) -> anyhow::Result<()> {
fn apply_vf_curve(&self, curve: &IndexMap<u8, NvidiaCurvePoint>) -> 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<NvidiaVfPoint> {
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<u8, NvidiaCurvePoint>,
info: &ClockClientClkVfPointsInfoV1,
status: &ClockClientClkVfPointsStatusV3,
mut control: ClockClientClkVfPointsControlV1,
(min_offset_mhz, max_offset_mhz): (i32, i32),
) -> anyhow::Result<ClockClientClkVfPointsControlV1> {
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<i32> {
(0..count)
.map(|i| unsafe { control.vf_points[i].data.prog.freq_offset_khz })
.collect()
}
fn offset_curve(points: &[(u8, i32)]) -> IndexMap<u8, NvidiaCurvePoint> {
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");
}
}
}
@@ -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
+2 -2
View File
@@ -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 = <b>The following values are clock offsets for each P-State, going from highest to lowest.</b>
no-clocks-data = No clocks data available
reset-oc-tooltip = Warning: this resets all clock settings to defaults!
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.
<span weight = "heavy" underline = "single">Use at your own risk</span>.
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)
+6 -2
View File
@@ -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
+32 -9
View File
@@ -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<GpuStatsSection>,
@@ -47,7 +47,10 @@ pub enum OcPageMsg {
update: PageUpdate,
initial: bool,
},
ClocksTable(Option<ClocksTable>),
ClocksTable {
table: Option<ClocksTable>,
vf_curve_is_configured: bool,
},
ProfileModesTable(Option<PowerProfileModesTable>),
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();
}
}
@@ -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<i32>)> {
@@ -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)
@@ -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);
}
@@ -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<Arc<ClocksTable>>),
Clocks {
table: Option<Arc<ClocksTable>>,
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 = &gtk::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 = &gtk::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 = &gtk::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<Self>,
) -> ComponentParts<Self> {
@@ -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(),
);
}
}
+93 -79
View File
@@ -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<RefCell<Vec<NvidiaVfPoint>>>,
stats: Rc<RefCell<Arc<DeviceStats>>>,
allow_editing: BoolBinding,
@@ -61,6 +57,11 @@ pub struct VfCurveEditor {
selected_range_end: Rc<Cell<Option<usize>>>,
}
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 = &gtk::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<Self>,
) -> relm4::ComponentParts<Self> {
@@ -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<u8, config::CurvePoint> {
pub fn get_configured_curve(&self) -> IndexMap<u8, config::NvidiaCurvePoint> {
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)
}
+13
View File
@@ -88,6 +88,12 @@ pub struct ClocksConfiguration {
deserialize_with = "int_map::deserialize"
)]
pub mem_vf_curve: IndexMap<u8, CurvePoint>,
#[serde(
default,
skip_serializing_if = "IndexMap::is_empty",
deserialize_with = "int_map::deserialize"
)]
pub nvidia_gpu_vf_curve: IndexMap<u8, NvidiaCurvePoint>,
pub voltage_offset: Option<i32>,
pub voltage_boost: Option<i32>,
}
@@ -146,6 +152,13 @@ pub struct CurvePoint {
pub clockspeed: Option<i32>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct NvidiaCurvePoint {
pub clockspeed_offset: i32,
pub voltage: Option<u32>,
}
mod int_map {
use indexmap::IndexMap;
use serde::{Deserialize, Deserializer, de::Error};
+2
View File
@@ -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`