# administer with pw-cli, pw-mon, pw-top commands # # performance tuning: # # HAZARDS FOR MOBY: # - high-priority threads are liable to stall the lima GPU driver, and leave a half-functional OS state. # - symptom is messages like this (with stack traces) in dmesg or journalctl: # - "[drm:lima_sched_timedout_job] *ERROR* lima job timeout" # - and the UI locks up for a couple seconds, and then pipewire + wireplumber crash (but not pipewire-pulse) # - related, unconfirmed symptoms: # - "sched: RT throttling activated" # - "BUG: KFENCE: use-after-free read in vchan_complete" # - this one seems to be recoverable # - likely to be triggered when using a small pipewire buffer (512 samples), by simple tasks like opening pavucontrol. # - but a lengthier buffer is no sure way to dodge it: it will happen (less frequently) even for buffers of 2048 samples. # - seems ANY priority < 0 triggers this, independent of the `nice` setting. # - i only tried SCHED_FIFO, not SCHED_RR (round robin) for the realtime threads. # - solution is some combination of: # - DON'T USE RTKIT. rtkit only supports SCHED_FIFO and SCHED_RR: there's no way to use it only for adjusting `nice` values. # - in pipewire.conf, remove all reference to libpipewire-module-rt. # - it's loaded by default. i can either provide a custom pipewire.conf which doesn't load it, or adjust its config so that it intentionally fails. # - without rtkit working, pipewire's module-rt doesn't allow niceness < -11. adjusting `nice`ness here seems to have little effect anyway. # - longer term, rtkit (or just rlimit based pipewire module-rt) would be cool to enable: it *does* reduce underruns. { config, lib, pkgs, ... }: let cfg = config.sane.programs.pipewire; in { sane.programs.pipewire = { configOption = with lib; mkOption { default = {}; type = types.submodule { options.min-quantum = mkOption { type = types.int; default = 16; }; options.max-quantum = mkOption { type = types.int; default = 2048; }; }; }; suggestedPrograms = [ # "rtkit" "wireplumber" ]; # sandbox.method = "landlock"; #< works, including without rtkit sandbox.method = "bwrap"; #< also works, but can't claim the full scheduling priority it wants sandbox.whitelistAudio = true; # sandbox.whitelistDbus = [ # # dbus is used for rtkit integration # # rtkit runs on the system bus. # # xdg-desktop-portal then exposes this to the user bus. # # therefore, user bus should be all that's needed, but... # # xdg-desktop-portal-wlr depends on pipewire, hence pipewire has to start before xdg-desktop-portal. # # then, pipewire has to talk specifically to rtkit (system) and not go through xdp. # # "user" # "system" # ]; sandbox.wrapperType = "inplace"; #< its config files refer to its binaries by full path sandbox.extraConfig = [ "--sanebox-keep-namespace" "pid" ]; sandbox.capabilities = [ # if rtkit isn't present, and sandboxing is via landlock, these capabilities allow pipewire to claim higher scheduling priority "ipc_lock" "sys_nice" ]; sandbox.usePortal = false; sandbox.extraPaths = [ "/dev/snd" # desko/lappy don't need these, but moby complains if not present "/dev/video0" "/dev/video1" "/dev/video2" ]; sandbox.extraHomePaths = [ # pulseaudio cookie ".config/pulse" ".config/pipewire" ]; # note the .conf.d approach: using ~/.config/pipewire/pipewire.conf directly breaks all audio, # presumably because that deletes the defaults entirely whereas the .conf.d approach selectively overrides defaults fs.".config/pipewire/pipewire.conf.d/10-sane-config.conf".symlink.text = '' # config docs: # - # useful to run `pw-top` to see that these settings are actually having effect, # and `pw-metadata` to see if any settings conflict (e.g. max-quantum < min-quantum) # # restart pipewire after editing these files: # - `systemctl --user restart pipewire` # - pipewire users will likely stop outputting audio until they are also restarted # # there's seemingly two buffers for the mic (see: ) # 1. Pipewire buffering out of the driver and into its own member. # 2. Pipewire buffering into each specific app (e.g. Dino). # note that pipewire default config includes `clock.power-of-two-quantum = true` context.properties = { default.clock.min-quantum = ${builtins.toString cfg.config.min-quantum} default.clock.max-quantum = ${builtins.toString cfg.config.max-quantum} } ''; # reduce realtime scheduling priority to prevent GPU instability, # but see the top of this file for other solutions. # fs.".config/pipewire/pipewire.conf.d/20-sane-rtkit.conf".symlink.text = '' # # documented inside # context.modules = [{ # name = libpipewire-module-rt # args = { # nice.level = 0 # rt.prio = 0 # #rt.time.soft = -1 # #rt.time.hard = -1 # rlimits.enabled = false # rtportal.enabled = false # rtkit.enabled = true # #uclamp.min = 0 # #uclamp.max = 1024 # } # flags = [ ifexists nofail ] # }] # ''; # fs.".config/pipewire/pipewire-pulse.conf.d/20-sane-rtkit.conf".symlink.text = '' # # documented: # context.modules = [{ # name = libpipewire-module-rt # args = { # nice.level = 0 # rt.prio = 0 # #rt.time.soft = -1 # #rt.time.hard = -1 # rlimits.enabled = false # rtportal.enabled = false # rtkit.enabled = true # #uclamp.min = 0 # #uclamp.max = 1024 # } # flags = [ ifexists nofail ] # }] # ''; # see: # defaults to placing the socket in /run/user/$id/{pipewire-0,pipewire-0-manager,...} # but that's trickier to sandbox env.PIPEWIRE_RUNTIME_DIR = "$XDG_RUNTIME_DIR/pipewire"; services.pipewire = { description = "pipewire: multimedia service"; partOf = [ "sound" ]; # depends = [ "rtkit" ]; # depends = [ "xdg-desktop-portal" ]; # for Realtime portal (dependency cycle) # env PIPEWIRE_LOG_SYSTEMD=false" # env PIPEWIRE_DEBUG="*:3,mod.raop*:5,pw.rtsp-client*:5" command = pkgs.writeShellScript "pipewire-start" '' mkdir -p $PIPEWIRE_RUNTIME_DIR # nice -n -21 comes from pipewire defaults (niceness: -11) exec nice -n -21 pipewire ''; readiness.waitExists = [ "$PIPEWIRE_RUNTIME_DIR/pipewire-0" "$PIPEWIRE_RUNTIME_DIR/pipewire-0-manager" ]; cleanupCommand = ''rm -f "$PIPEWIRE_RUNTIME_DIR/{pipewire-0,pipewire-0.lock,pipewire-0-manager,pipewire-0-manager.lock}"''; }; services.pipewire-pulse = { description = "pipewire-pulse: Pipewire compatibility layer for PulseAudio clients"; depends = [ "pipewire" ]; partOf = [ "sound" ]; command = pkgs.writeShellScript "pipewire-pulse-start" '' mkdir -p $XDG_RUNTIME_DIR/pulse exec nice -n -21 pipewire-pulse ''; readiness.waitExists = [ "$XDG_RUNTIME_DIR/pulse/native" "$XDG_RUNTIME_DIR/pulse/pid" ]; cleanupCommand = ''rm -f "$XDG_RUNTIME_DIR/pulse/{native,pid}"''; }; }; # taken from nixos/modules/services/desktops/pipewire/pipewire.nix # removed 32-bit compatibility stuff environment.etc = lib.mkIf cfg.enabled { "alsa/conf.d/49-pipewire-modules.conf".text = '' pcm_type.pipewire { libs.native = ${cfg.package}/lib/alsa-lib/libasound_module_pcm_pipewire.so ; } ctl_type.pipewire { libs.native = ${cfg.package}/lib/alsa-lib/libasound_module_ctl_pipewire.so ; } ''; "alsa/conf.d/50-pipewire.conf".source = "${cfg.package}/share/alsa/alsa.conf.d/50-pipewire.conf"; "alsa/conf.d/99-pipewire-default.conf".source = "${cfg.package}/share/alsa/alsa.conf.d/99-pipewire-default.conf"; }; services.udev.packages = lib.mkIf cfg.enabled [ cfg.package ]; }