Wave-2 PW1, second half. The companion commit wires `PYROWAVE_QUEUE_PRIORITY` into the Linux PyroWave device; this is what makes it work on a packaged host. Measured on .21 (RTX 5070 Ti, NVIDIA 610.43.02), same binary in both arms: as packaged (no capability) every class refused, REALTIME *and* HIGH -> default priority same binary, cap_sys_nice+ep granted REALTIME on the FIRST attempt, no downgrade RADV behaves the same way. So this is not the RADV-specific "expect one downgrade to HIGH" the plan predicted — without the capability there is no elevated priority at all, on any vendor, and the knob is decoration. Worth being precise about what is being granted, because it is a network-facing daemon. CAP_SYS_NICE permits raising scheduling priority (nice, ioprio, affinity, RT class) and nothing else: no filesystem access, no network privilege, no user switching, and it is NOT setuid. The repo already ships exactly this capability on its gamescope binary for the same reason. Two side effects that will otherwise confuse someone debugging: a capability-carrying binary is AT_SECURE, so the loader ignores LD_LIBRARY_PATH/LD_PRELOAD for it (note this box was propped up by exactly such a shim during the ffmpeg-9 soname break — that workaround would now be silently ignored), and core dumps are suppressed by default. Per packaging path, because none of them are the same: - Arch: a `_grant_sched_capability` in the scriptlet, called from post_install AND post_upgrade — a replaced binary is a new inode, so the capability does not survive an upgrade by itself. - Debian: the same setcap in the postinst `configure` branch. - RPM: `%caps(cap_sys_nice=ep)` on the binary in `%files`, which is the rpm-native form — rpm then applies it on install, restores it on upgrade, and verifies it. A `%post setcap` does none of those. - NixOS: `security.wrappers`, because a store path is read-only and shared and cannot be setcap'd. The unit's ExecStart moves to `config.security.wrapperDir` — without that the wrapper exists and the service still runs the uncapped store path, which is the whole failure this fixes. - Steam Deck: setcap in the installer's sudo block. That box needs it most (one small Van Gogh GPU shared between the game and the encode). The binary lives under $HOME, so unlike the /etc drop-ins it survives a SteamOS A/B update on its own and needs no atomic-keep entry — but it does need re-applying after each rebuild, which re-running the installer does. - Bazzite sysext: at IMAGE BUILD time, before mksquashfs. It cannot be done in the merge hook (a merged sysext's /usr is read-only squashfs) and it cannot ride in from the RPM either — rpm keeps capabilities in its own header and `rpm2cpio | cpio` carries only the payload, so the staged file arrives with none. mksquashfs does record security.capability (only security.selinux is excluded), so a setcap on the staging tree is what lands in the image. Needs root/CAP_SETFCAP; a plain-user CI build warns and ships without it rather than failing a release over a performance lever. Every one of them is best-effort and cannot fail an install: a box without libcap, or a filesystem that cannot store capabilities, simply runs at default priority exactly as it does today. Documented in the same PR — the configuration row now says the packages grant it, and running-as-a-service gets a section explaining what it is, how to check it (`getcap`), and how to remove it (`setcap -r`, or just `PYROWAVE_QUEUE_PRIORITY=off`), including the two debugging side effects. Verified: the Arch scriptlet grants the capability from a fake package root exactly as pacman would invoke it, and the resulting binary reaches REALTIME end to end on the RTX 5070 Ti; the RPM spec's %caps line parses under rpmspec in a Fedora 41 container; the NixOS module parses under nix-instantiate; all five edited shell scripts pass `bash -n`. No Rust file changed in this commit, so the CI-parity Rust gates from the companion commit still stand.
13 KiB
title, description
| title | description |
|---|---|
| Running as a Service | Start the host at boot — for a desktop you log into, or a fully headless always-on machine. |
Running serve in a terminal is fine for trying Punktfunk out. To make a machine an
always-available host, run it as a service. First, what that service starts — then the two cases, a
desktop you log into and a fully headless box.
What the unit starts
The bundled unit runs serve --gamestream, so it serves both the native punktfunk/1 plane and
stock Moonlight clients. Every Linux package installs that unit as it ships and
only rewrites the binary path, so a host you installed from apt, dnf, pacman or the Bazzite sysext
has GameStream on. See what yours starts:
systemctl --user cat punktfunk-host
For a native-only host (no GameStream — its pairing runs over plain HTTP and its legacy
encryption is weaker; see Security & Safe Use), drop the flag. The packaged unit is
a package file that an upgrade replaces, so override ExecStart with a drop-in rather than editing
it:
systemctl --user edit punktfunk-host
[Service]
ExecStart=
ExecStart=/usr/bin/punktfunk-host serve
The empty ExecStart= is required — without it systemd adds a second command instead of replacing
the first — and the path must match the one systemctl --user cat printed (the distro packages use
/usr/bin). Save, then systemctl --user restart punktfunk-host.
Windows is the other way round: an install from the setup .exe leaves GameStream off unless you
tick it, and it is configured differently — see Windows below.
A. A desktop you log into
If you sit at the machine (or it auto-logs-in to a desktop), run the host as a systemd user service that starts with your session.
Put your host.env in place first. The unit reads ~/.config/punktfunk/host.env and won't start
until that file exists — no package creates it for you, they only ship a template to copy. The
defaults in it are right for an ordinary desktop; your distro and desktop guides say if yours wants
a different template (on Bazzite it's host.env.bazzite):
mkdir -p ~/.config/punktfunk
# /usr/share/punktfunk/ on Fedora/Arch/Bazzite, /usr/share/punktfunk-host/ on Ubuntu
cp /usr/share/punktfunk/host.env.example ~/.config/punktfunk/host.env
Installed from a package (apt, dnf, pacman, or the Bazzite sysext) — the unit is already at
/usr/lib/systemd/user/punktfunk-host.service, with its ExecStart pointing at the installed
binary. There's nothing to copy:
systemctl --user daemon-reload # the sysext route needs this; harmless elsewhere
systemctl --user enable --now punktfunk-host
Built from source — install the unit from your checkout, and take host.env from there too
(cp scripts/host.env.example ~/.config/punktfunk/host.env). The unit's ExecStart points at
%h/punktfunk/target/release/punktfunk-host (%h is your home directory), so edit the copy if your
checkout lives somewhere else:
mkdir -p ~/.config/systemd/user
cp scripts/punktfunk-host.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now punktfunk-host
Don't do the copy on a packaged install: a unit in ~/.config/systemd/user/ shadows the packaged
one, and the source unit points at a build tree you don't have — the service then fails with
status=203/EXEC.
The host now starts whenever you log in. Check it with systemctl --user status punktfunk-host.
You don't need to export anything for it. The host finds the live compositor session itself on
every connect and works out where to reach it (WAYLAND_DISPLAY, XDG_RUNTIME_DIR, the session bus,
sway's SWAYSOCK, Hyprland's instance signature) from the running compositor — so host.env is for
policy, not session plumbing, and systemctl --user import-environment is not a prerequisite.
Restart the host with your desktop
Add one drop-in so the host follows your session's lifetime:
mkdir -p ~/.config/systemd/user/punktfunk-host.service.d
# /usr/share/punktfunk/ on Fedora/Arch/Bazzite, /usr/share/punktfunk-host/ on Ubuntu,
# scripts/ in a source checkout
cp /usr/share/punktfunk/punktfunk-host-desktop-session.conf \
~/.config/systemd/user/punktfunk-host.service.d/desktop-session.conf
systemctl --user daemon-reload
systemctl --user reenable punktfunk-host
systemctl --user restart punktfunk-host
Without it, restarting Plasma or GNOME — a crash, a log out and back in, "restart the shell" — leaves the host running against a compositor that no longer exists. It keeps listening and answering, and every session after that fails at capture, which is a confusing way to find out. The drop-in makes a compositor restart a host restart.
Skip it on the headless/appliance route below (which has its own session unit), and on Sway or
Hyprland, which don't hand their session to systemd: they never reach graphical-session.target, so
the drop-in is harmless there but does nothing. To make the host come and go with the session on
those, start it from the compositor's own config instead of enabling the unit — add
exec systemctl --user start punktfunk-host to your sway config, or
exec-once = systemctl --user start punktfunk-host to Hyprland's — and leave the unit itself
disabled (systemctl --user disable punktfunk-host), so it isn't also started at login.
B. A headless, always-on host
To run with no monitor and no login — a machine in a closet that's always ready — you need two things: a desktop session that comes up at boot, and the host service started without a login.
Start by making the host service start at boot even when nobody logs in:
sudo loginctl enable-linger "$USER"
Then bring up a session automatically. How you do that is desktop-specific — auto-login, lock disable, and the session unit differ per compositor, so each is documented on its own page:
- GNOME: GNOME → Headless session.
- KDE Plasma: KDE → Headless session.
- Steam / gamescope: gamescope — the host launches its own session per client, so there's no separate session unit.
Once a session comes up at boot, enable the host user service (section A) and reboot. The host comes up on that session.
Headless Bazzite
On Bazzite, the host launches its own gamescope/Steam session per client, so you don't need a separate session unit — see Bazzite and gamescope.
Windows
Punktfunk has first-class Linux and Windows hosts. On Windows it ships as a signed installer with an SCM service and a virtual-display driver — including Punktfunk's own indirect display driver the host pushes frames straight into. The Windows host is newer than the Linux host. (Not to be confused with the Windows client, which streams to a Windows PC.)
On Windows the host runs as a LocalSystem service that launches into the interactive session, so it
captures the secure desktop (UAC / lock screen) and survives reboots with nobody logged in — the same
model Sunshine/Apollo use. Because it runs at that privilege level, keep it on a trusted network and be
deliberate about which machine you host on — see Security & Safe Use.
The easy path is the signed installer: download punktfunk-host-setup-<ver>.exe from the package
registry (punktfunk-host-windows) and run it. It drops the host
into C:\Program Files\punktfunk, installs the bundled pf-vdisplay virtual-display driver, and
registers + starts the service for you (/VERYSILENT for unattended). Upgrades and uninstall are
handled through Add/Remove Programs.
Prefer the CLI? Run punktfunk-host service install from an elevated prompt — see
Windows Host. For hardware encode you need a GPU — NVIDIA (NVENC), AMD (AMF), or
Intel (QSV); the host falls back to software H.264 without one.
GameStream on Windows. Unlike the Linux unit, the installer leaves Moonlight compatibility
off — it's a checkbox in the wizard (/MERGETASKS="gamestream" to select it unattended). There's
no ExecStart to edit here: the service launches whatever PUNKTFUNK_HOST_CMD in
%ProgramData%\punktfunk\host.env says, which is also where the rest of the Windows host's
configuration lives. To change it later, from an elevated prompt:
punktfunk-host service install --gamestream=on # or --gamestream=off
punktfunk-host service restart
Registering the service by hand is the exception. A bare punktfunk-host service install writes a
fresh host.env with PUNKTFUNK_HOST_CMD left commented out, and with no value set the service
falls back to serve --gamestream — so add --gamestream=off to that command if you want the
native-only host.
Firewall scope. The installer opens the streaming + console ports on Private and Domain networks only — not Public. If your LAN is (mis)classified Public, clients won't connect until you set it to Private (Windows Settings → Network), and the host logs a warning when it's on a Public network. For a trusted network Windows insists is Public, tick "Allow connections on Public networks" at install (or pass
--allow-public-networktoservice install). See Security & Safe Use for the reasoning.
Verifying
After a reboot, from another machine on the network:
punktfunk reachable 192.168.1.50 # exit 0 = the host answered, 2 = it didn't
punktfunk hosts list --probe # every saved host, online or offline
punktfunk is the headless client CLI — it ships in the Linux client packages (punktfunk-client)
and with the Windows client. From a source checkout, punktfunk-probe --discover browses the LAN
instead; it's a dev tool and isn't packaged. Or just open a native client / Moonlight and look for
the host.
If the host answers, it's up. If not, check journalctl --user -u punktfunk-host on the host — on
a Windows host, run punktfunk-host service status from an elevated prompt on the machine itself.
GPU scheduling priority
The Linux packages give the host binary one Linux capability, CAP_SYS_NICE, and it is worth
knowing why it is there and how to take it away.
The PyroWave codec encodes on the same GPU shader cores your game is using, so a demanding game can crowd it out and the stream's frame rate drops with it. The fix is to ask the driver to schedule the encode ahead of the game, and every driver we tested gates that request on this capability: without it the request is simply refused and nothing changes. The other codecs use a separate video engine on the GPU and are unaffected either way.
CAP_SYS_NICE lets a process raise its own scheduling priority. It grants no access to files,
the network or other users' processes, and it is not the same as running as root — the host
still runs as you, under your user session.
To check, or to take it away:
getcap /usr/bin/punktfunk-host # shows cap_sys_nice=ep when granted
sudo setcap -r /usr/bin/punktfunk-host # remove it; streaming still works
Removing it costs you nothing unless you stream PyroWave, and you can also just set
PYROWAVE_QUEUE_PRIORITY=off to stop the host asking. Note that a package update replaces the
binary and re-applies the capability.
Two side effects, if you are debugging the host: a binary carrying a capability is treated as
security-sensitive by the dynamic loader, so LD_LIBRARY_PATH and LD_PRELOAD are ignored for it,
and it does not write core dumps by default.
Stopping and removing
After a Linux package update the user service keeps running the old binary until it's restarted, and
a package can't restart another user's --user units for you — Updating has the
update command for every install method and the restart that finishes the job. The Windows installer
restarts its own service.
To stop the host for now:
systemctl --user stop punktfunk-host # Linux
punktfunk-host service stop # Windows, elevated prompt
To stop it for good, so it doesn't come back at login or boot:
# add punktfunk-web (the console) and punktfunk-kde-session (the headless KDE route) if you enabled them
systemctl --user disable --now punktfunk-host
rm -rf ~/.config/systemd/user/punktfunk-host.service.d # any drop-ins you added
sudo loginctl disable-linger "$USER" # only if you enabled lingering
On Windows, punktfunk-host service uninstall from an elevated prompt stops the service, removes it,
and removes the firewall rules it added. To remove the whole install instead, use Add/Remove Programs.
Neither removes ~/.config/punktfunk (Linux) or %ProgramData%\punktfunk (Windows) — your
certificate, pairings and console password stay, so a reinstall picks up where you left off. See
Uninstall to clear them out.