mirror of
https://github.com/ilya-zlobintsev/LACT.git
synced 2025-02-25 18:55:26 -06:00
feat!: add initial Nvidia support (#388)
* feat: abstract GpuController * test? * fix: dont re-initialize nvidia gpu * feat: more info on nvidia * feat: core stats on nvidia * fix: nvidia uses milliwatts not microwatts * feat: include device info and stats responses in debug snapshot * chore: avoid logging msg twice * fix: correctly handle only secondary line series being present in a plot * feat: more reporting * feat: support for setting the power cap * chore: avoid trying to initialize drm for non-amd gpus * chore: avoid drawing primary plot label when only secondary values are present * feat: hide unknown values on the info page * feat: report cuda cores * chore: limit power usage value accuracy * Threaded plot render * Better supersampler implementation * Better to display nothing than do long freeze * Fix plot throttling jumping around * Further improve rendering by using filled legend * Spawn render thread with minimum priority * Optimize Cairo bindings * Simplify code as we no longer need to track initial state Signed-off-by: Alik Aslanyan <inline0@pm.me> * Add plotters package override for opt-level 3 in release * Immediately react to size changes of the widget, don't wait for new data * feat: nvidia fan control * Scale plots in GTK, instead of Cairo for Trillinear filtering, rewrite supersampling * feat: pstate reporting * doc: update README to mention that nvidia is now supported * doc: add historical data to readme * doc: improve screenshot alignemnt * doc: add nvidia driver note --------- Signed-off-by: Alik Aslanyan <inline0@pm.me> Co-authored-by: Alik Aslanyan <inline0@pm.me>
This commit is contained in:
parent
dbb24c5bd2
commit
7cabe614c2
39
Cargo.lock
generated
39
Cargo.lock
generated
@ -695,7 +695,7 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
||||
dependencies = [
|
||||
"libloading 0.8.5",
|
||||
"libloading 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1495,6 +1495,7 @@ version = "0.5.7"
|
||||
dependencies = [
|
||||
"amdgpu-sysfs",
|
||||
"anyhow",
|
||||
"bitflags 2.6.0",
|
||||
"chrono",
|
||||
"futures",
|
||||
"indexmap",
|
||||
@ -1503,6 +1504,7 @@ dependencies = [
|
||||
"libflate",
|
||||
"nix",
|
||||
"notify",
|
||||
"nvml-wrapper",
|
||||
"os-release",
|
||||
"pciid-parser",
|
||||
"serde",
|
||||
@ -1649,7 +1651,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1824,6 +1826,27 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvml-wrapper"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/ilya-zlobintsev/nvml-wrapper?branch=lact#8e9f9c5738e167d2ef1e776ac86f388f23ee6d12"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"libloading 0.8.5",
|
||||
"nvml-wrapper-sys",
|
||||
"static_assertions",
|
||||
"thiserror",
|
||||
"wrapcenum-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvml-wrapper-sys"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/ilya-zlobintsev/nvml-wrapper?branch=lact#8e9f9c5738e167d2ef1e776ac86f388f23ee6d12"
|
||||
dependencies = [
|
||||
"libloading 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc"
|
||||
version = "0.2.7"
|
||||
@ -3165,6 +3188,18 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wrapcenum-derive"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a76ff259533532054cfbaefb115c613203c73707017459206380f03b3b3f266e"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.3.1"
|
||||
|
22
README.md
22
README.md
@ -2,19 +2,23 @@
|
||||
|
||||
<img src="res/io.github.lact-linux.png" alt="icon" width="100"/>
|
||||
|
||||
This application allows you to control your AMD GPU on a Linux system.
|
||||
This application allows you to control your AMD or Nvidia GPU on a Linux system.
|
||||
|
||||
| GPU info | Overclocking | Fan control |
|
||||
|----------------------------------------------|----------------------------------------------|---------------------------------------------|
|
||||
||||
|
||||
| Historical data |
|
||||
||
|
||||
|
||||
Current features:
|
||||
|
||||
- Viewing information about the GPU
|
||||
- Power/thermals monitoring
|
||||
- Power and thermals monitoring, power limit configuration
|
||||
- Fan curve control
|
||||
- Overclocking (GPU/VRAM clockspeed, voltage)
|
||||
- Power states configuration
|
||||
- Overclocking (GPU/VRAM clockspeed and voltage, currently AMD only)
|
||||
- Power states configuration (AMD only)
|
||||
|
||||
Both AMD and Nvidia functionality works on X11, Wayland or even headless sessions.
|
||||
|
||||
# Installation
|
||||
|
||||
@ -33,6 +37,8 @@ Current features:
|
||||
**Why is there no AppImage/Flatpak/other universal format?**
|
||||
See [here](./pkg/README.md).
|
||||
|
||||
Note: Nvidia support requires the Nvidia proprietary driver with CUDA libraries installed.
|
||||
|
||||
## Development builds
|
||||
|
||||
To get latest fixes or features that have not yet been released in a stable version, there are packages built from the latest commit that you can install from the [test release](https://github.com/ilya-zlobintsev/LACT/releases/tag/test-build) or using the `lact-git` AUR package on Arch-based distros.
|
||||
@ -49,6 +55,8 @@ You can now use the GUI to change settings and view information.
|
||||
|
||||
# Hardware support
|
||||
|
||||
## AMD
|
||||
|
||||
LACT for the most part does not implement features on a per-generation basis, rather it exposes the functionality that is available in the driver for the current system.
|
||||
However the following table shows what functionality can be expected for a given generation.
|
||||
|
||||
@ -72,6 +80,10 @@ However the following table shows what functionality can be expected for a given
|
||||
GPUs not listed here will still work, but might not have full functionality available.
|
||||
Monitoring/system info will be available everywhere. Integrated GPUs might also only have basic configuration available.
|
||||
|
||||
## Nvidia
|
||||
|
||||
Anything Maxwell or newer should work, but generation support has not yet been tested thoroughly.
|
||||
|
||||
# Configuration
|
||||
|
||||
There is a configuration file available in `/etc/lact/config.yaml`. Most of the settings are accessible through the GUI, but some of them may be useful to be edited manually (like `admin_groups` to specify who has access to the daemon)
|
||||
@ -86,7 +98,7 @@ However, some systems may have different user configuration. In particular, this
|
||||
|
||||
To fix socket permissions in such configurations, edit `/etc/lact/config.yaml` and add your username or group as the first entry in `admin_groups` under `daemon`, and restart the service (`sudo systemctl restart lactd`).
|
||||
|
||||
# Overclocking
|
||||
# Overclocking (AMD)
|
||||
|
||||
The overclocking functionality is disabled by default in the driver. There are two ways to enable it:
|
||||
- By using the "enable overclocking" option in the LACT GUI. This will create a file in `/etc/modprobe.d` that enables the required driver options. This is the easiest way and it should work for most people.
|
||||
|
@ -29,6 +29,8 @@ tokio = { workspace = true, features = [
|
||||
futures = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
|
||||
nvml-wrapper = { git = "https://github.com/ilya-zlobintsev/nvml-wrapper", branch = "lact" }
|
||||
bitflags = "2.6.0"
|
||||
pciid-parser = { version = "0.7", features = ["serde"] }
|
||||
serde_yaml = "0.9"
|
||||
vulkano = { version = "0.34.1", default-features = false }
|
||||
|
1026
lact-daemon/src/server/gpu_controller/amd.rs
Normal file
1026
lact-daemon/src/server/gpu_controller/amd.rs
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
554
lact-daemon/src/server/gpu_controller/nvidia.rs
Normal file
554
lact-daemon/src/server/gpu_controller/nvidia.rs
Normal file
@ -0,0 +1,554 @@
|
||||
use crate::{
|
||||
config::{self, FanControlSettings},
|
||||
server::vulkan::get_vulkan_info,
|
||||
};
|
||||
|
||||
use super::{fan_control::FanCurve, FanControlHandle, GpuController};
|
||||
use amdgpu_sysfs::{
|
||||
gpu_handle::power_profile_mode::PowerProfileModesTable,
|
||||
hw_mon::{HwMon, Temperature},
|
||||
};
|
||||
use anyhow::{anyhow, Context};
|
||||
use futures::future::LocalBoxFuture;
|
||||
use lact_schema::{
|
||||
ClocksInfo, ClockspeedStats, DeviceInfo, DeviceStats, DrmInfo, DrmMemoryInfo, FanControlMode,
|
||||
FanStats, GpuPciInfo, LinkInfo, PmfwInfo, PowerState, PowerStates, PowerStats, VoltageStats,
|
||||
VramStats,
|
||||
};
|
||||
use nvml_wrapper::{
|
||||
bitmasks::device::ThrottleReasons,
|
||||
enum_wrappers::device::{Clock, ClockType, TemperatureSensor, TemperatureThreshold},
|
||||
Device, Nvml,
|
||||
};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
collections::HashMap,
|
||||
fmt::Write,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::{select, sync::Notify, time::sleep};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
pub struct NvidiaGpuController {
|
||||
pub nvml: Rc<Nvml>,
|
||||
pub pci_slot_id: String,
|
||||
pub pci_info: GpuPciInfo,
|
||||
pub sysfs_path: PathBuf,
|
||||
pub fan_control_handle: RefCell<Option<FanControlHandle>>,
|
||||
}
|
||||
|
||||
impl NvidiaGpuController {
|
||||
fn device(&self) -> Device<'_> {
|
||||
self.nvml
|
||||
.device_by_pci_bus_id(self.pci_slot_id.as_str())
|
||||
.expect("Can no longer get device")
|
||||
}
|
||||
|
||||
async fn start_curve_fan_control_task(
|
||||
&self,
|
||||
curve: FanCurve,
|
||||
settings: FanControlSettings,
|
||||
) -> anyhow::Result<()> {
|
||||
// Stop existing task to re-apply new curve
|
||||
self.stop_fan_control().await?;
|
||||
|
||||
let device = self.device();
|
||||
device
|
||||
.temperature(TemperatureSensor::Gpu)
|
||||
.context("Could not read temperature")?;
|
||||
|
||||
let fan_count = device.num_fans().context("Could not read fan count")?;
|
||||
if fan_count == 0 {
|
||||
return Err(anyhow!("Device has no fans"));
|
||||
}
|
||||
|
||||
let mut notify_guard = self
|
||||
.fan_control_handle
|
||||
.try_borrow_mut()
|
||||
.map_err(|err| anyhow!("Lock error: {err}"))?;
|
||||
|
||||
let notify = Rc::new(Notify::new());
|
||||
let task_notify = notify.clone();
|
||||
|
||||
let nvml = self.nvml.clone();
|
||||
let pci_slot_id = self.pci_slot_id.clone();
|
||||
debug!("spawning new fan control task");
|
||||
|
||||
let handle = tokio::task::spawn_local(async move {
|
||||
let mut device = nvml
|
||||
.device_by_pci_bus_id(pci_slot_id.as_str())
|
||||
.expect("Can no longer get device");
|
||||
|
||||
let mut last_pwm = (None, Instant::now());
|
||||
let mut last_temp = 0;
|
||||
|
||||
let interval = Duration::from_millis(settings.interval_ms);
|
||||
let spindown_delay = Duration::from_millis(settings.spindown_delay_ms.unwrap_or(0));
|
||||
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
|
||||
let change_threshold = settings.change_threshold.unwrap_or(0) as i32;
|
||||
|
||||
loop {
|
||||
select! {
|
||||
() = sleep(interval) => (),
|
||||
() = task_notify.notified() => break,
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_wrap)]
|
||||
let current_temp = device
|
||||
.temperature(TemperatureSensor::Gpu)
|
||||
.expect("Could not read temperature") as i32;
|
||||
|
||||
if (last_temp - current_temp).abs() < change_threshold {
|
||||
trace!("temperature changed from {last_temp}°C to {current_temp}°C, which is less than the {change_threshold}°C threshold, skipping speed adjustment");
|
||||
continue;
|
||||
}
|
||||
|
||||
let target_pwm = curve.pwm_at_temp(Temperature {
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
current: Some(current_temp as f32),
|
||||
crit: None,
|
||||
crit_hyst: None,
|
||||
});
|
||||
let now = Instant::now();
|
||||
|
||||
if let (Some(previous_pwm), previous_timestamp) = last_pwm {
|
||||
let diff = now - previous_timestamp;
|
||||
if target_pwm < previous_pwm && diff < spindown_delay {
|
||||
trace!(
|
||||
"delaying fan spindown ({}ms left)",
|
||||
(spindown_delay - diff).as_millis()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
last_pwm = (Some(target_pwm), now);
|
||||
last_temp = current_temp;
|
||||
|
||||
trace!("fan control tick: setting pwm to {target_pwm}");
|
||||
|
||||
for fan in 0..fan_count {
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
if let Err(err) =
|
||||
device.set_fan_speed(fan, (f64::from(target_pwm) / 2.5) as u32)
|
||||
{
|
||||
error!("could not set fan speed: {err}, disabling fan control");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!("exited fan control task");
|
||||
});
|
||||
|
||||
*notify_guard = Some((notify, handle));
|
||||
|
||||
debug!(
|
||||
"started fan control with interval {}ms",
|
||||
settings.interval_ms
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop_fan_control(&self) -> anyhow::Result<()> {
|
||||
let mut fail_on_error = false;
|
||||
|
||||
let maybe_notify = self
|
||||
.fan_control_handle
|
||||
.try_borrow_mut()
|
||||
.map_err(|err| anyhow!("Lock error: {err}"))?
|
||||
.take();
|
||||
if let Some((notify, handle)) = maybe_notify {
|
||||
notify.notify_one();
|
||||
handle.await?;
|
||||
fail_on_error = true;
|
||||
}
|
||||
|
||||
let mut device = self.device();
|
||||
let fan_count = device.num_fans().context("Could not get fan count")?;
|
||||
for i in 0..fan_count {
|
||||
if let Err(err) = device
|
||||
.set_default_fan_speed(i)
|
||||
.context("Could not reset fan control to default")
|
||||
{
|
||||
if fail_on_error {
|
||||
return Err(err);
|
||||
}
|
||||
error!("{err:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_get_power_states(&self) -> anyhow::Result<PowerStates> {
|
||||
let device = self.device();
|
||||
|
||||
let supported_states = device
|
||||
.supported_performance_states()
|
||||
.context("Could not get supported pstates")?;
|
||||
|
||||
let mut power_states = PowerStates::default();
|
||||
|
||||
for pstate in supported_states {
|
||||
let (gpu_min, gpu_max) = device
|
||||
.min_max_clock_of_pstate(ClockType::Graphics, pstate)
|
||||
.context("Could not read GPU pstates")?;
|
||||
|
||||
power_states.core.push(PowerState {
|
||||
enabled: true,
|
||||
min_value: Some(u64::from(gpu_min)),
|
||||
value: u64::from(gpu_max),
|
||||
index: Some(
|
||||
pstate
|
||||
.as_c()
|
||||
.try_into()
|
||||
.expect("Power state always fits in u8"),
|
||||
),
|
||||
});
|
||||
|
||||
let (mem_min, mem_max) = device
|
||||
.min_max_clock_of_pstate(ClockType::Mem, pstate)
|
||||
.context("Could not read memory pstates")?;
|
||||
|
||||
power_states.vram.push(PowerState {
|
||||
enabled: true,
|
||||
min_value: Some(u64::from(mem_min)),
|
||||
value: u64::from(mem_max),
|
||||
index: Some(
|
||||
pstate
|
||||
.as_c()
|
||||
.try_into()
|
||||
.expect("Power state always fits in u8"),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(power_states)
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuController for NvidiaGpuController {
|
||||
fn get_id(&self) -> anyhow::Result<String> {
|
||||
let GpuPciInfo {
|
||||
device_pci_info,
|
||||
subsystem_pci_info,
|
||||
} = &self.pci_info;
|
||||
|
||||
Ok(format!(
|
||||
"{}:{}-{}:{}-{}",
|
||||
device_pci_info.vendor_id,
|
||||
device_pci_info.model_id,
|
||||
subsystem_pci_info.vendor_id,
|
||||
subsystem_pci_info.model_id,
|
||||
self.pci_slot_id
|
||||
))
|
||||
}
|
||||
|
||||
fn get_pci_info(&self) -> Option<&GpuPciInfo> {
|
||||
Some(&self.pci_info)
|
||||
}
|
||||
|
||||
fn get_path(&self) -> &Path {
|
||||
&self.sysfs_path
|
||||
}
|
||||
|
||||
fn get_info(&self) -> DeviceInfo {
|
||||
let device = self.device();
|
||||
|
||||
let vulkan_info = match get_vulkan_info(
|
||||
&self.pci_info.device_pci_info.vendor_id,
|
||||
&self.pci_info.device_pci_info.model_id,
|
||||
) {
|
||||
Ok(info) => Some(info),
|
||||
Err(err) => {
|
||||
warn!("could not load vulkan info: {err}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
DeviceInfo {
|
||||
pci_info: Some(Cow::Borrowed(&self.pci_info)),
|
||||
vulkan_info,
|
||||
driver: format!(
|
||||
"nvidia {}",
|
||||
self.nvml.sys_driver_version().unwrap_or_default()
|
||||
), // NVML should always be "nvidia"
|
||||
vbios_version: device
|
||||
.vbios_version()
|
||||
.map_err(|err| error!("could not get VBIOS version: {err}"))
|
||||
.ok(),
|
||||
link_info: LinkInfo {
|
||||
current_width: device.current_pcie_link_width().map(|v| v.to_string()).ok(),
|
||||
current_speed: device
|
||||
.pcie_link_speed()
|
||||
.map(|v| {
|
||||
let mut output = format!("{} GT/s", v / 1000);
|
||||
if let Ok(gen) = device.current_pcie_link_gen() {
|
||||
let _ = write!(output, " PCIe gen {gen}");
|
||||
}
|
||||
output
|
||||
})
|
||||
.ok(),
|
||||
max_width: device.max_pcie_link_width().map(|v| v.to_string()).ok(),
|
||||
max_speed: device
|
||||
.max_pcie_link_speed()
|
||||
.ok()
|
||||
.and_then(|v| v.as_integer())
|
||||
.map(|v| {
|
||||
let mut output = format!("{} GT/s", v / 1000);
|
||||
if let Ok(gen) = device.current_pcie_link_gen() {
|
||||
let _ = write!(output, " PCIe gen {gen}");
|
||||
}
|
||||
output
|
||||
}),
|
||||
},
|
||||
drm_info: Some(DrmInfo {
|
||||
device_name: device.name().ok(),
|
||||
pci_revision_id: None,
|
||||
family_name: device.architecture().map(|arch| arch.to_string()).ok(),
|
||||
family_id: None,
|
||||
asic_name: None,
|
||||
chip_class: device.architecture().map(|arch| arch.to_string()).ok(),
|
||||
compute_units: None,
|
||||
cuda_cores: device.num_cores().ok(),
|
||||
vram_type: None,
|
||||
vram_clock_ratio: 1.0,
|
||||
vram_bit_width: device.current_pcie_link_width().ok(),
|
||||
vram_max_bw: None,
|
||||
l1_cache_per_cu: None,
|
||||
l2_cache: None,
|
||||
l3_cache_mb: None,
|
||||
memory_info: device
|
||||
.bar1_memory_info()
|
||||
.map(|info| DrmMemoryInfo {
|
||||
cpu_accessible_used: info.used,
|
||||
cpu_accessible_total: info.total,
|
||||
resizeable_bar: None,
|
||||
})
|
||||
.ok(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn hw_monitors(&self) -> &[HwMon] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn get_pci_slot_name(&self) -> Option<String> {
|
||||
Some(self.pci_slot_id.clone())
|
||||
}
|
||||
|
||||
#[allow(
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss
|
||||
)]
|
||||
fn get_stats(&self, gpu_config: Option<&config::Gpu>) -> DeviceStats {
|
||||
let device = self.device();
|
||||
|
||||
let mut temps = HashMap::new();
|
||||
|
||||
if let Ok(temp) = device.temperature(TemperatureSensor::Gpu) {
|
||||
let crit = device
|
||||
.temperature_threshold(TemperatureThreshold::Shutdown)
|
||||
.map(|value| value as f32)
|
||||
.ok();
|
||||
|
||||
temps.insert(
|
||||
"GPU".to_owned(),
|
||||
Temperature {
|
||||
current: Some(temp as f32),
|
||||
crit,
|
||||
crit_hyst: None,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
let fan_settings = gpu_config.and_then(|config| config.fan_control_settings.as_ref());
|
||||
|
||||
let pwm_current = if device.num_fans().is_ok_and(|num| num > 0) {
|
||||
device
|
||||
.fan_speed(0)
|
||||
.ok()
|
||||
.map(|value| (f64::from(value) * 2.55) as u8)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let vram = device
|
||||
.memory_info()
|
||||
.map(|info| VramStats {
|
||||
total: Some(info.total),
|
||||
used: Some(info.used),
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let active_pstate = device
|
||||
.performance_state()
|
||||
.map(|pstate| pstate.as_c() as usize)
|
||||
.ok();
|
||||
|
||||
DeviceStats {
|
||||
temps,
|
||||
fan: FanStats {
|
||||
control_enabled: gpu_config.is_some_and(|config| config.fan_control_enabled),
|
||||
control_mode: fan_settings.map(|settings| settings.mode),
|
||||
static_speed: fan_settings.map(|settings| settings.static_speed),
|
||||
curve: fan_settings.map(|settings| settings.curve.0.clone()),
|
||||
spindown_delay_ms: fan_settings.and_then(|settings| settings.spindown_delay_ms),
|
||||
change_threshold: fan_settings.and_then(|settings| settings.change_threshold),
|
||||
speed_current: None,
|
||||
speed_max: None,
|
||||
speed_min: None,
|
||||
pwm_current,
|
||||
pmfw_info: PmfwInfo::default(),
|
||||
},
|
||||
power: PowerStats {
|
||||
average: None,
|
||||
current: device.power_usage().map(|mw| f64::from(mw) / 1000.0).ok(),
|
||||
cap_current: device
|
||||
.power_management_limit()
|
||||
.map(|mw| f64::from(mw) / 1000.0)
|
||||
.ok(),
|
||||
cap_max: device
|
||||
.power_management_limit_constraints()
|
||||
.map(|constraints| f64::from(constraints.max_limit) / 1000.0)
|
||||
.ok(),
|
||||
cap_min: device
|
||||
.power_management_limit_constraints()
|
||||
.map(|constraints| f64::from(constraints.min_limit) / 1000.0)
|
||||
.ok(),
|
||||
cap_default: device
|
||||
.power_management_limit_default()
|
||||
.map(|mw| f64::from(mw) / 1000.0)
|
||||
.ok(),
|
||||
},
|
||||
busy_percent: device
|
||||
.utilization_rates()
|
||||
.map(|utilization| u8::try_from(utilization.gpu).expect("Invalid percentage"))
|
||||
.ok(),
|
||||
vram,
|
||||
clockspeed: ClockspeedStats {
|
||||
gpu_clockspeed: device.clock_info(Clock::Graphics).map(Into::into).ok(),
|
||||
vram_clockspeed: device.clock_info(Clock::Memory).map(Into::into).ok(),
|
||||
current_gfxclk: None,
|
||||
},
|
||||
throttle_info: device.current_throttle_reasons().ok().map(|reasons| {
|
||||
reasons
|
||||
.iter()
|
||||
.filter(|reason| *reason != ThrottleReasons::GPU_IDLE)
|
||||
.map(|reason| {
|
||||
let mut name = String::new();
|
||||
bitflags::parser::to_writer(&reason, &mut name).unwrap();
|
||||
(name, vec![])
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
voltage: VoltageStats::default(), // Voltage reporting is not supported
|
||||
performance_level: None,
|
||||
core_power_state: active_pstate,
|
||||
memory_power_state: active_pstate,
|
||||
pcie_power_state: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_clocks_info(&self) -> anyhow::Result<ClocksInfo> {
|
||||
Ok(ClocksInfo::default())
|
||||
}
|
||||
|
||||
fn get_power_states(&self, _gpu_config: Option<&config::Gpu>) -> PowerStates {
|
||||
self.try_get_power_states().unwrap_or_else(|err| {
|
||||
warn!("could not get pstates info: {err:#}");
|
||||
PowerStates::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn get_power_profile_modes(&self) -> anyhow::Result<PowerProfileModesTable> {
|
||||
Err(anyhow!("Not supported on Nvidia"))
|
||||
}
|
||||
|
||||
fn reset_pmfw_settings(&self) {}
|
||||
|
||||
fn vbios_dump(&self) -> anyhow::Result<Vec<u8>> {
|
||||
Err(anyhow!("Not supported on Nvidia"))
|
||||
}
|
||||
|
||||
fn apply_config<'a>(
|
||||
&'a self,
|
||||
config: &'a config::Gpu,
|
||||
) -> LocalBoxFuture<'a, anyhow::Result<()>> {
|
||||
Box::pin(async {
|
||||
let mut device = self.device();
|
||||
|
||||
if let Some(cap) = config.power_cap {
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
let cap = (cap * 1000.0) as u32;
|
||||
|
||||
let current_cap = device
|
||||
.power_management_limit()
|
||||
.context("Could not get current cap")?;
|
||||
|
||||
if current_cap != cap {
|
||||
debug!("setting power cap to {cap}");
|
||||
device
|
||||
.set_power_management_limit(cap)
|
||||
.context("Could not set power cap")?;
|
||||
}
|
||||
} else {
|
||||
let current_cap = device.power_management_limit();
|
||||
let default_cap = device.power_management_limit_default();
|
||||
|
||||
if let (Ok(current_cap), Ok(default_cap)) = (current_cap, default_cap) {
|
||||
if current_cap != default_cap {
|
||||
debug!("resetting power cap to {default_cap}");
|
||||
device
|
||||
.set_power_management_limit(default_cap)
|
||||
.context("Could not reset power cap")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if config.fan_control_enabled {
|
||||
let settings = config
|
||||
.fan_control_settings
|
||||
.as_ref()
|
||||
.context("Fan control enabled with no settings")?;
|
||||
match settings.mode {
|
||||
FanControlMode::Static => {
|
||||
self.stop_fan_control()
|
||||
.await
|
||||
.context("Could not reset fan control")?;
|
||||
|
||||
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
|
||||
let speed = (settings.static_speed * 100.0) as u32;
|
||||
|
||||
let fan_count = device.num_fans().context("Could not get fan count")?;
|
||||
for fan in 0..fan_count {
|
||||
device
|
||||
.set_fan_speed(fan, speed)
|
||||
.context("Could not reset fan speed to default")?;
|
||||
}
|
||||
}
|
||||
FanControlMode::Curve => {
|
||||
self.start_curve_fan_control_task(settings.curve.clone(), settings.clone())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.stop_fan_control()
|
||||
.await
|
||||
.context("Could not reset fan control")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn cleanup_clocks(&self) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -2,7 +2,10 @@ use super::{
|
||||
gpu_controller::{fan_control::FanCurve, GpuController},
|
||||
system::{self, detect_initramfs_type, PP_FEATURE_MASK_PATH},
|
||||
};
|
||||
use crate::config::{self, default_fan_static_speed, Config, FanControlSettings, Profile};
|
||||
use crate::{
|
||||
config::{self, default_fan_static_speed, Config, FanControlSettings, Profile},
|
||||
server::gpu_controller::{AmdGpuController, NvidiaGpuController},
|
||||
};
|
||||
use amdgpu_sysfs::{
|
||||
gpu_handle::{power_profile_mode::PowerProfileModesTable, PerformanceLevel, PowerLevelKind},
|
||||
sysfs::SysFS,
|
||||
@ -16,6 +19,7 @@ use lact_schema::{
|
||||
};
|
||||
use libflate::gzip;
|
||||
use nix::libc;
|
||||
use nvml_wrapper::{error::NvmlError, Nvml};
|
||||
use os_release::OS_RELEASE;
|
||||
use pciid_parser::Database;
|
||||
use serde_json::json;
|
||||
@ -76,7 +80,7 @@ const SNAPSHOT_HWMON_FILE_PREFIXES: &[&str] =
|
||||
#[derive(Clone)]
|
||||
pub struct Handler {
|
||||
pub config: Rc<RefCell<Config>>,
|
||||
pub gpu_controllers: Rc<BTreeMap<String, GpuController>>,
|
||||
pub gpu_controllers: Rc<BTreeMap<String, Box<dyn GpuController>>>,
|
||||
confirm_config_tx: Rc<RefCell<Option<oneshot::Sender<ConfirmCommand>>>>,
|
||||
pub config_last_saved: Arc<Mutex<Instant>>,
|
||||
}
|
||||
@ -103,7 +107,7 @@ impl<'a> Handler {
|
||||
.expect("pci file name should be valid unicode");
|
||||
|
||||
if controllers.values().any(|controller| {
|
||||
controller.handle.get_pci_slot_name() == Some(&slot_name)
|
||||
controller.get_pci_slot_name().as_ref() == Some(&slot_name)
|
||||
}) {
|
||||
debug!("found intialized drm entry for device {:?}", device.path());
|
||||
} else {
|
||||
@ -279,12 +283,12 @@ impl<'a> Handler {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn controller_by_id(&self, id: &str) -> anyhow::Result<&GpuController> {
|
||||
fn controller_by_id(&self, id: &str) -> anyhow::Result<&dyn GpuController> {
|
||||
Ok(self
|
||||
.gpu_controllers
|
||||
.get(id)
|
||||
.as_ref()
|
||||
.context("No controller with such id")?)
|
||||
.context("No controller with such id")?
|
||||
.as_ref())
|
||||
}
|
||||
|
||||
pub fn list_devices(&'a self) -> Vec<DeviceListEntry> {
|
||||
@ -292,8 +296,7 @@ impl<'a> Handler {
|
||||
.iter()
|
||||
.map(|(id, controller)| {
|
||||
let name = controller
|
||||
.pci_info
|
||||
.as_ref()
|
||||
.get_pci_info()
|
||||
.and_then(|pci_info| pci_info.device_pci_info.model.clone());
|
||||
DeviceListEntry {
|
||||
id: id.to_owned(),
|
||||
@ -453,7 +456,7 @@ impl<'a> Handler {
|
||||
command: SetClocksCommand,
|
||||
) -> anyhow::Result<u64> {
|
||||
if let SetClocksCommand::Reset = command {
|
||||
self.controller_by_id(id)?.handle.reset_clocks_table()?;
|
||||
self.controller_by_id(id)?.cleanup_clocks()?;
|
||||
}
|
||||
|
||||
self.edit_gpu_config(id.to_owned(), |gpu_config| {
|
||||
@ -478,10 +481,7 @@ impl<'a> Handler {
|
||||
}
|
||||
|
||||
pub fn get_power_profile_modes(&self, id: &str) -> anyhow::Result<PowerProfileModesTable> {
|
||||
let modes_table = self
|
||||
.controller_by_id(id)?
|
||||
.handle
|
||||
.get_power_profile_modes()?;
|
||||
let modes_table = self.controller_by_id(id)?.get_power_profile_modes()?;
|
||||
Ok(modes_table)
|
||||
}
|
||||
|
||||
@ -533,7 +533,7 @@ impl<'a> Handler {
|
||||
}
|
||||
|
||||
for controller in self.gpu_controllers.values() {
|
||||
let controller_path = controller.handle.get_path();
|
||||
let controller_path = controller.get_path();
|
||||
|
||||
for device_file in SNAPSHOT_DEVICE_FILES {
|
||||
let full_path = controller_path.join(device_file);
|
||||
@ -546,7 +546,7 @@ impl<'a> Handler {
|
||||
add_path_to_archive(&mut archive, &full_path)?;
|
||||
}
|
||||
|
||||
for hw_mon in &controller.handle.hw_monitors {
|
||||
for hw_mon in controller.hw_monitors() {
|
||||
let hw_mon_path = hw_mon.get_path();
|
||||
let hw_mon_entries =
|
||||
std::fs::read_dir(hw_mon_path).context("Could not read HwMon dir")?;
|
||||
@ -590,7 +590,7 @@ impl<'a> Handler {
|
||||
.and_then(|config| config.gpus().ok()?.get(id));
|
||||
|
||||
let data = json!({
|
||||
"pci_info": controller.pci_info,
|
||||
"pci_info": controller.get_pci_info(),
|
||||
"info": controller.get_info(),
|
||||
"stats": controller.get_stats(gpu_config),
|
||||
"clocks_info": controller.get_clocks_info().ok(),
|
||||
@ -710,9 +710,9 @@ impl<'a> Handler {
|
||||
.unwrap_or(false);
|
||||
|
||||
for (id, controller) in &*self.gpu_controllers {
|
||||
if !disable_clocks_cleanup && controller.handle.get_clocks_table().is_ok() {
|
||||
if !disable_clocks_cleanup {
|
||||
debug!("resetting clocks table");
|
||||
if let Err(err) = controller.handle.reset_clocks_table() {
|
||||
if let Err(err) = controller.cleanup_clocks() {
|
||||
error!("could not reset the clocks table: {err}");
|
||||
}
|
||||
}
|
||||
@ -726,7 +726,7 @@ impl<'a> Handler {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_controllers() -> anyhow::Result<BTreeMap<String, GpuController>> {
|
||||
fn load_controllers() -> anyhow::Result<BTreeMap<String, Box<dyn GpuController>>> {
|
||||
let mut controllers = BTreeMap::new();
|
||||
|
||||
let base_path = match env::var("_LACT_DRM_SYSFS_PATH") {
|
||||
@ -742,6 +742,17 @@ fn load_controllers() -> anyhow::Result<BTreeMap<String, GpuController>> {
|
||||
}
|
||||
});
|
||||
|
||||
let nvml = match Nvml::init() {
|
||||
Ok(nvml) => {
|
||||
info!("NVML initialized");
|
||||
Some(Rc::new(nvml))
|
||||
}
|
||||
Err(err) => {
|
||||
info!("Nvidia support disabled, {err}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
for entry in base_path
|
||||
.read_dir()
|
||||
.map_err(|error| anyhow!("Failed to read sysfs: {error}"))?
|
||||
@ -755,12 +766,52 @@ fn load_controllers() -> anyhow::Result<BTreeMap<String, GpuController>> {
|
||||
if name.starts_with("card") && !name.contains('-') {
|
||||
trace!("trying gpu controller at {:?}", entry.path());
|
||||
let device_path = entry.path().join("device");
|
||||
match GpuController::new_from_path(device_path, &pci_db) {
|
||||
match AmdGpuController::new_from_path(device_path, &pci_db) {
|
||||
Ok(controller) => match controller.get_id() {
|
||||
Ok(id) => {
|
||||
let path = controller.get_path();
|
||||
debug!("initialized GPU controller {id} for path {path:?}",);
|
||||
controllers.insert(id, controller);
|
||||
|
||||
if let Some(nvml) = nvml.clone() {
|
||||
if let Some(pci_slot_id) = controller.get_pci_slot_name() {
|
||||
match nvml.device_by_pci_bus_id(pci_slot_id.as_str()) {
|
||||
Ok(_) => {
|
||||
let controller = NvidiaGpuController {
|
||||
nvml,
|
||||
pci_slot_id,
|
||||
pci_info: controller.get_pci_info().expect(
|
||||
"Initialized NVML device without PCI info somehow",
|
||||
).clone(),
|
||||
sysfs_path: path.to_owned(),
|
||||
fan_control_handle: RefCell::default(),
|
||||
};
|
||||
match controller.get_id() {
|
||||
Ok(id) => {
|
||||
info!("initialized Nvidia GPU controller {id} for path {path:?}");
|
||||
controllers.insert(
|
||||
id,
|
||||
Box::new(controller) as Box<dyn GpuController>,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
error!("could not get Nvidia GPU id: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(NvmlError::NotFound) => {
|
||||
debug!("PCI slot {pci_slot_id} not found in NVML");
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"could not initialize Nvidia GPU at {path:?}: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("initialized GPU controller {id} for path {path:?}");
|
||||
controllers.insert(id, Box::new(controller) as Box<dyn GpuController>);
|
||||
}
|
||||
Err(err) => warn!("could not initialize controller: {err:#}"),
|
||||
},
|
||||
|
@ -217,7 +217,7 @@ impl AsyncComponent for AppModel {
|
||||
sender: AsyncComponentSender<Self>,
|
||||
root: &Self::Root,
|
||||
) {
|
||||
trace!("update {msg:#?}");
|
||||
trace!("processing state update");
|
||||
if let Err(err) = self.handle_msg(msg, sender.clone(), root, widgets).await {
|
||||
show_error(root, &err);
|
||||
}
|
||||
|
@ -89,9 +89,9 @@ impl WidgetImpl for Plot {
|
||||
#[derive(Default, Clone)]
|
||||
#[cfg_attr(feature = "bench", derive(Clone))]
|
||||
pub struct PlotData {
|
||||
line_series: BTreeMap<String, Vec<(i64, f64)>>,
|
||||
secondary_line_series: BTreeMap<String, Vec<(i64, f64)>>,
|
||||
throttling: Vec<(i64, (String, bool))>,
|
||||
pub(super) line_series: BTreeMap<String, Vec<(i64, f64)>>,
|
||||
pub(super) secondary_line_series: BTreeMap<String, Vec<(i64, f64)>>,
|
||||
pub(super) throttling: Vec<(i64, (String, bool))>,
|
||||
}
|
||||
|
||||
impl PlotData {
|
||||
|
@ -198,17 +198,31 @@ impl RenderRequest {
|
||||
let data = &self.data;
|
||||
|
||||
// Determine the start and end dates of the data series.
|
||||
let start_date = data
|
||||
let start_date_main = data
|
||||
.line_series_iter()
|
||||
.filter_map(|(_, data)| Some(data.first()?.0))
|
||||
.min()
|
||||
.unwrap_or_default();
|
||||
let end_date = data
|
||||
let start_date_secondary = data
|
||||
.secondary_line_series_iter()
|
||||
.filter_map(|(_, data)| Some(data.first()?.0))
|
||||
.min()
|
||||
.unwrap_or_default();
|
||||
let end_date_main = data
|
||||
.line_series_iter()
|
||||
.map(|(_, value)| value)
|
||||
.filter_map(|data| Some(data.first()?.0))
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
let end_date_secondary = data
|
||||
.secondary_line_series_iter()
|
||||
.map(|(_, value)| value)
|
||||
.filter_map(|data| Some(data.first()?.0))
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
|
||||
let start_date = max(start_date_main, start_date_secondary);
|
||||
let end_date = max(end_date_main, end_date_secondary);
|
||||
|
||||
// Calculate the maximum value for the y-axis.
|
||||
let mut maximum_value = data
|
||||
@ -225,10 +239,17 @@ impl RenderRequest {
|
||||
|
||||
root.fill(&WHITE)?; // Fill the background with white color.
|
||||
|
||||
let y_label_area_relative_size =
|
||||
if data.line_series.is_empty() && !data.secondary_line_series.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
self.y_label_area_relative_size
|
||||
};
|
||||
|
||||
// Set up the main chart with axes and labels.
|
||||
let mut chart = ChartBuilder::on(&root)
|
||||
.x_label_area_size(RelativeSize::Smaller(0.05))
|
||||
.y_label_area_size(RelativeSize::Smaller(self.y_label_area_relative_size))
|
||||
.y_label_area_size(RelativeSize::Smaller(y_label_area_relative_size))
|
||||
.right_y_label_area_size(RelativeSize::Smaller(
|
||||
self.secondary_y_label_relative_area_size,
|
||||
))
|
||||
@ -263,7 +284,7 @@ impl RenderRequest {
|
||||
// Configure the secondary axes (for the secondary y-axis).
|
||||
chart
|
||||
.configure_secondary_axes()
|
||||
.y_label_formatter(&|x| format!("{x}{}", self.secondary_value_suffix.as_str()))
|
||||
.y_label_formatter(&|x: &f64| format!("{x}{}", self.secondary_value_suffix.as_str()))
|
||||
.y_labels(10)
|
||||
.label_style(("sans-serif", RelativeSize::Smaller(0.08)))
|
||||
.draw()
|
||||
|
@ -1,10 +1,11 @@
|
||||
use crate::app::page_section::PageSection;
|
||||
use gtk::glib::{self, object::ObjectExt, subclass::object::DerivedObjectProperties, Object};
|
||||
use lact_client::schema::{DeviceInfo, DeviceStats, DrmInfo};
|
||||
use std::fmt::Write;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct HardwareInfoSection(ObjectSubclass<imp::HardwareInfoSection>)
|
||||
@extends gtk::Box, gtk::Widget,
|
||||
@extends gtk::Box, gtk::Widget, PageSection,
|
||||
@implements gtk::Orientable, gtk::Accessible, gtk::Buildable;
|
||||
}
|
||||
|
||||
@ -71,23 +72,40 @@ impl HardwareInfoSection {
|
||||
}
|
||||
|
||||
if let Some(drm_info) = &info.drm_info {
|
||||
self.set_gpu_family(drm_info.family_name.clone());
|
||||
self.set_asic_name(drm_info.asic_name.clone());
|
||||
self.set_compute_units(drm_info.compute_units.to_string());
|
||||
if let Some(family) = drm_info.family_name.as_deref() {
|
||||
self.set_gpu_family(family);
|
||||
}
|
||||
if let Some(asic) = drm_info.asic_name.as_deref() {
|
||||
self.set_asic_name(asic);
|
||||
}
|
||||
if let Some(units) = drm_info.compute_units {
|
||||
self.set_compute_units(units.to_string());
|
||||
}
|
||||
if let Some(cores) = drm_info.cuda_cores {
|
||||
self.set_cuda_cores(cores.to_string());
|
||||
}
|
||||
if let Some(vram_type) = drm_info.vram_type.as_deref() {
|
||||
self.set_vram_type(vram_type);
|
||||
}
|
||||
if let Some(max_bw) = &drm_info.vram_max_bw {
|
||||
self.set_peak_vram_bandwidth(format!("{max_bw} GiB/s"));
|
||||
}
|
||||
|
||||
self.set_vram_type(drm_info.vram_type.clone());
|
||||
self.set_peak_vram_bandwidth(format!("{} GiB/s", drm_info.vram_max_bw));
|
||||
self.set_l1_cache(format!("{} KiB", drm_info.l1_cache_per_cu / 1024));
|
||||
self.set_l2_cache(format!("{} KiB", drm_info.l2_cache / 1024));
|
||||
self.set_l3_cache(format!("{} MiB", drm_info.l3_cache_mb));
|
||||
if let Some(l1) = drm_info.l1_cache_per_cu {
|
||||
self.set_l1_cache(format!("{} KiB", l1 / 1024));
|
||||
}
|
||||
if let Some(l2) = drm_info.l2_cache {
|
||||
self.set_l2_cache(format!("{} KiB", l2 / 1024));
|
||||
}
|
||||
if let Some(l3) = drm_info.l3_cache_mb {
|
||||
self.set_l3_cache(format!("{l3} MiB"));
|
||||
}
|
||||
|
||||
if let Some(memory_info) = &drm_info.memory_info {
|
||||
let rebar = if memory_info.resizeable_bar {
|
||||
"Enabled"
|
||||
} else {
|
||||
"Disabled"
|
||||
};
|
||||
self.set_rebar(rebar);
|
||||
if let Some(rebar) = memory_info.resizeable_bar {
|
||||
let rebar = if rebar { "Enabled" } else { "Disabled" };
|
||||
self.set_rebar(rebar);
|
||||
}
|
||||
|
||||
self.set_cpu_accessible_vram(format!(
|
||||
"{} MiB",
|
||||
@ -96,7 +114,7 @@ impl HardwareInfoSection {
|
||||
}
|
||||
}
|
||||
|
||||
self.set_driver_used(info.driver);
|
||||
self.set_driver_used(info.driver.as_str());
|
||||
|
||||
if let Some(vbios) = &info.vbios_version {
|
||||
self.set_vbios_version(vbios.clone());
|
||||
@ -118,7 +136,7 @@ impl HardwareInfoSection {
|
||||
fn reset(&self) {
|
||||
let properties = imp::HardwareInfoSection::derived_properties();
|
||||
for property in properties {
|
||||
self.set_property(property.name(), "Unknown");
|
||||
self.set_property(property.name(), "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -161,6 +179,8 @@ mod imp {
|
||||
#[property(get, set)]
|
||||
compute_units: RefCell<String>,
|
||||
#[property(get, set)]
|
||||
cuda_cores: RefCell<String>,
|
||||
#[property(get, set)]
|
||||
vbios_version: RefCell<String>,
|
||||
#[property(get, set)]
|
||||
driver_used: RefCell<String>,
|
||||
@ -203,7 +223,20 @@ mod imp {
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for HardwareInfoSection {}
|
||||
impl ObjectImpl for HardwareInfoSection {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
let obj = self.obj();
|
||||
|
||||
for child in obj.observe_children().into_iter().flatten() {
|
||||
if let Ok(row) = child.downcast::<InfoRow>() {
|
||||
row.connect_value_notify(|row| {
|
||||
row.set_visible(!row.value().is_empty());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for HardwareInfoSection {}
|
||||
impl BoxImpl for HardwareInfoSection {}
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::app::page_section::PageSection;
|
||||
use gtk::glib::{self, Object};
|
||||
use lact_client::schema::{DeviceStats, PowerStats};
|
||||
use std::fmt::Write;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct GpuStatsSection(ObjectSubclass<imp::GpuStatsSection>)
|
||||
@ -38,12 +39,16 @@ impl GpuStatsSection {
|
||||
let voltage = format!("{:.3} V", stats.voltage.gpu.unwrap_or(0) as f64 / 1000f64);
|
||||
self.set_voltage(voltage);
|
||||
|
||||
let temperature = stats
|
||||
.temps
|
||||
.get("junction")
|
||||
.or_else(|| stats.temps.get("edge"))
|
||||
.and_then(|temp| temp.current)
|
||||
.unwrap_or(0.0);
|
||||
let temperature = if stats.temps.len() == 1 {
|
||||
stats.temps.values().next().unwrap().current
|
||||
} else {
|
||||
stats
|
||||
.temps
|
||||
.get("junction")
|
||||
.or_else(|| stats.temps.get("edge"))
|
||||
.and_then(|temp| temp.current)
|
||||
}
|
||||
.unwrap_or(0.0);
|
||||
self.set_temperature(format!("{temperature}°C"));
|
||||
|
||||
self.set_gpu_usage(format!("{}%", stats.busy_percent.unwrap_or(0)));
|
||||
@ -60,7 +65,7 @@ impl GpuStatsSection {
|
||||
.or(power_average);
|
||||
|
||||
self.set_power_usage(format!(
|
||||
"<b>{}/{} W</b>",
|
||||
"<b>{:.1}/{} W</b>",
|
||||
power_current.unwrap_or(0.0),
|
||||
power_cap_current.unwrap_or(0.0)
|
||||
));
|
||||
@ -73,7 +78,11 @@ impl GpuStatsSection {
|
||||
let type_text: Vec<String> = throttle_info
|
||||
.iter()
|
||||
.map(|(throttle_type, details)| {
|
||||
format!("{throttle_type} ({})", details.join(", "))
|
||||
let mut out = throttle_type.to_string();
|
||||
if !details.is_empty() {
|
||||
let _ = write!(out, "({})", details.join(", "));
|
||||
}
|
||||
out
|
||||
})
|
||||
.collect();
|
||||
let text = type_text.join(", ");
|
||||
|
@ -1,6 +1,5 @@
|
||||
use gtk::glib::{self, Object};
|
||||
use lact_client::schema::PowerState;
|
||||
use std::fmt::Display;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct PowerStateRow(ObjectSubclass<imp::PowerStateRow>)
|
||||
@ -9,8 +8,14 @@ glib::wrapper! {
|
||||
}
|
||||
|
||||
impl PowerStateRow {
|
||||
pub fn new<T: Display>(power_state: PowerState<T>, index: u8, value_suffix: &str) -> Self {
|
||||
let title = format!("{}: {} {}", index, power_state.value, value_suffix);
|
||||
pub fn new(power_state: PowerState, index: u8, value_suffix: &str) -> Self {
|
||||
let index = power_state.index.unwrap_or(index);
|
||||
|
||||
let value_text = match power_state.min_value {
|
||||
Some(min) if min != power_state.value => format!("{min}-{}", power_state.value),
|
||||
_ => power_state.value.to_string(),
|
||||
};
|
||||
let title = format!("{index}: {value_text} {value_suffix}");
|
||||
Object::builder()
|
||||
.property("enabled", power_state.enabled)
|
||||
.property("title", title)
|
||||
|
@ -11,7 +11,6 @@ use gtk::{
|
||||
ListBoxRow, Widget,
|
||||
};
|
||||
use lact_client::schema::PowerState;
|
||||
use std::fmt::Display;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct PowerStatesList(ObjectSubclass<imp::PowerStatesList>)
|
||||
@ -32,11 +31,7 @@ impl PowerStatesList {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn set_power_states<T: Display>(
|
||||
&self,
|
||||
power_states: Vec<PowerState<T>>,
|
||||
value_suffix: &str,
|
||||
) {
|
||||
pub fn set_power_states(&self, power_states: Vec<PowerState>, value_suffix: &str) {
|
||||
let store = gio::ListStore::new::<PowerStateRow>();
|
||||
for (i, state) in power_states.into_iter().enumerate() {
|
||||
let index = u8::try_from(i).expect("Power state index doesn't fit in u8?");
|
||||
|
@ -1,6 +1,11 @@
|
||||
mod fan_curve_frame;
|
||||
mod pmfw_frame;
|
||||
|
||||
use std::{
|
||||
rc::Rc,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
@ -40,6 +45,7 @@ pub struct ThermalsPage {
|
||||
fan_curve_frame: FanCurveFrame,
|
||||
fan_control_mode_stack: Stack,
|
||||
fan_control_mode_stack_switcher: StackSwitcher,
|
||||
is_amd: Rc<AtomicBool>,
|
||||
|
||||
overdrive_enabled: Option<bool>,
|
||||
}
|
||||
@ -98,11 +104,19 @@ impl ThermalsPage {
|
||||
|
||||
container.append(&fan_control_section);
|
||||
|
||||
fan_control_mode_stack.connect_visible_child_name_notify(|stack| {
|
||||
if stack.visible_child_name() == Some("automatic".into()) {
|
||||
show_fan_control_warning()
|
||||
let is_amd = Rc::new(AtomicBool::new(false));
|
||||
|
||||
fan_control_mode_stack.connect_visible_child_name_notify(clone!(
|
||||
#[strong]
|
||||
is_amd,
|
||||
move |stack| {
|
||||
if stack.visible_child_name() == Some("automatic".into())
|
||||
&& is_amd.load(Ordering::SeqCst)
|
||||
{
|
||||
show_fan_control_warning()
|
||||
}
|
||||
}
|
||||
});
|
||||
));
|
||||
|
||||
Self {
|
||||
pmfw_warning_label,
|
||||
@ -115,22 +129,29 @@ impl ThermalsPage {
|
||||
fan_control_mode_stack_switcher,
|
||||
pmfw_frame,
|
||||
overdrive_enabled: system_info.amdgpu_overdrive_enabled,
|
||||
is_amd,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_info(&self, info: &DeviceInfo) {
|
||||
let pmfw_disabled = info.drm_info.as_ref().is_some_and(|info| {
|
||||
debug!(
|
||||
"family id: {}, overdrive enabled {:?}",
|
||||
"family id: {:?}, overdrive enabled {:?}",
|
||||
info.family_id, self.overdrive_enabled
|
||||
);
|
||||
(info.family_id >= AMDGPU_FAMILY_GC_11_0_0) && (self.overdrive_enabled != Some(true))
|
||||
(info.family_id.unwrap_or(0) >= AMDGPU_FAMILY_GC_11_0_0)
|
||||
&& (self.overdrive_enabled != Some(true))
|
||||
});
|
||||
self.pmfw_warning_label.set_visible(pmfw_disabled);
|
||||
|
||||
let sensitive = self.fan_control_mode_stack_switcher.is_sensitive() && !pmfw_disabled;
|
||||
self.fan_control_mode_stack_switcher
|
||||
.set_sensitive(sensitive);
|
||||
|
||||
self.is_amd.store(
|
||||
matches!(info.driver.as_str(), "radeon" | "amdgpu"),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_stats(&self, stats: &DeviceStats, initial: bool) {
|
||||
|
@ -1,68 +1,67 @@
|
||||
using Gtk 4.0;
|
||||
|
||||
template $GraphsWindow: Window {
|
||||
default-height: 400;
|
||||
default-width: 1200;
|
||||
title: "Historical data";
|
||||
hide-on-close: true;
|
||||
default-height: 400;
|
||||
default-width: 1200;
|
||||
title: "Historical data";
|
||||
hide-on-close: true;
|
||||
|
||||
Grid {
|
||||
margin-top: 10;
|
||||
margin-bottom: 10;
|
||||
margin-start: 10;
|
||||
margin-end: 10;
|
||||
Grid {
|
||||
margin-top: 10;
|
||||
margin-bottom: 10;
|
||||
margin-start: 10;
|
||||
margin-end: 10;
|
||||
row-spacing: 20;
|
||||
column-spacing: 20;
|
||||
|
||||
row-spacing: 20;
|
||||
column-spacing: 20;
|
||||
$Plot temperature_plot {
|
||||
title: "Temperature";
|
||||
hexpand: true;
|
||||
value-suffix: "°C";
|
||||
y-label-area-relative-size: 0.15;
|
||||
|
||||
$Plot temperature_plot {
|
||||
title: "Temperature";
|
||||
hexpand: true;
|
||||
value-suffix: "°C";
|
||||
y-label-area-relative-size: 0.15;
|
||||
layout {
|
||||
column: 0;
|
||||
row: 0;
|
||||
}
|
||||
}
|
||||
|
||||
layout {
|
||||
column: 0;
|
||||
row: 0;
|
||||
}
|
||||
}
|
||||
$Plot fan_plot {
|
||||
title: "Fan speed";
|
||||
hexpand: true;
|
||||
value-suffix: "RPM";
|
||||
secondary-value-suffix: "%";
|
||||
y-label-area-relative-size: 0.25;
|
||||
secondary-y-label-area-relative-size: 0.15;
|
||||
|
||||
$Plot fan_plot {
|
||||
title: "Fan speed";
|
||||
hexpand: true;
|
||||
value-suffix: "RPM";
|
||||
secondary-value-suffix: "%";
|
||||
y-label-area-relative-size: 0.25;
|
||||
secondary-y-label-area-relative-size: 0.15;
|
||||
layout {
|
||||
column: 0;
|
||||
row: 1;
|
||||
}
|
||||
}
|
||||
|
||||
layout {
|
||||
column: 0;
|
||||
row: 1;
|
||||
}
|
||||
}
|
||||
$Plot clockspeed_plot {
|
||||
title: "Clockspeed";
|
||||
hexpand: true;
|
||||
value-suffix: "MHz";
|
||||
y-label-area-relative-size: 0.3;
|
||||
|
||||
$Plot clockspeed_plot {
|
||||
title: "Clockspeed";
|
||||
hexpand: true;
|
||||
value-suffix: "MHz";
|
||||
y-label-area-relative-size: 0.25;
|
||||
layout {
|
||||
column: 1;
|
||||
row: 0;
|
||||
}
|
||||
}
|
||||
|
||||
layout {
|
||||
column: 1;
|
||||
row: 0;
|
||||
}
|
||||
}
|
||||
$Plot power_plot {
|
||||
title: "Power usage";
|
||||
hexpand: true;
|
||||
value-suffix: "W";
|
||||
y-label-area-relative-size: 0.2;
|
||||
|
||||
$Plot power_plot {
|
||||
title: "Power usage";
|
||||
hexpand: true;
|
||||
value-suffix: "W";
|
||||
y-label-area-relative-size: 0.2;
|
||||
|
||||
layout {
|
||||
column: 1;
|
||||
row: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
layout {
|
||||
column: 1;
|
||||
row: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,12 @@ template $HardwareInfoSection: $PageSection {
|
||||
selectable: true;
|
||||
}
|
||||
|
||||
$InfoRow {
|
||||
name: "Cuda Cores:";
|
||||
value: bind template.cuda_cores;
|
||||
selectable: true;
|
||||
}
|
||||
|
||||
$InfoRow {
|
||||
name: "VBIOS Version:";
|
||||
value: bind template.vbios_version;
|
||||
|
@ -93,7 +93,7 @@ pub struct DeviceInfo<'a> {
|
||||
#[serde(borrow)]
|
||||
pub pci_info: Option<Cow<'a, GpuPciInfo>>,
|
||||
pub vulkan_info: Option<VulkanInfo>,
|
||||
pub driver: &'a str,
|
||||
pub driver: String,
|
||||
pub vbios_version: Option<String>,
|
||||
pub link_info: LinkInfo,
|
||||
pub drm_info: Option<DrmInfo>,
|
||||
@ -103,19 +103,19 @@ pub struct DeviceInfo<'a> {
|
||||
pub struct DrmInfo {
|
||||
pub device_name: Option<String>,
|
||||
pub pci_revision_id: Option<u32>,
|
||||
pub family_name: String,
|
||||
#[serde(default)]
|
||||
pub family_id: u32,
|
||||
pub asic_name: String,
|
||||
pub chip_class: String,
|
||||
pub compute_units: u32,
|
||||
pub vram_type: String,
|
||||
pub family_name: Option<String>,
|
||||
pub family_id: Option<u32>,
|
||||
pub asic_name: Option<String>,
|
||||
pub chip_class: Option<String>,
|
||||
pub compute_units: Option<u32>,
|
||||
pub cuda_cores: Option<u32>,
|
||||
pub vram_type: Option<String>,
|
||||
pub vram_clock_ratio: f64,
|
||||
pub vram_bit_width: u32,
|
||||
pub vram_max_bw: String,
|
||||
pub l1_cache_per_cu: u32,
|
||||
pub l2_cache: u32,
|
||||
pub l3_cache_mb: u32,
|
||||
pub vram_bit_width: Option<u32>,
|
||||
pub vram_max_bw: Option<String>,
|
||||
pub l1_cache_per_cu: Option<u32>,
|
||||
pub l2_cache: Option<u32>,
|
||||
pub l3_cache_mb: Option<u32>,
|
||||
pub memory_info: Option<DrmMemoryInfo>,
|
||||
}
|
||||
|
||||
@ -123,7 +123,7 @@ pub struct DrmInfo {
|
||||
pub struct DrmMemoryInfo {
|
||||
pub cpu_accessible_used: u64,
|
||||
pub cpu_accessible_total: u64,
|
||||
pub resizeable_bar: bool,
|
||||
pub resizeable_bar: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
@ -148,7 +148,7 @@ impl From<ClocksTableGen> for ClocksInfo {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct LinkInfo {
|
||||
pub current_width: Option<String>,
|
||||
pub current_speed: Option<String>,
|
||||
@ -182,7 +182,7 @@ pub struct PciInfo {
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct DeviceStats {
|
||||
pub fan: FanStats,
|
||||
pub clockspeed: ClockspeedStats,
|
||||
@ -198,7 +198,7 @@ pub struct DeviceStats {
|
||||
pub throttle_info: Option<BTreeMap<String, Vec<String>>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct FanStats {
|
||||
pub control_enabled: bool,
|
||||
pub control_mode: Option<FanControlMode>,
|
||||
@ -224,26 +224,26 @@ pub struct PmfwInfo {
|
||||
pub minimum_pwm: Option<FanInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
|
||||
pub struct ClockspeedStats {
|
||||
pub gpu_clockspeed: Option<u64>,
|
||||
pub current_gfxclk: Option<u16>,
|
||||
pub vram_clockspeed: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
|
||||
pub struct VoltageStats {
|
||||
pub gpu: Option<u64>,
|
||||
pub northbridge: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
|
||||
pub struct VramStats {
|
||||
pub total: Option<u64>,
|
||||
pub used: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
|
||||
pub struct PowerStats {
|
||||
pub average: Option<f64>,
|
||||
pub current: Option<f64>,
|
||||
@ -253,10 +253,10 @@ pub struct PowerStats {
|
||||
pub cap_default: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct PowerStates {
|
||||
pub core: Vec<PowerState<u64>>,
|
||||
pub vram: Vec<PowerState<u64>>,
|
||||
pub core: Vec<PowerState>,
|
||||
pub vram: Vec<PowerState>,
|
||||
}
|
||||
|
||||
impl PowerStates {
|
||||
@ -266,9 +266,11 @@ impl PowerStates {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
pub struct PowerState<T> {
|
||||
pub struct PowerState {
|
||||
pub enabled: bool,
|
||||
pub value: T,
|
||||
pub min_value: Option<u64>,
|
||||
pub value: u64,
|
||||
pub index: Option<u8>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
Loading…
Reference in New Issue
Block a user