Skip to content

[Bug]: dGPU still resumed every 2 s on 6.4.0 — #294's guard is bypassed via the dGPU's sibling audio function, and iGPU telemetry reports the dGPU #302

Description

@drpars

Affected Component: rog-control-center (GUI / Slint / System Tray) — root cause in rog-platform
Version: 6.4.0.r0.ge6c1469c-1, and identical on main (main is currently the 6.4.0 tag, 0 commits ahead)
Session: Wayland (Hyprland)

Issue Description

This is a follow-up to #293, which #294 fixed and closed. The guard #294 added is correct and it
does fire — but there is a second path into NVML that never reaches it, so on 6.4.0 the dGPU is
still resumed roughly every 2 seconds while the RCC window is open.

The guard keys on the Device::is_dgpu flag. Device::find() also returns the dGPU's sibling PCI
function
— on this machine 0000:01:00.1, the NVIDIA HD Audio controller (10DE:228E, class
040300) — with is_dgpu: false but pci_id: "10DE:228E". Every guard in gpu_pci.rs is written
if self.is_dgpu && runtime_status != Active, while the NVML fallback below it is written
if self.pci_id.starts_with("10DE"). A non-display NVIDIA function passes between the two: the
guard skips it because the flag is false, and the fallback claims it because the vendor matches.

get_gpu_telemetry() then treats that audio function as the integrated GPU, calls get_temp() and
get_usage_pct() on it, falls through to NVML, and Nvml::init() + device_by_index(0) resumes the
sleeping dGPU. This also produces a second, user-visible symptom: the iGPU temperature and usage
shown in the GUI are the dGPU's NVML values
, not the integrated GPU's.

Improvement over 6.3.11 is real but small: the card now manages about 6% sleep with the window open,
where before it managed 0%.

Root cause

1. A non-display sibling function enters the device list as a non-dGPU NVIDIA device

rog-platform/src/gpu_pci.rs, in Device::find():

if dgpu || (!parent.is_empty() && sysname.contains(&parent)) {   // line 476
    if dgpu {
        info!("Found dgpu {id} at {:?}", device.sysname());
    } else {
        info!("Found additional device {id} at {:?}", device.sysname());
    }
    parent = get_parent(&device);
    devices.push(Self {
        dev_path: PathBuf::from(device.syspath()),
        is_dgpu: dgpu,                                            // false for 01:00.1
        pci_id: id.to_string(),                                   // but still "10DE:228E"
    });
}

There is no class check on the sysname.contains(&parent) branch, so the audio function of the GPU
is admitted alongside it.

2. The guard checks the flag, the fallback checks the vendor

fn probe_hwmon(&self, read: fn(&Path) -> Option<f32>, nvml: fn() -> Option<f32>) -> Option<f32> {
    if self.is_dgpu                                               // line 281 — false here, guard skipped
        && self.get_runtime_status().unwrap_or(GfxPower::Unknown) != GfxPower::Active
    {
        return None;
    }
    // ... 01:00.1 has no hwmon and no DRM node, so both lookups miss ...
    if self.pci_id.to_uppercase().starts_with(NVIDIA_PCI_VENDOR) { // line 315 — matches
        return nvml();                                            // -> Nvml::init(), device_by_index(0)
    }
    None
}

get_usage_pct() has the same shape: guard at line 333, NVML fallback at line 372.

3. get_gpu_telemetry() routes it through the iGPU branch

if let Ok(devices) = Device::find() {
    for device in devices {
        if device.is_dgpu() {
            if dgpu_active { /* correctly skipped while suspended */ }
        } else {
            telemetry.igpu_temp  = device.get_temp().unwrap_or(-1.0);      // 01:00.1 -> NVML -> dGPU
            telemetry.igpu_usage = device.get_usage_pct().unwrap_or(-1.0); // 01:00.1 -> NVML -> dGPU
        }
    }
}

4. The break ends enumeration before the real iGPU is ever seen

if !parent.is_empty() && !sysname.contains(&parent) {   // line 492
    break;
}

On this machine the dGPU is 0000:01:00.0 and the integrated Radeon 680M is 0000:05:00.0. Once
01:00.1 has been consumed, the next PCI device does not match parent and the scan stops — the
iGPU is never enumerated at all. This is why the iGPU fields have nothing left to report but the
NVML value picked up in step 2. get_gpu_names() uses a separate scan that filters on display class
and has no break, which is why the GUI shows the correct iGPU name next to the wrong numbers.

5. The NVML handle now persists for the process lifetime

fn nvml() -> Option<&'static nvml_wrapper::Nvml> {
    static NVML: OnceLock<nvml_wrapper::Nvml> = OnceLock::new();   // line 182

This is a good change in itself, but it means one unguarded call is permanent: after the first
Nvml::init() the NVIDIA device nodes stay open for as long as RCC runs. In #293 /dev/nvidia0 was
open in 3.4% of /proc/<pid>/fd scans; on 6.4.0 it is open in 100% of them.

Steps to reproduce

  1. Hybrid mode, dGPU runtime-suspended:
    cat /sys/bus/pci/devices/<dgpu>/power/runtime_statussuspended
  2. Open ROG Control Center and leave the window visible.
  3. Watch power/runtime_status and power/runtime_suspended_time.
  4. Compare the "iGPU" temperature in the GUI against the integrated GPU's own hwmon
    (/sys/bus/pci/devices/<igpu>/hwmon/hwmon*/temp1_input).

Reproduces on any machine where the dGPU's audio function is enumerated (i.e. essentially every
Optimus laptop) — nothing here is specific to this board.

Measurements

All sampling reads only sysfs and /proc/<pid>/fd.

The card is still held awake. 60 s with the window open, against a 60 s control with RCC closed:

window open      sleep gained:  3937 ms / 60 s   (6.0%)   active gained: 62148 ms
RCC closed       sleep gained: 60079 ms / 60 s (100.0%)   active gained:     0 ms

NVML is entered while the card is still asleep. /proc/<pid>/fd scanned from process start
(206140 scans in 30 s), timestamps relative to launch:

0.000  runtime_status = suspended
0.404  /dev/nvidiactl opened          <- card still 'suspended'; the guard never saw this call
0.674  runtime_status -> resuming
2.152  runtime_status -> active
2.153  /dev/nvidia0, /dev/nvidia-caps/nvidia-cap2, /dev/nvidia-uvm opened
22.357 runtime_status -> suspending
22.973 runtime_status -> suspended
24.625 runtime_status -> resuming    <- next telemetry tick wakes it again
26.103 runtime_status -> active

It is NVML, not the renderer. The only NVIDIA library mapped into the process is
/usr/lib/libnvidia-ml.so.610.57.04; the EGL vendor actually loaded is libEGL_mesa.so, with no
libGLX_nvidia, no libEGL_nvidia and no Vulkan loader present.

RCC's own log names the device. With RUST_LOG=rog_platform=trace:

[INFO ] Found dgpu 10DE:2520 at "0000:01:00.0"
[INFO ] Found additional device 10DE:228E at "0000:01:00.1"
[TRACE] Looking at PCI device "0000:01:00.0"
[TRACE] Looking at PCI device "0000:01:00.1"      <- last device of every scan; 05:00.0 never reached

(Minor: the trace!("Matched dGPU {id} ... by checking display connections") line is also printed
for 10DE:228E, because it is logged before is_display_class() decides. Harmless, but misleading
when reading logs.)

The iGPU fields report the dGPU. Under a CPU/APU load, so that the two sensors separate:

real integrated GPU (amdgpu hwmon, 0000:05:00.0)   62 °C
dGPU (nvidia-smi)                                  50 °C
GUI "iGPU Status | AMD Radeon 680M"                50 °C   <- follows the dGPU
GUI "dGPU Status | GA106M [RTX 3060 Mobile]"       50 °C

Idle, both GUI fields read exactly the same number at every sample.

Expected behaviour

  • Opening the GUI should not resume a sleeping dGPU. Any NVML entry point should be gated on the
    dGPU's runtime_status, whichever device object happens to reach it.
  • The iGPU temperature/usage fields should report the integrated GPU, or nothing — never the dGPU's
    NVML readings.

Suggested fix

  1. Do not admit non-display functions to the device list. Add the class check to the sibling
    branch at line 476, e.g. if dgpu || (!parent.is_empty() && sysname.contains(&parent) && is_display_class(&class)).
    The audio function has no hwmon, no DRM node and no telemetry to contribute.
  2. Gate the NVML fallback on the vendor test that reaches it, not on is_dgpu. Wherever
    pci_id starting with 10DE is enough to call NVML (lines 315 and 372), the dGPU's
    runtime_status should be the thing that decides — get_gpu_power_status() == GfxPower::Active,
    a passive sysfs read that (as measured in [Bug]: rog-control-center telemetry loop calls NVML unconditionally, resuming a runtime-suspended dGPU while the window is open #293) does not wake the device. That keeps the guard
    correct even if some other non-dGPU NVIDIA function shows up on another board.
  3. Reconsider the break at line 492. It assumes the iGPU is enumerated before the dGPU. On
    boards where the dGPU comes first, enumeration stops after the dGPU's own functions and the iGPU
    is never found, which is what leaves the iGPU telemetry fields to be filled by whatever the
    fallback returns.

System details

  • Distro: Arch Linux
  • Kernel: 7.1.8-zen1-3-zen
  • Desktop: Hyprland (Wayland)
  • Model: ROG Strix G513RM (board G513RM)
  • dGPU: NVIDIA GA106M [GeForce RTX 3060 Mobile / Max-Q], PCI 0000:01:00.0 (audio function 0000:01:00.1)
  • iGPU: AMD Radeon 680M, PCI 0000:05:00.0
  • NVIDIA driver: 610.57.04 (open kernel module)
  • /etc/modprobe.d: options nvidia NVreg_EnableS0ixPowerManagement=1 NVreg_DynamicPowerManagement=0x02
  • asusctl / rog-control-center: 6.4.0.r0.ge6c1469c-1 (from the [ogc] repo)

The investigation, the measurements above and this report were produced with the assistance of
Claude (Anthropic's Claude Code). Every figure was measured on the system described above; nothing
is inferred or estimated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingrog-control-centerROG Control Center GUIrog-platformGPU Switching / Armoury / WMI

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions