Skip to content

fix: block NVIDIA HDA audio ALSA devices to prevent GPU wakeups - #107

Closed
felipecwb wants to merge 2 commits into
OpenGamingCollective:mainfrom
felipecwb:fix/nvidia-hda-audio-wakeups
Closed

fix: block NVIDIA HDA audio ALSA devices to prevent GPU wakeups#107
felipecwb wants to merge 2 commits into
OpenGamingCollective:mainfrom
felipecwb:fix/nvidia-hda-audio-wakeups

Conversation

@felipecwb

@felipecwb felipecwb commented Jul 27, 2026

Copy link
Copy Markdown

Summary

The NVIDIA GPU block was not preventing GPU wake-ups caused by the NVIDIA HDA audio controller. When the GPU is in D3cold and a sound server (pipewire/pulseaudio) periodically probes the ALSA devices belonging to the HDA function, the GPU wakes up because both PCI functions share the same power domain.

Root cause

NVIDIA GPUs are multi-function PCI devices:

  • Function 0 (0000:64:00.0): VGA controller (class 0x0300)
  • Function 1 (0000:64:00.1): HDA audio controller (class 0x0403)
  • Function 2: USB xHCI host controller (only on some GPUs, e.g. TU106)
  • Function 3: USB Type-C UCSI controller (only on some GPUs)

read_gpu() only collects devices with class 0x03 (display controllers), so the HDA audio function is never registered as a GPU device. The existing pci_to_inode() does try to block the .1 PCI sysfs directory via the .0.1 string replacement, but sound servers don't touch sysfs — they open the character devices under /dev/snd/ (e.g. controlC0, pcmC0D3p, hwC0D0). Those inodes were never added to the block list, so the eBPF file_open/inode_permission hooks let the open() through, the HDA controller transitions to D0, and since it shares the power domain with the VGA function the whole GPU wakes up.

NVIDIA PCI function numbering (hardware convention)

The function numbers are a hardware convention, not random:

Function Device type Present on
0 VGA / 3D controller Always
1 HDA audio controller Multi-function GPUs only
2 USB xHCI host controller Some (e.g. TU106 RTX 2060/2070)
3 USB Type-C UCSI controller Some

This is confirmed by NVIDIA's own RTD3 power management documentation, which explicitly lists removing "Audio device PCI function, USB xHCI Host Controller function as well as USB Type-C UCSI Controller PCI function" — and by real-world lspci output across many GPU generations:

01:00.0 VGA compatible controller [0300]: NVIDIA Corporation ... [10de:1f15]
01:00.1 Audio device [0403]:                NVIDIA Corporation ... [10de:10f9]
01:00.2 USB controller [0c03]:              NVIDIA Corporation ... [10de:1ada]
01:00.3 Serial controller [0700]:           NVIDIA Corporation ... [10de:1adb]

The PCI bus/device numbers (e.g. 64:00, 01:00) vary per machine depending on motherboard PCIe topology, but the function numbers are fixed. Function 1 is always the HDA audio controller when the GPU is a multi-function device. Deriving the HDA sibling via .replace(".0", ".1") is the correct and robust approach — and is already what the existing pci_to_inode() does for the sysfs path. If the GPU is a single-function device with no audio function, 0000:64:00.1 simply doesn't exist and the function returns an empty list.

ALSA sysfs layout (important for the matching logic)

In /sys/class/sound, only the cardN entry has a device symlink that resolves directly to the PCI address. Sub-devices (controlC{N}, pcmC{N}D{x}p, hwC{N}D{x}) have a device symlink that points back at the cardN sysfs dir, not the PCI device. Example from the bug reporter's machine:

$ ls /dev/snd/by-path/
pci-0000:64:00.1 -> ../controlC0   ← NVIDIA HDA (card 0)
pci-0000:65:00.1 -> ../controlC1   ← AMD iGPU audio (card 1)
pci-0000:65:00.6 -> ../controlC2   ← Realtek audio (card 2)

So the matching is done in two steps:

  1. Find the cardN whose device symlink resolves to the HDA PCI address (0000:64:00.1), and remember N (the ALSA card number).
  2. Collect the inode of every /dev/snd/* node that belongs to that card (matching C{N} followed by D or end-of-name, to avoid card 0 matching card 10+).

For the bug reporter's system, this correctly blocks exactly: controlC0, hwC0D0, pcmC0D3p, pcmC0D7p, pcmC0D8p, pcmC0D9p — and skips the AMD/Realtek cards 1 and 2.

Fix

Added nvidia_hda_snd_inodes() in inode.rs — takes the GPU's VGA PCI address, derives the HDA sibling as function 1, scans /sys/class/sound for the cardN whose device symlink resolves to the HDA PCI address, then collects the inodes of all /dev/snd/* nodes belonging to that card. These inodes are blocked/unblocked alongside the existing nvidia_to_inode and backlight_to_inode calls in block_gpu()/unblock_gpu(), so the fix applies in every mode (integrated, manual, smart) without requiring the experimental nvidia block flag.

This is not a flag-gated experiment — it's a straightforward fix to the per-GPU block path. The HDA inodes are now treated like the /dev/nvidia0 and backlight inodes: part of the standard NVIDIA block.

Test Plan

  • cargo build / nix flake check passes
  • cardwire set integrated blocks the NVIDIA GPU; cardwire list --json shows blocked: true
  • Monitor /sys/bus/pci/devices/0000:64:00.0/power_state — GPU stays in D3cold even when pipewire/pulseaudio runs
  • dmesg no longer repeats nvidia 0000:64:00.0: Enabling HDA controller
  • Audio output that does not route through the NVIDIA HDMI (e.g. laptop speakers, headset) still works
  • cardwire set hybrid unblocks the GPU and the HDA ALSA devices are accessible again

Checklist

  • My code follows the style guidelines of this project (cargo fmt)
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the mdBook documentation
  • My changes generate no new warnings (clippy/clang) — needs CI
  • New and existing unit tests pass locally with my changes — needs CI

@luytan

luytan commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

looks good but it shouldn't be behind the exp-nvidia-block setting, exp-nvidia-block is used to block files that are shared by multiple nvidia gpus (like /dev/nvidiactl or the vulkan icd), i've named it experimental so people can't complain when something breaks.
Unless multiple nvidia GPUs can share the same audio pci card, it should be universal, my laptop(AMD) also got audio nodes that could be blocked

@luytan luytan self-assigned this Jul 27, 2026
@felipecwb
felipecwb marked this pull request as draft July 27, 2026 13:01
@felipecwb
felipecwb force-pushed the fix/nvidia-hda-audio-wakeups branch 3 times, most recently from 37e35ed to c2d3b53 Compare July 27, 2026 19:33
NVIDIA GPUs are multi-function PCI devices: function 0 is the VGA
controller and function 1 is the HDA audio controller. Both functions
share the same PCI power domain, so when a sound server (pipewire or
pulseaudio) opens an ALSA device belonging to the HDA function, the GPU
is woken up from D3cold.

The existing GPU block already covered the PCI sysfs directory of the
audio function (via pci_to_inode's .0 -> .1 replacement), but sound
servers do not access sysfs; they open the character devices under
/dev/snd/ (e.g. controlC1, pcmC1D0p, hwC1D0). Without blocking those
inodes the GPU keeps being woken up by periodical audio checks.

Add nvidia_hda_snd_inodes() which scans /sys/class/sound for entries
whose "device" symlink points at the GPU's HDA sibling PCI address
(function 1), then collects the inode of the matching /dev/snd/* node.
These inodes are blocked/unblocked alongside the existing nvidia and
backlight inodes in block_gpu()/unblock_gpu(), so the fix applies in
every mode (integrated, manual, smart) without requiring the
experimental nvidia block flag.
@felipecwb
felipecwb force-pushed the fix/nvidia-hda-audio-wakeups branch from c2d3b53 to bb44c42 Compare July 27, 2026 19:42
@felipecwb

Copy link
Copy Markdown
Author

didn't worked as expected

@felipecwb felipecwb closed this Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants