diff --git a/docs/CONFIG.md b/docs/CONFIG.md index 3c11c029..ede51268 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -188,6 +188,8 @@ gpus: max_voltage: 1200 # Voltage offset value in mV for RDNA and newer AMD GPUs. voltage_offset: 0 + # Nvidia core voltage boost, in percent (0-100) of available boost, not total voltage + voltage_boost: 0 # GPU V/F curve with voltage and frequency points per each power state. # Applicable only to AMD GCN and RDNA1 GPUs. Overrides the values of min/max clock/voltage fields. diff --git a/lact-daemon/src/server/gpu_controller/nvidia.rs b/lact-daemon/src/server/gpu_controller/nvidia.rs index d332c3b0..0b98ccfa 100644 --- a/lact-daemon/src/server/gpu_controller/nvidia.rs +++ b/lact-daemon/src/server/gpu_controller/nvidia.rs @@ -13,7 +13,7 @@ use amdgpu_sysfs::{ gpu_handle::{PowerLevelId, fan_control::FanInfo, power_profile_mode::PowerProfileModesTable}, hw_mon::Temperature, }; -use anyhow::{Context, anyhow, bail}; +use anyhow::{Context, anyhow, bail, ensure}; use driver::DriverHandle; use futures::{FutureExt, future::LocalBoxFuture}; use indexmap::IndexMap; @@ -21,8 +21,8 @@ use lact_schema::{ ActivePowerStates, CacheInfo, ClocksInfo, ClocksTable, ClockspeedStats, DeviceApiInfo, DeviceFlag, DeviceInfo, DeviceStats, DeviceType, DrmInfo, DrmMemoryInfo, FanControlMode, FanStats, IntelDrmInfo, LinkInfo, NvidiaClockOffset, NvidiaClocksTable, NvidiaThermalInfo, - NvidiaVfPoint, PmfwInfo, PowerState, PowerStates, PowerStats, ProcessInfo, ProcessList, - ProcessType, ProcessUtilizationType, TemperatureEntry, VoltageStats, VramStats, + NvidiaVfPoint, NvidiaVoltageBoost, PmfwInfo, PowerState, PowerStates, PowerStats, ProcessInfo, + ProcessList, ProcessType, ProcessUtilizationType, TemperatureEntry, VoltageStats, VramStats, config::{CurvePoint, FanControlSettings, FanCurve, GpuConfig}, }; use nvapi::NvApi; @@ -68,6 +68,7 @@ pub struct NvidiaGpuController { last_applied_vram_locked_clocks: RefCell>, // Check if reset is needed to avoid unnecessarily going to nvapi vf_curve_written: Cell, + voltage_boost_written: Cell, // Used as the initial value on cards which do not report base VF points themselves (Turing) base_vf_curve: RefCell>>, } @@ -144,6 +145,7 @@ impl NvidiaGpuController { last_applied_gpu_locked_clocks: RefCell::new(None), 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), }) } @@ -1087,6 +1089,17 @@ impl GpuController for NvidiaGpuController { .inspect_err(|err| warn!("could not get VF curve: {err:#}")) .unwrap_or_default(); + let voltage_boost = self.nvapi.as_ref().and_then(|(nvapi, handle)| { + unsafe { nvapi.get_voltage_boost(*handle) } + .inspect_err(|err| warn!("could not get voltage boost: {err:#}")) + .ok() + .map(|current| NvidiaVoltageBoost { + current: current.into(), + min: 0, + max: 100, + }) + }); + let table = NvidiaClocksTable { gpu_offsets, mem_offsets, @@ -1095,6 +1108,7 @@ impl GpuController for NvidiaGpuController { gpu_clock_range, vram_clock_range, gpu_vf_curve, + voltage_boost, }; Ok(ClocksInfo { @@ -1198,6 +1212,27 @@ impl GpuController for NvidiaGpuController { .context("Could not apply VF curve")?; } + if let Some(percent) = clocks.voltage_boost { + let (nvapi, handle) = self.nvapi.as_ref().context("NvAPI not available")?; + + // The driver field is a single byte, out of range values would be truncated instead of rejected + let percent = + u8::try_from(percent.clamp(0, 100)).expect("Clamped value fits into u8"); + debug!("applying voltage boost {percent}%"); + + unsafe { nvapi.set_voltage_boost(*handle, percent) } + .context("Could not apply voltage boost")?; + self.voltage_boost_written.set(true); + + // verify the boost was applied + let applied = unsafe { nvapi.get_voltage_boost(*handle) } + .context("Could not verify voltage boost")?; + ensure!( + applied == percent, + "Voltage boost was not applied: requested {percent}%, driver reports {applied}%" + ); + } + if config.fan_control_enabled { let settings = config .fan_control_settings @@ -1317,6 +1352,13 @@ impl GpuController for NvidiaGpuController { self.reset_vf_curve().context("Could not reset VF curve")?; } + if self.voltage_boost_written.get() { + let (nvapi, handle) = self.nvapi.as_ref().context("NvAPI not available")?; + unsafe { nvapi.set_voltage_boost(*handle, 0) } + .context("Could not reset voltage boost")?; + self.voltage_boost_written.set(false); + } + Ok(()) } diff --git a/lact-daemon/src/server/gpu_controller/nvidia/nvapi.rs b/lact-daemon/src/server/gpu_controller/nvidia/nvapi.rs index f4d2b787..c80a22a8 100644 --- a/lact-daemon/src/server/gpu_controller/nvidia/nvapi.rs +++ b/lact-daemon/src/server/gpu_controller/nvidia/nvapi.rs @@ -29,6 +29,8 @@ const QUERY_NVAPI_GET_ERROR_MESSAGE: u32 = 0x6c2d048c; // Undocumented calls const QUERY_NVAPI_THERMALS: u32 = 0x65fe3aad; const QUERY_NVAPI_VOLTAGE: u32 = 0x465f9bcf; +const QUERY_NVAPI_VOLTAGE_BOOST_GET: u32 = 0x9df23ca1; +const QUERY_NVAPI_VOLTAGE_BOOST_SET: u32 = 0xb9306d9b; const QUERY_NVAPI_GPU_CLOCK_CLIENT_CLK_VF_POINTS_GET_STATUS: u32 = 0x21537ad4; const QUERY_NVAPI_GPU_CLOCK_CLIENT_CLK_VF_POINTS_GET_INFO: u32 = 0x507b4b59; const QUERY_NVAPI_GPU_CLOCK_CLIENT_CLK_VF_POINTS_SET_CONTROL: u32 = 0x733e009; @@ -111,18 +113,34 @@ impl NvApi { } pub unsafe fn get_voltage(&self, handle: NvPhysicalGpuHandle) -> anyhow::Result { - let mut data = NvApiVoltage { - #[allow(clippy::cast_possible_truncation)] - version: make_version::(1), - flags: 0, - padding_1: [0; 8], - value_uv: 0, - padding_2: [0; 8], - }; + let mut data = NvApiVoltage::default(); self.physical_gpu_query(handle, &mut data, QUERY_NVAPI_VOLTAGE)?; - Ok(data.value_uv) + Ok(data.rails[0].current_voltage_uv) + } + + pub unsafe fn get_voltage_boost(&self, handle: NvPhysicalGpuHandle) -> anyhow::Result { + let mut data = NvApiVoltageBoost::default(); + + self.physical_gpu_query(handle, &mut data, QUERY_NVAPI_VOLTAGE_BOOST_GET)?; + + Ok(data.percent) + } + + pub unsafe fn set_voltage_boost( + &self, + handle: NvPhysicalGpuHandle, + percent: u8, + ) -> anyhow::Result<()> { + let mut data = NvApiVoltageBoost { + percent, + ..Default::default() + }; + + self.physical_gpu_query(handle, &mut data, QUERY_NVAPI_VOLTAGE_BOOST_SET)?; + + Ok(()) } pub unsafe fn clock_client_clk_vf_points_get_info( @@ -488,14 +506,52 @@ impl NvApiThermals { } } +// ensure the sizes used in the version fields stay correct +const _: () = assert!(mem::size_of::() == 76); +const _: () = assert!(mem::size_of::() == 40); + #[repr(C)] #[derive(Debug)] struct NvApiVoltage { - version: u32, - flags: u32, - padding_1: [u32; 8], - value_uv: u32, - padding_2: [u32; 8], + version: NvU32, + rsvd: [NvU8; 32], + rails: [NvApiVoltageRail; 1], +} + +impl Default for NvApiVoltage { + fn default() -> Self { + Self { + version: make_version::(1), + rsvd: [0; 32], + rails: [NvApiVoltageRail::default()], + } + } +} + +#[repr(C)] +#[derive(Debug, Default)] +struct NvApiVoltageRail { + rail_id: NvU32, + current_voltage_uv: NvU32, + rsvd: [NvU8; 32], +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct NvApiVoltageBoost { + version: NvU32, + percent: NvU8, + rsvd: [NvU8; 32], +} + +impl Default for NvApiVoltageBoost { + fn default() -> Self { + Self { + version: make_version::(1), + percent: 0, + rsvd: [0; 32], + } + } } #[repr(C)] diff --git a/lact-daemon/src/snapshots/lact_daemon__config__tests__parse_doc.snap b/lact-daemon/src/snapshots/lact_daemon__config__tests__parse_doc.snap index defb9f0e..d259be16 100644 --- a/lact-daemon/src/snapshots/lact_daemon__config__tests__parse_doc.snap +++ b/lact-daemon/src/snapshots/lact_daemon__config__tests__parse_doc.snap @@ -61,6 +61,7 @@ gpus: 3: clockspeed: 920 voltage_offset: 0 + voltage_boost: 0 power_profile_mode_index: 0 custom_power_profile_mode_hueristics: - - 0 diff --git a/lact-gui/i18n/en/lact_gui.ftl b/lact-gui/i18n/en/lact_gui.ftl index 2135f1b7..7d3443a8 100644 --- a/lact-gui/i18n/en/lact_gui.ftl +++ b/lact-gui/i18n/en/lact_gui.ftl @@ -196,6 +196,8 @@ min-gpu-clock = Minimum GPU Clock (MHz) min-vram-clock = Minimum VRAM Clock (MHz) min-gpu-voltage = Minimum GPU Voltage (mV) gpu-voltage-offset = GPU voltage offset (mV) +gpu-voltage-boost = GPU Voltage Boost (%) +gpu-voltage-boost-tooltip = Controls how much of the additional voltage headroom defined by the driver is available. 100% means all of this headroom, not 100% of total GPU voltage. More headroom may sustain higher clockspeeds but increases power draw and heat. gpu-pstate-clock-offset = GPU P-State {$pstate} Clock Offset (MHz) vram-pstate-clock-offset = VRAM P-State {$pstate} Clock Offset (MHz) gpu-pstate-clock = GPU P-State {$pstate} Clock (MHz) diff --git a/lact-gui/src/app/pages/oc_page/clocks_frame/adjustment_group.rs b/lact-gui/src/app/pages/oc_page/clocks_frame/adjustment_group.rs index 75505c40..e04d01f2 100644 --- a/lact-gui/src/app/pages/oc_page/clocks_frame/adjustment_group.rs +++ b/lact-gui/src/app/pages/oc_page/clocks_frame/adjustment_group.rs @@ -22,7 +22,8 @@ impl ClockCategory { | ClockspeedType::GpuClockOffset(_) => ClockCategory::CoreClock, ClockspeedType::MinVoltage | ClockspeedType::MaxVoltage - | ClockspeedType::VoltageOffset => ClockCategory::CoreVoltage, + | ClockspeedType::VoltageOffset + | ClockspeedType::VoltageBoost => ClockCategory::CoreVoltage, ClockspeedType::MaxMemoryClock | ClockspeedType::MinMemoryClock | ClockspeedType::MemClockOffset(_) => ClockCategory::VramClock, diff --git a/lact-gui/src/app/pages/oc_page/clocks_frame/adjustment_row.rs b/lact-gui/src/app/pages/oc_page/clocks_frame/adjustment_row.rs index 3586a410..66f0d59c 100644 --- a/lact-gui/src/app/pages/oc_page/clocks_frame/adjustment_row.rs +++ b/lact-gui/src/app/pages/oc_page/clocks_frame/adjustment_row.rs @@ -7,11 +7,11 @@ use crate::{ }; use gtk::{ glib::{SignalHandlerId, object::ObjectExt}, - prelude::{AdjustmentExt, EditableExt, OrientableExt, RangeExt, ScaleExt, WidgetExt}, + prelude::{AdjustmentExt, BoxExt, EditableExt, OrientableExt, RangeExt, ScaleExt, WidgetExt}, }; use i18n_embed_fl::fl; use lact_schema::request::ClockspeedType; -use relm4::prelude::FactoryComponent; +use relm4::{RelmWidgetExt, prelude::FactoryComponent}; pub struct ClockAdjustmentRow { clock_type: ClockspeedType, @@ -28,6 +28,20 @@ pub struct ClocksData { pub max: i32, pub custom_title: Option, pub is_secondary: bool, + pub step: i32, +} + +impl Default for ClocksData { + fn default() -> Self { + Self { + current: 0, + min: 0, + max: 0, + custom_title: None, + is_secondary: false, + step: 10, + } + } } impl ClocksData { @@ -36,8 +50,7 @@ impl ClocksData { current, min, max, - is_secondary: false, - custom_title: None, + ..Default::default() } } } @@ -69,31 +82,55 @@ impl FactoryComponent for ClockAdjustmentRow { gtk::Box { set_orientation: gtk::Orientation::Horizontal, - #[name = "title_label"] - gtk::Label { - set_xalign: 0.0, - #[watch] - set_markup: &match &self.custom_title { - Some(title) => title.clone(), - None => { - match self.clock_type { - ClockspeedType::MaxCoreClock => fl!(I18N, "max-gpu-clock"), - ClockspeedType::MaxMemoryClock => fl!(I18N, "max-vram-clock"), - ClockspeedType::MaxVoltage => fl!(I18N, "max-gpu-voltage"), - ClockspeedType::MinCoreClock => fl!(I18N, "min-gpu-clock"), - ClockspeedType::MinMemoryClock => fl!(I18N, "min-vram-clock"), - ClockspeedType::MinVoltage => fl!(I18N, "min-gpu-voltage"), - ClockspeedType::VoltageOffset => fl!(I18N, "gpu-voltage-offset"), - ClockspeedType::GpuClockOffset(pstate) => fl!(I18N, "gpu-pstate-clock-offset", pstate = pstate), - ClockspeedType::MemClockOffset(pstate) => fl!(I18N, "vram-pstate-clock-offset", pstate = pstate), - ClockspeedType::GpuVfCurveClock(pstate) => fl!(I18N, "gpu-pstate-clock", pstate = pstate), - ClockspeedType::MemVfCurveClock(pstate) => fl!(I18N, "mem-pstate-clock", pstate = pstate), - ClockspeedType::GpuVfCurveVoltage(pstate) => fl!(I18N, "gpu-pstate-clock-voltage", pstate = pstate), - ClockspeedType::MemVfCurveVoltage(pstate) => fl!(I18N, "mem-pstate-clock-voltage", pstate = pstate), - ClockspeedType::Reset => unreachable!(), + #[name = "title_box"] + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 5, + + gtk::Label { + set_xalign: 0.0, + #[watch] + set_markup: &match &self.custom_title { + Some(title) => title.clone(), + None => { + match self.clock_type { + ClockspeedType::MaxCoreClock => fl!(I18N, "max-gpu-clock"), + ClockspeedType::MaxMemoryClock => fl!(I18N, "max-vram-clock"), + ClockspeedType::MaxVoltage => fl!(I18N, "max-gpu-voltage"), + ClockspeedType::MinCoreClock => fl!(I18N, "min-gpu-clock"), + ClockspeedType::MinMemoryClock => fl!(I18N, "min-vram-clock"), + ClockspeedType::MinVoltage => fl!(I18N, "min-gpu-voltage"), + ClockspeedType::VoltageOffset => fl!(I18N, "gpu-voltage-offset"), + ClockspeedType::VoltageBoost => fl!(I18N, "gpu-voltage-boost"), + ClockspeedType::GpuClockOffset(pstate) => fl!(I18N, "gpu-pstate-clock-offset", pstate = pstate), + ClockspeedType::MemClockOffset(pstate) => fl!(I18N, "vram-pstate-clock-offset", pstate = pstate), + ClockspeedType::GpuVfCurveClock(pstate) => fl!(I18N, "gpu-pstate-clock", pstate = pstate), + ClockspeedType::MemVfCurveClock(pstate) => fl!(I18N, "mem-pstate-clock", pstate = pstate), + ClockspeedType::GpuVfCurveVoltage(pstate) => fl!(I18N, "gpu-pstate-clock-voltage", pstate = pstate), + ClockspeedType::MemVfCurveVoltage(pstate) => fl!(I18N, "mem-pstate-clock-voltage", pstate = pstate), + ClockspeedType::Reset => unreachable!(), + } } - } - } + }, + }, + + gtk::MenuButton { + set_icon_name: "dialog-information-symbolic", + set_always_show_arrow: false, + add_css_class: "flat", + set_visible: self.clock_type == ClockspeedType::VoltageBoost, + + #[wrap(Some)] + set_popover = >k::Popover { + gtk::Label { + set_margin_all: 5, + set_label: &fl!(I18N, "gpu-voltage-boost-tooltip"), + set_wrap: true, + set_wrap_mode: gtk::pango::WrapMode::Word, + set_max_width_chars: 55, + } + }, + }, }, gtk::Scale { @@ -128,7 +165,7 @@ impl FactoryComponent for ClockAdjustmentRow { data.current as f64, data.min as f64, data.max as f64, - 10.0, + data.step as f64, 10.0, ); @@ -178,7 +215,7 @@ impl FactoryComponent for ClockAdjustmentRow { label_group, input_group, } => { - label_group.add_widget(&widgets.title_label); + label_group.add_widget(&widgets.title_box); input_group.add_widget(&widgets.input_button); } ClockAdjustmentRowMsg::SetVisible(visible) => { diff --git a/lact-gui/src/app/pages/oc_page/clocks_frame/mod.rs b/lact-gui/src/app/pages/oc_page/clocks_frame/mod.rs index d58c01b9..07eeabb4 100644 --- a/lact-gui/src/app/pages/oc_page/clocks_frame/mod.rs +++ b/lact-gui/src/app/pages/oc_page/clocks_frame/mod.rs @@ -420,7 +420,7 @@ impl ClocksFrame { min: sclk_offset_min, max: sclk_offset_max, custom_title: Some(fl!(I18N, "gpu-clock-offset")), - is_secondary: false, + ..Default::default() }, ); } @@ -457,8 +457,7 @@ impl ClocksFrame { current: level.clockspeed, min: min_sclk, max: max_sclk, - is_secondary: false, - custom_title: None, + ..Default::default() }, ); } @@ -477,8 +476,7 @@ impl ClocksFrame { current: level.voltage, min: min_vddc, max: max_vddc, - is_secondary: false, - custom_title: None, + ..Default::default() }, ); } @@ -540,7 +538,7 @@ impl ClocksFrame { min, max, is_secondary, - custom_title: None, + ..Default::default() }, ); } @@ -600,6 +598,19 @@ impl ClocksFrame { nvidia_clock_offset_to_data(offset, *pstate > 0), ); } + + if let Some(voltage_boost) = table.voltage_boost { + self.set_clock( + ClockspeedType::VoltageBoost, + ClocksData { + current: voltage_boost.current, + min: voltage_boost.min, + max: voltage_boost.max, + step: 1, + ..Default::default() + }, + ); + } } fn set_intel_table(&mut self, table: &IntelClocksTable) { @@ -664,7 +675,7 @@ fn nvidia_clock_offset_to_data(clock_info: &NvidiaClockOffset, is_secondary: boo current: clock_info.current, min: clock_info.min, max: clock_info.max, - custom_title: None, is_secondary, + ..Default::default() } } diff --git a/lact-schema/src/config.rs b/lact-schema/src/config.rs index b94f7bd2..8538f50e 100644 --- a/lact-schema/src/config.rs +++ b/lact-schema/src/config.rs @@ -89,6 +89,7 @@ pub struct ClocksConfiguration { )] pub mem_vf_curve: IndexMap, pub voltage_offset: Option, + pub voltage_boost: Option, } impl ClocksConfiguration { @@ -102,6 +103,7 @@ impl ClocksConfiguration { ClockspeedType::MinMemoryClock => self.min_memory_clock = value, ClockspeedType::MinVoltage => self.min_voltage = value, ClockspeedType::VoltageOffset => self.voltage_offset = value, + ClockspeedType::VoltageBoost => self.voltage_boost = value, ClockspeedType::GpuClockOffset(pstate) => match value { Some(value) => { self.gpu_clock_offsets.insert(pstate, value); diff --git a/lact-schema/src/lib.rs b/lact-schema/src/lib.rs index 33ba2ac3..4f542f9d 100644 --- a/lact-schema/src/lib.rs +++ b/lact-schema/src/lib.rs @@ -478,6 +478,16 @@ pub struct NvidiaClocksTable { pub vram_clock_range: Option<(u32, u32)>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub gpu_vf_curve: Vec, + #[serde(default)] + pub voltage_boost: Option, +} + +/// Nvidia core voltage boost, in percent +#[derive(Serialize, Deserialize, Default, Debug, Clone, Copy)] +pub struct NvidiaVoltageBoost { + pub current: i32, + pub min: i32, + pub max: i32, } #[derive(Serialize, Deserialize, Default, Debug, Clone, Copy)] diff --git a/lact-schema/src/request.rs b/lact-schema/src/request.rs index b4a38ed8..0ded08f3 100644 --- a/lact-schema/src/request.rs +++ b/lact-schema/src/request.rs @@ -163,6 +163,7 @@ pub enum ClockspeedType { MinVoltage, MaxVoltage, VoltageOffset, + VoltageBoost, MaxMemoryClock, MinMemoryClock,