From c9e6ec7b23a24616def690cd3d1ae40153c8e2c0 Mon Sep 17 00:00:00 2001 From: Lumpiasty Date: Mon, 18 May 2026 17:19:04 +0200 Subject: [PATCH] add audio-rt module to fight xruns under cpu load EasyEffects DSP plugins (rnnoise, deepfilternet, crystalizer) drop audio when the system is under load. Layered workaround with per-optimization toggles for bisecting impact: - cgroup-based CPU partitioning: dedicated audio.slice with AllowedCPUs pinning, restricted app/session/background slices for everything else - rlimits so PipeWire's module-rt sets SCHED_FIFO 88 directly instead of going through RTKit's priority-10 ceiling - persistent watcher that re-applies performance governor on audio cores (gamemoded/PPD keep resetting it) - ananicy rule pinning easyeffects to nice -12 for non-RT DSP threads - znver4-tuned rebuilds of easyeffects and its DSP deps Master switch + per-feature toggles via lumpiasty.audioRt.*; all enabled by default on acer host. None of this fully eliminates dropouts on this thermally constrained laptop but each layer is independently testable. --- home-modules/pc.nix | 5 + hosts/acer.nix | 1 + modules/default.nix | 1 + modules/desktop/audio-rt.nix | 233 +++++++++++++++++++++++++++++++++ modules/desktop/pulseaudio.nix | 12 +- 5 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 modules/desktop/audio-rt.nix diff --git a/home-modules/pc.nix b/home-modules/pc.nix index 58d2c26..7ac208b 100644 --- a/home-modules/pc.nix +++ b/home-modules/pc.nix @@ -41,6 +41,11 @@ ]; programs.librewolf.enable = true; services.easyeffects.enable = true; + systemd.user.services.easyeffects.Service = lib.mkIf osConfig.lumpiasty.audioRt.cpuPartitioning { + # Move easyeffects into audio.slice (defined in modules/desktop/audio-rt.nix) + # which has AllowedCPUs= — pins all DSP work to the reserved cores. + Slice = "audio.slice"; + }; programs.chromium.enable = true; programs.chromium.package = pkgs.ungoogled-chromium; diff --git a/hosts/acer.nix b/hosts/acer.nix index bccea9b..4a2b3a2 100644 --- a/hosts/acer.nix +++ b/hosts/acer.nix @@ -71,6 +71,7 @@ rec { amdCpu = true; noMitigations = false; enablePulseaudio = true; + audioRt.enable = true; sshd = true; users.user = true; # users.drugi = true; diff --git a/modules/default.nix b/modules/default.nix index ecc2178..f01d345 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -20,6 +20,7 @@ desktop/plasma.nix desktop/touchpad.nix desktop/pulseaudio.nix + desktop/audio-rt.nix desktop/tailscale.nix ]; } \ No newline at end of file diff --git a/modules/desktop/audio-rt.nix b/modules/desktop/audio-rt.nix new file mode 100644 index 0000000..185d1da --- /dev/null +++ b/modules/desktop/audio-rt.nix @@ -0,0 +1,233 @@ +{ config, lib, pkgs, ... }: + +# Workarounds for audio xruns under CPU load. +# +# Each optimization is independently toggleable so behavior can be bisected. +# `lumpiasty.audioRt.enable` is the master switch; individual sub-flags default +# to `true` when the master is on and can be flipped per-host to test impact. + +let + cfg = config.lumpiasty.audioRt; + + marchFlags = " -march=znver4 -O3"; + + # --------------------------------------------------------------------------- + # Per-build-system helpers (see commit history for rationale on LTO choices). + # --------------------------------------------------------------------------- + withMarch = pkg: pkg.overrideAttrs (old: { + env = (old.env or {}) // { + NIX_CFLAGS_COMPILE = + ((old.env or {}).NIX_CFLAGS_COMPILE or old.NIX_CFLAGS_COMPILE or "") + + marchFlags; + }; + }); + + cmakePkg = pkg: pkg.overrideAttrs (old: { + env = (old.env or {}) // { + NIX_CFLAGS_COMPILE = + ((old.env or {}).NIX_CFLAGS_COMPILE or old.NIX_CFLAGS_COMPILE or "") + + marchFlags; + }; + cmakeFlags = (old.cmakeFlags or []) ++ [ "-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON" ]; + preConfigure = (old.preConfigure or "") + "\nexport AR=gcc-ar\n"; + }); + + rustPkg = pkg: pkg.overrideAttrs (old: { + RUSTFLAGS = (old.RUSTFLAGS or "") + " -C target-cpu=znver4"; + }); + +in + +{ + options.lumpiasty.audioRt = { + enable = lib.mkEnableOption "Audio RT scheduling and CPU isolation"; + + audioCpus = lib.mkOption { + type = lib.types.str; + default = "12-15"; + description = "CPU list reserved for audio services (systemd cpuset syntax)."; + }; + + nonAudioCpus = lib.mkOption { + type = lib.types.str; + default = "0-11"; + description = "CPU list for everything else."; + }; + + # ------ Individual optimization toggles ------ + + cpuPartitioning = lib.mkOption { + type = lib.types.bool; + default = cfg.enable; + description = '' + Cgroup-based CPU partitioning via dedicated audio.slice and + restricted app/session/background slices. + ''; + }; + + rtLimits = lib.mkOption { + type = lib.types.bool; + default = cfg.enable; + description = '' + Raise rlimits (RTPRIO=95, MEMLOCK=infinity) for the audio group + so PipeWire's module-rt can set SCHED_FIFO 88 directly instead + of going through RTKit's priority-10 ceiling. + ''; + }; + + performanceGovernor = lib.mkOption { + type = lib.types.bool; + default = cfg.enable; + description = '' + Keep cpufreq governor `performance` on the audio cores so they + stay boosted regardless of measured utilization. + ''; + }; + + ananicy = lib.mkOption { + type = lib.types.bool; + default = cfg.enable; + description = '' + Run ananicy-cpp with a rule that pins easyeffects to nice -12 so + its non-RT DSP threads get scheduler preference under load. + ''; + }; + + optimisedBinaries = lib.mkOption { + type = lib.types.bool; + default = cfg.enable; + description = '' + Rebuild easyeffects and its DSP dependencies with -march=znver4 -O3 + (and LTO for cmake builds, target-cpu for rust builds). + ''; + }; + }; + + config = lib.mkMerge [ + + # --- Optimised binary builds --------------------------------------------- + (lib.mkIf (cfg.enable && cfg.optimisedBinaries) { + nixpkgs.overlays = [ + (final: prev: { + easyeffects = cmakePkg (prev.easyeffects.override { + fftw = withMarch prev.fftw; + fftwFloat = withMarch prev.fftwFloat; + speexdsp = withMarch prev.speexdsp; + rubberband = withMarch prev.rubberband; + soundtouch = withMarch prev.soundtouch; + zita-convolver = withMarch prev.zita-convolver; + webrtc-audio-processing = withMarch prev.webrtc-audio-processing; + rnnoise = withMarch prev.rnnoise; + libebur128 = cmakePkg prev.libebur128; + libbs2b = withMarch prev.libbs2b; + lilv = withMarch prev.lilv; + onetbb = cmakePkg prev.onetbb; + calf = cmakePkg prev.calf; + lsp-plugins = withMarch prev.lsp-plugins; + zam-plugins = withMarch prev.zam-plugins; + mda_lv2 = withMarch prev.mda_lv2; + deepfilternet = rustPkg prev.deepfilternet; + }); + }) + ]; + }) + + # --- RT scheduling rlimits ---------------------------------------------- + (lib.mkIf (cfg.enable && cfg.rtLimits) { + security.pam.loginLimits = [ + { domain = "@audio"; type = "-"; item = "rtprio"; value = "95"; } + { domain = "@audio"; type = "-"; item = "memlock"; value = "unlimited"; } + { domain = "@audio"; type = "-"; item = "nice"; value = "-20"; } + ]; + systemd.user.extraConfig = '' + DefaultLimitRTPRIO=95 + DefaultLimitMEMLOCK=infinity + ''; + }) + + # --- CPU partitioning (cgroup-based) ------------------------------------ + # + # Cgroup hierarchy under user@.service: + # ├── app.slice AllowedCPUs= (Steam-launched apps) + # ├── session.slice AllowedCPUs= (kwin, plasmashell, kded) + # ├── background.slice AllowedCPUs= (akonadi, polkit) + # └── audio.slice AllowedCPUs= (pipewire, easyeffects) + # + # Reasoning: + # - No isolcpus= : breaks scheduler load balancing on the rest of the system. + # - No nohz_full= : amd-pstate can't sample utilization in tickless mode + # so cores get clamped at minimum frequency. + # - No rcu_nocbs= : microsecond-scale jitter is irrelevant at 21ms quantum. + (lib.mkIf (cfg.enable && cfg.cpuPartitioning) { + systemd.user.extraConfig = '' + CPUAffinity=${cfg.nonAudioCpus} + ''; + systemd.settings.Manager.CPUAffinity = cfg.nonAudioCpus; + + # Delegate the cpuset controller to user managers so user-level slices + # can use AllowedCPUs=. + systemd.services."user@".serviceConfig.Delegate = "cpu cpuset io memory pids"; + + systemd.user.slices = { + app.sliceConfig.AllowedCPUs = cfg.nonAudioCpus; + session.sliceConfig.AllowedCPUs = cfg.nonAudioCpus; + background.sliceConfig.AllowedCPUs = cfg.nonAudioCpus; + audio = { + description = "Audio services pinned to reserved CPU cores"; + sliceConfig.AllowedCPUs = cfg.audioCpus; + }; + }; + + # easyeffects.service Slice= is set in home-modules/pc.nix. + systemd.user.services.pipewire.serviceConfig.Slice = "audio.slice"; + systemd.user.services.pipewire-pulse.serviceConfig.Slice = "audio.slice"; + systemd.user.services.wireplumber.serviceConfig.Slice = "audio.slice"; + }) + + # --- Performance governor on audio cores -------------------------------- + (lib.mkIf (cfg.enable && cfg.performanceGovernor) { + systemd.services.audio-cores-performance = { + description = "Keep performance governor on audio cores"; + wantedBy = [ "multi-user.target" ]; + after = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + Restart = "always"; + RestartSec = "5s"; + # Expand systemd CPU list ("12-15" / "12,13,14,15") into a flat list. + ExecStart = pkgs.writeShellScript "audio-cores-performance" '' + cpus=$(echo "${cfg.audioCpus}" | ${pkgs.coreutils}/bin/tr ',' ' ' | \ + ${pkgs.gawk}/bin/awk '{ + for (i=1; i<=NF; i++) { + if (match($i, /^([0-9]+)-([0-9]+)$/, m)) + for (j=m[1]; j<=m[2]; j++) print j + else print $i + } + }') + while true; do + for cpu in $cpus; do + cur=$(cat /sys/devices/system/cpu/cpu$cpu/cpufreq/scaling_governor) + if [ "$cur" != "performance" ]; then + echo performance > /sys/devices/system/cpu/cpu$cpu/cpufreq/scaling_governor + fi + done + sleep 2 + done + ''; + }; + }; + }) + + # --- Ananicy rule for easyeffects --------------------------------------- + (lib.mkIf (cfg.enable && cfg.ananicy) { + services.ananicy = { + enable = true; + package = pkgs.ananicy-cpp; + extraRules = [ + { name = "easyeffects"; type = "Audio"; nice = -12; } + ]; + }; + }) + + ]; +} diff --git a/modules/desktop/pulseaudio.nix b/modules/desktop/pulseaudio.nix index 6b97377..7a1b530 100644 --- a/modules/desktop/pulseaudio.nix +++ b/modules/desktop/pulseaudio.nix @@ -20,13 +20,21 @@ # no need to redefine it in your config for now) #media-session.enable = true; + extraConfig.pipewire."99-quantum" = { + "context.properties" = { + "default.clock.quantum" = 1024; + "default.clock.min-quantum" = 1024; + "default.clock.max-quantum" = 8192; + }; + }; + wireplumber.configPackages = [ (pkgs.writeTextDir "share/wireplumber/wireplumber.conf.d/99-alsa-nova-3.conf" '' monitor.alsa.rules = [ { matches = [ { - node.name = "alsa_output.usb-SteelSeries_Arctis_Nova_3-00.analog-stereo" + node.name = "alsa_output.usb-SteelSeries_Arctis_Nova_7-00.analog-stereo" } ] actions = { @@ -34,7 +42,7 @@ audio.format = "S24LE" audio.rate = 96000 api.alsa.period-size = 1024 - api.alsa.period-num = 4 + api.alsa.period-num = 8 api.alsa.disable-batch = false } }