mirror of
https://github.com/ilya-zlobintsev/LACT.git
synced 2026-11-03 12:50:31 -06:00
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:
1 parent
c9fa4879a2
commit
6f02740b2f
13 files changed
+707
-227
No files matched your search
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 = >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<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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = >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<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)
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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`
|
||||
|
||||
Reference in new issue
Block a user