Merge branch 'master' into display-info

This commit is contained in:
Ilya Zlobintsev
2026-06-04 12:56:10 +03:00
3574 changed files with 1496 additions and 787 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Docker Cache
uses: AndreKurait/docker-cache@0.6.0
uses: AndreKurait/docker-cache@eb0683e3972ed3066587b7199d3a0e9bd114bd3c #0.6.0
with:
key: docker-${{ matrix.target-os }}-${{ hashFiles('pkg/images/') }}
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
- name: Install clippy
run: rustup component add clippy
- name: Run clippy
run: cargo clippy --all --all-features --verbose
run: cargo clippy --all --all-features -- --deny clippy::all
check-audit:
runs-on: ubuntu-24.04
Generated
+4 -4
View File
@@ -31,9 +31,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "amdgpu-sysfs"
version = "0.20.1"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e50f319907827f2c8dcd43fd1472e6103384bc3e8764a370db3db7a4e983817f"
checksum = "4c67148270d0240773b7c69d6a98949a0f1e20fe91a756add562b5e6d3de3cf2"
dependencies = [
"enum_dispatch",
"serde",
@@ -1855,9 +1855,9 @@ dependencies = [
[[package]]
name = "libdrm_amdgpu_sys"
version = "0.8.14"
version = "0.8.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b63f219bdee8484c65e0f54f81b6c671f1ab433f460e40c3174dea688abf31b"
checksum = "1e11704169b7b5a043ec5627aa0433019cf67d07f233002e1233edf67c2e662b"
dependencies = [
"libc",
"libloading",
+1 -1
View File
@@ -13,7 +13,7 @@ members = [
edition = "2024"
[workspace.dependencies]
amdgpu-sysfs = { version = "0.20.1", features = ["serde"] }
amdgpu-sysfs = { version = "0.21.0", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_with = { version = "3.18.0", default-features = false, features = [
"macros",
+10 -10
View File
@@ -54,14 +54,14 @@
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/amdgpu-sysfs/amdgpu-sysfs-0.20.1.crate",
"sha256": "e50f319907827f2c8dcd43fd1472e6103384bc3e8764a370db3db7a4e983817f",
"dest": "cargo/vendor/amdgpu-sysfs-0.20.1"
"url": "https://static.crates.io/crates/amdgpu-sysfs/amdgpu-sysfs-0.21.0.crate",
"sha256": "4c67148270d0240773b7c69d6a98949a0f1e20fe91a756add562b5e6d3de3cf2",
"dest": "cargo/vendor/amdgpu-sysfs-0.21.0"
},
{
"type": "inline",
"contents": "{\"package\": \"e50f319907827f2c8dcd43fd1472e6103384bc3e8764a370db3db7a4e983817f\", \"files\": {}}",
"dest": "cargo/vendor/amdgpu-sysfs-0.20.1",
"contents": "{\"package\": \"4c67148270d0240773b7c69d6a98949a0f1e20fe91a756add562b5e6d3de3cf2\", \"files\": {}}",
"dest": "cargo/vendor/amdgpu-sysfs-0.21.0",
"dest-filename": ".cargo-checksum.json"
},
{
@@ -2173,14 +2173,14 @@
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/libdrm_amdgpu_sys/libdrm_amdgpu_sys-0.8.14.crate",
"sha256": "3b63f219bdee8484c65e0f54f81b6c671f1ab433f460e40c3174dea688abf31b",
"dest": "cargo/vendor/libdrm_amdgpu_sys-0.8.14"
"url": "https://static.crates.io/crates/libdrm_amdgpu_sys/libdrm_amdgpu_sys-0.8.16.crate",
"sha256": "1e11704169b7b5a043ec5627aa0433019cf67d07f233002e1233edf67c2e662b",
"dest": "cargo/vendor/libdrm_amdgpu_sys-0.8.16"
},
{
"type": "inline",
"contents": "{\"package\": \"3b63f219bdee8484c65e0f54f81b6c671f1ab433f460e40c3174dea688abf31b\", \"files\": {}}",
"dest": "cargo/vendor/libdrm_amdgpu_sys-0.8.14",
"contents": "{\"package\": \"1e11704169b7b5a043ec5627aa0433019cf67d07f233002e1233edf67c2e662b\", \"files\": {}}",
"dest": "cargo/vendor/libdrm_amdgpu_sys-0.8.16",
"dest-filename": ".cargo-checksum.json"
},
{
+1 -1
View File
@@ -45,7 +45,7 @@ After=multi-user.target
[Service]
Environment=FLATPAK_INSTALL_USER=$USER
ExecStart=$DAEMON_SH_PATH
ExecStart=bash $DAEMON_SH_PATH
Nice=-10
Restart=on-failure
+1 -1
View File
@@ -30,7 +30,7 @@ pub async fn info(ctx: CliContext<'_>) -> Result<()> {
println!("{gpu_line}");
println!("{}", "=".repeat(gpu_line.len()));
let info = ctx.client.get_device_info(&id).await?;
let info = ctx.client.get_device_info(&id, Some(false)).await?;
let stats = ctx.client.get_device_stats(&id).await?;
let elements = info.info_elements(Some(&stats));
+14 -2
View File
@@ -4,7 +4,7 @@ mod macros;
pub use lact_schema as schema;
use lact_schema::{
ProcessList, ProfileRule,
DeviceApiInfo, ProcessList, ProfileRule,
config::{GpuConfig, Profile, ProfileHooks},
};
@@ -126,12 +126,24 @@ impl DaemonClient {
self.make_request(Request::ListDevices).await
}
pub async fn get_device_info(
&self,
id: &str,
include_api_info: Option<bool>,
) -> anyhow::Result<DeviceInfo> {
self.make_request(Request::DeviceInfo {
id,
include_api_info,
})
.await
}
request_plain!(get_system_info, SystemInfo, SystemInfo);
request_plain!(enable_overdrive, EnableOverdrive, String);
request_plain!(disable_overdrive, DisableOverdrive, String);
request_plain!(generate_debug_snapshot, GenerateSnapshot, String);
request_plain!(reset_config, RestConfig, ());
request_with_id!(get_device_info, DeviceInfo, DeviceInfo);
request_with_id!(get_device_api_info, DeviceApiInfo, DeviceApiInfo);
request_with_id!(get_device_stats, DeviceStats, DeviceStats);
request_with_id!(get_device_clocks_info, DeviceClocksInfo, ClocksInfo);
request_with_id!(
+1 -1
View File
@@ -37,7 +37,7 @@ nvml-wrapper = "0.12.1"
bitflags = "2.11.1"
pciid-parser = { version = "0.8", features = ["serde"] }
zbus = { version = "5.14.0", default-features = false, features = ["tokio"] }
libdrm_amdgpu_sys = { version = "0.8.13", default-features = false, features = [
libdrm_amdgpu_sys = { version = "0.8.16", default-features = false, features = [
"dynamic_loading",
] }
tar = "0.4.45"
+15 -23
View File
@@ -17,12 +17,12 @@ use server::{Server, handle_stream, handler::Handler};
use std::sync::Arc;
use std::{os::unix::net::UnixStream as StdUnixStream, time::Duration};
use tokio::net::UnixStream;
use tokio::runtime::LocalOptions;
use tokio::sync::Notify;
use tokio::time::timeout;
use tokio::{
runtime,
signal::unix::{SignalKind, signal},
task::LocalSet,
};
use tracing::level_filters::LevelFilter;
use tracing::{Instrument, debug, debug_span, error, info, warn};
@@ -46,7 +46,7 @@ const SHUTDOWN_SIGNALS: [SignalKind; 4] = [
pub fn run() -> anyhow::Result<()> {
let rt = runtime::Builder::new_current_thread()
.enable_all()
.build()
.build_local(LocalOptions::default())
.expect("Could not initialize tokio runtime");
rt.block_on(async {
let config = Config::load_or_create()?;
@@ -59,20 +59,16 @@ pub fn run() -> anyhow::Result<()> {
ensure_sufficient_uptime().await;
LocalSet::new()
.run_until(async move {
let server = Server::new(config).await?;
let handler = server.handler.clone();
let server = Server::new(config).await?;
let handler = server.handler.clone();
tokio::task::spawn_local(listen_config_changes(handler.clone()));
tokio::task::spawn_local(listen_exit_signals(handler.clone()));
tokio::task::spawn_local(listen_device_events(handler.clone()));
tokio::task::spawn_local(suspend::listen_events(handler));
tokio::task::spawn_local(listen_config_changes(handler.clone()));
tokio::task::spawn_local(listen_exit_signals(handler.clone()));
tokio::task::spawn_local(listen_device_events(handler.clone()));
tokio::task::spawn_local(suspend::listen_events(handler));
server.run().await;
Ok(())
})
.await
server.run().await;
Ok(())
})
}
@@ -84,18 +80,14 @@ pub fn run() -> anyhow::Result<()> {
pub fn run_embedded(stream: StdUnixStream) -> anyhow::Result<()> {
let rt = runtime::Builder::new_current_thread()
.enable_all()
.build()
.build_local(LocalOptions::default())
.expect("Could not initialize tokio runtime");
rt.block_on(async {
LocalSet::new()
.run_until(async move {
let config = Config::default();
let handler = Handler::new(config).await?;
let stream = UnixStream::try_from(stream)?;
let config = Config::default();
let handler = Handler::new(config).await?;
let stream = UnixStream::try_from(stream)?;
handle_stream(stream, handler).await
})
.await
handle_stream(stream, handler).await
})
}
+5 -1
View File
@@ -158,7 +158,11 @@ async fn handle_request<'a>(
Request::Ping => ok_response(ping()),
Request::SystemInfo => ok_response(system::info().await?),
Request::ListDevices => ok_response(handler.list_devices().await),
Request::DeviceInfo { id } => ok_response(handler.get_device_info(id).await?),
Request::DeviceInfo {
id,
include_api_info,
} => ok_response(handler.get_device_info(id, include_api_info).await?),
Request::DeviceApiInfo { id } => ok_response(handler.get_device_api_info(id).await?),
Request::DeviceStats { id } => ok_response(handler.get_gpu_stats(id).await?),
Request::DeviceClocksInfo { id } => ok_response(handler.get_clocks_info(id).await?),
Request::DisplaysInfo { id } => ok_response(handler.get_displays_info(id).await?),
+30 -1
View File
@@ -7,16 +7,20 @@ mod nvidia;
use amd::AmdGpuController;
use intel::IntelGpuController;
use lact_schema::DeviceApiInfo;
use lact_schema::DeviceType;
use lact_schema::DisplaysInfo;
use lact_schema::ProcessList;
#[cfg(feature = "nvidia")]
use nvidia::NvidiaGpuController;
use tokio::join;
pub const VENDOR_AMD: &str = "1002";
pub const VENDOR_NVIDIA: &str = "10DE";
use crate::server::handler::{AMD_DRM, INTEL_DRM, NVML};
use crate::server::opencl::get_opencl_info;
use crate::server::vulkan::get_vulkan_info;
use amdgpu_sysfs::gpu_handle::power_profile_mode::PowerProfileModesTable;
use anyhow::Context;
use anyhow::anyhow;
@@ -44,7 +48,32 @@ pub trait GpuController {
fn device_type(&self) -> DeviceType;
fn get_info(&self, unique_vendor: bool) -> LocalBoxFuture<'_, DeviceInfo>;
fn get_info(
&self,
unique_vendor: bool,
include_api_info: bool,
) -> LocalBoxFuture<'_, DeviceInfo>;
fn get_api_info(&self, unique_vendor: bool) -> LocalBoxFuture<'_, DeviceApiInfo> {
async move {
let common = self.controller_info();
let (vulkan_result, opencl_instances) = join!(
get_vulkan_info(common),
get_opencl_info(common, unique_vendor)
);
let vulkan_instances = vulkan_result.unwrap_or_else(|err| {
warn!("could not load vulkan info: {err:#}");
vec![]
});
DeviceApiInfo {
vulkan_instances,
opencl_instances,
}
}
.boxed_local()
}
fn friendly_name(&self) -> Option<String>;
+63 -53
View File
@@ -1,16 +1,12 @@
use super::{CommonControllerInfo, FanControlHandle, GpuController, VENDOR_AMD};
use crate::server::{
gpu_controller::common::{
fan_control::FanCurveExt,
fdinfo::{self, DrmUtilMap},
},
opencl::get_opencl_info,
vulkan::get_vulkan_info,
use crate::server::gpu_controller::common::{
fan_control::FanCurveExt,
fdinfo::{self, DrmUtilMap},
};
use amdgpu_sysfs::{
error::Error,
gpu_handle::{
CommitHandle, GpuHandle, PerformanceLevel, PowerLevelKind,
CommitHandle, GpuHandle, PerformanceLevel, PowerLevelId, PowerLevelKind,
fan_control::FanCurve as PmfwCurve,
overdrive::{ClocksTable, ClocksTableGen},
power_profile_mode::PowerProfileModesTable,
@@ -19,12 +15,13 @@ use amdgpu_sysfs::{
sysfs::SysFS,
};
use anyhow::{Context, anyhow, bail};
use futures::{FutureExt, future::LocalBoxFuture, join};
use futures::{FutureExt, future::LocalBoxFuture};
use lact_schema::{
AmdCacheInstance, AmdIpInfo, CacheInfo, CacheType, ClocksInfo, ClockspeedStats, DeviceFlag,
DeviceInfo, DeviceStats, DeviceType, DisplayConnector, DisplaysInfo, DrmInfo, FanControlMode,
FanStats, IntelDrmInfo, LinkInfo, PmfwInfo, PowerState, PowerStates, PowerStats, ProcessList,
ProcessUtilizationType, RopInfo, TemperatureEntry, VoltageStats, VramStats,
ActivePowerStates, AmdCacheInstance, AmdIpInfo, CacheInfo, CacheType, ClocksInfo,
ClockspeedStats, DeviceApiInfo, DeviceFlag, DeviceInfo, DeviceStats, DeviceType,
DisplayConnector, DisplaysInfo, DrmInfo, FanControlMode, FanStats, IntelDrmInfo, LinkInfo,
PmfwInfo, PowerState, PowerStates, PowerStats, ProcessList, ProcessUtilizationType, RopInfo,
TemperatureEntry, VoltageStats, VramStats,
config::{ClocksConfiguration, FanControlSettings, FanCurve, GpuConfig},
};
use libdrm_amdgpu_sys::AMDGPU::{GpuMetrics, HW_IP::HW_IP_TYPE, ThrottlerBit, ThrottlerType};
@@ -445,7 +442,9 @@ impl AmdGpuController {
.unwrap_or_default();
if attempt < MAX_PSTATE_READ_ATTEMPTS
&& levels.iter().any(|value| *value >= u64::from(u16::MAX))
&& levels
.iter()
.any(|level| level.value >= u64::from(u16::MAX))
{
debug!("GPU reported nonsensical {kind:?} power state values, retrying: {levels:?}");
return self.get_power_states_kind(gpu_config, kind, attempt + 1);
@@ -453,15 +452,18 @@ impl AmdGpuController {
levels
.into_iter()
.enumerate()
.map(|(i, value)| {
let i = u8::try_from(i).unwrap();
let enabled = enabled_states.is_none_or(|enabled| enabled.contains(&i));
.map(|level| {
let enabled = match level.id {
PowerLevelId::Index(index) => {
enabled_states.is_none_or(|enabled| enabled.contains(&index))
}
PowerLevelId::Sleep => true,
};
PowerState {
enabled,
min_value: None,
value,
index: Some(i),
value: level.value,
id: Some(level.id),
}
})
.collect()
@@ -734,16 +736,17 @@ impl GpuController for AmdGpuController {
})
}
fn get_info(&self, unique_vendor: bool) -> LocalBoxFuture<'_, DeviceInfo> {
fn get_info(
&self,
unique_vendor: bool,
include_api_info: bool,
) -> LocalBoxFuture<'_, DeviceInfo> {
Box::pin(async move {
let (vulkan_result, opencl_instances) = join!(
get_vulkan_info(&self.common),
get_opencl_info(&self.common, unique_vendor)
);
let vulkan_instances = vulkan_result.unwrap_or_else(|err| {
warn!("could not load vulkan info: {err:#}");
vec![]
});
let api_info = if include_api_info {
self.get_api_info(unique_vendor).await
} else {
DeviceApiInfo::default()
};
let pci_info = Some(self.common.pci_info.clone());
let driver = self.handle.get_driver().to_owned();
@@ -782,11 +785,10 @@ impl GpuController for AmdGpuController {
DeviceInfo {
pci_info,
vulkan_instances,
api_info,
driver,
vbios_version,
link_info,
opencl_instances,
drm_info,
flags,
}
@@ -1015,24 +1017,34 @@ impl GpuController for AmdGpuController {
temps,
busy_percent: self.handle.get_busy_percent().ok(),
performance_level: self.handle.get_power_force_performance_level().ok(),
core_power_state: self
.handle
.get_core_clock_levels()
.inspect_err(|err| debug!("could not get active core power state: {err:#}"))
.ok()
.and_then(|levels| levels.active),
memory_power_state: self
.handle
.get_memory_clock_levels()
.inspect_err(|err| debug!("could not get active memory power state: {err:#}"))
.ok()
.and_then(|levels| levels.active),
pcie_power_state: self
.handle
.get_pcie_clock_levels()
.inspect_err(|err| debug!("could not get active PCIe power state: {err:#}"))
.ok()
.and_then(|levels| levels.active),
active_power_states: {
let active_power_states = ActivePowerStates {
core: self
.handle
.get_core_clock_levels()
.inspect_err(|err| debug!("could not get active core power state: {err:#}"))
.ok()
.and_then(|levels| levels.active),
memory: self
.handle
.get_memory_clock_levels()
.inspect_err(|err| {
debug!("could not get active memory power state: {err:#}");
})
.ok()
.and_then(|levels| levels.active),
pcie: self
.handle
.get_pcie_clock_levels()
.inspect_err(|err| debug!("could not get active PCIe power state: {err:#}"))
.ok()
.and_then(|levels| levels.active),
};
(active_power_states.core.is_some()
|| active_power_states.memory.is_some()
|| active_power_states.pcie.is_some())
.then_some(active_power_states)
},
throttle_info,
}
}
@@ -1162,7 +1174,7 @@ impl GpuController for AmdGpuController {
.set_power_force_performance_level(performance_level)
.context("Failed to set power performance level")?;
}
PerformanceLevel::High | PerformanceLevel::Low => {
_ => {
deferred_performance_level = Some(performance_level);
}
}
@@ -1452,8 +1464,6 @@ fn get_drm_handle(
common: &CommonControllerInfo,
libdrm_amdgpu: &LibDrmAmdgpu,
) -> anyhow::Result<DrmHandle> {
use std::os::unix::io::AsRawFd;
let path = common.get_drm_render()?;
let drm_file = fs::OpenOptions::new()
.read(true)
@@ -1461,7 +1471,7 @@ fn get_drm_handle(
.open(&path)
.with_context(|| format!("Could not open drm file at {}", path.display()))?;
let (handle, _, _) = libdrm_amdgpu
.init_device_handle(drm_file.as_raw_fd())
.init_device_handle_with_fd(drm_file)
.map_err(|err| anyhow!("Could not open drm handle, error code {err}"))?;
Ok(handle)
}
+18 -22
View File
@@ -6,20 +6,16 @@ use crate::{
IntelDrm, drm_i915_gem_memory_class_I915_MEMORY_CLASS_DEVICE,
drm_xe_memory_class_DRM_XE_MEM_REGION_CLASS_VRAM,
},
server::{
gpu_controller::common::fdinfo::{self, DrmUtilMap},
opencl::get_opencl_info,
vulkan::get_vulkan_info,
},
server::gpu_controller::common::fdinfo::{self, DrmUtilMap},
};
use amdgpu_sysfs::{gpu_handle::power_profile_mode::PowerProfileModesTable, hw_mon::Temperature};
use anyhow::{Context, anyhow, bail};
use futures::{future::LocalBoxFuture, join};
use futures::future::LocalBoxFuture;
use lact_schema::{
ClocksInfo, ClocksTable, ClockspeedStats, DeviceInfo, DeviceStats, DeviceType, DisplaysInfo,
DrmInfo, DrmMemoryInfo, FanStats, IntelClocksTable, IntelDrmInfo, LinkInfo, PowerState,
PowerStates, PowerStats, ProcessList, ProcessUtilizationType, TemperatureEntry, VoltageStats,
VramStats, config::GpuConfig,
ClocksInfo, ClocksTable, ClockspeedStats, DeviceApiInfo, DeviceInfo, DeviceStats, DeviceType,
DisplaysInfo, DrmInfo, DrmMemoryInfo, FanStats, IntelClocksTable, IntelDrmInfo, LinkInfo,
PowerState, PowerStates, PowerStats, ProcessList, ProcessUtilizationType, TemperatureEntry,
VoltageStats, VramStats, config::GpuConfig,
};
use std::{
borrow::Cow,
@@ -592,16 +588,17 @@ impl GpuController for IntelGpuController {
self.common.pci_info.device_pci_info.model.clone()
}
fn get_info(&self, unique_vendor: bool) -> LocalBoxFuture<'_, DeviceInfo> {
fn get_info(
&self,
unique_vendor: bool,
include_api_info: bool,
) -> LocalBoxFuture<'_, DeviceInfo> {
Box::pin(async move {
let (vulkan_result, opencl_instances) = join!(
get_vulkan_info(&self.common),
get_opencl_info(&self.common, unique_vendor)
);
let vulkan_instances = vulkan_result.unwrap_or_else(|err| {
warn!("could not load vulkan info: {err:#}");
vec![]
});
let api_info = if include_api_info {
self.get_api_info(unique_vendor).await
} else {
DeviceApiInfo::default()
};
let vram_info = self.get_vram_info();
@@ -617,12 +614,11 @@ impl GpuController for IntelGpuController {
DeviceInfo {
pci_info: Some(self.common.pci_info.clone()),
vulkan_instances,
api_info,
driver: self.common.driver.clone(),
vbios_version: None,
link_info: LinkInfo::default(),
drm_info: Some(drm_info),
opencl_instances,
flags: vec![],
}
})
@@ -772,7 +768,7 @@ impl GpuController for IntelGpuController {
enabled: true,
min_value: None,
value,
index: None,
id: None,
})
})
.collect();
+39 -34
View File
@@ -4,30 +4,26 @@ pub mod nvapi;
use super::{CommonControllerInfo, FanControlHandle, GpuController};
use crate::{
bindings::nvidia::NvPhysicalGpuHandle,
server::{
gpu_controller::{
NvApi,
common::{fan_control::FanCurveExt, resolve_process_name},
nvidia::nvapi::{CLOCK_CLIENT_CLK_VF_POINT_TYPE_PROG, ClockClientClkVfPointInfoV1},
},
opencl::get_opencl_info,
vulkan::get_vulkan_info,
server::gpu_controller::{
NvApi,
common::{fan_control::FanCurveExt, resolve_process_name},
nvidia::nvapi::{CLOCK_CLIENT_CLK_VF_POINT_TYPE_PROG, ClockClientClkVfPointInfoV1},
},
};
use amdgpu_sysfs::{
gpu_handle::{fan_control::FanInfo, power_profile_mode::PowerProfileModesTable},
gpu_handle::{PowerLevelId, fan_control::FanInfo, power_profile_mode::PowerProfileModesTable},
hw_mon::Temperature,
};
use anyhow::{Context, anyhow, bail};
use driver::DriverHandle;
use futures::{FutureExt, future::LocalBoxFuture, join};
use futures::{FutureExt, future::LocalBoxFuture};
use indexmap::IndexMap;
use lact_schema::{
CacheInfo, ClocksInfo, ClocksTable, ClockspeedStats, DeviceFlag, DeviceInfo, DeviceStats,
DeviceType, DisplaysInfo, DrmInfo, DrmMemoryInfo, FanControlMode, FanStats, IntelDrmInfo,
LinkInfo, NvidiaClockOffset, NvidiaClocksTable, NvidiaVfPoint, PmfwInfo, PowerState,
PowerStates, PowerStats, ProcessInfo, ProcessList, ProcessType, ProcessUtilizationType,
TemperatureEntry, VoltageStats, VramStats,
ActivePowerStates, CacheInfo, ClocksInfo, ClocksTable, ClockspeedStats, DeviceApiInfo,
DeviceFlag, DeviceInfo, DeviceStats, DeviceType, DisplaysInfo, DrmInfo, DrmMemoryInfo,
FanControlMode, FanStats, IntelDrmInfo, LinkInfo, NvidiaClockOffset, NvidiaClocksTable,
NvidiaVfPoint, PmfwInfo, PowerState, PowerStates, PowerStats, ProcessInfo, ProcessList,
ProcessType, ProcessUtilizationType, TemperatureEntry, VoltageStats, VramStats,
config::{CurvePoint, FanControlSettings, FanCurve, GpuConfig},
};
use nvml_wrapper::{
@@ -354,12 +350,12 @@ impl NvidiaGpuController {
enabled: true,
min_value: Some(u64::from(gpu_min)),
value: u64::from(gpu_max),
index: Some(
id: Some(PowerLevelId::Index(
pstate
.as_c()
.try_into()
.expect("Power state always fits in u8"),
),
)),
});
let (mem_min, mem_max) = device
@@ -370,12 +366,12 @@ impl NvidiaGpuController {
enabled: true,
min_value: Some(u64::from(mem_min)),
value: u64::from(mem_max),
index: Some(
id: Some(PowerLevelId::Index(
pstate
.as_c()
.try_into()
.expect("Power state always fits in u8"),
),
)),
});
}
@@ -535,23 +531,24 @@ impl GpuController for NvidiaGpuController {
.or_else(|| self.common.pci_info.device_pci_info.model.clone())
}
fn get_info(&self, unique_vendor: bool) -> LocalBoxFuture<'_, DeviceInfo> {
fn get_info(
&self,
unique_vendor: bool,
include_api_info: bool,
) -> LocalBoxFuture<'_, DeviceInfo> {
Box::pin(async move {
let (vulkan_result, opencl_instances) = join!(
get_vulkan_info(&self.common),
get_opencl_info(&self.common, unique_vendor)
);
let vulkan_instances = vulkan_result.unwrap_or_else(|err| {
warn!("could not load vulkan info: {err:#}");
vec![]
});
let api_info = if include_api_info {
self.get_api_info(unique_vendor).await
} else {
DeviceApiInfo::default()
};
let device = self.device();
let driver_handle = self.driver_handle.as_ref();
DeviceInfo {
pci_info: Some(self.common.pci_info.clone()),
vulkan_instances,
api_info,
driver: format!(
"nvidia {}",
self.nvml.sys_driver_version().unwrap_or_default()
@@ -585,7 +582,6 @@ impl GpuController for NvidiaGpuController {
output
}),
},
opencl_instances,
drm_info: Some(DrmInfo {
device_name: device.name().ok(),
pci_revision_id: None,
@@ -753,7 +749,14 @@ impl GpuController for NvidiaGpuController {
let active_pstate = device
.performance_state()
.map(|pstate| pstate.as_c() as usize)
.map(|pstate| {
PowerLevelId::Index(
pstate
.as_c()
.try_into()
.expect("Power state always fits in u8"),
)
})
.ok();
let fan_range = device.min_max_fan_speed().ok();
@@ -835,9 +838,11 @@ impl GpuController for NvidiaGpuController {
..Default::default()
},
performance_level: None,
core_power_state: active_pstate,
memory_power_state: active_pstate,
pcie_power_state: None,
active_power_states: active_pstate.map(|active_pstate| ActivePowerStates {
core: Some(active_pstate),
memory: Some(active_pstate),
pcie: None,
}),
}
}
+24 -7
View File
@@ -14,8 +14,9 @@ use amdgpu_sysfs::gpu_handle::{
};
use anyhow::{Context, anyhow, bail};
use lact_schema::{
ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, DisplaysInfo, FanControlMode, FanOptions,
PmfwOptions, PowerStates, ProcessList, ProfileRule, ProfileWatcherState, ProfilesInfo,
ClocksInfo, DeviceApiInfo, DeviceInfo, DeviceListEntry, DeviceStats, DisplaysInfo,
FanControlMode, FanOptions, PmfwOptions, PowerStates, ProcessList, ProfileRule,
ProfileWatcherState, ProfilesInfo,
config::{
FanControlSettings, FanCurve, GpuConfig, Profile, ProfileHooks, default_fan_static_speed,
},
@@ -423,7 +424,23 @@ impl<'a> Handler {
entries
}
pub async fn get_device_info(&'a self, id: &str) -> anyhow::Result<DeviceInfo> {
pub async fn get_device_info(
&'a self,
id: &str,
include_api_info: Option<bool>,
) -> anyhow::Result<DeviceInfo> {
let controllers = self.gpu_controllers.read().await;
let controller = controllers
.get(id)
.ok_or_else(|| anyhow!("Controller '{id}' not found"))?;
let unique_vendor = controller_vendor_is_unique(controller, id, &controllers);
let include_api_info = include_api_info.unwrap_or(true);
Ok(controller.get_info(unique_vendor, include_api_info).await)
}
pub async fn get_device_api_info(&'a self, id: &str) -> anyhow::Result<DeviceApiInfo> {
let controllers = self.gpu_controllers.read().await;
let controller = controllers
.get(id)
@@ -431,7 +448,7 @@ impl<'a> Handler {
let unique_vendor = controller_vendor_is_unique(controller, id, &controllers);
Ok(controller.get_info(unique_vendor).await)
Ok(controller.get_api_info(unique_vendor).await)
}
pub async fn get_gpu_stats(&'a self, id: &str) -> anyhow::Result<DeviceStats> {
@@ -785,7 +802,7 @@ impl<'a> Handler {
let data = json!({
"pci_info": controller.controller_info().pci_info.clone(),
"info": controller.get_info(unique_vendor).await,
"info": controller.get_info(unique_vendor, true).await,
"stats": controller.get_stats(gpu_config),
"clocks_info": controller.get_clocks_info(gpu_config).ok(),
"power_profile_modes": controller.get_power_profile_modes().ok(),
@@ -1193,13 +1210,13 @@ async fn apply_config_to_controllers(
#[cfg(all(test, not(miri)))]
pub(crate) fn read_pci_db() -> Database {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/tests/data/pci.ids");
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../tests/snapshots/pci.ids");
Database::read_from_file(&path).unwrap()
}
#[cfg(all(test, miri))]
pub(crate) fn read_pci_db() -> Database {
let bytes = include_bytes!("../../src/tests/data/pci.ids");
let bytes = include_bytes!("../../../tests/snapshots/pci.ids");
Database::parse_db(std::io::Cursor::new(bytes)).unwrap()
}
@@ -1,3 +0,0 @@
0: 500Mhz
1: 16Mhz *
2: 2400Mhz
+49 -58
View File
@@ -9,7 +9,6 @@ use lact_schema::config::GpuConfig;
use mock_fs::MockSysfs;
use std::{fs, path::PathBuf, sync::OnceLock};
use tempfile::tempdir;
use tokio::task::LocalSet;
fn init_tracing() {
static TRACING_LOCK: OnceLock<()> = OnceLock::new();
@@ -25,7 +24,7 @@ fn init_tracing() {
async fn snapshot_everything() {
init_tracing();
let test_data_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/tests/data");
let test_data_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../tests/snapshots");
let pci_db = read_pci_db();
for vendor_dir in fs::read_dir(test_data_dir).unwrap().flatten() {
@@ -55,78 +54,70 @@ async fn snapshot_everything() {
}
}
#[tokio::test]
#[tokio::test(flavor = "local")]
#[cfg_attr(miri, ignore)]
async fn apply_settings() {
init_tracing();
let local_set = LocalSet::new();
local_set.spawn_local(async move {
let test_data_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/tests/data");
let pci_db = read_pci_db();
let test_data_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../tests/snapshots");
let pci_db = read_pci_db();
for vendor_dir in fs::read_dir(test_data_dir).unwrap().flatten() {
if !vendor_dir.metadata().unwrap().is_dir() {
continue;
}
for vendor_dir in fs::read_dir(test_data_dir).unwrap().flatten() {
if !vendor_dir.metadata().unwrap().is_dir() {
continue;
}
for device_dir in fs::read_dir(vendor_dir.path()).unwrap().flatten() {
for entry in fs::read_dir(device_dir.path()).unwrap().flatten() {
let name = entry.file_name();
let name = name.to_str().unwrap();
for device_dir in fs::read_dir(vendor_dir.path()).unwrap().flatten() {
for entry in fs::read_dir(device_dir.path()).unwrap().flatten() {
let name = entry.file_name();
let name = name.to_str().unwrap();
#[allow(clippy::case_sensitive_file_extension_comparisons)]
if name.starts_with("config") && name.ends_with(".yaml") {
let test_key = format!(
"apply_config/{}/{}/{name}",
vendor_dir.file_name().to_string_lossy(),
device_dir.file_name().to_string_lossy()
);
let raw_gpu_config = fs::read_to_string(entry.path()).unwrap();
let gpu_config: GpuConfig =
serde_norway::from_str(&raw_gpu_config).unwrap();
#[allow(clippy::case_sensitive_file_extension_comparisons)]
if name.starts_with("config") && name.ends_with(".yaml") {
let test_key = format!(
"apply_config/{}/{}/{name}",
vendor_dir.file_name().to_string_lossy(),
device_dir.file_name().to_string_lossy()
);
let raw_gpu_config = fs::read_to_string(entry.path()).unwrap();
let gpu_config: GpuConfig = serde_norway::from_str(&raw_gpu_config).unwrap();
let mock_fs_dir = tempdir().unwrap();
let mock_fs_dir = tempdir().unwrap();
let mock_fs = MockSysfs::new(device_dir.path());
let writes = mock_fs.writes.clone();
let mock_fs = MockSysfs::new(device_dir.path());
let writes = mock_fs.writes.clone();
let mount = easy_fuser::spawn_mount(mock_fs, mock_fs_dir.path(), &[], 1)
.expect("Could not mount mock fs");
let mount = easy_fuser::spawn_mount(mock_fs, mock_fs_dir.path(), &[], 1)
.expect("Could not mount mock fs");
let handler =
Handler::with_base_path(mock_fs_dir.path(), Config::default(), &pci_db)
.await
.unwrap();
let gpu_id = &handler.list_devices().await[0].id;
handler
.config
.write()
let handler =
Handler::with_base_path(mock_fs_dir.path(), Config::default(), &pci_db)
.await
.gpus_mut()
.unwrap()
.insert(gpu_id.clone(), gpu_config);
.unwrap();
let gpu_id = &handler.list_devices().await[0].id;
handler.apply_current_config().await.unwrap();
handler
.config
.write()
.await
.gpus_mut()
.unwrap()
.insert(gpu_id.clone(), gpu_config);
mount.join();
mock_fs_dir.close().unwrap();
handler.apply_current_config().await.unwrap();
let write_commands = writes
.lock()
.unwrap()
.iter()
.map(|(name, contents)| {
format!("{}: {contents}", name.to_str().unwrap())
})
.collect::<Vec<String>>();
assert_debug_snapshot!(test_key, write_commands);
}
mount.join();
mock_fs_dir.close().unwrap();
let write_commands = writes
.lock()
.unwrap()
.iter()
.map(|(name, contents)| format!("{}: {contents}", name.to_str().unwrap()))
.collect::<Vec<String>>();
assert_debug_snapshot!(test_key, write_commands);
}
}
}
});
local_set.await;
}
}
@@ -143,55 +143,75 @@ expression: device_info
"core": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 800
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 1100
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 2700
}
],
"vram": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 400
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 800
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 800
},
{
"enabled": true,
"index": 3,
"id": {
"Index": 3
},
"value": 800
}
]
},
"stats": {
"active_power_states": {
"core": {
"Index": 0
},
"memory": {
"Index": 0
}
},
"busy_percent": 0,
"clockspeed": {
"gpu_clockspeed": 800,
"sensors": {}
},
"core_power_state": 0,
"fan": {
"control_enabled": false,
"pmfw_info": {}
},
"memory_power_state": 0,
"performance_level": "auto",
"power": {
"average": 3.103,
@@ -473,51 +473,75 @@ expression: device_info
"core": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 300
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 0
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 1900
}
],
"vram": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 100
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 500
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 625
},
{
"enabled": true,
"index": 3,
"id": {
"Index": 3
},
"value": 875
}
]
},
"stats": {
"active_power_states": {
"core": {
"Index": 1
},
"memory": {
"Index": 3
},
"pcie": {
"Index": 1
}
},
"busy_percent": 0,
"clockspeed": {
"gpu_clockspeed": 31,
"sensors": {},
"vram_clockspeed": 875
},
"core_power_state": 1,
"fan": {
"control_enabled": false,
"pmfw_info": {},
@@ -528,8 +552,6 @@ expression: device_info
"speed_max": 3200,
"speed_min": 0
},
"memory_power_state": 3,
"pcie_power_state": 1,
"power": {
"average": 16.0,
"cap_current": 130.0,
@@ -473,39 +473,59 @@ expression: device_info
"core": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 300
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 1900
}
],
"vram": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 100
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 500
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 625
},
{
"enabled": true,
"index": 3,
"id": {
"Index": 3
},
"value": 875
}
]
},
"stats": {
"active_power_states": {
"memory": {
"Index": 3
},
"pcie": {
"Index": 1
}
},
"busy_percent": 0,
"clockspeed": {
"gpu_clockspeed": 10,
@@ -522,8 +542,6 @@ expression: device_info
"speed_max": 3400,
"speed_min": 0
},
"memory_power_state": 3,
"pcie_power_state": 1,
"power": {
"average": 29.0,
"cap_current": 220.0,
@@ -251,71 +251,103 @@ expression: device_info
"core": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 300
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 600
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 900
},
{
"enabled": true,
"index": 3,
"id": {
"Index": 3
},
"value": 1145
},
{
"enabled": true,
"index": 4,
"id": {
"Index": 4
},
"value": 1215
},
{
"enabled": true,
"index": 5,
"id": {
"Index": 5
},
"value": 1257
},
{
"enabled": true,
"index": 6,
"id": {
"Index": 6
},
"value": 1300
},
{
"enabled": true,
"index": 7,
"id": {
"Index": 7
},
"value": 1366
}
],
"vram": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 300
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 1000
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 1750
}
]
},
"stats": {
"active_power_states": {
"core": {
"Index": 2
},
"memory": {
"Index": 2
},
"pcie": {
"Index": 1
}
},
"busy_percent": 11,
"clockspeed": {
"gpu_clockspeed": 798,
"sensors": {},
"vram_clockspeed": 1750
},
"core_power_state": 2,
"fan": {
"control_enabled": false,
"pmfw_info": {},
@@ -326,8 +358,6 @@ expression: device_info
"speed_max": 3200,
"speed_min": 0
},
"memory_power_state": 2,
"pcie_power_state": 1,
"performance_level": "auto",
"power": {
"average": 41.045,
@@ -433,51 +433,75 @@ expression: device_info
"core": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 500
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 2605
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 2700
}
],
"vram": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 96
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 541
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 675
},
{
"enabled": true,
"index": 3,
"id": {
"Index": 3
},
"value": 925
}
]
},
"stats": {
"active_power_states": {
"core": {
"Index": 1
},
"memory": {
"Index": 0
},
"pcie": {
"Index": 1
}
},
"busy_percent": 0,
"clockspeed": {
"gpu_clockspeed": 20,
"sensors": {},
"vram_clockspeed": 96
},
"core_power_state": 1,
"fan": {
"control_enabled": false,
"pmfw_info": {},
@@ -488,8 +512,6 @@ expression: device_info
"speed_max": 3100,
"speed_min": 0
},
"memory_power_state": 0,
"pcie_power_state": 1,
"power": {
"average": 3.0,
"cap_current": 120.0,
@@ -395,39 +395,59 @@ expression: device_info
"core": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 0
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 0
}
],
"vram": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 96
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 541
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 675
},
{
"enabled": true,
"index": 3,
"id": {
"Index": 3
},
"value": 875
}
]
},
"stats": {
"active_power_states": {
"memory": {
"Index": 0
},
"pcie": {
"Index": 1
}
},
"busy_percent": 0,
"clockspeed": {
"gpu_clockspeed": 0,
@@ -444,8 +464,6 @@ expression: device_info
"speed_max": 3300,
"speed_min": 0
},
"memory_power_state": 0,
"pcie_power_state": 1,
"power": {
"average": 3.0,
"cap_current": 100.0,
@@ -435,39 +435,62 @@ expression: device_info
"core": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 500
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 2660
}
],
"vram": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 96
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 456
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 673
},
{
"enabled": true,
"index": 3,
"id": {
"Index": 3
},
"value": 1000
}
]
},
"stats": {
"active_power_states": {
"core": {
"Index": 0
},
"memory": {
"Index": 3
},
"pcie": {
"Index": 1
}
},
"busy_percent": 3,
"clockspeed": {
"gpu_clockspeed": 500,
@@ -478,7 +501,6 @@ expression: device_info
"target_gpu_clockspeed": 500,
"vram_clockspeed": 1000
},
"core_power_state": 0,
"fan": {
"control_enabled": false,
"pmfw_info": {},
@@ -489,8 +511,6 @@ expression: device_info
"speed_max": 3400,
"speed_min": 0
},
"memory_power_state": 3,
"pcie_power_state": 1,
"performance_level": "auto",
"power": {
"average": 36.0,
@@ -263,44 +263,66 @@ expression: device_info
"core": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 255
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 0
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 1841
}
],
"vram": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 96
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 456
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 772
},
{
"enabled": true,
"index": 3,
"id": {
"Index": 3
},
"value": 1000
}
]
},
"stats": {
"active_power_states": {
"core": {
"Index": 1
},
"memory": {
"Index": 0
}
},
"busy_percent": 0,
"clockspeed": {
"gpu_clockspeed": 0,
@@ -314,7 +336,6 @@ expression: device_info
"target_gpu_clockspeed": 1200,
"vram_clockspeed": 96
},
"core_power_state": 1,
"fan": {
"control_enabled": false,
"pmfw_info": {},
@@ -325,7 +346,6 @@ expression: device_info
"speed_max": 4900,
"speed_min": 0
},
"memory_power_state": 0,
"performance_level": "manual",
"power": {
"average": 1.0,
@@ -263,51 +263,72 @@ expression: device_info
"core": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 255
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 0
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 2208
}
],
"vram": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 96
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 456
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 772
},
{
"enabled": true,
"index": 3,
"id": {
"Index": 3
},
"value": 1124
}
]
},
"stats": {
"active_power_states": {
"core": {
"Index": 1
},
"memory": {
"Index": 0
}
},
"busy_percent": 0,
"clockspeed": {
"gpu_clockspeed": 0,
"sensors": {},
"vram_clockspeed": 96
},
"core_power_state": 1,
"fan": {
"control_enabled": false,
"pmfw_info": {},
@@ -318,7 +339,6 @@ expression: device_info
"speed_max": 4900,
"speed_min": 0
},
"memory_power_state": 0,
"performance_level": "auto",
"power": {
"average": 1.0,
@@ -354,44 +354,69 @@ expression: device_info
"core": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 500
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 77
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 2254
}
],
"vram": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 96
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 456
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 772
},
{
"enabled": true,
"index": 3,
"id": {
"Index": 3
},
"value": 1218
}
]
},
"stats": {
"active_power_states": {
"core": {
"Index": 1
},
"memory": {
"Index": 1
},
"pcie": {
"Index": 2
}
},
"busy_percent": 2,
"clockspeed": {
"gpu_clockspeed": 2011,
@@ -406,7 +431,6 @@ expression: device_info
"target_gpu_clockspeed": 2011,
"vram_clockspeed": 456
},
"core_power_state": 1,
"fan": {
"control_enabled": false,
"pmfw_info": {
@@ -458,8 +482,6 @@ expression: device_info
100
]
},
"memory_power_state": 1,
"pcie_power_state": 2,
"performance_level": "manual",
"power": {
"average": 32.0,
@@ -354,51 +354,72 @@ expression: device_info
"core": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 500
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 31
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 2219
}
],
"vram": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 96
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 456
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 772
},
{
"enabled": true,
"index": 3,
"id": {
"Index": 3
},
"value": 1249
}
]
},
"stats": {
"active_power_states": {
"core": {
"Index": 1
},
"memory": {
"Index": 3
}
},
"busy_percent": 3,
"clockspeed": {
"gpu_clockspeed": 31,
"sensors": {},
"vram_clockspeed": 1249
},
"core_power_state": 1,
"fan": {
"control_enabled": false,
"pmfw_info": {
@@ -442,7 +463,6 @@ expression: device_info
100
]
},
"memory_power_state": 3,
"performance_level": "auto",
"power": {
"average": 68.0,
@@ -353,54 +353,83 @@ expression: device_info
"core": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 500
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 789
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 2070
}
],
"vram": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 96
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 456
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 772
},
{
"enabled": true,
"index": 3,
"id": {
"Index": 3
},
"value": 875
},
{
"enabled": true,
"index": 4,
"id": {
"Index": 4
},
"value": 1124
},
{
"enabled": true,
"index": 5,
"id": {
"Index": 5
},
"value": 1258
}
]
},
"stats": {
"active_power_states": {
"core": {
"Index": 1
},
"memory": {
"Index": 4
},
"pcie": {
"Index": 1
}
},
"busy_percent": 25,
"clockspeed": {
"gpu_clockspeed": 900,
@@ -415,7 +444,6 @@ expression: device_info
"target_gpu_clockspeed": 900,
"vram_clockspeed": 1124
},
"core_power_state": 1,
"fan": {
"control_enabled": false,
"pmfw_info": {
@@ -460,8 +488,6 @@ expression: device_info
100
]
},
"memory_power_state": 4,
"pcie_power_state": 1,
"performance_level": "auto",
"power": {
"average": 49.0,
@@ -353,61 +353,99 @@ expression: device_info
"core": [
{
"enabled": true,
"index": 0,
"value": 500
"id": "Sleep",
"value": 19
},
{
"enabled": true,
"index": 1,
"value": 16
"id": {
"Index": 0
},
"value": 615
},
{
"enabled": true,
"index": 2,
"value": 2400
"id": {
"Index": 1
},
"value": 800
},
{
"enabled": true,
"id": {
"Index": 2
},
"value": 888
},
{
"enabled": true,
"id": {
"Index": 3
},
"value": 1000
}
],
"vram": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 96
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 456
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 772
},
{
"enabled": true,
"index": 3,
"id": {
"Index": 3
},
"value": 875
},
{
"enabled": true,
"index": 4,
"id": {
"Index": 4
},
"value": 1124
},
{
"enabled": true,
"index": 5,
"id": {
"Index": 5
},
"value": 1258
}
]
},
"stats": {
"active_power_states": {
"core": "Sleep",
"memory": {
"Index": 5
},
"pcie": {
"Index": 1
}
},
"busy_percent": 3,
"clockspeed": {
"gpu_clockspeed": 167,
"sensors": {},
"vram_clockspeed": 1258
},
"core_power_state": 1,
"fan": {
"control_enabled": false,
"pmfw_info": {
@@ -459,8 +497,6 @@ expression: device_info
100
]
},
"memory_power_state": 5,
"pcie_power_state": 1,
"performance_level": "auto",
"power": {
"average": 55.0,
@@ -178,45 +178,61 @@ expression: device_info
"core": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 600
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 848
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 2900
}
],
"vram": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 400
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 800
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 1000
}
]
},
"stats": {
"active_power_states": {
"core": {
"Index": 1
}
},
"busy_percent": 3,
"clockspeed": {
"gpu_clockspeed": 848,
"sensors": {}
},
"core_power_state": 1,
"fan": {
"control_enabled": false,
"pmfw_info": {}
@@ -109,50 +109,68 @@ expression: device_info
"core": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 200
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 1100
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 1600
}
],
"vram": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 400
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 600
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 687
},
{
"enabled": true,
"index": 3,
"id": {
"Index": 3
},
"value": 687
}
]
},
"stats": {
"active_power_states": {
"core": {
"Index": 0
}
},
"busy_percent": 0,
"clockspeed": {
"gpu_clockspeed": 200,
"sensors": {}
},
"core_power_state": 0,
"fan": {
"control_enabled": false,
"pmfw_info": {}
@@ -241,76 +241,107 @@ expression: device_info
"core": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 852
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 991
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 1138
},
{
"enabled": true,
"index": 3,
"id": {
"Index": 3
},
"value": 1269
},
{
"enabled": true,
"index": 4,
"id": {
"Index": 4
},
"value": 1312
},
{
"enabled": true,
"index": 5,
"id": {
"Index": 5
},
"value": 1474
},
{
"enabled": true,
"index": 6,
"id": {
"Index": 6
},
"value": 1538
},
{
"enabled": true,
"index": 7,
"id": {
"Index": 7
},
"value": 1590
}
],
"vram": [
{
"enabled": true,
"index": 0,
"id": {
"Index": 0
},
"value": 167
},
{
"enabled": true,
"index": 1,
"id": {
"Index": 1
},
"value": 500
},
{
"enabled": true,
"index": 2,
"id": {
"Index": 2
},
"value": 700
},
{
"enabled": true,
"index": 3,
"id": {
"Index": 3
},
"value": 920
}
]
},
"stats": {
"active_power_states": {
"core": {
"Index": 0
},
"memory": {
"Index": 0
}
},
"busy_percent": 0,
"clockspeed": {
"gpu_clockspeed": 26,
"sensors": {},
"vram_clockspeed": 167
},
"core_power_state": 0,
"fan": {
"control_enabled": false,
"pmfw_info": {},
@@ -321,7 +352,6 @@ expression: device_info
"speed_max": 3500,
"speed_min": 0
},
"memory_power_state": 0,
"performance_level": "auto",
"power": {
"average": 8.0,
+1
View File
@@ -37,6 +37,7 @@ i18n-embed-fl = { workspace = true }
rust-embed = { workspace = true }
plotters = { version = "0.3.7", default-features = false, features = [
"area_series",
"line_series",
"full_palette",
"point_series",
+1
View File
@@ -196,6 +196,7 @@ vram-pstates = VRAM Power States
pstates-manual-needed = Performance level must be set to 'manual' to toggle power states
enable-pstate-config = Enable power state configuration
menu = Menu
show-historical-charts = Show Graphs
show-process-monitor = Show Process Monitor
generate-debug-snapshot = Generate Debug Snapshot
-6
View File
@@ -13,9 +13,3 @@
.main-sidebar-container .sidebar-section {
margin: 4px 8px 8px 8px;
}
.bootstrap-spinner-large {
min-width: 64px;
min-height: 64px;
-gtk-icon-size: 64px;
}
+148 -121
View File
@@ -6,6 +6,7 @@ pub mod graphs_window;
mod info_dialog;
mod info_row;
mod info_row_level;
mod loader;
pub(crate) mod msg;
mod overdrive_dialog;
mod page_section;
@@ -20,6 +21,7 @@ use crate::{
APP_ID, CONFIG, GUI_VERSION, I18N,
app::{
about_dialog::{AboutDialog, AboutDialogMsg},
ext::RelmLaunchable as _,
gpu_selector::GpuSelector,
info_dialog::{
InfoDialog, InfoDialogConfirmation, InfoDialogData, InfoDialogId, InfoDialogMsg,
@@ -45,7 +47,7 @@ use gtk::{
use i18n_embed_fl::fl;
use lact_client::{ConnectionStatusMsg, DaemonClient};
use lact_schema::{
DeviceFlag, DeviceListEntry, DeviceStats, DeviceType, GIT_COMMIT, SystemInfo,
DeviceApiInfo, DeviceFlag, DeviceListEntry, DeviceStats, DeviceType, GIT_COMMIT, SystemInfo,
args::GuiArgs,
config::{GpuConfig, Profile},
request::{ConfirmCommand, ProfileBase, SetClocksCommand},
@@ -62,9 +64,11 @@ use pages::{
use relm4::{
AsyncComponentSender, Component, ComponentController, MessageBroker, RelmObjectExt,
RelmWidgetExt,
binding::BoolBinding,
actions::{AccelsPlus, ActionGroupName, RelmAction, RelmActionGroup},
binding::{Binding as _, BoolBinding},
css,
loading_widgets::LoadingWidgets,
new_action_group, new_stateless_action,
prelude::{AsyncComponent, AsyncComponentParts},
tokio::{self, time::sleep},
view,
@@ -88,6 +92,14 @@ const DEFAULT_WINDOW_HEIGHT: i32 = 750;
const CONFIRM_RESPONSE_APPLY: &str = "confirm";
const CONFIRM_RESPONSE_REVERT: &str = "revert";
macro_rules! setup_actions {
($(($grp:ident, $action:ty, $callback:expr),)*) => {
$(
$grp.add_action(RelmAction::<$action>::new_stateless(move |_| $callback));
)*
};
}
pub struct AppModel {
daemon_client: DaemonClient,
graphs_window: relm4::Controller<GraphsWindow>,
@@ -98,6 +110,7 @@ pub struct AppModel {
info_dialog: relm4::Controller<InfoDialog>,
ui_sensitive: BoolBinding,
is_reconnecting: BoolBinding,
info_page: relm4::Controller<InformationPage>,
oc_page: relm4::Controller<OcPage>,
@@ -114,6 +127,10 @@ pub struct AppModel {
system_info: SystemInfo,
device_flags: Vec<DeviceFlag>,
device_driver: String,
application: gtk::Application,
dump_vbios_action: gtk::gio::SimpleAction,
}
#[derive(Debug)]
@@ -130,6 +147,22 @@ impl AsyncComponent for AppModel {
type Output = ();
type CommandOutput = Option<CommandOutput>;
menu! {
app_menu: {
section! {
&fl!(I18N, "show-process-monitor") => ProcessMonitorAction,
},
section! {
&fl!(I18N, "generate-debug-snapshot") => GenerateDebugSnapshotAction,
&fl!(I18N, "dump-vbios") => DumpVBiosAction,
},
section! {
&fl!(I18N, "preferences") => PreferencesAction,
&fl!(I18N, "about") => AboutAction,
},
}
}
view! {
#[root]
adw::ApplicationWindow::builder()
@@ -166,6 +199,7 @@ impl AsyncComponent for AppModel {
set_orientation: gtk::Orientation::Vertical,
set_vexpand: true,
add_css_class: "main-sidebar-container",
add_binding: (&model.ui_sensitive, "sensitive"),
model.gpu_selector.widget().clone() {},
@@ -215,75 +249,17 @@ impl AsyncComponent for AppModel {
set_child = &adw::ToolbarView {
#[name = "content_header"]
add_top_bar = &adw::HeaderBar {
pack_end = &gtk::MenuButton {
set_icon_name: "open-menu-symbolic",
#[wrap(Some)]
#[name = "header_menu_popover"]
set_popover = &gtk::Popover {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
add_css_class: "header-settings-popover-container",
gtk::Button {
set_label: &fl!(I18N, "show-process-monitor"),
connect_clicked[header_menu_popover] => move |_| {
header_menu_popover.popdown();
APP_BROKER.send(AppMsg::ShowProcessMonitor);
},
add_css_class: "flat",
},
gtk::Separator {},
gtk::Button {
set_label: &fl!(I18N, "generate-debug-snapshot"),
connect_clicked[header_menu_popover] => move |_| {
header_menu_popover.popdown();
APP_BROKER.send(AppMsg::DebugSnapshot);
},
add_css_class: "flat",
},
gtk::Button {
set_label: &fl!(I18N, "dump-vbios"),
connect_clicked[header_menu_popover] => move |_| {
header_menu_popover.popdown();
APP_BROKER.send(AppMsg::DumpVBios);
},
add_css_class: "flat",
#[watch]
set_sensitive: model.device_flags.contains(&DeviceFlag::DumpableVBios),
},
gtk::Separator {},
gtk::Button {
set_label: &fl!(I18N, "preferences"),
connect_clicked[header_menu_popover] => move |_| {
header_menu_popover.popdown();
APP_BROKER.send(AppMsg::ShowPreferencesDialog);
},
add_css_class: "flat",
},
gtk::Button {
set_label: &fl!(I18N, "about"),
connect_clicked[header_menu_popover] => move |_| {
header_menu_popover.popdown();
APP_BROKER.send(AppMsg::ShowAboutDialog);
},
add_css_class: "flat",
},
}
},
set_tooltip_text: Some(&fl!(I18N, "menu")),
set_menu_model: Some(&app_menu),
add_binding: (&model.ui_sensitive, "sensitive"),
},
pack_end = &gtk::Button {
set_label: &fl!(I18N, "show-historical-charts"),
connect_clicked => move |_| APP_BROKER.send(AppMsg::ShowGraphsWindow),
add_binding: (&model.ui_sensitive, "sensitive"),
},
},
@@ -297,6 +273,11 @@ impl AsyncComponent for AppModel {
connect_button_clicked => AppMsg::ShowOverdriveDialog,
},
add_top_bar = &adw::Banner {
set_title: &fl!(I18N, "reconnecting-to-daemon"),
add_binding: (&model.is_reconnecting, "revealed"),
},
#[wrap(Some)]
set_content = &gtk::ScrolledWindow {
set_hscrollbar_policy: gtk::PolicyType::Never,
@@ -341,23 +322,11 @@ impl AsyncComponent for AppModel {
}
}
},
#[name = "reconnecting_dialog"]
adw::Dialog {
set_title: &fl!(I18N, "daemon-connection-lost"),
set_content_width: 300,
set_content_height: 80,
set_can_close: false,
#[wrap(Some)]
set_child = &gtk::Label {
set_margin_all: 10,
set_label: &fl!(I18N, "reconnecting-to-daemon"),
}
},
}
fn init_loading_widgets(root: Self::Root) -> Option<LoadingWidgets> {
let loader_picture = loader::new();
view! {
#[local]
root {
@@ -367,9 +336,8 @@ impl AsyncComponent for AppModel {
set_valign: gtk::Align::Center,
set_halign: gtk::Align::Center,
gtk::Spinner {
add_css_class: "bootstrap-spinner-large",
start: (),
#[local_ref]
loader_picture -> gtk::Picture {
}
}
}
@@ -463,31 +431,23 @@ impl AsyncComponent for AppModel {
let info_page = InformationPage::detach_default();
let oc_page = OcPage::builder()
.launch(settings_changed.clone())
.forward(sender.input_sender(), |msg| msg);
let thermals_page = ThermalsPage::builder().launch(()).detach();
let oc_page =
OcPage::launch(settings_changed.clone()).forward(sender.input_sender(), |msg| msg);
let thermals_page = ThermalsPage::detach_default();
let software_page = SoftwarePage::builder()
.launch((system_info.clone(), daemon_client.embedded))
.detach();
let software_page = SoftwarePage::detach((system_info.clone(), daemon_client.embedded));
let crash_page = CrashPage::builder()
.launch(String::new())
.forward(sender.input_sender(), |msg| msg);
let crash_page = CrashPage::launch_default().forward(sender.input_sender(), |msg| msg);
let overdrive_dialog = OverdriveDialog::builder()
.launch((system_info.clone(), root.clone().upcast()))
.detach();
let overdrive_dialog =
OverdriveDialog::detach((system_info.clone(), root.clone().upcast()));
let preferences_dialog = PreferencesDialog::builder()
.launch((system_info.clone(), root.clone()))
.detach();
let preferences_dialog = PreferencesDialog::detach((system_info.clone(), root.clone()));
let about_dialog = AboutDialog::builder().launch(root.clone()).detach();
let info_dialog = InfoDialog::builder()
.launch(root.clone())
.forward(sender.input_sender(), |msg| msg);
let about_dialog = AboutDialog::detach(root.clone());
let info_dialog =
InfoDialog::launch(root.clone()).forward(sender.input_sender(), |msg| msg);
let graphs_window = GraphsWindow::detach_default();
let process_monitor_window = ProcessMonitorWindow::detach_default();
@@ -496,9 +456,36 @@ impl AsyncComponent for AppModel {
.launch((devices, initial_gpu_id.clone()))
.forward(sender.input_sender(), AppMsg::SelectGpu);
let profile_selector = ProfileSelector::builder()
.launch(())
.forward(sender.input_sender(), |msg| msg);
let profile_selector =
ProfileSelector::launch_default().forward(sender.input_sender(), |msg| msg);
// create action group and actions for app menu
// action group and actions are declared at the bottom of the file
let mut actions = RelmActionGroup::<AppActionGroup>::new();
let application = root
.application()
.expect("Failed to get application from root window");
setup_actions! {
(actions, ProcessMonitorAction, APP_BROKER.send(AppMsg::ShowProcessMonitor)),
(actions, GenerateDebugSnapshotAction, APP_BROKER.send(AppMsg::DebugSnapshot)),
(actions, PreferencesAction, APP_BROKER.send(AppMsg::ShowPreferencesDialog)),
(actions, AboutAction, APP_BROKER.send(AppMsg::ShowAboutDialog)),
(actions, QuitAction, APP_BROKER.send(AppMsg::Quit)),
}
// this action is enabled/disabled conditionally in [Self::update_gpu_data_full]
// when the device flags are set, only if the device contains the DumpableVBios flag
let dump_vbios_action = {
let action = RelmAction::<DumpVBiosAction>::new_stateless(move |_| {
APP_BROKER.send(AppMsg::DumpVBios);
});
let gio_action = action.gio_action().clone();
actions.add_action(action);
gio_action.set_enabled(false);
gio_action
};
application.set_accelerators_for_action::<PreferencesAction>(&["<Control>comma"]);
application.set_accelerators_for_action::<QuitAction>(&["<Control>q"]);
root.insert_action_group(AppActionGroup::NAME, Some(&actions.into_action_group()));
let mut model = AppModel {
daemon_client,
@@ -516,11 +503,14 @@ impl AsyncComponent for AppModel {
gpu_selector,
profile_selector,
ui_sensitive: BoolBinding::new(false),
is_reconnecting: BoolBinding::new(false),
stats_task_handle: None,
settings_changed,
system_info,
device_flags: vec![],
device_driver: String::new(),
dump_vbios_action,
application,
};
if let Err(err) = model.reload_profiles(None).await {
@@ -662,13 +652,28 @@ impl AppModel {
AppMsg::ReloadData { full } => {
self.settings_changed.set_value(false);
let gpu_id = self.get_selected_gpu_id()?;
let gpu_id = Self::get_selected_gpu_id()?;
if full {
self.update_gpu_data_full(gpu_id, sender).await?;
} else {
self.update_gpu_data(gpu_id, sender).await?;
}
}
AppMsg::ReloadApiInfo => {
let gpu_id = Self::get_selected_gpu_id()?;
match self.daemon_client.get_device_api_info(&gpu_id).await {
Ok(api_info) => {
self.software_page
.emit(SoftwarePageMsg::DeviceApiInfo(Some(api_info)));
}
Err(err) => {
error!("could not fetch API info: {err:#}");
self.software_page.emit(SoftwarePageMsg::DeviceApiInfo(Some(
DeviceApiInfo::default(),
)));
}
}
}
AppMsg::ShowPreferencesDialog => {
self.preferences_dialog.emit(PreferencesDialogMsg::Show);
}
@@ -733,7 +738,7 @@ impl AppModel {
filters: vec![json_filter],
..Default::default()
};
let file_picker = OpenDialog::builder().launch(settings);
let file_picker = OpenDialog::launch(settings);
file_picker.emit(OpenDialogMsg::Open);
let stream = file_picker.into_stream();
@@ -752,7 +757,7 @@ impl AppModel {
is_modal: true,
..Default::default()
};
let diag = SaveDialog::builder().launch(settings);
let diag = SaveDialog::launch(settings);
diag.emit(SaveDialogMsg::SaveAs(format!(
"LACT-profile-{}.json",
name.as_deref().unwrap_or("default")
@@ -791,7 +796,7 @@ impl AppModel {
});
}
AppMsg::ApplyChanges => {
self.apply_settings(self.get_selected_gpu_id()?, root, &sender)
self.apply_settings(Self::get_selected_gpu_id()?, root, &sender)
.await
.inspect_err(|_| {
sender.input(AppMsg::ReloadData { full: false });
@@ -801,7 +806,7 @@ impl AppModel {
sender.input(AppMsg::ReloadData { full: false });
}
AppMsg::ResetClocks => {
let gpu_id = self.get_selected_gpu_id()?;
let gpu_id = Self::get_selected_gpu_id()?;
self.daemon_client
.set_clocks_value(&gpu_id, SetClocksCommand::reset())
.await?;
@@ -811,7 +816,7 @@ impl AppModel {
sender.input(AppMsg::ReloadData { full: false });
}
AppMsg::ResetPmfw => {
let gpu_id = self.get_selected_gpu_id()?;
let gpu_id = Self::get_selected_gpu_id()?;
self.daemon_client.reset_pmfw(&gpu_id).await?;
self.daemon_client
.confirm_pending_config(ConfirmCommand::Confirm)
@@ -826,7 +831,7 @@ impl AppModel {
.emit(ProcessMonitorWindowMsg::Show);
}
AppMsg::DumpVBios => {
self.dump_vbios(&self.get_selected_gpu_id()?, root, sender.clone())
self.dump_vbios(&Self::get_selected_gpu_id()?, root, sender.clone())
.await?;
}
AppMsg::DebugSnapshot => {
@@ -867,7 +872,7 @@ impl AppModel {
}
AppMsg::FetchProcessList => {
if self.process_monitor_window.widget().is_visible()
&& let Ok(gpu_id) = self.get_selected_gpu_id()
&& let Ok(gpu_id) = Self::get_selected_gpu_id()
{
match self.daemon_client.get_process_list(&gpu_id).await {
Ok(process_list) => {
@@ -882,9 +887,13 @@ impl AppModel {
}
AppMsg::ConnectionStatus(status) => match status {
ConnectionStatusMsg::Disconnected => {
widgets.reconnecting_dialog.present(Some(root))
self.ui_sensitive.set(false);
self.is_reconnecting.set(true);
}
ConnectionStatusMsg::Reconnected => {
self.ui_sensitive.set(true);
self.is_reconnecting.set(false);
}
ConnectionStatusMsg::Reconnected => widgets.reconnecting_dialog.force_close(),
},
AppMsg::EvaluateProfile(rule, sender) => {
match self.daemon_client.evaluate_profile_rule(rule).await {
@@ -918,6 +927,9 @@ impl AppModel {
handle.abort();
}
}
AppMsg::Quit => {
self.application.quit();
}
}
Ok(())
}
@@ -973,12 +985,14 @@ impl AppModel {
}
fn set_selected_gpu_id(gpu_id: String) {
CONFIG.write().edit(|config| {
config.selected_gpu = Some(gpu_id);
});
if !Self::get_selected_gpu_id().is_ok_and(|current_id| current_id == gpu_id) {
CONFIG.write().edit(|config| {
config.selected_gpu = Some(gpu_id);
});
}
}
fn get_selected_gpu_id(&self) -> anyhow::Result<String> {
fn get_selected_gpu_id() -> anyhow::Result<String> {
CONFIG
.read()
.selected_gpu
@@ -1013,10 +1027,12 @@ impl AppModel {
sender: AsyncComponentSender<AppModel>,
) -> anyhow::Result<()> {
self.ui_sensitive.set_value(false);
self.software_page
.emit(SoftwarePageMsg::DeviceApiInfo(None));
let daemon_client = self.daemon_client.clone();
let info_buf = daemon_client
.get_device_info(&gpu_id)
.get_device_info(&gpu_id, Some(false))
.await
.context("Could not fetch info")?;
let info = Arc::new(info_buf);
@@ -1035,6 +1051,8 @@ impl AppModel {
}
self.device_flags = info.flags.clone();
self.dump_vbios_action
.set_enabled(self.device_flags.contains(&DeviceFlag::DumpableVBios));
self.device_driver = info.driver.clone();
let update = PageUpdate::Info(info.clone());
@@ -1043,8 +1061,9 @@ impl AppModel {
update: update.clone(),
initial: true,
});
self.software_page
.emit(SoftwarePageMsg::DeviceInfo(info.clone()));
sender.input(AppMsg::ReloadApiInfo);
self.thermals_page.emit(ThermalsPageMsg::Update {
update: update.clone(),
initial: true,
@@ -1419,3 +1438,11 @@ async fn create_connection() -> anyhow::Result<(DaemonClient, Option<anyhow::Err
}
}
}
new_action_group!(pub AppActionGroup, "app");
new_stateless_action!(pub ProcessMonitorAction, AppActionGroup, "show-process-monitor");
new_stateless_action!(pub GenerateDebugSnapshotAction, AppActionGroup, "generate-debug-snapshot");
new_stateless_action!(pub DumpVBiosAction, AppActionGroup, "dump-vbios");
new_stateless_action!(pub PreferencesAction, AppActionGroup, "preferences");
new_stateless_action!(pub AboutAction, AppActionGroup, "about");
new_stateless_action!(pub QuitAction, AppActionGroup, "quit");
+18 -3
View File
@@ -21,15 +21,30 @@ impl FlowBoxExt for FlowBox {
.unwrap()
}
}
pub trait RelmDefaultLauchable: Component {
fn detach_default() -> relm4::Controller<Self>;
pub trait RelmLaunchable: Component {
fn launch(init: <Self as Component>::Init) -> relm4::component::Connector<Self>;
fn detach(init: <Self as Component>::Init) -> relm4::Controller<Self> {
Self::launch(init).detach()
}
}
impl<T: Component> RelmLaunchable for T {
fn launch(init: <Self as Component>::Init) -> relm4::component::Connector<Self> {
Self::builder().launch(init)
}
}
pub trait RelmDefaultLauchable: RelmLaunchable {
fn launch_default() -> relm4::component::Connector<Self>;
fn detach_default() -> relm4::Controller<Self>;
}
impl<S: Default, T: Component<Init = S>> RelmDefaultLauchable for T {
fn detach_default() -> relm4::Controller<Self> {
Self::builder().launch(S::default()).detach()
Self::detach(S::default())
}
fn launch_default() -> relm4::component::Connector<Self> {
+38 -36
View File
@@ -83,49 +83,51 @@ impl relm4::Component for GraphsWindow {
},
},
add_top_bar = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 10,
set_margin_horizontal: 10,
set_margin_top: 5,
set_halign: gtk::Align::End,
add_top_bar = &gtk::Revealer {
#[watch]
set_visible: model.edit_mode.value(),
set_reveal_child: model.edit_mode.value(),
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 10,
set_margin_horizontal: 10,
set_margin_top: 5,
set_halign: gtk::Align::End,
gtk::Label {
set_label: &fl!(I18N, "graphs-per-row"),
},
gtk::Label {
set_label: &fl!(I18N, "graphs-per-row"),
},
gtk::SpinButton {
set_numeric: true,
set_snap_to_ticks: true,
set_range: (1.0, 5.0),
set_increments: (1.0, 1.0),
bind: &model.plots_per_row,
connect_value_notify => GraphsWindowMsg::NotifyPlotsPerRow,
},
gtk::SpinButton {
set_numeric: true,
set_snap_to_ticks: true,
set_range: (1.0, 5.0),
set_increments: (1.0, 1.0),
bind: &model.plots_per_row,
connect_value_notify => GraphsWindowMsg::NotifyPlotsPerRow,
},
gtk::Label {
set_label: &fl!(I18N, "time-period-seconds"),
},
gtk::Label {
set_label: &fl!(I18N, "time-period-seconds"),
},
gtk::SpinButton {
set_adjustment: &model.time_period_seconds_adj,
connect_value_notify => GraphsWindowMsg::NotifyPlotsPerRow,
},
gtk::SpinButton {
set_adjustment: &model.time_period_seconds_adj,
connect_value_notify => GraphsWindowMsg::NotifyPlotsPerRow,
},
gtk::Button {
set_label: &fl!(I18N, "reset-button"),
set_tooltip: &fl!(I18N, "reset-all-graphs-tooltip"),
add_css_class: "destructive-action",
connect_clicked => GraphsWindowMsg::SetConfig(default_plots()),
},
gtk::Button {
set_label: &fl!(I18N, "reset-button"),
set_tooltip: &fl!(I18N, "reset-all-graphs-tooltip"),
add_css_class: "destructive-action",
connect_clicked => GraphsWindowMsg::SetConfig(default_plots()),
},
gtk::Button {
set_icon_name: "list-add-symbolic",
connect_clicked => GraphsWindowMsg::AddPlot,
set_tooltip: &fl!(I18N, "add-graph"),
},
gtk::Button {
set_icon_name: "list-add-symbolic",
connect_clicked => GraphsWindowMsg::AddPlot,
set_tooltip: &fl!(I18N, "add-graph"),
},
}
},
#[wrap(Some)]
+11 -6
View File
@@ -101,12 +101,15 @@ mod benches {
},
stat::{StatType, StatsData},
};
use amdgpu_sysfs::{gpu_handle::PerformanceLevel, hw_mon::Temperature};
use amdgpu_sysfs::{
gpu_handle::{PerformanceLevel, PowerLevelId},
hw_mon::Temperature,
};
use divan::{Bencher, counter::ItemsCount};
use jiff::Timestamp;
use lact_schema::{
ClockspeedStats, DeviceStats, FanStats, PmfwInfo, PowerStats, TemperatureEntry,
VoltageStats, VramStats,
ActivePowerStates, ClockspeedStats, DeviceStats, FanStats, PmfwInfo, PowerStats,
TemperatureEntry, VoltageStats, VramStats,
};
use std::{
collections::HashMap,
@@ -158,7 +161,11 @@ mod benches {
target_gpu_clockspeed: None,
sensors: HashMap::new(),
},
core_power_state: Some(0),
active_power_states: Some(ActivePowerStates {
core: Some(PowerLevelId::Index(0)),
memory: Some(PowerLevelId::Index(3)),
pcie: Some(PowerLevelId::Index(1)),
}),
fan: FanStats {
control_enabled: false,
pmfw_info: PmfwInfo::default(),
@@ -170,8 +177,6 @@ mod benches {
speed_min: Some(0),
..Default::default()
},
memory_power_state: Some(3),
pcie_power_state: Some(1),
performance_level: Some(PerformanceLevel::Auto),
power: PowerStats {
average: Some(36.0),
@@ -70,6 +70,8 @@ impl relm4::factory::FactoryComponent for PlotComponent {
set_time_period_seconds: self.time_period.value() as i64,
add_binding: (&self.print_extra_info, "print-extra-info"),
set_stats: self.selected_stats(),
connect_frame_rendered[sender] => move || {
sender.input(PlotComponentMsg::FrameRendered);
},
@@ -193,8 +195,6 @@ impl relm4::factory::FactoryComponent for PlotComponent {
}
}
sender.input(PlotComponentMsg::UpdatedSelection);
let print_extra_info = BoolBinding::new(false);
print_extra_info.connect_value_notify(move |_| {
sender.input(PlotComponentMsg::Redraw);
+67
View File
@@ -0,0 +1,67 @@
use gtk::{
glib::{self, ControlFlow},
prelude::*,
};
use std::{cell::Cell, rc::Rc, time::Duration};
const APP_SVG: &[u8] = include_bytes!("../../../res/io.github.ilya_zlobintsev.LACT.svg");
const FRAME_COUNT: usize = 12;
const FRAME_INTERVAL: Duration = Duration::from_millis(65);
const SIZE: i32 = 192;
pub(super) fn new() -> gtk::Picture {
let textures = Rc::new(build_textures());
let picture = gtk::Picture::new();
picture.set_size_request(SIZE, SIZE);
picture.set_can_shrink(false);
picture.set_keep_aspect_ratio(true);
picture.set_paintable(Some(&textures[0]));
let frame = Rc::new(Cell::new(0));
let picture_weak = picture.downgrade();
glib::timeout_add_local(FRAME_INTERVAL, move || {
let Some(picture) = picture_weak.upgrade() else {
return ControlFlow::Break;
};
let next_frame = (frame.get() + 1) % textures.len();
frame.set(next_frame);
picture.set_paintable(Some(&textures[next_frame]));
ControlFlow::Continue
});
picture
}
fn build_textures() -> Vec<gtk::gdk::Texture> {
let svg = std::str::from_utf8(APP_SVG)
.expect("app SVG should be valid UTF-8")
.replace(
"<svg height=\"128px\" viewBox=\"0 0 128 128\" width=\"128px\"",
&format!("<svg height=\"{SIZE}px\" viewBox=\"0 0 128 128\" width=\"{SIZE}px\""),
);
(0..FRAME_COUNT)
.map(|frame| {
let angle = -(frame as i32 * 360 / FRAME_COUNT as i32);
let svg = svg
.replace(
"<g id=\"left-fan-blades\">",
&format!(
"<g id=\"left-fan-blades\" transform=\"rotate({angle} 41.4375 69.824219)\">"
),
)
.replace(
"<g id=\"right-fan-blades\">",
&format!(
"<g id=\"right-fan-blades\" transform=\"rotate({angle} 86.5625 69.824219)\">"
),
);
gtk::gdk::Texture::from_bytes(&glib::Bytes::from_owned(svg.into_bytes()))
.expect("loader SVG frame should be loadable")
})
.collect()
}
+2
View File
@@ -14,6 +14,7 @@ pub enum AppMsg {
ReloadData {
full: bool,
},
ReloadApiInfo,
Stats(Arc<DeviceStats>),
ProfilesPolled(Arc<ProfilesInfo>),
ApplyChanges,
@@ -54,4 +55,5 @@ pub enum AppMsg {
ExportProfile(Option<String>),
ConnectionStatus(ConnectionStatusMsg),
Crash(String),
Quit,
}
+2 -1
View File
@@ -6,6 +6,7 @@ mod power_states;
mod vf_curve;
use super::PageUpdate;
use crate::app::ext::RelmLaunchable as _;
use crate::app::pages::oc_page::gpu_stats_section::GpuStatsSectionMsg;
use crate::app::pages::oc_page::vf_curve::{VfCurveEditor, VfCurveEditorMsg};
use crate::app::{ext::RelmDefaultLauchable, msg::AppMsg};
@@ -88,7 +89,7 @@ impl relm4::Component for OcPage {
let performance_frame =
PerformanceFrame::launch_default().forward(sender.input_sender(), |msg| msg);
let vf_curve_editor = VfCurveEditor::builder().launch(settings_changed).detach();
let vf_curve_editor = VfCurveEditor::detach(settings_changed);
let model = Self {
stats_section,
@@ -293,5 +293,13 @@ fn level_friendly_name(level: PerformanceLevel) -> String {
PerformanceLevel::Low => fl!(I18N, "performance-level-low"),
PerformanceLevel::High => fl!(I18N, "performance-level-high"),
PerformanceLevel::Manual => fl!(I18N, "performance-level-manual"),
PerformanceLevel::ProfileStandard
| PerformanceLevel::ProfileMinSclk
| PerformanceLevel::ProfileMinMclk
| PerformanceLevel::ProfilePeak
| PerformanceLevel::PerfDeterminism
| PerformanceLevel::ProfileExit => {
unreachable!("unsupported performance level in selector")
}
}
}
@@ -2,6 +2,7 @@ use super::power_states_list::PowerStatesList;
use crate::{
APP_BROKER, I18N,
app::{
ext::RelmLaunchable as _,
msg::AppMsg,
page_section_expander::PageSectionExpander,
pages::oc_page::power_states::power_states_list::{
@@ -18,7 +19,7 @@ use i18n_embed_fl::fl;
use indexmap::IndexMap;
use lact_schema::{DeviceStats, PowerStates};
use relm4::{
Component, ComponentController, ComponentParts, ComponentSender, RelmObjectExt,
ComponentController, ComponentParts, ComponentSender, RelmObjectExt,
binding::{Binding, BoolBinding},
css,
};
@@ -101,18 +102,14 @@ impl relm4::SimpleComponent for PowerStatesFrame {
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let core_states_list = PowerStatesList::builder()
.launch(PowerStatesListOptions {
title: fl!(I18N, "gpu-pstates"),
value_suffix: fl!(I18N, "mhz"),
})
.detach();
let vram_states_list = PowerStatesList::builder()
.launch(PowerStatesListOptions {
title: fl!(I18N, "vram-pstates"),
value_suffix: fl!(I18N, "mhz"),
})
.detach();
let core_states_list = PowerStatesList::detach(PowerStatesListOptions {
title: fl!(I18N, "gpu-pstates"),
value_suffix: fl!(I18N, "mhz"),
});
let vram_states_list = PowerStatesList::detach(PowerStatesListOptions {
title: fl!(I18N, "vram-pstates"),
value_suffix: fl!(I18N, "mhz"),
});
let states_configured = BoolBinding::new(false);
@@ -157,10 +154,12 @@ impl relm4::SimpleComponent for PowerStatesFrame {
));
}
PowerStatesFrameMsg::Stats(stats) => {
self.core_states_list
.emit(PowerStatesListMsg::ActiveState(stats.core_power_state));
self.vram_states_list
.emit(PowerStatesListMsg::ActiveState(stats.memory_power_state));
self.core_states_list.emit(PowerStatesListMsg::ActiveState(
stats.active_power_states.and_then(|states| states.core),
));
self.vram_states_list.emit(PowerStatesListMsg::ActiveState(
stats.active_power_states.and_then(|states| states.memory),
));
}
PowerStatesFrameMsg::VramClockRatio(ratio) => {
self.vram_clock_ratio = ratio;
@@ -1,6 +1,7 @@
use crate::app::pages::oc_page::power_states::power_states_row::{
PowerStateRow, PowerStateRowMsg, PowerStateRowOptions,
};
use amdgpu_sysfs::gpu_handle::PowerLevelId;
use gtk::prelude::{FrameExt, WidgetExt};
use lact_schema::PowerState;
use relm4::{
@@ -23,7 +24,7 @@ pub struct PowerStatesListOptions {
#[derive(Debug)]
pub enum PowerStatesListMsg {
PowerStates(Vec<PowerState>, f64),
ActiveState(Option<usize>),
ActiveState(Option<PowerLevelId>),
Configurable(bool),
}
@@ -92,10 +93,7 @@ impl relm4::SimpleComponent for PowerStatesList {
self.is_active_indicator_visible
.set_value(active_idx.is_some());
for (i, row) in self.states.iter().enumerate() {
let is_active = row
.power_state
.index
.is_some_and(|index| Some(usize::from(index)) == active_idx);
let is_active = row.power_state.id == active_idx;
self.states.send(i, PowerStateRowMsg::Active(is_active));
}
@@ -112,7 +110,10 @@ impl PowerStatesList {
self.states
.iter()
.filter(|row| row.enabled.value())
.filter_map(|row| row.power_state.index)
.filter_map(|row| match row.power_state.id? {
PowerLevelId::Index(index) => Some(index),
PowerLevelId::Sleep => None,
})
.collect()
}
@@ -1,4 +1,5 @@
use crate::{APP_BROKER, app::msg::AppMsg};
use amdgpu_sysfs::gpu_handle::PowerLevelId;
use gtk::prelude::{BoxExt, OrientableExt, WidgetExt};
use lact_schema::PowerState;
use relm4::{RelmObjectExt, RelmWidgetExt, binding::BoolBinding, css};
@@ -50,6 +51,7 @@ impl relm4::factory::FactoryComponent for PowerStateRow {
append = &gtk::CheckButton {
add_binding: (&self.enabled, "active"),
add_binding: (&self.configurable, "visible"),
set_sensitive: matches!(self.power_state.id, Some(PowerLevelId::Index(_))),
},
append = &gtk::Label {
@@ -57,11 +59,16 @@ impl relm4::factory::FactoryComponent for PowerStateRow {
#[watch]
set_class_active: (css::DIM_LABEL, !self.active.value()),
set_label: &{
let index_text = match self.power_state.id {
Some(PowerLevelId::Index(index)) => index.to_string(),
Some(PowerLevelId::Sleep) => "S".to_owned(),
None => String::new(),
};
let value_text = match self.power_state.min_value {
Some(min) if min != self.power_state.value => format!("{min}-{}", self.power_state.value),
_ => self.power_state.value.to_string(),
};
format!("{}: {value_text} {}", index.current_index(), self.value_suffix)
format!("{index_text}: {value_text} {}", self.value_suffix)
},
},
}
+111 -6
View File
@@ -2,6 +2,7 @@ use crate::{
I18N,
app::{APP_BROKER, graphs_window::plot::PlotColorScheme, msg::AppMsg},
};
use amdgpu_sysfs::gpu_handle::PowerLevelId;
use gtk::{
gdk,
prelude::{
@@ -16,7 +17,7 @@ use lact_schema::{ClocksTable, DeviceStats, NvidiaVfPoint, config};
use plotters::{
chart::{ChartBuilder, SeriesLabelPosition},
prelude::{Circle, EmptyElement, IntoDrawingArea as _, Rectangle, Text},
series::{DashedLineSeries, LineSeries, PointSeries},
series::{AreaSeries, DashedLineSeries, LineSeries, PointSeries},
style::{Color as _, ShapeStyle, TextStyle, text_anchor::Pos},
};
use plotters_cairo::CairoBackend;
@@ -46,9 +47,13 @@ pub struct VfCurveEditor {
global_settings_changed: BoolBinding,
cursor_position: Rc<Cell<Option<(f64, f64)>>>,
hovered_coords: Rc<Cell<Option<(u32, u32)>>>,
hovered_point: Rc<Cell<Option<usize>>>,
dragging_point: Rc<Cell<Option<usize>>>,
drag_modifiers: Rc<Cell<gdk::ModifierType>>,
selected_range_start: Rc<Cell<Option<usize>>>,
selected_range_end: Rc<Cell<Option<usize>>>,
}
#[derive(Debug)]
@@ -151,6 +156,8 @@ impl relm4::Component for VfCurveEditor {
#[watch]
set_cursor: if model.dragging_point.get().is_some() {
gdk::Cursor::from_name("move", None)
} else if model.selected_range_start.get().is_some() && model.selected_range_end.get().is_none() {
gdk::Cursor::from_name("col-resize", None)
} else {
None
}.as_ref(),
@@ -196,8 +203,9 @@ impl relm4::Component for VfCurveEditor {
add_css_class: "warning",
add_binding: (&model.allow_editing, "active"),
connect_toggled => move |_| {
connect_toggled[drawing_area] => move |_| {
APP_BROKER.send(AppMsg::SettingsChanged);
drawing_area.queue_draw();
}
},
@@ -270,9 +278,12 @@ impl relm4::Component for VfCurveEditor {
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),
hovered_coords: Rc::new(Cell::new(None)),
hovered_point: Rc::new(Cell::new(None)),
dragging_point: Rc::new(Cell::new(None)),
drag_modifiers: Rc::new(Cell::new(gdk::ModifierType::empty())),
selected_range_start: Rc::new(Cell::new(None)),
selected_range_end: Rc::new(Cell::new(None)),
};
let widgets = view_output!();
@@ -313,15 +324,35 @@ impl relm4::Component for VfCurveEditor {
self.drag_modifiers.set(modifiers);
}
VfCurveEditorMsg::DragStart => {
if let Some(point) = self.hovered_point.get()
&& self.allow_editing.value()
{
if !self.allow_editing.value() {
return;
}
if let Some(point) = self.hovered_point.get() {
self.dragging_point.set(Some(point));
} else {
self.selected_range_start
.set(self.hovered_coords.get().map(|(x, _)| x as usize));
self.selected_range_end.set(None);
}
}
VfCurveEditorMsg::DragEnd => {
if self.dragging_point.take().is_some() {
APP_BROKER.send(AppMsg::SettingsChanged);
} else if self.allow_editing.value()
&& let Some(selected_start) = self.selected_range_start.get()
{
if let Some(selected_end) = self.hovered_coords.get().map(|(x, _)| x as usize)
&& self.points.borrow().iter().any(|p| {
cmp::min(selected_start, selected_end) < (p.voltage as usize)
&& (p.voltage as usize) < cmp::max(selected_start, selected_end)
})
{
self.selected_range_end.set(Some(selected_end));
} else {
self.selected_range_start.set(None);
self.selected_range_end.set(None);
}
}
}
VfCurveEditorMsg::FlattenCurve => {
@@ -458,7 +489,11 @@ impl VfCurveEditor {
{
let mut label = format!("Current: {current_clock} MHz @ {current_voltage} mV");
if stats.core_power_state != Some(0) {
if stats
.active_power_states
.and_then(|states| states.core)
.is_some_and(|state| state != PowerLevelId::Index(0))
{
label.push_str(" (Idle)");
}
@@ -491,18 +526,43 @@ impl VfCurveEditor {
let active_style = colors.text;
let hovered_style = colors.accent_bg;
let selected_style = colors.accent_bg.mix(0.8);
let selected_area_style = colors.text.mix(0.1);
let hovered_point = self
.dragging_point
.get()
.or_else(|| self.hovered_point.get());
if !self.allow_editing.value() {
self.selected_range_start.set(None);
self.selected_range_end.set(None);
}
if let Some((selected_start, selected_end)) = self.get_selected_range() {
let x_values = [selected_start, selected_end];
chart
.draw_series(AreaSeries::new(
x_values.map(|x| (x as u32, y_spec.end)),
0,
selected_area_style,
))
.unwrap();
}
let main_series = chart
.draw_series(PointSeries::of_element(
points.iter().map(vf_point_coords).enumerate(),
3,
ShapeStyle::from(&colors.success).filled(),
&|(i, coord), mut size, mut style| {
if let Some((selected_start, selected_end)) = self.get_selected_range() {
let voltage = coord.0 as usize;
if selected_start < voltage && voltage < selected_end {
style.color = selected_style.to_rgba();
}
}
let is_active = self.stats.borrow().voltage.gpu == Some(coord.0 as u64);
if is_active {
style.color = active_style.to_rgba();
@@ -588,6 +648,7 @@ impl VfCurveEditor {
.cursor_position
.get()
.and_then(|(x, y)| translate((x as i32, y as i32)));
self.hovered_coords.set(hovered_coords);
let hovered_point = hovered_coords.and_then(|(voltage, freq)| {
points
@@ -606,6 +667,16 @@ impl VfCurveEditor {
});
self.hovered_point.set(hovered_point);
if let Some(point_idx) = self.dragging_point.get()
&& let Some((selected_start, selected_end)) = self.get_selected_range()
{
let voltage = points[point_idx].voltage as usize;
if voltage < selected_start || voltage > selected_end {
self.selected_range_start.set(None);
self.selected_range_end.set(None);
}
}
if let Some((_voltage, freq)) = hovered_coords
&& let Some(point_idx) = self.dragging_point.get()
{
@@ -623,6 +694,16 @@ impl VfCurveEditor {
point.freq = new_freq;
}
}
} else if let Some((selected_start, selected_end)) = self.get_selected_range() {
for point in points.iter_mut() {
let voltage = point.voltage as usize;
if selected_start < voltage && voltage < selected_end {
let new_freq = (point.freq as i32 + drag_delta) as u32;
if new_freq > 0 {
point.freq = new_freq;
}
}
}
} else {
points[point_idx].freq = new_freq;
}
@@ -642,6 +723,30 @@ impl VfCurveEditor {
(cmp::min(start, end), cmp::max(end, start))
}
fn get_selected_range(&self) -> Option<(usize, usize)> {
match (
self.selected_range_start.get(),
self.selected_range_end.get(),
) {
(Some(selected_start), Some(selected_end)) => Some((
cmp::min(selected_start, selected_end),
cmp::max(selected_start, selected_end),
)),
(Some(selected_start), None) => {
let selected_end = self
.hovered_coords
.get()
.map(|(x, _)| x as usize)
.unwrap_or(selected_start);
Some((
cmp::min(selected_start, selected_end),
cmp::max(selected_start, selected_end),
))
}
_ => None,
}
}
pub fn is_empty(&self) -> bool {
self.points.borrow().is_empty()
}
+181 -159
View File
@@ -1,6 +1,7 @@
use crate::app::ext::FlowBoxExt;
mod vulkan;
use crate::app::loader;
use crate::{
GUI_VERSION, I18N, REPO_URL,
app::{
@@ -13,14 +14,14 @@ use gtk::prelude::*;
use i18n_embed_fl::fl;
use indexmap::IndexMap;
use lact_client::schema::{GIT_COMMIT, SystemInfo};
use lact_schema::{DeviceInfo, OpenCLInfo, VulkanInfo};
use lact_schema::{DeviceApiInfo, OpenCLInfo, VulkanInfo};
use relm4::{Component, ComponentController, ComponentParts, ComponentSender, RelmWidgetExt};
use relm4_components::simple_combo_box::{SimpleComboBox, SimpleComboBoxMsg};
use std::{fmt::Write, sync::Arc};
use std::fmt::Write as _;
use vulkan::feature_window::{VulkanFeature, VulkanFeaturesWindow};
pub struct SoftwarePage {
device_info: Option<Arc<DeviceInfo>>,
device_api_info: Option<DeviceApiInfo>,
vulkan_driver_selector: relm4::Controller<SimpleComboBox<String>>,
opencl_platform_selector: relm4::Controller<SimpleComboBox<String>>,
@@ -30,7 +31,7 @@ pub struct SoftwarePage {
#[derive(Debug)]
pub enum SoftwarePageMsg {
DeviceInfo(Arc<DeviceInfo>),
DeviceApiInfo(Option<DeviceApiInfo>),
ShowVulkanFeatures,
ShowVulkanExtensions,
SelectionChanged,
@@ -63,163 +64,179 @@ impl relm4::SimpleComponent for SoftwarePage {
},
},
#[name = "vulkan_stack"]
match model.selected_vulkan_info() {
Some(info) => {
PageSection::new("Vulkan") {
append_child = &gtk::FlowBox {
set_orientation: gtk::Orientation::Horizontal,
set_column_spacing: 10,
set_homogeneous: true,
set_min_children_per_line: 2,
set_max_children_per_line: 4,
set_selection_mode: gtk::SelectionMode::None,
append_child = &InfoRow {
set_name: fl!(I18N, "instance"),
append_child = model.vulkan_driver_selector.widget(),
} -> vulkan_instance_item: gtk::FlowBoxChild {
#[watch]
set_visible: model.vulkan_driver_selector.model().variants.len() > 1,
},
append_child = &InfoRow {
set_name: fl!(I18N, "device-name"),
#[watch]
set_value: info.device_name.as_str(),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "api-version"),
#[watch]
set_value: info.api_version.as_str(),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "driver-name"),
#[watch]
set_value: info.driver.name.as_deref().unwrap_or_default(),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "driver-version"),
#[watch]
set_value: info.driver.info.as_deref().unwrap_or_default(),
set_selectable: true,
},
append_child = &InfoRow {
set_value: fl!(I18N, "features"),
set_icon: "go-next-symbolic".to_string(),
connect_clicked => SoftwarePageMsg::ShowVulkanFeatures,
},
append_child = &InfoRow {
set_value: fl!(I18N, "extensions"),
set_icon: "go-next-symbolic".to_string(),
connect_clicked => SoftwarePageMsg::ShowVulkanExtensions,
},
},
}
}
None => {
PageSection::new("Vulkan") {
append_child = &gtk::Label {
set_label: &fl!(I18N, "device-not-found", kind = "Vulkan"),
set_halign: gtk::Align::Start,
},
}
}
#[local_ref]
loader_picture -> gtk::Picture {
set_halign: gtk::Align::Center,
set_valign: gtk::Align::Start,
#[watch]
set_visible: model.device_api_info.is_none(),
},
#[name = "opencl_stack"]
match model.selected_opencl_info() {
Some(info) => {
PageSection::new("OpenCL") {
append_child = &gtk::FlowBox {
set_orientation: gtk::Orientation::Horizontal,
set_column_spacing: 10,
set_homogeneous: true,
set_min_children_per_line: 2,
set_max_children_per_line: 4,
set_selection_mode: gtk::SelectionMode::None,
gtk::Box {
set_spacing: 15,
set_orientation: gtk::Orientation::Vertical,
#[watch]
set_visible: model.device_api_info.is_some(),
set_visible: false,
append_child = &InfoRow {
set_name: fl!(I18N, "platform-name"),
append_child = model.opencl_platform_selector.widget(),
} -> opencl_platform_item: gtk::FlowBoxChild {
#[watch]
set_visible: model.opencl_platform_selector.model().variants.len() > 1,
},
#[name = "vulkan_stack"]
match model.selected_vulkan_info() {
Some(info) => {
PageSection::new("Vulkan") {
append_child = &gtk::FlowBox {
set_orientation: gtk::Orientation::Horizontal,
set_column_spacing: 10,
set_homogeneous: true,
set_min_children_per_line: 2,
set_max_children_per_line: 4,
set_selection_mode: gtk::SelectionMode::None,
append_child = &InfoRow {
set_name: fl!(I18N, "platform-name"),
#[watch]
set_value: info.platform_name.as_str(),
set_selectable: true,
} -> opencl_platform_name_item: gtk::FlowBoxChild {
#[watch]
set_visible: model.opencl_platform_selector.model().variants.len() == 1,
append_child = &InfoRow {
set_name: fl!(I18N, "instance"),
append_child = model.vulkan_driver_selector.widget(),
} -> vulkan_instance_item: gtk::FlowBoxChild {
#[watch]
set_visible: model.vulkan_driver_selector.model().variants.len() > 1,
},
append_child = &InfoRow {
set_name: fl!(I18N, "device-name"),
#[watch]
set_value: info.device_name.as_str(),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "api-version"),
#[watch]
set_value: info.api_version.as_str(),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "driver-name"),
#[watch]
set_value: info.driver.name.as_deref().unwrap_or_default(),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "driver-version"),
#[watch]
set_value: info.driver.info.as_deref().unwrap_or_default(),
set_selectable: true,
},
append_child = &InfoRow {
set_value: fl!(I18N, "features"),
set_icon: "go-next-symbolic".to_string(),
connect_clicked => SoftwarePageMsg::ShowVulkanFeatures,
},
append_child = &InfoRow {
set_value: fl!(I18N, "extensions"),
set_icon: "go-next-symbolic".to_string(),
connect_clicked => SoftwarePageMsg::ShowVulkanExtensions,
},
},
append_child = &InfoRow {
set_name: fl!(I18N, "device-name"),
#[watch]
set_value: info.device_name.as_str(),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "version"),
#[watch]
set_value: info.version.as_str(),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "driver-version"),
#[watch]
set_value: info.driver_version.as_str(),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "cl-c-version"),
#[watch]
set_value: info.c_version.as_str(),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "compute-units"),
#[watch]
set_value: info.compute_units.to_string(),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "workgroup-size"),
#[watch]
set_value: info.workgroup_size.to_string(),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "global-memory"),
#[watch]
set_value: formatting::fmt_human_bytes(info.global_memory, None),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "local-memory"),
#[watch]
set_value: formatting::fmt_human_bytes(info.local_memory, None),
set_selectable: true,
},
},
}
}
}
None => {
PageSection::new("OpenCL") {
append_child = &gtk::Label {
set_label: &fl!(I18N, "device-not-found", kind = "OpenCL"),
set_halign: gtk::Align::Start,
},
None => {
PageSection::new("Vulkan") {
append_child = &gtk::Label {
set_label: &fl!(I18N, "device-not-found", kind = "Vulkan"),
set_halign: gtk::Align::Start,
},
}
}
}
},
#[name = "opencl_stack"]
match model.selected_opencl_info() {
Some(info) => {
PageSection::new("OpenCL") {
append_child = &gtk::FlowBox {
set_orientation: gtk::Orientation::Horizontal,
set_column_spacing: 10,
set_homogeneous: true,
set_min_children_per_line: 2,
set_max_children_per_line: 4,
set_selection_mode: gtk::SelectionMode::None,
append_child = &InfoRow {
set_name: fl!(I18N, "platform-name"),
append_child = model.opencl_platform_selector.widget(),
} -> opencl_platform_item: gtk::FlowBoxChild {
#[watch]
set_visible: model.opencl_platform_selector.model().variants.len() > 1,
},
append_child = &InfoRow {
set_name: fl!(I18N, "platform-name"),
#[watch]
set_value: info.platform_name.as_str(),
set_selectable: true,
} -> opencl_platform_name_item: gtk::FlowBoxChild {
#[watch]
set_visible: model.opencl_platform_selector.model().variants.len() == 1,
},
append_child = &InfoRow {
set_name: fl!(I18N, "device-name"),
#[watch]
set_value: info.device_name.as_str(),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "version"),
#[watch]
set_value: info.version.as_str(),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "driver-version"),
#[watch]
set_value: info.driver_version.as_str(),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "cl-c-version"),
#[watch]
set_value: info.c_version.as_str(),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "compute-units"),
#[watch]
set_value: info.compute_units.to_string(),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "workgroup-size"),
#[watch]
set_value: info.workgroup_size.to_string(),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "global-memory"),
#[watch]
set_value: formatting::fmt_human_bytes(info.global_memory, None),
set_selectable: true,
},
append_child = &InfoRow {
set_name: fl!(I18N, "local-memory"),
#[watch]
set_value: formatting::fmt_human_bytes(info.local_memory, None),
set_selectable: true,
},
},
}
}
None => {
PageSection::new("OpenCL") {
append_child = &gtk::Label {
set_label: &fl!(I18N, "device-not-found", kind = "OpenCL"),
set_halign: gtk::Align::Start,
},
}
}
},
},
}
}
@@ -244,10 +261,10 @@ impl relm4::SimpleComponent for SoftwarePage {
.forward(sender.input_sender(), |_| SoftwarePageMsg::SelectionChanged);
let model = Self {
device_info: None,
vulkan_driver_selector,
opencl_platform_selector,
vulkan_window: None,
device_api_info: None,
};
let mut daemon_version = format!("{}-{}", system_info.version, system_info.profile);
@@ -273,6 +290,8 @@ impl relm4::SimpleComponent for SoftwarePage {
"{GUI_VERSION}-{gui_profile} (commit <a href=\"{gui_commit_link}\">{GIT_COMMIT}</a>)"
);
let loader_picture = loader::new();
let widgets = view_output!();
widgets.vulkan_stack.set_vhomogeneous(false);
@@ -283,7 +302,7 @@ impl relm4::SimpleComponent for SoftwarePage {
fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
match msg {
SoftwarePageMsg::DeviceInfo(info) => {
SoftwarePageMsg::DeviceApiInfo(Some(info)) => {
let mut vulkan_drivers = Vec::new();
for info in &info.vulkan_instances {
@@ -323,7 +342,10 @@ impl relm4::SimpleComponent for SoftwarePage {
active_index: selected_platform,
}));
self.device_info = Some(info);
self.device_api_info = Some(info);
}
SoftwarePageMsg::DeviceApiInfo(None) => {
self.device_api_info = None;
}
SoftwarePageMsg::ShowVulkanFeatures => {
if let Some(vulkan_info) = &self.selected_vulkan_info() {
@@ -352,7 +374,7 @@ impl SoftwarePage {
.model()
.active_index
.and_then(|idx| {
self.device_info
self.device_api_info
.as_ref()
.and_then(|info| info.vulkan_instances.get(idx))
})
@@ -363,7 +385,7 @@ impl SoftwarePage {
.model()
.active_index
.and_then(|idx| {
self.device_info
self.device_api_info
.as_ref()
.and_then(|info| info.opencl_instances.get(idx))
})
+1
View File
@@ -467,6 +467,7 @@ impl relm4::Component for ThermalsPage {
let msg = CurveSetupMsg {
curve: stats.fan.curve.clone().unwrap_or_else(default_fan_curve),
hw_based: self.has_pmfw,
current_temperatures: stats.temps.clone(),
temperature_key: stats.fan.temperature_key.clone(),
spindown_delay: stats.fan.spindown_delay_ms,
@@ -51,6 +51,8 @@ const PERCENTAGE_DRAG_MARGIN: f32 = 0.04;
#[derive(Clone)]
pub(super) struct FanCurveFrame {
pmfw_options: PmfwOptions,
/// PMFW fan control on AMD RDNA3+ with fixed length
hw_based_fan_curve: Rc<AtomicBool>,
data: Rc<RefCell<Vec<(i32, f32)>>>,
speed_range: Rc<RefCell<RangeInclusive<f32>>>,
@@ -89,6 +91,7 @@ pub(super) enum FanCurveFrameMsg {
#[derive(Debug)]
pub(super) struct CurveSetupMsg {
pub curve: FanCurveMap,
pub hw_based: bool,
pub current_temperatures: HashMap<String, TemperatureEntry>,
pub temperature_key: Option<String>,
pub speed_range: RangeInclusive<f32>,
@@ -156,14 +159,14 @@ impl relm4::Component for FanCurveFrame {
set_icon_name: "list-add-symbolic",
connect_clicked => FanCurveFrameMsg::AddPoint,
#[watch]
set_visible: model.pmfw_options.is_empty(),
set_visible: !model.hw_based_fan_curve.load(Ordering::SeqCst),
},
gtk::Button {
set_icon_name: "list-remove-symbolic",
connect_clicked => FanCurveFrameMsg::RemovePoint,
#[watch]
set_visible: model.pmfw_options.is_empty(),
set_visible: !model.hw_based_fan_curve.load(Ordering::SeqCst),
},
gtk::Button {
@@ -196,7 +199,7 @@ impl relm4::Component for FanCurveFrame {
#[template]
FanSettingRow {
#[watch]
set_visible: model.pmfw_options.is_empty(),
set_visible: !model.hw_based_fan_curve.load(Ordering::SeqCst),
#[template_child]
label {
@@ -220,7 +223,7 @@ impl relm4::Component for FanCurveFrame {
#[template]
FanSettingRow {
#[watch]
set_visible: model.pmfw_options.is_empty(),
set_visible: !model.hw_based_fan_curve.load(Ordering::SeqCst),
#[template_child]
label {
@@ -327,28 +330,9 @@ impl relm4::Component for FanCurveFrame {
let temp_keys = gtk::StringList::default();
let current_temp_key = U32Binding::new(0u32);
let change_signals = [
&spindown_delay_adj,
&change_threshold_adj,
&auto_threshold_adj,
]
.into_iter()
.map(|adj| {
let signal = adj.connect_value_changed(|_| {
APP_BROKER.send(AppMsg::SettingsChanged);
});
(adj.clone().upcast(), signal)
})
.chain([(
current_temp_key.clone().upcast(),
current_temp_key.connect_value_notify(|_| {
APP_BROKER.send(AppMsg::SettingsChanged);
}),
)])
.collect();
let model = Self {
let mut model = Self {
pmfw_options,
hw_based_fan_curve: Rc::new(AtomicBool::new(false)),
is_dragging: Rc::new(AtomicBool::new(false)),
speed_range: Rc::new(RefCell::new(DEFAULT_SPEED_RANGE)),
temperature_range: Rc::new(RefCell::new(DEFAULT_TEMP_RANGE)),
@@ -357,7 +341,7 @@ impl relm4::Component for FanCurveFrame {
auto_threshold_adj,
temp_keys,
current_temp_key,
change_signals,
change_signals: Rc::default(),
data: Rc::default(),
drag_coord: Rc::default(),
drag_point: Rc::default(),
@@ -367,8 +351,29 @@ impl relm4::Component for FanCurveFrame {
let label_size_group = gtk::SizeGroup::new(gtk::SizeGroupMode::Horizontal);
let spin_size_group = gtk::SizeGroup::new(gtk::SizeGroupMode::Horizontal);
let widgets = view_output!();
model.change_signals = [
&model.spindown_delay_adj,
&model.change_threshold_adj,
&model.auto_threshold_adj,
]
.into_iter()
.map(|adj| {
let signal = adj.connect_value_changed(|_| {
APP_BROKER.send(AppMsg::SettingsChanged);
});
(adj.clone().upcast(), signal)
})
.chain([(
model.current_temp_key.clone().upcast(),
model.current_temp_key.connect_value_notify(|_| {
APP_BROKER.send(AppMsg::SettingsChanged);
}),
)])
.collect();
ComponentParts { model, widgets }
}
@@ -386,6 +391,8 @@ impl relm4::Component for FanCurveFrame {
.collect();
*self.speed_range.borrow_mut() = msg.speed_range;
*self.temperature_range.borrow_mut() = msg.temperature_range.clone();
self.hw_based_fan_curve
.store(msg.hw_based, Ordering::SeqCst);
for (adj, signal) in self.change_signals.iter() {
adj.block_signal(signal);
@@ -529,7 +536,7 @@ impl FanCurveFrame {
}
fn temp_keys_available(&self) -> bool {
self.pmfw_options.is_empty()
!self.hw_based_fan_curve.load(Ordering::SeqCst)
&& self.temp_keys.n_items() > 1
&& (self.auto_threshold_adj.upper() == 0.0) // Disable key selection on nvidia
}
+2 -1
View File
@@ -4,6 +4,7 @@ mod profile_row;
pub mod profile_rule_window;
use crate::I18N;
use crate::app::ext::RelmLaunchable as _;
use crate::app::msg::AppMsg;
use crate::app::profiles::new_profile_dialog::NewProfileDialog;
use crate::app::profiles::profile_rename_dialog::ProfileRenameDialog;
@@ -299,7 +300,7 @@ impl Component for ProfileSelector {
auto_switch: *auto,
parent: root.clone().upcast::<gtk::Widget>(),
};
let rule_window = ProfileRuleWindow::builder().launch(params).into_stream();
let rule_window = ProfileRuleWindow::launch(params).into_stream();
sender.clone().oneshot_command(async move {
if let Some((name, rule, hooks)) = rule_window.recv_one().await {
+21 -9
View File
@@ -15,7 +15,7 @@ pub use response::Response;
use amdgpu_sysfs::{
gpu_handle::{
PerformanceLevel,
PerformanceLevel, PowerLevelId,
fan_control::FanInfo,
overdrive::{ClocksTable as _, ClocksTableGen as AmdClocksTableGen},
},
@@ -133,10 +133,8 @@ pub enum DeviceFlag {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DeviceInfo {
pub pci_info: Option<GpuPciInfo>,
#[serde(default)]
pub vulkan_instances: Vec<VulkanInfo>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub opencl_instances: Vec<OpenCLInfo>,
#[serde(flatten)]
pub api_info: DeviceApiInfo,
pub driver: String,
pub vbios_version: Option<String>,
pub link_info: LinkInfo,
@@ -330,6 +328,14 @@ impl DeviceInfo {
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct DeviceApiInfo {
#[serde(default)]
pub vulkan_instances: Vec<VulkanInfo>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub opencl_instances: Vec<OpenCLInfo>,
}
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct DrmInfo {
@@ -549,12 +555,18 @@ pub struct DeviceStats {
pub temps: HashMap<String, TemperatureEntry>,
pub busy_percent: Option<u8>,
pub performance_level: Option<PerformanceLevel>,
pub core_power_state: Option<usize>,
pub memory_power_state: Option<usize>,
pub pcie_power_state: Option<usize>,
pub active_power_states: Option<ActivePowerStates>,
pub throttle_info: Option<BTreeMap<String, Vec<String>>>,
}
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
pub struct ActivePowerStates {
pub core: Option<PowerLevelId>,
pub memory: Option<PowerLevelId>,
pub pcie: Option<PowerLevelId>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TemperatureEntry {
#[serde(flatten)]
@@ -693,7 +705,7 @@ pub struct PowerState {
pub enabled: bool,
pub min_value: Option<u64>,
pub value: u64,
pub index: Option<u8>,
pub id: Option<PowerLevelId>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
+5
View File
@@ -15,6 +15,11 @@ pub enum Request<'a> {
SystemInfo,
DeviceInfo {
id: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
include_api_info: Option<bool>,
},
DeviceApiInfo {
id: &'a str,
},
DeviceStats {
id: &'a str,
+7 -1
View File
@@ -35,7 +35,8 @@
},
"1458": {
"2424": "GIGABYTE RX 9070 XT Gaming OC",
"2426": "GIGABYTE RX 9070 XT Elite"
"2426": "GIGABYTE RX 9070 XT Elite",
"2437": "GIGABYTE RX 9070 XT Gaming OC ICE"
}
},
"744c": {
@@ -392,6 +393,11 @@
"f296": "Palit RTX 4090 GameRock OC"
}
},
"2689": {
"1458": {
"4162": "GIGABYTE RTX 4070 Ti SUPER WindForce MAX OC"
}
},
"2704": {
"10de": {
"167a": "NVIDIA RTX 4080 Founders Edition"
+4
View File
@@ -131,6 +131,7 @@
<path d="m 61.417969 69.824219 c 0 11.035156 -8.945313 19.980469 -19.980469 19.980469 s -19.980469 -8.945313 -19.980469 -19.980469 c 0 -11.035157 8.945313 -19.980469 19.980469 -19.980469 s 19.980469 8.945312 19.980469 19.980469 z m 0 0" fill="#171420"/>
<path d="m 108.523438 69.824219 c 0 12.128906 -9.832032 21.957031 -21.960938 21.957031 c -12.125 0 -21.957031 -9.828125 -21.957031 -21.957031 c 0 -12.128907 9.832031 -21.957031 21.957031 -21.957031 c 12.128906 0 21.960938 9.828124 21.960938 21.957031 z m 0 0" fill="url(#d)"/>
<path d="m 106.542969 69.824219 c 0 11.035156 -8.945313 19.980469 -19.980469 19.980469 s -19.980469 -8.945313 -19.980469 -19.980469 c 0 -11.035157 8.945313 -19.980469 19.980469 -19.980469 s 19.980469 8.945312 19.980469 19.980469 z m 0 0" fill="#171420"/>
<g id="left-fan-blades">
<path d="m 59.769531 66.050781 c -0.105469 -0.503906 -0.320312 -0.863281 -0.71875 -1.191406 c -3.167969 -2.617187 -7.558593 -2.441406 -13.179687 0.527344 c -0.367188 -0.363281 -0.761719 -0.675781 -1.207032 -0.941407 c 3.871094 -3.472656 7.402344 -4.933593 10.589844 -4.375 c 0.9375 0.164063 1.589844 -0.914062 1.007813 -1.667968 c -1.292969 -1.679688 -2.761719 -3.042969 -4.53125 -4.207032 c -0.429688 -0.285156 -0.835938 -0.386718 -1.347657 -0.335937 c -4.089843 0.386719 -7.074218 3.617187 -8.945312 9.691406 c -0.519531 0 -1.019531 0.0625 -1.523438 0.1875 c 0.28125 -5.195312 1.746094 -8.722656 4.394532 -10.582031 c 0.78125 -0.546875 0.480468 -1.769531 -0.464844 -1.894531 c -2.101562 -0.269531 -4.105469 -0.195313 -6.179688 0.230469 c -0.503906 0.105468 -0.863281 0.320312 -1.191406 0.714843 c -2.617187 3.167969 -2.441406 7.5625 0.527344 13.179688 c -0.367188 0.367187 -0.675781 0.765625 -0.941406 1.210937 c -3.476563 -3.875 -4.933594 -7.402344 -4.378906 -10.589844 c 0.164062 -0.941406 -0.910157 -1.589843 -1.667969 -1.007812 c -1.675781 1.292969 -3.042969 2.761719 -4.207031 4.53125 c -0.28125 0.429688 -0.382813 0.835938 -0.335938 1.347656 c 0.390625 4.089844 3.621094 7.074219 9.695312 8.945313 c 0 0.519531 0.058594 1.019531 0.1875 1.523437 c -5.195312 -0.285156 -8.722656 -1.75 -10.582031 -4.394531 c -0.550781 -0.78125 -1.769531 -0.480469 -1.894531 0.464844 c -0.269531 2.101562 -0.195312 4.105469 0.230469 6.179687 c 0.101562 0.503906 0.320312 0.863282 0.714843 1.191406 c 3.167969 2.617188 7.5625 2.441407 13.179688 -0.527343 c 0.367188 0.363281 0.765625 0.675781 1.207031 0.941406 c -3.871093 3.472656 -7.402343 4.933594 -10.585937 4.375 c -0.941406 -0.164063 -1.59375 0.914063 -1.011719 1.667969 c 1.292969 1.679687 2.765625 3.042968 4.53125 4.207031 c 0.429687 0.285156 0.839844 0.386719 1.351563 0.335937 c 4.089843 -0.386718 7.070312 -3.617187 8.945312 -9.691406 c 0.515625 0 1.015625 -0.0625 1.519531 -0.1875 c -0.28125 5.195313 -1.746093 8.722656 -4.394531 10.582032 c -0.777344 0.546874 -0.476562 1.769531 0.46875 1.890624 c 2.097656 0.273438 4.105469 0.199219 6.179688 -0.226562 c 0.503906 -0.105469 0.863281 -0.320312 1.1875 -0.71875 c 2.621093 -3.164062 2.445312 -7.558594 -0.527344 -13.175781 c 0.367187 -0.367188 0.679687 -0.765625 0.945312 -1.210938 c 3.472656 3.875 4.933594 7.402344 4.375 10.589844 c -0.164062 0.941406 0.914063 1.589844 1.667969 1.007813 c 1.675781 -1.292969 3.042969 -2.761719 4.207031 -4.53125 c 0.285156 -0.429688 0.382813 -0.835938 0.335938 -1.347657 c -0.390625 -4.089843 -3.621094 -7.074219 -9.691406 -8.945312 c 0 -0.519531 -0.0625 -1.019531 -0.1875 -1.523438 c 5.195312 0.285157 8.722656 1.746094 10.582031 4.394531 c 0.546875 0.78125 1.769531 0.480469 1.890625 -0.464843 c 0.273437 -2.101563 0.199218 -4.105469 -0.226563 -6.179688 z m -18.332031 9 c -2.886719 0 -5.230469 -2.339843 -5.230469 -5.226562 s 2.34375 -5.226563 5.230469 -5.226563 s 5.226562 2.339844 5.226562 5.226563 s -2.339843 5.226562 -5.226562 5.226562 z m 0 0"/>
<path d="m 39.914062 63.738281 c 0.28125 -5.195312 1.746094 -8.722656 4.394532 -10.582031 c 0.78125 -0.546875 0.480468 -1.769531 -0.464844 -1.894531 c -2.101562 -0.269531 -4.105469 -0.195313 -6.179688 0.230469 c -0.503906 0.105468 -0.863281 0.320312 -1.191406 0.714843 c -2.617187 3.167969 -2.441406 7.5625 0.527344 13.179688 z m 0 0" fill="url(#f)"/>
<path d="m 44.664062 64.445312 c 3.871094 -3.472656 7.402344 -4.933593 10.589844 -4.375 c 0.9375 0.164063 1.589844 -0.914062 1.007813 -1.667968 c -1.292969 -1.679688 -2.761719 -3.042969 -4.53125 -4.207032 c -0.429688 -0.285156 -0.835938 -0.386718 -1.347657 -0.335937 c -4.089843 0.386719 -7.074218 3.617187 -8.945312 9.691406 z m 0 0" fill="url(#g)"/>
@@ -140,8 +141,10 @@
<path d="m 38.207031 75.203125 c -3.871093 3.472656 -7.402343 4.933594 -10.585937 4.375 c -0.941406 -0.164063 -1.59375 0.914063 -1.011719 1.667969 c 1.292969 1.679687 2.765625 3.042968 4.53125 4.207031 c 0.429687 0.285156 0.839844 0.386719 1.351563 0.335937 c 4.089843 -0.386718 7.070312 -3.617187 8.945312 -9.691406 z m 0 0" fill="url(#k)"/>
<path d="m 35.351562 71.34375 c -5.195312 -0.28125 -8.722656 -1.746094 -10.582031 -4.390625 c -0.550781 -0.78125 -1.769531 -0.480469 -1.894531 0.464844 c -0.269531 2.101562 -0.195312 4.105469 0.230469 6.179687 c 0.101562 0.503906 0.320312 0.863282 0.714843 1.191406 c 3.167969 2.617188 7.5625 2.441407 13.179688 -0.527343 z m 0 0" fill="url(#l)"/>
<path d="m 36.058594 66.597656 c -3.476563 -3.875 -4.933594 -7.402344 -4.378906 -10.589844 c 0.164062 -0.941406 -0.910157 -1.589843 -1.667969 -1.007812 c -1.675781 1.292969 -3.042969 2.761719 -4.207031 4.53125 c -0.28125 0.429688 -0.382813 0.835938 -0.335938 1.347656 c 0.390625 4.089844 3.621094 7.074219 9.695312 8.945313 z m 0 0" fill="url(#m)"/>
</g>
<path d="m 47.898438 69.824219 c 0 3.574219 -2.894532 6.46875 -6.460938 6.46875 c -3.570312 0 -6.464844 -2.894531 -6.464844 -6.46875 s 2.894532 -6.46875 6.464844 -6.46875 c 3.566406 0 6.460938 2.894531 6.460938 6.46875 z m 0 0" fill="url(#n)"/>
<path d="m 46.957031 69.824219 c 0 3.050781 -2.472656 5.523437 -5.519531 5.523437 c -3.050781 0 -5.519531 -2.472656 -5.519531 -5.523437 s 2.46875 -5.523438 5.519531 -5.523438 c 3.046875 0 5.519531 2.472657 5.519531 5.523438 z m 0 0" fill="#342f3b"/>
<g id="right-fan-blades">
<path d="m 104.894531 66.050781 c -0.101562 -0.503906 -0.320312 -0.863281 -0.714843 -1.191406 c -3.167969 -2.617187 -7.5625 -2.441406 -13.179688 0.527344 c -0.367188 -0.363281 -0.765625 -0.675781 -1.207031 -0.941407 c 3.871093 -3.472656 7.402343 -4.933593 10.585937 -4.375 c 0.941406 0.164063 1.59375 -0.914062 1.011719 -1.667968 c -1.292969 -1.679688 -2.765625 -3.042969 -4.53125 -4.207032 c -0.429687 -0.285156 -0.839844 -0.386718 -1.351563 -0.335937 c -4.089843 0.386719 -7.070312 3.617187 -8.945312 9.691406 c -0.515625 0 -1.015625 0.0625 -1.519531 0.1875 c 0.28125 -5.195312 1.746093 -8.722656 4.394531 -10.582031 c 0.777344 -0.546875 0.476562 -1.769531 -0.46875 -1.894531 c -2.097656 -0.269531 -4.105469 -0.195313 -6.179688 0.230469 c -0.503906 0.105468 -0.863281 0.320312 -1.1875 0.714843 c -2.621093 3.167969 -2.445312 7.5625 0.527344 13.179688 c -0.367187 0.367187 -0.679687 0.765625 -0.945312 1.210937 c -3.472656 -3.875 -4.933594 -7.402344 -4.375 -10.589844 c 0.164062 -0.941406 -0.914063 -1.589843 -1.667969 -1.007812 c -1.675781 1.292969 -3.042969 2.761719 -4.207031 4.53125 c -0.285156 0.429688 -0.382813 0.835938 -0.335938 1.347656 c 0.390625 4.089844 3.621094 7.074219 9.691406 8.945313 c 0 0.519531 0.0625 1.019531 0.1875 1.523437 c -5.195312 -0.285156 -8.722656 -1.75 -10.582031 -4.394531 c -0.546875 -0.78125 -1.769531 -0.480469 -1.890625 0.464844 c -0.273437 2.101562 -0.199218 4.105469 0.226563 6.179687 c 0.105469 0.503906 0.320312 0.863282 0.71875 1.191406 c 3.167969 2.617188 7.558593 2.441407 13.179687 -0.527343 c 0.367188 0.363281 0.761719 0.675781 1.207032 0.941406 c -3.871094 3.472656 -7.402344 4.933594 -10.589844 4.375 c -0.9375 -0.164063 -1.589844 0.914063 -1.007813 1.667969 c 1.292969 1.679687 2.761719 3.042968 4.53125 4.207031 c 0.429688 0.285156 0.835938 0.386719 1.347657 0.335937 c 4.089843 -0.386718 7.074218 -3.617187 8.945312 -9.691406 c 0.519531 0 1.019531 -0.0625 1.523438 -0.1875 c -0.28125 5.195313 -1.746094 8.722656 -4.394532 10.582032 c -0.78125 0.546874 -0.480468 1.769531 0.464844 1.890624 c 2.101562 0.273438 4.105469 0.199219 6.179688 -0.226562 c 0.503906 -0.105469 0.863281 -0.320312 1.191406 -0.71875 c 2.617187 -3.164062 2.441406 -7.558594 -0.527344 -13.175781 c 0.367188 -0.367188 0.675781 -0.765625 0.941406 -1.210938 c 3.476563 3.875 4.933594 7.402344 4.378906 10.589844 c -0.164062 0.941406 0.910157 1.589844 1.667969 1.007813 c 1.675781 -1.292969 3.042969 -2.761719 4.207031 -4.53125 c 0.28125 -0.429688 0.382813 -0.835938 0.335938 -1.347657 c -0.390625 -4.089843 -3.621094 -7.074219 -9.695312 -8.945312 c 0 -0.519531 -0.058594 -1.019531 -0.1875 -1.523438 c 5.195312 0.285157 8.722656 1.746094 10.582031 4.394531 c 0.550781 0.78125 1.769531 0.480469 1.894531 -0.464843 c 0.269531 -2.101563 0.195312 -4.105469 -0.230469 -6.179688 z m -18.332031 9 c -2.886719 0 -5.226562 -2.339843 -5.226562 -5.226562 s 2.339843 -5.226563 5.226562 -5.226563 s 5.230469 2.339844 5.230469 5.226563 s -2.34375 5.226562 -5.230469 5.226562 z m 0 0"/>
<path d="m 85.042969 63.738281 c 0.28125 -5.195312 1.746093 -8.722656 4.394531 -10.582031 c 0.777344 -0.546875 0.476562 -1.769531 -0.46875 -1.894531 c -2.097656 -0.269531 -4.105469 -0.195313 -6.179688 0.230469 c -0.503906 0.105468 -0.863281 0.320312 -1.1875 0.714843 c -2.621093 3.167969 -2.445312 7.5625 0.527344 13.179688 z m 0 0" fill="url(#o)"/>
<path d="m 89.792969 64.445312 c 3.871093 -3.472656 7.402343 -4.933593 10.585937 -4.375 c 0.941406 0.164063 1.59375 -0.914062 1.011719 -1.667968 c -1.292969 -1.679688 -2.765625 -3.042969 -4.53125 -4.207032 c -0.429687 -0.285156 -0.839844 -0.386718 -1.351563 -0.335937 c -4.089843 0.386719 -7.070312 3.617187 -8.945312 9.691406 z m 0 0" fill="url(#p)"/>
@@ -151,6 +154,7 @@
<path d="m 83.335938 75.203125 c -3.871094 3.472656 -7.402344 4.933594 -10.589844 4.375 c -0.9375 -0.164063 -1.589844 0.914063 -1.007813 1.667969 c 1.292969 1.679687 2.761719 3.042968 4.53125 4.207031 c 0.429688 0.285156 0.835938 0.386719 1.347657 0.335937 c 4.089843 -0.386718 7.074218 -3.617187 8.945312 -9.691406 z m 0 0" fill="url(#t)"/>
<path d="m 80.476562 71.34375 c -5.195312 -0.28125 -8.722656 -1.746094 -10.582031 -4.390625 c -0.546875 -0.78125 -1.769531 -0.480469 -1.890625 0.464844 c -0.273437 2.101562 -0.199218 4.105469 0.226563 6.179687 c 0.105469 0.503906 0.320312 0.863282 0.71875 1.191406 c 3.167969 2.617188 7.558593 2.441407 13.179687 -0.527343 z m 0 0" fill="url(#u)"/>
<path d="m 81.183594 66.597656 c -3.472656 -3.875 -4.933594 -7.402344 -4.375 -10.589844 c 0.164062 -0.941406 -0.914063 -1.589843 -1.667969 -1.007812 c -1.675781 1.292969 -3.042969 2.761719 -4.207031 4.53125 c -0.285156 0.429688 -0.382813 0.835938 -0.335938 1.347656 c 0.390625 4.089844 3.621094 7.074219 9.691406 8.945313 z m 0 0" fill="url(#v)"/>
</g>
<path d="m 93.027344 69.824219 c 0 3.574219 -2.894532 6.46875 -6.464844 6.46875 c -3.566406 0 -6.460938 -2.894531 -6.460938 -6.46875 s 2.894532 -6.46875 6.460938 -6.46875 c 3.570312 0 6.464844 2.894531 6.464844 6.46875 z m 0 0" fill="url(#w)"/>
<path d="m 92.082031 69.824219 c 0 3.050781 -2.46875 5.523437 -5.519531 5.523437 c -3.046875 0 -5.519531 -2.472656 -5.519531 -5.523437 s 2.472656 -5.523438 5.519531 -5.523438 c 3.050781 0 5.519531 2.472657 5.519531 5.523438 z m 0 0" fill="#342f3b"/>
</svg>

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Some files were not shown because too many files have changed in this diff Show More