From 8da4c9eddc2c69479d12f6cc8ab4cbe98881a60a Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Fri, 14 Aug 2026 15:23:42 +0200 Subject: [PATCH 1/6] feat: implement GPU telemetry methods with runtime power management awareness to avoid waking suspended devices --- rog-control-center/Cargo.toml | 1 - rog-platform/src/gpu_pci.rs | 224 +++++++++++++++++++++++++++++++--- 2 files changed, 209 insertions(+), 16 deletions(-) diff --git a/rog-control-center/Cargo.toml b/rog-control-center/Cargo.toml index 3d13ba36d..82c16078c 100644 --- a/rog-control-center/Cargo.toml +++ b/rog-control-center/Cargo.toml @@ -46,7 +46,6 @@ futures-util.workspace = true thiserror.workspace = true udev.workspace = true serde_json.workspace = true -nvml-wrapper.workspace = true [dependencies.slint] git = "https://github.com/slint-ui/slint.git" diff --git a/rog-platform/src/gpu_pci.rs b/rog-platform/src/gpu_pci.rs index a1ef4ba01..b5a3a8e1b 100644 --- a/rog-platform/src/gpu_pci.rs +++ b/rog-platform/src/gpu_pci.rs @@ -211,6 +211,140 @@ impl Device { } } + /// Read the temperature (°C) of this GPU from sysfs hwmon. + /// + /// If this is a discrete GPU and it is not in the `Active` power state, + /// this immediately returns `Some(0.0)` without reading sysfs hwmon + /// nodes to prevent waking the PCIe device from runtime PM sleep. + pub fn get_temp(&self) -> Option { + if self.is_dgpu + && self.get_runtime_status().unwrap_or(GfxPower::Unknown) != GfxPower::Active + { + return Some(0.0); + } + + // 1. Direct hwmon directory under device path + let hwmon_dir = self.dev_path.join("hwmon"); + if let Ok(entries) = fs::read_dir(&hwmon_dir) { + for entry in entries.flatten() { + let temp_path = entry.path().join("temp1_input"); + if let Ok(temp_str) = fs::read_to_string(temp_path) { + if let Ok(temp_val) = temp_str.trim().parse::() { + return Some(temp_val / 1000.0); + } + } + } + } + + // 2. Global /sys/class/hwmon matching this device's sysfs path + if let Ok(entries) = fs::read_dir("/sys/class/hwmon") { + for entry in entries.flatten() { + let path = entry.path(); + let is_match = path.join("device").canonicalize().ok().is_some_and(|p| { + p == self.dev_path + || self.dev_path.starts_with(&p) + || p.starts_with(&self.dev_path) + }); + if is_match { + let temp_path = path.join("temp1_input"); + if let Ok(temp_str) = fs::read_to_string(temp_path) { + if let Ok(temp_val) = temp_str.trim().parse::() { + return Some(temp_val / 1000.0); + } + } + } + } + } + + // 3. Fallback to NVML if this is an NVIDIA device and hwmon is not available + if self.pci_id.to_uppercase().starts_with(NVIDIA_PCI_VENDOR) { + if let Ok(nvml) = nvml_wrapper::Nvml::init() { + if let Ok(device) = nvml.device_by_index(0) { + if let Ok(temp) = device + .temperature(nvml_wrapper::enum_wrappers::device::TemperatureSensor::Gpu) + { + return Some(temp as f32); + } + } + } + } + + None + } + + /// Read the GPU utilization percentage (0.0 - 100.0) from sysfs DRM nodes. + /// + /// If this is a discrete GPU and it is not in the `Active` power state, + /// this immediately returns `Some(0.0)` without reading sysfs DRM + /// nodes to prevent waking the PCIe device from runtime PM sleep. + pub fn get_usage_pct(&self) -> Option { + if self.is_dgpu + && self.get_runtime_status().unwrap_or(GfxPower::Unknown) != GfxPower::Active + { + return Some(0.0); + } + + // 1. Direct gpu_busy_percent under device path + let direct_busy = self.dev_path.join("gpu_busy_percent"); + if direct_busy.exists() { + if let Ok(val_str) = fs::read_to_string(direct_busy) { + if let Ok(val) = val_str.trim().parse::() { + return Some(val); + } + } + } + + // 2. DRM card directories under device path + let drm_dir = self.dev_path.join("drm"); + if let Ok(entries) = fs::read_dir(&drm_dir) { + for entry in entries.flatten() { + let busy_path = entry.path().join("device/gpu_busy_percent"); + if busy_path.exists() { + if let Ok(val_str) = fs::read_to_string(busy_path) { + if let Ok(val) = val_str.trim().parse::() { + return Some(val); + } + } + } + } + } + + // 3. Global /sys/class/drm matching this device's sysfs path + if let Ok(entries) = fs::read_dir("/sys/class/drm") { + for entry in entries.flatten() { + let path = entry.path(); + let is_match = path.join("device").canonicalize().ok().is_some_and(|p| { + p == self.dev_path + || self.dev_path.starts_with(&p) + || p.starts_with(&self.dev_path) + }); + if is_match { + let busy_path = path.join("device/gpu_busy_percent"); + if busy_path.exists() { + if let Ok(val_str) = fs::read_to_string(busy_path) { + if let Ok(val) = val_str.trim().parse::() { + return Some(val); + } + } + } + } + } + } + + // 4. Fallback to NVML if this is an NVIDIA device and DRM busy is not available + if self.pci_id.to_uppercase().starts_with(NVIDIA_PCI_VENDOR) { + if let Ok(nvml) = nvml_wrapper::Nvml::init() { + if let Ok(device) = nvml.device_by_index(0) { + if let Ok(rates) = device.utilization_rates() { + return Some(rates.gpu as f32); + } + } + } + } + + None + } + /// Enumerate PCI GPU devices via udev and identify the dGPU. pub fn find() -> Result> { let mut devices = Vec::new(); @@ -523,12 +657,23 @@ pub fn get_gpu_names() -> (String, String) { } pub fn get_igpu_temp() -> f32 { + let devices = Device::find().unwrap_or_default(); + if let Some(igpu) = devices.iter().find(|d| !d.is_dgpu()) { + if let Some(temp) = igpu.get_temp() { + return temp; + } + } if let Ok(entries) = std::fs::read_dir("/sys/class/hwmon") { for entry in entries.flatten() { let path = entry.path(); if let Ok(name) = std::fs::read_to_string(path.join("name")) { let name = name.trim(); - if name == "amdgpu" { + if name == "amdgpu" + || name == "i915" + || name == "xe" + || name == "k10temp" + || name == "coretemp" + { if let Ok(temp_str) = std::fs::read_to_string(path.join("temp1_input")) { if let Ok(temp_val) = temp_str.trim().parse::() { return temp_val / 1000.0; @@ -542,6 +687,12 @@ pub fn get_igpu_temp() -> f32 { } pub fn get_igpu_usage_pct() -> f32 { + let devices = Device::find().unwrap_or_default(); + if let Some(igpu) = devices.iter().find(|d| !d.is_dgpu()) { + if let Some(usage) = igpu.get_usage_pct() { + return usage; + } + } if let Ok(entries) = std::fs::read_dir("/sys/class/drm") { for entry in entries.flatten() { let path = entry.path(); @@ -554,7 +705,7 @@ pub fn get_igpu_usage_pct() -> f32 { if busy_path.exists() { if let Ok(vendor_str) = std::fs::read_to_string(path.join("device/vendor")) { let vendor = vendor_str.trim(); - if vendor == "0x1002" { + if vendor == "0x1002" || vendor == "0x8086" { if let Ok(val_str) = std::fs::read_to_string(busy_path) { if let Ok(val) = val_str.trim().parse::() { return val; @@ -570,13 +721,13 @@ pub fn get_igpu_usage_pct() -> f32 { } pub fn get_gpu_temp() -> f32 { - if let Ok(nvml) = nvml_wrapper::Nvml::init() { - if let Ok(device) = nvml.device_by_index(0) { - if let Ok(temp) = - device.temperature(nvml_wrapper::enum_wrappers::device::TemperatureSensor::Gpu) - { - return temp as f32; - } + if get_gpu_power_status() != GfxPower::Active { + return 0.0; + } + let devices = Device::find().unwrap_or_default(); + if let Some(dgpu) = devices.iter().find(|d| d.is_dgpu()) { + if let Some(temp) = dgpu.get_temp() { + return temp; } } if let Ok(entries) = std::fs::read_dir("/sys/class/hwmon") { @@ -584,7 +735,11 @@ pub fn get_gpu_temp() -> f32 { let path = entry.path(); if let Ok(name) = std::fs::read_to_string(path.join("name")) { let name = name.trim(); - if name == "amdgpu" || name == "nouveau" { + if name == "amdgpu" + || name == "nouveau" + || name == "nvidia" + || name == "nvidia_hwmon" + { if let Ok(temp_str) = std::fs::read_to_string(path.join("temp1_input")) { if let Ok(temp_val) = temp_str.trim().parse::() { return temp_val / 1000.0; @@ -598,11 +753,13 @@ pub fn get_gpu_temp() -> f32 { } pub fn get_gpu_usage_pct() -> f32 { - if let Ok(nvml) = nvml_wrapper::Nvml::init() { - if let Ok(device) = nvml.device_by_index(0) { - if let Ok(rates) = device.utilization_rates() { - return rates.gpu as f32; - } + if get_gpu_power_status() != GfxPower::Active { + return 0.0; + } + let devices = Device::find().unwrap_or_default(); + if let Some(dgpu) = devices.iter().find(|d| d.is_dgpu()) { + if let Some(usage) = dgpu.get_usage_pct() { + return usage; } } if let Ok(entries) = std::fs::read_dir("/sys/class/drm") { @@ -743,6 +900,43 @@ mod tests { Ok(()) } + #[test] + fn device_get_temp_and_usage_when_suspended( + ) -> std::result::Result<(), Box> { + let dir = TestDir::new("asusctl_test_temp_suspended"); + fs::create_dir_all(dir.join("power"))?; + fs::write(dir.join("power/runtime_status"), "suspended\n")?; + + let hwmon_dir = dir.join("hwmon/hwmon0"); + fs::create_dir_all(&hwmon_dir)?; + fs::write(hwmon_dir.join("temp1_input"), "55000\n")?; + fs::write(dir.join("gpu_busy_percent"), "80\n")?; + + let device = fake_device(dir.0.clone()); + // Discrete GPU in suspended state must return 0.0 without querying hwmon/drm + assert_eq!(device.get_temp(), Some(0.0)); + assert_eq!(device.get_usage_pct(), Some(0.0)); + Ok(()) + } + + #[test] + fn device_get_temp_and_usage_when_active() -> std::result::Result<(), Box> + { + let dir = TestDir::new("asusctl_test_temp_active"); + fs::create_dir_all(dir.join("power"))?; + fs::write(dir.join("power/runtime_status"), "active\n")?; + + let hwmon_dir = dir.join("hwmon/hwmon0"); + fs::create_dir_all(&hwmon_dir)?; + fs::write(hwmon_dir.join("temp1_input"), "62500\n")?; + fs::write(dir.join("gpu_busy_percent"), "45\n")?; + + let device = fake_device(dir.0.clone()); + assert_eq!(device.get_temp(), Some(62.5)); + assert_eq!(device.get_usage_pct(), Some(45.0)); + Ok(()) + } + #[test] #[ignore = "requires ASUS hardware with a dGPU"] fn live_dgpu_detection() -> std::result::Result<(), Box> { From 8a7846ec264ac2ce4dfc90deda6a9cc20471f496 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Fri, 14 Aug 2026 15:34:47 +0200 Subject: [PATCH 2/6] refactor(rog-platform): streamline GPU PCI helper functions and metric delegation --- rog-platform/src/gpu_pci.rs | 321 ++++++++++++------------------------ 1 file changed, 102 insertions(+), 219 deletions(-) diff --git a/rog-platform/src/gpu_pci.rs b/rog-platform/src/gpu_pci.rs index b5a3a8e1b..fa877b3dc 100644 --- a/rog-platform/src/gpu_pci.rs +++ b/rog-platform/src/gpu_pci.rs @@ -155,6 +155,40 @@ pub fn is_display_class(pci_class: &str) -> bool { u32::from_str_radix(pci_class, 16).is_ok_and(|class| class >> 16 == 0x03) } +fn read_hwmon_temp(dir: &Path) -> Option { + fs::read_to_string(dir.join("temp1_input")) + .ok()? + .trim() + .parse::() + .ok() + .map(|t| t / 1000.0) +} + +fn read_drm_busy(dir: &Path) -> Option { + fs::read_to_string(dir.join("device/gpu_busy_percent")) + .or_else(|_| fs::read_to_string(dir.join("gpu_busy_percent"))) + .ok()? + .trim() + .parse::() + .ok() +} + +fn read_nvml_temp() -> Option { + let nvml = nvml_wrapper::Nvml::init().ok()?; + let device = nvml.device_by_index(0).ok()?; + let temp = device + .temperature(nvml_wrapper::enum_wrappers::device::TemperatureSensor::Gpu) + .ok()?; + Some(temp as f32) +} + +fn read_nvml_usage() -> Option { + let nvml = nvml_wrapper::Nvml::init().ok()?; + let device = nvml.device_by_index(0).ok()?; + let rates = device.utilization_rates().ok()?; + Some(rates.gpu as f32) +} + // --- Device --- /// A PCI GPU device. @@ -183,18 +217,8 @@ impl Device { /// Read a file underneath the sys object. fn read_file(path: PathBuf) -> Result { - let path = path - .canonicalize() - .map_err(|e| PlatformError::Read(path.to_string_lossy().to_string(), e))?; - let mut data = String::new(); - let mut file = fs::OpenOptions::new() - .read(true) - .open(&path) - .map_err(|e| PlatformError::Read(path.to_string_lossy().to_string(), e))?; - trace!("read_file: {file:?}"); - file.read_to_string(&mut data) - .map_err(|e| PlatformError::Read(path.to_string_lossy().to_string(), e))?; - Ok(data) + fs::read_to_string(&path) + .map_err(|e| PlatformError::Read(path.to_string_lossy().to_string(), e)) } /// Read the runtime power status from sysfs. @@ -224,14 +248,10 @@ impl Device { } // 1. Direct hwmon directory under device path - let hwmon_dir = self.dev_path.join("hwmon"); - if let Ok(entries) = fs::read_dir(&hwmon_dir) { + if let Ok(entries) = fs::read_dir(self.dev_path.join("hwmon")) { for entry in entries.flatten() { - let temp_path = entry.path().join("temp1_input"); - if let Ok(temp_str) = fs::read_to_string(temp_path) { - if let Ok(temp_val) = temp_str.trim().parse::() { - return Some(temp_val / 1000.0); - } + if let Some(temp) = read_hwmon_temp(&entry.path()) { + return Some(temp); } } } @@ -246,11 +266,8 @@ impl Device { || p.starts_with(&self.dev_path) }); if is_match { - let temp_path = path.join("temp1_input"); - if let Ok(temp_str) = fs::read_to_string(temp_path) { - if let Ok(temp_val) = temp_str.trim().parse::() { - return Some(temp_val / 1000.0); - } + if let Some(temp) = read_hwmon_temp(&path) { + return Some(temp); } } } @@ -258,14 +275,8 @@ impl Device { // 3. Fallback to NVML if this is an NVIDIA device and hwmon is not available if self.pci_id.to_uppercase().starts_with(NVIDIA_PCI_VENDOR) { - if let Ok(nvml) = nvml_wrapper::Nvml::init() { - if let Ok(device) = nvml.device_by_index(0) { - if let Ok(temp) = device - .temperature(nvml_wrapper::enum_wrappers::device::TemperatureSensor::Gpu) - { - return Some(temp as f32); - } - } + if let Some(temp) = read_nvml_temp() { + return Some(temp); } } @@ -285,26 +296,15 @@ impl Device { } // 1. Direct gpu_busy_percent under device path - let direct_busy = self.dev_path.join("gpu_busy_percent"); - if direct_busy.exists() { - if let Ok(val_str) = fs::read_to_string(direct_busy) { - if let Ok(val) = val_str.trim().parse::() { - return Some(val); - } - } + if let Some(busy) = read_drm_busy(&self.dev_path) { + return Some(busy); } // 2. DRM card directories under device path - let drm_dir = self.dev_path.join("drm"); - if let Ok(entries) = fs::read_dir(&drm_dir) { + if let Ok(entries) = fs::read_dir(self.dev_path.join("drm")) { for entry in entries.flatten() { - let busy_path = entry.path().join("device/gpu_busy_percent"); - if busy_path.exists() { - if let Ok(val_str) = fs::read_to_string(busy_path) { - if let Ok(val) = val_str.trim().parse::() { - return Some(val); - } - } + if let Some(busy) = read_drm_busy(&entry.path()) { + return Some(busy); } } } @@ -319,13 +319,8 @@ impl Device { || p.starts_with(&self.dev_path) }); if is_match { - let busy_path = path.join("device/gpu_busy_percent"); - if busy_path.exists() { - if let Ok(val_str) = fs::read_to_string(busy_path) { - if let Ok(val) = val_str.trim().parse::() { - return Some(val); - } - } + if let Some(busy) = read_drm_busy(&path) { + return Some(busy); } } } @@ -333,12 +328,8 @@ impl Device { // 4. Fallback to NVML if this is an NVIDIA device and DRM busy is not available if self.pci_id.to_uppercase().starts_with(NVIDIA_PCI_VENDOR) { - if let Ok(nvml) = nvml_wrapper::Nvml::init() { - if let Ok(device) = nvml.device_by_index(0) { - if let Ok(rates) = device.utilization_rates() { - return Some(rates.gpu as f32); - } - } + if let Some(usage) = read_nvml_usage() { + return Some(usage); } } @@ -488,39 +479,25 @@ fn lscpi(vendor_device: &str) -> Result { pub fn find_connected_displays(gpu_path: &Path) -> Result> { let drm_path = gpu_path.join("drm"); - // Find card directory (card0 or card1) let card_dir = drm_path .read_dir() .map_err(|e| PlatformError::Read(drm_path.to_string_lossy().to_string(), e))? - .find_map(|entry| { - let entry = entry.ok()?; - let name = entry.file_name().into_string().ok()?; - if name.starts_with("card") { - Some(entry.path()) - } else { - None - } - }) + .flatten() + .find(|entry| entry.file_name().to_string_lossy().starts_with("card")) + .map(|entry| entry.path()) .ok_or(PlatformError::NotSupported)?; - // Collect display names - let displays: Vec = card_dir + let displays = card_dir .read_dir() .map_err(|e| PlatformError::Read(card_dir.to_string_lossy().to_string(), e))? + .flatten() .filter_map(|entry| { - let entry = entry.ok()?; let name = entry.file_name().into_string().ok()?; - - if name.contains('-') { - // Check connection status - let status_path = entry.path().join("status"); - let status = fs::read_to_string(status_path).ok()?; - - if status.trim() == "connected" { - name.split_once('-').map(|(_, display)| display.to_string()) - } else { - None - } + if name.contains('-') + && fs::read_to_string(entry.path().join("status")) + .is_ok_and(|status| status.trim() == "connected") + { + name.split_once('-').map(|(_, display)| display.to_string()) } else { None } @@ -539,48 +516,37 @@ pub fn find_connected_displays(gpu_path: &Path) -> Result> { /// 2. Direct PCI device detection (if dGPU devices are found) /// 3. ASUS gpu_mux_mode attribute pub fn get_gpu_power_status() -> GfxPower { - if asus_dgpu_disable_exists() { - if let Ok(disabled) = asus_dgpu_disabled() { - if disabled { - return GfxPower::AsusDisabled; - } - } + if asus_dgpu_disabled().unwrap_or(false) { + return GfxPower::AsusDisabled; } - let devices = Device::find().unwrap_or_default(); - - if let Some(dgpu) = devices.iter().find(|d| d.is_dgpu()) { + if let Some(dgpu) = Device::find() + .ok() + .and_then(|devs| devs.into_iter().find(|d| d.is_dgpu())) + { return dgpu.get_runtime_status().unwrap_or(GfxPower::Unknown); } - // No dGPU devices found — check the MUX attribute - if asus_gpu_mux_exists() { - if let Ok(discreet) = asus_gpu_mux_discreet() { - if discreet { - return GfxPower::AsusMuxDiscreet; - } - } + if asus_gpu_mux_discreet().unwrap_or(false) { + return GfxPower::AsusMuxDiscreet; } GfxPower::Unknown } fn lookup_amdgpu_name(device_id: &str, revision: &str) -> Option { - if let Ok(content) = std::fs::read_to_string("/usr/share/libdrm/amdgpu.ids") { - for line in content.lines() { - let line = line.trim(); - if line.starts_with('#') || line.is_empty() { - continue; - } - let parts: Vec<&str> = line.split(',').collect(); - if parts.len() >= 3 { - let d_id = parts[0].trim().to_lowercase(); - let r_id = parts[1].trim().to_lowercase(); - let name = parts[2].trim().to_string(); - if d_id == device_id && r_id == revision && !name.is_empty() { - return Some(name); - } - } + let content = fs::read_to_string("/usr/share/libdrm/amdgpu.ids").ok()?; + for line in content.lines().map(str::trim) { + if line.starts_with('#') || line.is_empty() { + continue; + } + let parts: Vec<&str> = line.split(',').map(str::trim).collect(); + if parts.len() >= 3 + && parts[0].eq_ignore_ascii_case(device_id) + && parts[1].eq_ignore_ascii_case(revision) + && !parts[2].is_empty() + { + return Some(parts[2].to_string()); } } None @@ -657,124 +623,41 @@ pub fn get_gpu_names() -> (String, String) { } pub fn get_igpu_temp() -> f32 { - let devices = Device::find().unwrap_or_default(); - if let Some(igpu) = devices.iter().find(|d| !d.is_dgpu()) { - if let Some(temp) = igpu.get_temp() { - return temp; - } - } - if let Ok(entries) = std::fs::read_dir("/sys/class/hwmon") { - for entry in entries.flatten() { - let path = entry.path(); - if let Ok(name) = std::fs::read_to_string(path.join("name")) { - let name = name.trim(); - if name == "amdgpu" - || name == "i915" - || name == "xe" - || name == "k10temp" - || name == "coretemp" - { - if let Ok(temp_str) = std::fs::read_to_string(path.join("temp1_input")) { - if let Ok(temp_val) = temp_str.trim().parse::() { - return temp_val / 1000.0; - } - } - } - } - } - } - -1.0 + Device::find() + .ok() + .and_then(|devs| devs.into_iter().find(|d| !d.is_dgpu())) + .and_then(|d| d.get_temp()) + .unwrap_or(-1.0) } pub fn get_igpu_usage_pct() -> f32 { - let devices = Device::find().unwrap_or_default(); - if let Some(igpu) = devices.iter().find(|d| !d.is_dgpu()) { - if let Some(usage) = igpu.get_usage_pct() { - return usage; - } - } - if let Ok(entries) = std::fs::read_dir("/sys/class/drm") { - for entry in entries.flatten() { - let path = entry.path(); - let name = path - .file_name() - .map(|n| n.to_string_lossy().into_owned()) - .unwrap_or_default(); - if name.starts_with("card") { - let busy_path = path.join("device/gpu_busy_percent"); - if busy_path.exists() { - if let Ok(vendor_str) = std::fs::read_to_string(path.join("device/vendor")) { - let vendor = vendor_str.trim(); - if vendor == "0x1002" || vendor == "0x8086" { - if let Ok(val_str) = std::fs::read_to_string(busy_path) { - if let Ok(val) = val_str.trim().parse::() { - return val; - } - } - } - } - } - } - } - } - -1.0 + Device::find() + .ok() + .and_then(|devs| devs.into_iter().find(|d| !d.is_dgpu())) + .and_then(|d| d.get_usage_pct()) + .unwrap_or(-1.0) } pub fn get_gpu_temp() -> f32 { if get_gpu_power_status() != GfxPower::Active { return 0.0; } - let devices = Device::find().unwrap_or_default(); - if let Some(dgpu) = devices.iter().find(|d| d.is_dgpu()) { - if let Some(temp) = dgpu.get_temp() { - return temp; - } - } - if let Ok(entries) = std::fs::read_dir("/sys/class/hwmon") { - for entry in entries.flatten() { - let path = entry.path(); - if let Ok(name) = std::fs::read_to_string(path.join("name")) { - let name = name.trim(); - if name == "amdgpu" - || name == "nouveau" - || name == "nvidia" - || name == "nvidia_hwmon" - { - if let Ok(temp_str) = std::fs::read_to_string(path.join("temp1_input")) { - if let Ok(temp_val) = temp_str.trim().parse::() { - return temp_val / 1000.0; - } - } - } - } - } - } - 0.0 + Device::find() + .ok() + .and_then(|devs| devs.into_iter().find(|d| d.is_dgpu())) + .and_then(|d| d.get_temp()) + .unwrap_or(0.0) } pub fn get_gpu_usage_pct() -> f32 { if get_gpu_power_status() != GfxPower::Active { return 0.0; } - let devices = Device::find().unwrap_or_default(); - if let Some(dgpu) = devices.iter().find(|d| d.is_dgpu()) { - if let Some(usage) = dgpu.get_usage_pct() { - return usage; - } - } - if let Ok(entries) = std::fs::read_dir("/sys/class/drm") { - for entry in entries.flatten() { - let path = entry.path().join("device/gpu_busy_percent"); - if path.exists() { - if let Ok(val_str) = std::fs::read_to_string(path) { - if let Ok(val) = val_str.trim().parse::() { - return val; - } - } - } - } - } - 0.0 + Device::find() + .ok() + .and_then(|devs| devs.into_iter().find(|d| d.is_dgpu())) + .and_then(|d| d.get_usage_pct()) + .unwrap_or(0.0) } #[cfg(test)] From 4237d80261235339e88b5279a813df69944257d5 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Fri, 14 Aug 2026 15:40:20 +0200 Subject: [PATCH 3/6] perf(gpu_pci): eliminate redundant udev scans with aggregated telemetry --- rog-control-center/src/ui/setup_system.rs | 9 ++-- rog-platform/src/gpu_pci.rs | 64 ++++++++++++----------- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/rog-control-center/src/ui/setup_system.rs b/rog-control-center/src/ui/setup_system.rs index 62f7f294c..efb5bb06a 100644 --- a/rog-control-center/src/ui/setup_system.rs +++ b/rog-control-center/src/ui/setup_system.rs @@ -143,13 +143,14 @@ pub fn setup_system_page( }; let cpu_temp = rog_platform::cpu::get_cpu_temp(); - let gpu_temp = rog_platform::gpu_pci::get_gpu_temp(); - let igpu_temp = rog_platform::gpu_pci::get_igpu_temp(); + let gpu_telemetry = rog_platform::gpu_pci::get_gpu_telemetry(); + let gpu_temp = gpu_telemetry.dgpu_temp; + let igpu_temp = gpu_telemetry.igpu_temp; let (cpu_fan, gpu_fan, mid_fan) = rog_platform::platform::get_fan_rpms(); let cpu_freq = rog_platform::cpu::get_cpu_frequency_mhz(); let ram_usage = rog_platform::cpu::get_ram_usage_pct(); - let gpu_usage = rog_platform::gpu_pci::get_gpu_usage_pct(); - let igpu_usage = rog_platform::gpu_pci::get_igpu_usage_pct(); + let gpu_usage = gpu_telemetry.dgpu_usage; + let igpu_usage = gpu_telemetry.igpu_usage; let curr_ticks = rog_platform::cpu::read_cpu_ticks(); let cpu_usage = if let (Some(p), Some(c)) = (&prev_ticks, &curr_ticks) { diff --git a/rog-platform/src/gpu_pci.rs b/rog-platform/src/gpu_pci.rs index fa877b3dc..b7345da2c 100644 --- a/rog-platform/src/gpu_pci.rs +++ b/rog-platform/src/gpu_pci.rs @@ -622,42 +622,46 @@ pub fn get_gpu_names() -> (String, String) { ) } -pub fn get_igpu_temp() -> f32 { - Device::find() - .ok() - .and_then(|devs| devs.into_iter().find(|d| !d.is_dgpu())) - .and_then(|d| d.get_temp()) - .unwrap_or(-1.0) -} - -pub fn get_igpu_usage_pct() -> f32 { - Device::find() - .ok() - .and_then(|devs| devs.into_iter().find(|d| !d.is_dgpu())) - .and_then(|d| d.get_usage_pct()) - .unwrap_or(-1.0) +/// Telemetry metrics for both integrated and discrete GPUs. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct GpuTelemetry { + pub igpu_temp: f32, + pub igpu_usage: f32, + pub dgpu_temp: f32, + pub dgpu_usage: f32, } -pub fn get_gpu_temp() -> f32 { - if get_gpu_power_status() != GfxPower::Active { - return 0.0; +impl Default for GpuTelemetry { + fn default() -> Self { + Self { + igpu_temp: -1.0, + igpu_usage: -1.0, + dgpu_temp: 0.0, + dgpu_usage: 0.0, + } } - Device::find() - .ok() - .and_then(|devs| devs.into_iter().find(|d| d.is_dgpu())) - .and_then(|d| d.get_temp()) - .unwrap_or(0.0) } -pub fn get_gpu_usage_pct() -> f32 { - if get_gpu_power_status() != GfxPower::Active { - return 0.0; +/// Retrieve telemetry metrics for all detected GPUs in a single udev scan. +pub fn get_gpu_telemetry() -> GpuTelemetry { + let mut telemetry = GpuTelemetry::default(); + let dgpu_active = get_gpu_power_status() == GfxPower::Active; + + if let Ok(devices) = Device::find() { + for device in devices { + if device.is_dgpu() { + if dgpu_active { + telemetry.dgpu_temp = device.get_temp().unwrap_or(0.0); + telemetry.dgpu_usage = device.get_usage_pct().unwrap_or(0.0); + } + } else { + telemetry.igpu_temp = device.get_temp().unwrap_or(-1.0); + telemetry.igpu_usage = device.get_usage_pct().unwrap_or(-1.0); + } + } } - Device::find() - .ok() - .and_then(|devs| devs.into_iter().find(|d| d.is_dgpu())) - .and_then(|d| d.get_usage_pct()) - .unwrap_or(0.0) + + telemetry } #[cfg(test)] From 481c8f6884822b9c3eda2728e6778fc2242bb7bf Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Fri, 14 Aug 2026 15:42:09 +0200 Subject: [PATCH 4/6] refactor(gpu_pci): remove external lspci process spawning and fix typos --- rog-platform/src/gpu_pci.rs | 26 ++---- rog-platform/tests/gpu_pci_tests.rs | 122 +++++++++++++--------------- 2 files changed, 65 insertions(+), 83 deletions(-) diff --git a/rog-platform/src/gpu_pci.rs b/rog-platform/src/gpu_pci.rs index b7345da2c..5b346da28 100644 --- a/rog-platform/src/gpu_pci.rs +++ b/rog-platform/src/gpu_pci.rs @@ -8,7 +8,6 @@ use std::fmt::Display; use std::fs::{self, OpenOptions}; use std::io::Read; use std::path::{Path, PathBuf}; -use std::process::Command; use std::str::FromStr; use log::{info, trace, warn}; @@ -417,13 +416,11 @@ impl Device { "Found ID_MODEL_FROM_DATABASE property {id} at {:?} : {label:?}", device.sysname() ); - dgpu = lscpi_dgpu_check(&label.to_string_lossy()); - } else { - trace!( - "Didn't find dGPU with standard methods, using last resort for id:{id} at {:?}", - device.sysname() - ); - dgpu = lscpi_dgpu_check(&lscpi(&id).unwrap_or_default()); + dgpu = lspci_dgpu_check(&label.to_string_lossy()); + } else if let Some(model) = device.property_value("ID_MODEL") { + dgpu = lspci_dgpu_check(&model.to_string_lossy()); + } else if id.starts_with(NVIDIA_PCI_VENDOR) { + dgpu = is_display_class(&class); } } @@ -454,8 +451,8 @@ impl Device { // --- Utility functions --- -/// Check an lspci label string for dGPU patterns. -pub fn lscpi_dgpu_check(label: &str) -> bool { +/// Check a device model or lspci label string for dGPU patterns. +pub fn lspci_dgpu_check(label: &str) -> bool { for pat in [ "Radeon RX", "AMD/ATI", "GeForce", "Geforce", "Quadro", "T1200", ] { @@ -466,15 +463,6 @@ pub fn lscpi_dgpu_check(label: &str) -> bool { false } -fn lscpi(vendor_device: &str) -> Result { - let mut cmd = Command::new("lspci"); - cmd.args([ - "-d", vendor_device, - ]); - let output = cmd.output().map_err(PlatformError::Io)?; - Ok(String::from_utf8_lossy(&output.stdout).into_owned()) -} - /// Find connected displays for a GPU by scanning its DRM card directory. pub fn find_connected_displays(gpu_path: &Path) -> Result> { let drm_path = gpu_path.join("drm"); diff --git a/rog-platform/tests/gpu_pci_tests.rs b/rog-platform/tests/gpu_pci_tests.rs index 0215168fc..51975a329 100644 --- a/rog-platform/tests/gpu_pci_tests.rs +++ b/rog-platform/tests/gpu_pci_tests.rs @@ -5,7 +5,7 @@ //! functions (`Device::find`, `get_gpu_power_status`) are tested via integration //! tests on machines with actual GPUs. -use rog_platform::gpu_pci::{lscpi_dgpu_check, GfxPower}; +use rog_platform::gpu_pci::{lspci_dgpu_check, GfxPower}; use std::str::FromStr; // --------------------------------------------------------------------------- @@ -19,8 +19,8 @@ fn gfx_power_from_str_active() { #[test] fn gfx_power_from_str_active_case_insensitive() { - assert_eq!(GfxPower::from_str("Active").unwrap(), GfxPower::Active); assert_eq!(GfxPower::from_str("ACTIVE").unwrap(), GfxPower::Active); + assert_eq!(GfxPower::from_str("Active").unwrap(), GfxPower::Active); } #[test] @@ -48,51 +48,68 @@ fn gfx_power_from_str_asus_mux_discreet() { } #[test] -fn gfx_power_from_str_unknown_fallback() { +fn gfx_power_from_str_handles_whitespace() { assert_eq!( - GfxPower::from_str("something_weird").unwrap(), - GfxPower::Unknown + GfxPower::from_str(" suspended\n").unwrap(), + GfxPower::Suspended ); - assert_eq!(GfxPower::from_str("").unwrap(), GfxPower::Unknown); - assert_eq!(GfxPower::from_str("UNKNOWN").unwrap(), GfxPower::Unknown); + assert_eq!(GfxPower::from_str("\tactive ").unwrap(), GfxPower::Active); } #[test] -fn gfx_power_from_str_handles_whitespace() { - assert_eq!(GfxPower::from_str(" active ").unwrap(), GfxPower::Active); +fn gfx_power_from_str_unknown_fallback() { assert_eq!( - GfxPower::from_str("\tsuspended\n").unwrap(), - GfxPower::Suspended + GfxPower::from_str("auto").unwrap(), + GfxPower::Unknown, + "unexpected kernel string should map to Unknown" ); + assert_eq!( + GfxPower::from_str("unsupported").unwrap(), + GfxPower::Unknown + ); + assert_eq!(GfxPower::from_str("").unwrap(), GfxPower::Unknown); + assert_eq!(GfxPower::from_str("garbage").unwrap(), GfxPower::Unknown); } // --------------------------------------------------------------------------- -// GfxPower – Display / Into<&str> +// GfxPower – Display round-trip // --------------------------------------------------------------------------- #[test] fn gfx_power_display_roundtrip() { let variants = [ - (GfxPower::Active, "active"), - (GfxPower::Suspended, "suspended"), - (GfxPower::AsusDisabled, "dgpu_disabled"), - (GfxPower::AsusMuxDiscreet, "asus_mux_discreet"), - (GfxPower::Unknown, "unknown"), + GfxPower::Active, + GfxPower::Suspended, + GfxPower::AsusDisabled, + GfxPower::AsusMuxDiscreet, + GfxPower::Unknown, ]; - for (variant, expected_str) in variants { - // Into<&str> - let s: &str = (&variant).into(); - assert_eq!(s, expected_str, "Into<&str> failed for {variant:?}"); + for &variant in &variants { + let s = variant.to_string(); + let parsed = GfxPower::from_str(&s).unwrap(); + assert_eq!(variant, parsed, "failed round-trip for {variant:?}"); + } +} - // Display - let displayed = format!("{variant}"); - assert_eq!(displayed, expected_str, "Display failed for {variant:?}"); +// --------------------------------------------------------------------------- +// GfxPower – Serde +// --------------------------------------------------------------------------- - // Roundtrip: from_str(display) should give back the same variant +#[test] +fn gfx_power_serde_roundtrip() { + let variants = [ + GfxPower::Active, + GfxPower::Suspended, + GfxPower::AsusDisabled, + GfxPower::AsusMuxDiscreet, + GfxPower::Unknown, + ]; + for &variant in &variants { + let json = serde_json::to_string(&variant).unwrap(); + let deserialized: GfxPower = serde_json::from_str(&json).unwrap(); assert_eq!( - GfxPower::from_str(&displayed).unwrap(), - variant, - "Roundtrip failed for {variant:?}" + variant, deserialized, + "serde round-trip failed for {variant:?}" ); } } @@ -107,7 +124,7 @@ fn gfx_power_default_is_unknown() { } // --------------------------------------------------------------------------- -// GfxPower – Copy / Clone / PartialEq +// GfxPower – Copy / Clone // --------------------------------------------------------------------------- #[test] @@ -120,83 +137,60 @@ fn gfx_power_copy_clone() { } // --------------------------------------------------------------------------- -// lscpi_dgpu_check – positive matches +// lspci_dgpu_check – positive matches // --------------------------------------------------------------------------- #[test] fn lspci_dgpu_check_radeon_rx() { - assert!(lscpi_dgpu_check("Radeon RX 6800M")); + assert!(lspci_dgpu_check("Radeon RX 6800M")); } #[test] fn lspci_dgpu_check_amd_ati() { - assert!(lscpi_dgpu_check("AMD/ATI Navi 22")); + assert!(lspci_dgpu_check("AMD/ATI Navi 22")); } #[test] fn lspci_dgpu_check_geforce() { - assert!(lscpi_dgpu_check("GeForce RTX 3080")); + assert!(lspci_dgpu_check("GeForce RTX 3080")); } #[test] fn lspci_dgpu_check_geforce_lowercase_f() { - assert!(lscpi_dgpu_check("Geforce GTX 1660")); + assert!(lspci_dgpu_check("Geforce GTX 1660")); } #[test] fn lspci_dgpu_check_quadro() { - assert!(lscpi_dgpu_check("Quadro T1000")); + assert!(lspci_dgpu_check("Quadro T1000")); } #[test] fn lspci_dgpu_check_t1200() { - assert!(lscpi_dgpu_check("T1200")); + assert!(lspci_dgpu_check("T1200")); } // --------------------------------------------------------------------------- -// lscpi_dgpu_check – negative matches +// lspci_dgpu_check – negative matches // --------------------------------------------------------------------------- #[test] fn lspci_dgpu_check_intel_igpu() { - assert!(!lscpi_dgpu_check("Intel Corporation UHD Graphics 630")); + assert!(!lspci_dgpu_check("Intel Corporation UHD Graphics 630")); } #[test] fn lspci_dgpu_check_empty_string() { - assert!(!lscpi_dgpu_check("")); + assert!(!lspci_dgpu_check("")); } #[test] fn lspci_dgpu_check_unrelated_device() { - assert!(!lscpi_dgpu_check("Realtek RTL8111/8168/8411")); + assert!(!lspci_dgpu_check("Realtek RTL8111/8168/8411")); } #[test] fn lspci_dgpu_check_partial_match_not_enough() { // "Radeon" alone should not match (the pattern requires "Radeon RX" or "AMD/ATI") - assert!(!lscpi_dgpu_check("Radeon Pro W6600")); -} - -// --------------------------------------------------------------------------- -// GfxPower – serialization (serde) -// --------------------------------------------------------------------------- - -#[test] -fn gfx_power_serde_roundtrip() { - let variants = [ - GfxPower::Active, - GfxPower::Suspended, - GfxPower::AsusDisabled, - GfxPower::AsusMuxDiscreet, - GfxPower::Unknown, - ]; - for variant in variants { - let json = serde_json::to_string(&variant).expect("serialize"); - let deserialized: GfxPower = serde_json::from_str(&json).expect("deserialize"); - assert_eq!( - deserialized, variant, - "serde roundtrip failed for {variant:?}" - ); - } + assert!(!lspci_dgpu_check("Radeon Pro W6600")); } From b52958427bfc1f39fd91f959922e348c05d1f269 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Fri, 14 Aug 2026 15:44:24 +0200 Subject: [PATCH 5/6] fix(rog-control-center): correct typo start_dgpu_status_mon and remove dead code --- rog-control-center/src/notify.rs | 41 ++------------------------------ 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/rog-control-center/src/notify.rs b/rog-control-center/src/notify.rs index eafdff20f..d29f71810 100644 --- a/rog-control-center/src/notify.rs +++ b/rog-control-center/src/notify.rs @@ -71,7 +71,7 @@ fn dgpu_status_for_tick( } } -fn start_dpu_status_mon(config: Arc>, gpu_status_tx: watch::Sender) { +fn start_dgpu_status_mon(config: Arc>, gpu_status_tx: watch::Sender) { use rog_platform::gpu_pci::{asus_dgpu_disabled, asus_gpu_mux_discreet, Device}; let find_dgpu = || { @@ -197,44 +197,7 @@ pub fn start_notifications( }); info!("Attempting to start plain dgpu status monitor"); - start_dpu_status_mon(config.clone(), gpu_status_tx); - - // GPU MUX Mode notif - // TODO: need to get armoury attrs and iter to find - // let enabled_notifications_copy = config.clone(); - // tokio::spawn(async move { - // let conn = zbus::Connection::system().await.map_err(|e| { - // error!("zbus signal: receive_notify_gpu_mux_mode: {e}"); - // e - // })?; - // let proxy = PlatformProxy::new(&conn).await.map_err(|e| { - // error!("zbus signal: receive_notify_gpu_mux_mode: {e}"); - // e - // })?; - - // let mut actual_mux_mode = GpuMode::Error; - // if let Ok(mode) = proxy.gpu_mux_mode().await { - // actual_mux_mode = GpuMode::from(mode); - // } - - // info!("Started zbus signal thread: receive_notify_gpu_mux_mode"); - // while let Some(e) = - // proxy.receive_gpu_mux_mode_changed().await.next().await { if let - // Ok(config) = enabled_notifications_copy.lock() { if - // !config.notifications.enabled || !config.notifications.receive_notify_gfx { - // continue; - // } - // } - // if let Ok(out) = e.get().await { - // let mode = GpuMode::from(out); - // if mode == actual_mux_mode { - // continue; - // } - // do_mux_notification("Reboot required. BIOS GPU MUX mode set to", - // &mode).ok(); } - // } - // Ok::<(), zbus::Error>(()) - // }); + start_dgpu_status_mon(config.clone(), gpu_status_tx); Ok(vec![blocking]) } From 731d772cdd58bdd11bf419d196ddfa7fd77280a8 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Fri, 14 Aug 2026 15:59:57 +0200 Subject: [PATCH 6/6] feat(rog-control-center): display translated suspended state for dGPU metrics --- rog-control-center/src/ui/setup_system.rs | 2 ++ .../translations/az/rog-control-center.po | 6 ++++ .../translations/en/rog-control-center.po | 6 ++++ .../translations/fr/rog-control-center.po | 6 ++++ .../translations/it/rog-control-center.po | 6 ++++ .../translations/pt_BR/rog-control-center.po | 6 ++++ .../translations/ru/rog-control-center.po | 6 ++++ .../translations/tr/rog-control-center.po | 6 ++++ .../translations/uk_UA/rog-control-center.po | 6 ++++ .../translations/zh_CN/rog-control-center.po | 6 ++++ rog-control-center/ui/pages/system.slint | 7 ++-- rog-platform/src/gpu_pci.rs | 36 ++++++++++--------- rog-platform/tests/gpu_pci_tests.rs | 16 ++++++++- 13 files changed, 95 insertions(+), 20 deletions(-) diff --git a/rog-control-center/src/ui/setup_system.rs b/rog-control-center/src/ui/setup_system.rs index efb5bb06a..a6f2ffd69 100644 --- a/rog-control-center/src/ui/setup_system.rs +++ b/rog-control-center/src/ui/setup_system.rs @@ -146,6 +146,7 @@ pub fn setup_system_page( let gpu_telemetry = rog_platform::gpu_pci::get_gpu_telemetry(); let gpu_temp = gpu_telemetry.dgpu_temp; let igpu_temp = gpu_telemetry.igpu_temp; + let dgpu_suspended = gpu_telemetry.dgpu_suspended; let (cpu_fan, gpu_fan, mid_fan) = rog_platform::platform::get_fan_rpms(); let cpu_freq = rog_platform::cpu::get_cpu_frequency_mhz(); let ram_usage = rog_platform::cpu::get_ram_usage_pct(); @@ -179,6 +180,7 @@ pub fn setup_system_page( data.set_cpu_temp_val(cpu_temp); data.set_gpu_temp_val(gpu_temp); data.set_igpu_temp_val(igpu_temp); + data.set_dgpu_suspended(dgpu_suspended); data.set_cpu_usage_val(cpu_usage); data.set_gpu_usage_val(gpu_usage); data.set_igpu_usage_val(igpu_usage); diff --git a/rog-control-center/translations/az/rog-control-center.po b/rog-control-center/translations/az/rog-control-center.po index 2f33f656a..355570de4 100644 --- a/rog-control-center/translations/az/rog-control-center.po +++ b/rog-control-center/translations/az/rog-control-center.po @@ -503,6 +503,12 @@ msgctxt "PageSystem" msgid "N/A" msgstr "Mövcud deyil" +#: rog-control-center/ui/pages/system.slint:373 +#: rog-control-center/ui/pages/system.slint:386 +msgctxt "PageSystem" +msgid "Suspended" +msgstr "Dayandırılıb" + #: rog-control-center/ui/pages/system.slint:351 #, fuzzy msgctxt "PageSystem" diff --git a/rog-control-center/translations/en/rog-control-center.po b/rog-control-center/translations/en/rog-control-center.po index b15792bc6..f6f769a56 100644 --- a/rog-control-center/translations/en/rog-control-center.po +++ b/rog-control-center/translations/en/rog-control-center.po @@ -574,6 +574,12 @@ msgctxt "PageSystem" msgid "dGPU Status" msgstr "" +#: rog-control-center/ui/pages/system.slint:373 +#: rog-control-center/ui/pages/system.slint:386 +msgctxt "PageSystem" +msgid "Suspended" +msgstr "" + #: rog-control-center/ui/pages/system.slint:394 msgctxt "PageSystem" msgid "Fan Speeds" diff --git a/rog-control-center/translations/fr/rog-control-center.po b/rog-control-center/translations/fr/rog-control-center.po index 918c20afa..8e6af5983 100644 --- a/rog-control-center/translations/fr/rog-control-center.po +++ b/rog-control-center/translations/fr/rog-control-center.po @@ -505,6 +505,12 @@ msgctxt "PageSystem" msgid "N/A" msgstr "N/A" +#: rog-control-center/ui/pages/system.slint:373 +#: rog-control-center/ui/pages/system.slint:386 +msgctxt "PageSystem" +msgid "Suspended" +msgstr "Suspendue" + #: rog-control-center/ui/pages/system.slint:351 #, fuzzy msgctxt "PageSystem" diff --git a/rog-control-center/translations/it/rog-control-center.po b/rog-control-center/translations/it/rog-control-center.po index 00c706386..4b8682d39 100644 --- a/rog-control-center/translations/it/rog-control-center.po +++ b/rog-control-center/translations/it/rog-control-center.po @@ -488,6 +488,12 @@ msgctxt "PageSystem" msgid "N/A" msgstr "N/D" +#: rog-control-center/ui/pages/system.slint:373 +#: rog-control-center/ui/pages/system.slint:386 +msgctxt "PageSystem" +msgid "Suspended" +msgstr "Sospesa" + #: rog-control-center/ui/pages/system.slint:351 msgctxt "PageSystem" msgid "Fan Speeds" diff --git a/rog-control-center/translations/pt_BR/rog-control-center.po b/rog-control-center/translations/pt_BR/rog-control-center.po index cdce33ed7..931c99886 100644 --- a/rog-control-center/translations/pt_BR/rog-control-center.po +++ b/rog-control-center/translations/pt_BR/rog-control-center.po @@ -503,6 +503,12 @@ msgctxt "PageSystem" msgid "N/A" msgstr "N/A" +#: rog-control-center/ui/pages/system.slint:373 +#: rog-control-center/ui/pages/system.slint:386 +msgctxt "PageSystem" +msgid "Suspended" +msgstr "Suspensa" + #: rog-control-center/ui/pages/system.slint:351 #, fuzzy msgctxt "PageSystem" diff --git a/rog-control-center/translations/ru/rog-control-center.po b/rog-control-center/translations/ru/rog-control-center.po index e6b5cc9f4..6310a67af 100644 --- a/rog-control-center/translations/ru/rog-control-center.po +++ b/rog-control-center/translations/ru/rog-control-center.po @@ -503,6 +503,12 @@ msgctxt "PageSystem" msgid "N/A" msgstr "Н/Д" +#: rog-control-center/ui/pages/system.slint:373 +#: rog-control-center/ui/pages/system.slint:386 +msgctxt "PageSystem" +msgid "Suspended" +msgstr "Приостановлено" + #: rog-control-center/ui/pages/system.slint:351 #, fuzzy msgctxt "PageSystem" diff --git a/rog-control-center/translations/tr/rog-control-center.po b/rog-control-center/translations/tr/rog-control-center.po index b92db59a1..040252366 100644 --- a/rog-control-center/translations/tr/rog-control-center.po +++ b/rog-control-center/translations/tr/rog-control-center.po @@ -498,6 +498,12 @@ msgctxt "PageSystem" msgid "N/A" msgstr "N/A" +#: rog-control-center/ui/pages/system.slint:373 +#: rog-control-center/ui/pages/system.slint:386 +msgctxt "PageSystem" +msgid "Suspended" +msgstr "Askıda" + #: rog-control-center/ui/pages/system.slint:351 #, fuzzy msgctxt "PageSystem" diff --git a/rog-control-center/translations/uk_UA/rog-control-center.po b/rog-control-center/translations/uk_UA/rog-control-center.po index 910c7e20a..2b7f95a16 100644 --- a/rog-control-center/translations/uk_UA/rog-control-center.po +++ b/rog-control-center/translations/uk_UA/rog-control-center.po @@ -502,6 +502,12 @@ msgctxt "PageSystem" msgid "N/A" msgstr "Н/Д" +#: rog-control-center/ui/pages/system.slint:373 +#: rog-control-center/ui/pages/system.slint:386 +msgctxt "PageSystem" +msgid "Suspended" +msgstr "Призупинено" + #: rog-control-center/ui/pages/system.slint:351 #, fuzzy msgctxt "PageSystem" diff --git a/rog-control-center/translations/zh_CN/rog-control-center.po b/rog-control-center/translations/zh_CN/rog-control-center.po index a2431beac..c2740b0d6 100644 --- a/rog-control-center/translations/zh_CN/rog-control-center.po +++ b/rog-control-center/translations/zh_CN/rog-control-center.po @@ -503,6 +503,12 @@ msgctxt "PageSystem" msgid "N/A" msgstr "无" +#: rog-control-center/ui/pages/system.slint:373 +#: rog-control-center/ui/pages/system.slint:386 +msgctxt "PageSystem" +msgid "Suspended" +msgstr "已挂起" + #: rog-control-center/ui/pages/system.slint:351 #, fuzzy msgctxt "PageSystem" diff --git a/rog-control-center/ui/pages/system.slint b/rog-control-center/ui/pages/system.slint index 154383066..4558e681b 100644 --- a/rog-control-center/ui/pages/system.slint +++ b/rog-control-center/ui/pages/system.slint @@ -173,6 +173,7 @@ export global SystemPageData { in-out property igpu_temp_val: -1.0; in-out property igpu_usage_val: -1.0; in-out property has_igpu: false; + in-out property dgpu_suspended: false; } export component PageSystem inherits Rectangle { @@ -369,9 +370,9 @@ export component PageSystem inherits Rectangle { color: Palette.control-foreground; } Text { - text: SystemPageData.gpu_temp_val > 0.0 ? (Math.round(SystemPageData.gpu_temp_val) + " °C") : @tr("N/A"); + text: SystemPageData.dgpu_suspended ? @tr("Suspended") : (SystemPageData.gpu_temp_val >= 0.0 ? (Math.round(SystemPageData.gpu_temp_val) + " °C") : @tr("N/A")); font-weight: 700; - color: SystemPageData.gpu_temp_val > 80 ? #ef4444 : (SystemPageData.gpu_temp_val > 65 ? #eab308 : (SystemPageData.gpu_temp_val > 0.0 ? #22c55e : Palette.control-foreground)); + color: SystemPageData.dgpu_suspended ? Palette.control-foreground : (SystemPageData.gpu_temp_val > 80 ? #ef4444 : (SystemPageData.gpu_temp_val > 65 ? #eab308 : (SystemPageData.gpu_temp_val >= 0.0 ? #22c55e : Palette.control-foreground))); } } @@ -382,7 +383,7 @@ export component PageSystem inherits Rectangle { color: Palette.control-foreground; } Text { - text: SystemPageData.gpu_usage_val >= 0.0 ? (Math.round(SystemPageData.gpu_usage_val) + "%") : @tr("N/A"); + text: SystemPageData.dgpu_suspended ? @tr("Suspended") : (SystemPageData.gpu_usage_val >= 0.0 ? (Math.round(SystemPageData.gpu_usage_val) + "%") : @tr("N/A")); font-weight: 700; color: Palette.control-foreground; } diff --git a/rog-platform/src/gpu_pci.rs b/rog-platform/src/gpu_pci.rs index 5b346da28..233c49115 100644 --- a/rog-platform/src/gpu_pci.rs +++ b/rog-platform/src/gpu_pci.rs @@ -234,16 +234,16 @@ impl Device { } } - /// Read the temperature (°C) of this GPU from sysfs hwmon. + /// Read the temperature (°C) of this GPU from sysfs hwmon with NVML fallback. /// /// If this is a discrete GPU and it is not in the `Active` power state, - /// this immediately returns `Some(0.0)` without reading sysfs hwmon - /// nodes to prevent waking the PCIe device from runtime PM sleep. + /// this immediately returns `None` without accessing hwmon or NVML to prevent + /// waking the PCIe device from runtime PM sleep. pub fn get_temp(&self) -> Option { if self.is_dgpu && self.get_runtime_status().unwrap_or(GfxPower::Unknown) != GfxPower::Active { - return Some(0.0); + return None; } // 1. Direct hwmon directory under device path @@ -282,16 +282,16 @@ impl Device { None } - /// Read the GPU utilization percentage (0.0 - 100.0) from sysfs DRM nodes. + /// Read the GPU utilization percentage (0.0 - 100.0) from sysfs DRM nodes with NVML fallback. /// /// If this is a discrete GPU and it is not in the `Active` power state, - /// this immediately returns `Some(0.0)` without reading sysfs DRM - /// nodes to prevent waking the PCIe device from runtime PM sleep. + /// this immediately returns `None` without accessing DRM sysfs or NVML to prevent + /// waking the PCIe device from runtime PM sleep. pub fn get_usage_pct(&self) -> Option { if self.is_dgpu && self.get_runtime_status().unwrap_or(GfxPower::Unknown) != GfxPower::Active { - return Some(0.0); + return None; } // 1. Direct gpu_busy_percent under device path @@ -617,6 +617,7 @@ pub struct GpuTelemetry { pub igpu_usage: f32, pub dgpu_temp: f32, pub dgpu_usage: f32, + pub dgpu_suspended: bool, } impl Default for GpuTelemetry { @@ -624,8 +625,9 @@ impl Default for GpuTelemetry { Self { igpu_temp: -1.0, igpu_usage: -1.0, - dgpu_temp: 0.0, - dgpu_usage: 0.0, + dgpu_temp: -1.0, + dgpu_usage: -1.0, + dgpu_suspended: false, } } } @@ -633,14 +635,16 @@ impl Default for GpuTelemetry { /// Retrieve telemetry metrics for all detected GPUs in a single udev scan. pub fn get_gpu_telemetry() -> GpuTelemetry { let mut telemetry = GpuTelemetry::default(); - let dgpu_active = get_gpu_power_status() == GfxPower::Active; + let power_status = get_gpu_power_status(); + let dgpu_active = power_status == GfxPower::Active; + telemetry.dgpu_suspended = power_status == GfxPower::Suspended; if let Ok(devices) = Device::find() { for device in devices { if device.is_dgpu() { if dgpu_active { - telemetry.dgpu_temp = device.get_temp().unwrap_or(0.0); - telemetry.dgpu_usage = device.get_usage_pct().unwrap_or(0.0); + telemetry.dgpu_temp = device.get_temp().unwrap_or(-1.0); + telemetry.dgpu_usage = device.get_usage_pct().unwrap_or(-1.0); } } else { telemetry.igpu_temp = device.get_temp().unwrap_or(-1.0); @@ -788,9 +792,9 @@ mod tests { fs::write(dir.join("gpu_busy_percent"), "80\n")?; let device = fake_device(dir.0.clone()); - // Discrete GPU in suspended state must return 0.0 without querying hwmon/drm - assert_eq!(device.get_temp(), Some(0.0)); - assert_eq!(device.get_usage_pct(), Some(0.0)); + // Discrete GPU in suspended state must return None without querying hwmon/drm/nvml + assert_eq!(device.get_temp(), None); + assert_eq!(device.get_usage_pct(), None); Ok(()) } diff --git a/rog-platform/tests/gpu_pci_tests.rs b/rog-platform/tests/gpu_pci_tests.rs index 51975a329..2213c6979 100644 --- a/rog-platform/tests/gpu_pci_tests.rs +++ b/rog-platform/tests/gpu_pci_tests.rs @@ -5,9 +5,23 @@ //! functions (`Device::find`, `get_gpu_power_status`) are tested via integration //! tests on machines with actual GPUs. -use rog_platform::gpu_pci::{lspci_dgpu_check, GfxPower}; +use rog_platform::gpu_pci::{lspci_dgpu_check, GfxPower, GpuTelemetry}; use std::str::FromStr; +// --------------------------------------------------------------------------- +// GpuTelemetry – Default +// --------------------------------------------------------------------------- + +#[test] +fn gpu_telemetry_default_values() { + let telemetry = GpuTelemetry::default(); + assert_eq!(telemetry.igpu_temp, -1.0); + assert_eq!(telemetry.igpu_usage, -1.0); + assert_eq!(telemetry.dgpu_temp, -1.0); + assert_eq!(telemetry.dgpu_usage, -1.0); + assert!(!telemetry.dgpu_suspended); +} + // --------------------------------------------------------------------------- // GfxPower – FromStr // ---------------------------------------------------------------------------