Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32b484bede | ||
|
|
651a7a82a1 | ||
|
|
4d383811c0 | ||
|
|
42ee6c5628 | ||
|
|
08eaf337e8 | ||
|
|
39869031be | ||
|
|
55f361cb92 | ||
|
|
2079411f4f | ||
|
|
4d1a1348c0 |
@@ -344,10 +344,31 @@ jobs:
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends meson ninja-build glslc git || true
|
||||
apt-get build-dep -y gamescope || true
|
||||
# NOT best-effort. `build-dep gamescope` resolves the distro's much older packaged
|
||||
# gamescope — where noble has one at all — so it misses what the master tree needs, and
|
||||
# wayland-protocols is the gap that actually stops the build: meson dies in
|
||||
# protocol/meson.build with "Neither a subproject directory nor a wayland-protocols.wrap
|
||||
# file was found", because the tree has no wrap fallback for it. That is what happened on
|
||||
# the v0.26.0 tag: the step warned and skipped, the job stayed green, and the release
|
||||
# shipped with no gamescope .deb while the notes said it had one.
|
||||
apt-get install -y --no-install-recommends wayland-protocols
|
||||
# The remaining Arch makedepends the older packaged gamescope does not necessarily pull.
|
||||
# Best-effort: meson falls back or does without, and a name that moves between Ubuntu
|
||||
# releases should not fail the job. (No libstdc++ static package is needed here — g++
|
||||
# ships libstdc++.a, which is why only Fedora tripped the sanity check.)
|
||||
apt-get install -y --no-install-recommends libglm-dev cmake libxcursor-dev || true
|
||||
if bash packaging/gamescope/build-punktfunk-gamescope.sh \
|
||||
--destdir "$PWD/gs-stage" --prefix /usr --jobs "$(nproc)"; then
|
||||
install -Dm0755 gs-stage/usr/bin/punktfunk-gamescope gs-cache/punktfunk-gamescope
|
||||
else
|
||||
# Best-effort on canary, HARD on a stable tag — same reasoning as rpm.yml. A warning
|
||||
# here is what let v0.26.0 promise an apt-installable gamescope that was never built.
|
||||
case "$GITHUB_REF" in
|
||||
refs/tags/v*)
|
||||
echo "::error::punktfunk-gamescope failed to build on noble — a stable tag must not ship without it (the release notes and docs-site say it is installable)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo "::warning::punktfunk-gamescope failed to build on noble — no .deb this run (gamescope sessions stay SDR)"
|
||||
fi
|
||||
|
||||
@@ -357,6 +378,13 @@ jobs:
|
||||
if [ -x gs-cache/punktfunk-gamescope ] && gs-cache/punktfunk-gamescope --version >/dev/null 2>&1; then
|
||||
bash packaging/debian/build-gamescope-deb.sh --binary gs-cache/punktfunk-gamescope
|
||||
else
|
||||
# Authoritative, and covers the cache path the build step skips — same as rpm.yml.
|
||||
case "$GITHUB_REF" in
|
||||
refs/tags/v*)
|
||||
echo "::error::no usable punktfunk-gamescope — a stable tag must not ship without its .deb"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo "::warning::no usable punktfunk-gamescope — skipping its .deb"
|
||||
fi
|
||||
|
||||
|
||||
@@ -206,10 +206,33 @@ jobs:
|
||||
dnf -y install dnf-plugins-core meson ninja-build glslc || true
|
||||
dnf builddep -y gamescope || true
|
||||
dnf -y install xorg-x11-server-Xwayland-devel || true
|
||||
# NOT best-effort: build-punktfunk-gamescope.sh appends `-static-libstdc++` to LDFLAGS
|
||||
# (so the binary still starts on SteamOS's older libstdc++ — see its comment), and
|
||||
# without the static library meson's very FIRST sanity check dies with
|
||||
# "cannot find -lstdc++ / have you installed the static version", so nothing builds at
|
||||
# all. That is what happened on the v0.26.0 tag: both Fedora bases warned and skipped,
|
||||
# the job stayed green, and the release shipped with no gamescope RPM while the notes
|
||||
# said it had one. A rename here must be LOUD, hence no `|| true`.
|
||||
dnf -y install libstdc++-static
|
||||
# The rest of the Arch package's makedepends that Fedora's older packaged gamescope does
|
||||
# not necessarily pull. Best-effort: unlike the static runtime, meson finds fallbacks or
|
||||
# does without, and a name that moves between Fedora releases should not fail the job.
|
||||
dnf -y install wayland-protocols-devel glm-devel cmake libXcursor-devel || true
|
||||
if bash packaging/gamescope/build-punktfunk-gamescope.sh \
|
||||
--destdir "$PWD/gs-stage" --prefix /usr --jobs "$(nproc)"; then
|
||||
install -Dm0755 gs-stage/usr/bin/punktfunk-gamescope gs-cache/punktfunk-gamescope
|
||||
else
|
||||
# Best-effort on canary, HARD on a stable tag. A `::warning::` here is what let
|
||||
# v0.26.0 publish release notes promising a Fedora gamescope package that did not
|
||||
# exist: the step "succeeded", the job was green, and the only evidence was a warning
|
||||
# nobody reads. A release must not be able to make a claim its own CI silently
|
||||
# dropped, so on `refs/tags/v*` this is an error.
|
||||
case "$GITHUB_REF" in
|
||||
refs/tags/v*)
|
||||
echo "::error::punktfunk-gamescope failed to build for f${{ matrix.fedver }} — a stable tag must not ship without it (the release notes and docs-site say it is installable)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo "::warning::punktfunk-gamescope failed to build for f${{ matrix.fedver }} — the sysext ships without it (gamescope sessions stay SDR)"
|
||||
fi
|
||||
|
||||
@@ -227,6 +250,15 @@ jobs:
|
||||
--binary gs-cache/punktfunk-gamescope \
|
||||
--release "$PF_RELEASE"
|
||||
else
|
||||
# The authoritative "does this package exist" decision, and the one that also covers
|
||||
# the cache path — the build step above is skipped entirely on a cache hit, so a stale
|
||||
# or empty cache would otherwise reach here and skip in silence on a release.
|
||||
case "$GITHUB_REF" in
|
||||
refs/tags/v*)
|
||||
echo "::error::no usable punktfunk-gamescope for f${{ matrix.fedver }} — a stable tag must not ship without its RPM"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo "::warning::no usable punktfunk-gamescope for f${{ matrix.fedver }} — skipping its RPM"
|
||||
fi
|
||||
|
||||
|
||||
+16
-2
@@ -290,7 +290,19 @@ same shader cores a game saturates; NVENC is immune because it has its own ASIC.
|
||||
ladder REALTIME → HIGH → no-priority, stepping only on refusal; a refused class can never fail the
|
||||
open. The extension probe reuses the `dev_ext_props` already fetched for `queue_family_foreign` and
|
||||
takes KHR or the EXT alias — the same spelling pf-zerocopy probes, so the two cannot disagree.
|
||||
⭐ **Needs `CAP_SYS_NICE`**, which the packaging now grants; without it the lever does nothing.
|
||||
⭐ **Needs `CAP_SYS_NICE`**, which the packaging granted in `0.26.0-1`; without it the lever does
|
||||
nothing.
|
||||
🛑 **Corrected in `0.26.0-2`: the packaging no longer grants it, and must not.** Every channel that
|
||||
did (Arch `.install`, RPM `%caps()`, the Bazzite sysext image, the deb postinst, the NixOS
|
||||
`security.wrappers` entry) broke desktop streaming on KDE outright — field-reported on CachyOS and
|
||||
Bazzite as `KWin does not expose zkde_screencast_unstable_v1 to this client`. KWin identifies a
|
||||
client by resolving its `/proc/<pid>/exe` against an installed `.desktop`, and the kernel refuses
|
||||
that readlink to any reader whose effective set is not a superset of the target's **permitted**
|
||||
set (`cap_ptrace_access_check`) — KWin has no capabilities, so a capability-carrying host is
|
||||
unidentifiable and the restricted globals are never advertised. Neither `prctl(PR_SET_DUMPABLE, 1)`
|
||||
nor systemd `AmbientCapabilities=` rescues it; only an uncapped process is identifiable. The lever
|
||||
therefore stays wired but unexercised on a stock install (the ladder degrades to default priority),
|
||||
and is opt-in for gamescope-only hosts, which have no such identity check.
|
||||
- **PW5 — two encoder handles.** `Encoder::Impl` owns exactly one each of `wavelet_img_high_res`,
|
||||
`bucket_buffer`, `meta_buffer`, `block_stat_buffer`, `payload_data`, `quant_buffer`, and
|
||||
`Impl::encode` *opens* by discarding them (an image barrier with `VK_IMAGE_LAYOUT_UNDEFINED` as the
|
||||
@@ -414,7 +426,9 @@ emulator itself would land it outside both.
|
||||
|
||||
⏳ **Owed on glass:** iPhone + Bluetooth listen, Apple TV stats overlay, MacBook audio listen, the
|
||||
Deck HEVC/4:4:4 retest, a Windows wake-from-sleep cycle, and the PyroWave-under-game-load A/B on a
|
||||
Linux host with `CAP_SYS_NICE` actually granted — the number this whole wave is aimed at.
|
||||
Linux host with `CAP_SYS_NICE` actually granted — the number this whole wave is aimed at. ⚠ That
|
||||
last one now needs a **gamescope-only** host, or a hand-granted capability on a box you are not
|
||||
streaming the KDE desktop from: see the `0.26.0-2` correction under PW1 above.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -65,6 +65,14 @@ pub(crate) fn stamp_color_bits(bitstream: &mut [u8], seq_offset: usize, bt2020_p
|
||||
/// repeated value is read as more blocks of the same frame. That is why PW5's alternating encoder
|
||||
/// handles need `pyrowave_encoder_set_next_sequence`, and why a test asserts this reader sees
|
||||
/// +1 mod 8 across the pair.
|
||||
///
|
||||
/// Its only caller is the Linux backend — alternating encoder handles are a Linux-side concern, and
|
||||
/// the Windows backend drives pyrowave's compat device with a single handle. The rest of this module
|
||||
/// really is shared (`packet_boundary` and `stamp_color_bits` have callers on both), so the exemption
|
||||
/// is scoped to this one item rather than the file: `dead_code` stays live on Linux, where the caller
|
||||
/// lives and where its disappearing would be a real finding. Windows builds with `-D warnings`, so
|
||||
/// without this the host and tray clippy legs fail to compile the lib at all.
|
||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
pub(crate) fn wire_sequence(bitstream: &[u8], packet_offset: usize) -> Option<u8> {
|
||||
let lo = *bitstream.get(packet_offset + 2)?;
|
||||
let hi = *bitstream.get(packet_offset + 3)?;
|
||||
|
||||
@@ -13,8 +13,11 @@
|
||||
//! So an interactive Plasma session does NOT hand it to a bare client — the host packages ship
|
||||
//! `io.unom.Punktfunk.Host.desktop` (`Exec=/usr/bin/punktfunk-host`,
|
||||
//! `X-KDE-Wayland-Interfaces=zkde_screencast_unstable_v1,…`) so it is present before the host first
|
||||
//! connects. The headless test path instead exposes it to bare clients via
|
||||
//! `KWIN_WAYLAND_NO_PERMISSION_CHECKS=1`. The compositor backend must implement
|
||||
//! connects. That identification is also why **the host binary must carry no file capability**: a
|
||||
//! process holding capabilities KWin lacks is one the kernel will not let KWin resolve
|
||||
//! `/proc/<pid>/exe` for, so it can never be matched to a `.desktop` no matter how correctly the
|
||||
//! file is installed (see [`capability_denial_hint`]). The headless test path instead exposes it to
|
||||
//! bare clients via `KWIN_WAYLAND_NO_PERMISSION_CHECKS=1`. The compositor backend must implement
|
||||
//! `createVirtualOutput`: the **DRM backend** (any version) or the **VirtualBackend since KWin
|
||||
//! 6.5.6** (`kwin_wayland --virtual`); on `--virtual` < 6.5.6 the request fails with
|
||||
//! "Could not find output". We talk raw Wayland on `$WAYLAND_DISPLAY`, so the host must run inside
|
||||
@@ -1071,6 +1074,74 @@ impl Drop for StopOnDrop {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extra sentence appended to every "KWin never advertised the screencast global" error when this
|
||||
/// process carries capabilities — the one cause that is completely invisible from the Wayland side.
|
||||
///
|
||||
/// KWin authorizes a restricted interface by resolving the *client's* `/proc/<pid>/exe` and
|
||||
/// matching it against an installed `.desktop`. The kernel refuses that readlink to any reader
|
||||
/// whose effective set is not a superset of the target's **permitted** set
|
||||
/// (`cap_ptrace_access_check`), and KWin has no capabilities at all. So a host binary carrying any
|
||||
/// file capability is simply unidentifiable: `executablePath()` comes back empty, no `.desktop` can
|
||||
/// match, and the global is never advertised — indistinguishable, from here, from a missing
|
||||
/// `.desktop`. Neither half of the obvious workaround helps: `prctl(PR_SET_DUMPABLE, 1)` leaves the
|
||||
/// permitted-set check failing, and moving the grant to systemd `AmbientCapabilities=` lands the
|
||||
/// capability in the same permitted set. Only an uncapped binary is identifiable.
|
||||
///
|
||||
/// This is not hypothetical: 0.26.0-1 setcap'd `cap_sys_nice` on the host for the GPU-priority
|
||||
/// lever and took out desktop streaming on every KDE box until the capability was removed again.
|
||||
fn capability_denial_hint() -> String {
|
||||
let permitted = std::fs::read_to_string("/proc/self/status")
|
||||
.ok()
|
||||
.and_then(|status| permitted_caps_from_status(&status));
|
||||
match permitted {
|
||||
Some(caps) if caps != 0 => format!(
|
||||
" — NOTE: this process carries capabilities (CapPrm={caps:#018x}), which is enough on \
|
||||
its own to cause this: the kernel then refuses KWin the /proc/<pid>/exe read it \
|
||||
identifies clients by, so no .desktop can match however correctly it is installed. \
|
||||
Clear them with `sudo setcap -r /usr/bin/punktfunk-host` and restart the host"
|
||||
),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The permitted-capability mask out of a `/proc/<pid>/status` body, or `None` if the field is
|
||||
/// absent/unparseable. The kernel prints it as a tab-separated 16-digit hex word with no `0x`
|
||||
/// (`CapPrm:\t0000000000800000` = CAP_SYS_NICE), which is what the split-and-radix-16 parse below
|
||||
/// expects — split out from [`capability_denial_hint`] purely so that shape is testable without a
|
||||
/// capability-carrying process to point at.
|
||||
fn permitted_caps_from_status(status: &str) -> Option<u64> {
|
||||
let field = status.lines().find(|l| l.starts_with("CapPrm:"))?;
|
||||
u64::from_str_radix(field.split_whitespace().nth(1)?, 16).ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod capability_hint_tests {
|
||||
use super::*;
|
||||
|
||||
/// Verbatim from a `cap_sys_nice=ep` process on CachyOS — the case that broke 0.26.0-1.
|
||||
const CAPPED: &str = "Name:\tpunktfunk-host\nUid:\t1000\t1000\t1000\t1000\nCapPrm:\t0000000000800000\nCapEff:\t0000000000800000\n";
|
||||
/// ...and from the same binary with no capability, where the hint must stay silent.
|
||||
const CLEAN: &str = "Name:\tpunktfunk-host\nUid:\t1000\t1000\t1000\t1000\nCapPrm:\t0000000000000000\nCapEff:\t0000000000000000\n";
|
||||
|
||||
#[test]
|
||||
fn parses_the_kernels_permitted_mask() {
|
||||
assert_eq!(permitted_caps_from_status(CAPPED), Some(0x0080_0000));
|
||||
assert_eq!(permitted_caps_from_status(CLEAN), Some(0));
|
||||
// CapPrm is not guaranteed present (older/again-different kernels): stay quiet, never panic.
|
||||
assert_eq!(permitted_caps_from_status("Name:\tx\n"), None);
|
||||
assert_eq!(permitted_caps_from_status("CapPrm:\tzzzz\n"), None);
|
||||
assert_eq!(permitted_caps_from_status("CapPrm:\n"), None);
|
||||
}
|
||||
|
||||
/// A capability-free host must not append the hint — the message it decorates is also printed
|
||||
/// on genuinely missing `.desktop` files, and a spurious "you have capabilities" line would
|
||||
/// send the reader chasing a setcap that was never there. The test process has no capabilities.
|
||||
#[test]
|
||||
fn silent_without_capabilities() {
|
||||
assert_eq!(capability_denial_hint(), "");
|
||||
}
|
||||
}
|
||||
|
||||
/// Readiness probe: connect to the KWin Wayland socket, roundtrip the registry, and confirm
|
||||
/// the privileged `zkde_screencast` global is actually advertised. This is exactly what
|
||||
/// [`run`] needs before it can create a virtual output, so a session-bringup script can poll
|
||||
@@ -1090,7 +1161,8 @@ pub fn probe() -> Result<()> {
|
||||
it on the host's .desktop X-KDE-Wayland-Interfaces (install \
|
||||
io.unom.Punktfunk.Host.desktop with Exec=/usr/bin/punktfunk-host, then re-login so KWin \
|
||||
re-reads it — the grant is cached per-exe on first connect), or set \
|
||||
KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 for the headless test; needs KWin ≥ 6.5.6"
|
||||
KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 for the headless test; needs KWin ≥ 6.5.6{}",
|
||||
capability_denial_hint()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
@@ -1134,7 +1206,9 @@ fn run_existing(
|
||||
anyhow!(
|
||||
"KWin does not expose zkde_screencast_unstable_v1 to this client — install the host's \
|
||||
.desktop (io.unom.Punktfunk.Host.desktop, X-KDE-Wayland-Interfaces) and re-login so \
|
||||
KWin authorizes it, or run KWin with KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 (headless test)"
|
||||
KWin authorizes it, or run KWin with KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 (headless \
|
||||
test){}",
|
||||
capability_denial_hint()
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -1223,7 +1297,9 @@ fn run(
|
||||
anyhow!(
|
||||
"KWin does not expose zkde_screencast_unstable_v1 to this client — install the host's \
|
||||
.desktop (io.unom.Punktfunk.Host.desktop, X-KDE-Wayland-Interfaces) and re-login so \
|
||||
KWin authorizes it, or run KWin with KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 (headless test)"
|
||||
KWin authorizes it, or run KWin with KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 (headless \
|
||||
test){}",
|
||||
capability_denial_hint()
|
||||
)
|
||||
})?;
|
||||
|
||||
|
||||
@@ -195,6 +195,16 @@ bash /usr/share/punktfunk/bazzite/kde-desktop-setup.sh
|
||||
|
||||
Gaming Mode needs none of this — it auto-attaches.
|
||||
|
||||
> **Version 0.26.0-1 cannot stream the Desktop, and nothing on this page fixes it.** Every session
|
||||
> ends with `KWin does not expose zkde_screencast_unstable_v1 to this client`. That image gave the
|
||||
> host binary the `CAP_SYS_NICE` capability, and Linux will not let KWin read the `/proc/<pid>/exe`
|
||||
> it uses to work out which program is asking — so the grant above can never match, however
|
||||
> correctly it is installed. Reinstalling does not help: it looks like a missing `.desktop` file and
|
||||
> is not one. The image's `/usr` is read-only, so it cannot be repaired on the box — take the next
|
||||
> image with `sudo punktfunk-sysext update`, then
|
||||
> `systemctl --user restart punktfunk-host`. Gaming Mode is unaffected; gamescope has no such
|
||||
> identity check.
|
||||
|
||||
## Run as an always-on host
|
||||
|
||||
Bazzite hosts are typically headless. Enable the host service and linger so it starts at boot — see
|
||||
|
||||
@@ -241,7 +241,7 @@ notes for context.
|
||||
| `PUNKTFUNK_NVENC_SPLIT_ARBITRATE` | `1` | Opt-in: let the host change its split-encode decision **live**, mid-session, as the pixel rate moves, instead of only choosing once at session start. Currently wired on the Linux direct-NVENC path. Only interesting alongside `PUNKTFUNK_SPLIT_ENCODE=auto` at very high pixel rates. |
|
||||
| `PUNKTFUNK_GPU_PRIORITY_CLASS` | `off` · `normal` · `high` · `realtime` · `auto` | **(Windows)** GPU scheduling priority for capture/encode under a GPU-saturating game. Default `auto` (starts `high`, upgrades to `realtime` when it's safe — e.g. HAGS off); `high` pins the static pre-gate behaviour; `realtime` is the strongest lever but can freeze NVENC on some setups. |
|
||||
| `PUNKTFUNK_IDD_DEPTH` | `N` (default `2`) | **(Windows)** IDD-push pipeline depth. `1` cuts latency once GPU priority is raised; higher smooths a contended GPU. |
|
||||
| `PYROWAVE_QUEUE_PRIORITY` | `realtime` *(default)* · `high` · `off` | [PyroWave](/docs/pyrowave) sessions only. PyroWave encodes on the same GPU shader cores a game uses, so a demanding game can starve it and the frame rate drops. This asks the driver to schedule the encode ahead of the game. `realtime` tries the strongest class and falls back to `high`; `high` asks only for the middle one; `off` disables the request. A driver that refuses simply encodes at normal priority — it can never stop a session starting. The packages grant the host the `CAP_SYS_NICE` capability this needs; on a host built or installed by hand it will be refused, and the host says so once at session start. Set `off` if you see the desktop stutter while streaming. |
|
||||
| `PYROWAVE_QUEUE_PRIORITY` | `realtime` *(default)* · `high` · `off` | [PyroWave](/docs/pyrowave) sessions only. PyroWave encodes on the same GPU shader cores a game uses, so a demanding game can starve it and the frame rate drops. This asks the driver to schedule the encode ahead of the game. `realtime` tries the strongest class and falls back to `high`; `high` asks only for the middle one; `off` disables the request. A driver that refuses simply encodes at normal priority — it can never stop a session starting. This needs the `CAP_SYS_NICE` capability, which the packages deliberately do **not** grant: a host holding a capability cannot be identified by KWin and loses desktop streaming entirely (see [Running as a service](/docs/running-as-a-service#gpu-scheduling-priority)). The request is therefore refused on a stock install and the host says so once at session start. Set `off` if you see the desktop stutter while streaming. |
|
||||
|
||||
## Diagnostics
|
||||
|
||||
|
||||
@@ -207,33 +207,41 @@ a Windows host, run `punktfunk-host service status` from an elevated prompt on t
|
||||
|
||||
## 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 host binary carries **no Linux capability**, and on a KDE desktop it must not.
|
||||
|
||||
The [PyroWave](/docs/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`. Version 0.26.0-1 granted it for that reason — and it broke desktop streaming on
|
||||
every KDE box, so 0.26.0-2 takes it away again. The other codecs use a separate video engine on the
|
||||
GPU and were never affected.
|
||||
|
||||
`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.
|
||||
The two cannot coexist. To hand the host its virtual display, KWin first has to work out *which*
|
||||
program is asking, which it does by reading the connecting process's `/proc/<pid>/exe` and matching
|
||||
it against the `.desktop` file the packages install. Linux refuses that read for any process holding
|
||||
a capability the reader does not also hold — and KWin holds none. So a host with `CAP_SYS_NICE` is a
|
||||
host KWin cannot identify, and every session fails with:
|
||||
|
||||
To check, or to take it away:
|
||||
|
||||
```sh
|
||||
getcap /usr/bin/punktfunk-host # shows cap_sys_nice=ep when granted
|
||||
sudo setcap -r /usr/bin/punktfunk-host # remove it; streaming still works
|
||||
```
|
||||
KWin virtual output failed: KWin does not expose zkde_screencast_unstable_v1 to this client
|
||||
```
|
||||
|
||||
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.
|
||||
which looks exactly like a missing `.desktop` file and cannot be fixed by reinstalling. Moving the
|
||||
grant into the systemd unit does not help either — same capability, same refused read.
|
||||
|
||||
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.
|
||||
If you are on 0.26.0-1, update. On the Bazzite image the `/usr` is read-only, so the only repair is
|
||||
the next image (`sudo punktfunk-sysext update`). Elsewhere you can clear it by hand:
|
||||
|
||||
```sh
|
||||
getcap /usr/bin/punktfunk-host # prints nothing when correct
|
||||
sudo setcap -r /usr/bin/punktfunk-host # clear it, then restart the host
|
||||
```
|
||||
|
||||
Losing the capability costs frame pacing under a GPU-bound game and nothing else — the host asks for
|
||||
the elevated priority, is refused, and encodes at the normal one. `PYROWAVE_QUEUE_PRIORITY=off`
|
||||
stops it asking at all. If you stream only with gamescope (Steam Gaming Mode) you can grant the
|
||||
capability yourself and keep the pacing, at the cost of desktop streaming; gamescope has no such
|
||||
identity check.
|
||||
|
||||
## Stopping and removing
|
||||
|
||||
|
||||
@@ -173,8 +173,10 @@ systemctl --user enable --now punktfunk-host # the user unit is now under /u
|
||||
```
|
||||
The udev rule, sysctl, and systemd **user** unit all live under `/usr/lib`, so the merged sysext
|
||||
exposes them. `systemd-sysext refresh` re-merges after a reboot. (One HDR nuance of the sysext
|
||||
path: file capabilities don't survive it, so gamescope runs without `CAP_SYS_NICE` — everything
|
||||
works, frame pacing is marginally worse than the pacman install, whose `.install` sets the cap.)
|
||||
path: the image ships gamescope without `CAP_SYS_NICE`, so its frame pacing is marginally worse —
|
||||
everything works. Note the host binary carries no capability on *either* path, deliberately: one
|
||||
would make the host unidentifiable to KWin and break desktop streaming, see
|
||||
[Running as a service](https://punktfunk.io/docs/running-as-a-service#gpu-scheduling-priority).)
|
||||
|
||||
## Steam Deck — the client (what the Decky plugin launches)
|
||||
|
||||
|
||||
@@ -14,9 +14,17 @@
|
||||
# instead of 8-bit SDR (the host prefers that name on PATH and attempts HDR by default). Mirrors
|
||||
# the Bazzite image's fold-in, including the honesty check: the binary is verified by executing
|
||||
# its `+pfhdr` banner, never trusted by filename. Omit it and the image is exactly what it was —
|
||||
# the host then stays SDR on that backend, by design. (No CAP_SYS_NICE inside the image: file
|
||||
# capabilities don't survive this squashfs path — gamescope runs without it, pacing slightly
|
||||
# worse, same as the Bazzite sysext.)
|
||||
# the host then stays SDR on that backend, by design.
|
||||
#
|
||||
# No CAP_SYS_NICE inside the image, for either binary. ⚠ NOT because capabilities are lost on the
|
||||
# way in — that was this comment's earlier claim and it is false: mksquashfs records
|
||||
# security.capability, and the published Bazzite 0.26.0-1 image really did carry `cap_sys_nice=ep`
|
||||
# on usr/bin/punktfunk-host. It is left out on purpose. A capability on the HOST binary makes it
|
||||
# unidentifiable to KWin (which resolves a client's /proc/<pid>/exe to match it against a .desktop,
|
||||
# and cannot read it for a capability-carrying process) and kills every Desktop-mode session — see
|
||||
# packaging/bazzite/build-sysext.sh, which now hard-fails if one is staged. `punktfunk-gamescope`
|
||||
# is a compositor, not a KWin client, so it is unaffected by that rule and simply runs without the
|
||||
# capability here, pacing slightly worse.
|
||||
set -euo pipefail
|
||||
|
||||
GAMESCOPE=""
|
||||
|
||||
@@ -12,33 +12,48 @@ _ensure_punktfunk_group() {
|
||||
getent group punktfunk >/dev/null 2>&1 || groupadd --system punktfunk 2>/dev/null || true
|
||||
}
|
||||
|
||||
# CAP_SYS_NICE on the host binary — the GPU-scheduling grant.
|
||||
# NO capability on the host binary — and an active removal of the one 0.26.0-1 granted.
|
||||
#
|
||||
# WHY: PyroWave encodes on the GPU's shader cores, so a GPU-bound game starves it (measured: the
|
||||
# encode dispatch goes from ~2 ms to 15-18 ms at 95 % game load). The fix is an elevated
|
||||
# global-priority Vulkan queue, which the driver gates on CAP_SYS_NICE — measured 2026-08-08 on an
|
||||
# RTX 5070 Ti: WITHOUT the capability every priority class is refused, WITH it the encoder is
|
||||
# granted REALTIME on the first attempt. RADV is the same. Without this line the knob exists and
|
||||
# does nothing. Same capability, same mechanism, as our gamescope package sets on its own binary.
|
||||
# 0.26.0-1 ran `setcap cap_sys_nice=ep` here, to let the encoder open an elevated global-priority
|
||||
# Vulkan queue (PyroWave shares the GPU's shader cores with the game; measured 2026-08-08 on an
|
||||
# RTX 5070 Ti, the encode dispatch goes ~2 ms -> 15-18 ms at 95 % game load without it). That grant
|
||||
# BROKE DESKTOP STREAMING ON EVERY KDE BOX, and it cannot be made to work — the two are mutually
|
||||
# exclusive at the kernel level:
|
||||
#
|
||||
# NARROW: CAP_SYS_NICE only permits raising scheduling priority (nice/ioprio/affinity/RT class). It
|
||||
# grants no filesystem, network or user-switching privilege, and it is NOT setuid.
|
||||
# KWin hands out its restricted Wayland protocols (zkde_screencast_unstable_v1, which mints our
|
||||
# virtual output, and org_kde_kwin_fake_input, which injects input) only to a client it can
|
||||
# IDENTIFY, by resolving that client's /proc/<pid>/exe and matching it against an installed
|
||||
# .desktop's Exec= (ours is io.unom.Punktfunk.Host.desktop). The kernel refuses that readlink to
|
||||
# any reader whose effective set is not a superset of the target's PERMITTED set
|
||||
# (cap_ptrace_access_check), and KWin holds no capabilities. So the moment this binary carries a
|
||||
# capability it becomes unidentifiable: KWin's executablePath() is empty, nothing matches, the
|
||||
# globals are never advertised, and every session dies with
|
||||
# "KWin does not expose zkde_screencast_unstable_v1 to this client" after 8 retries — while
|
||||
# looking exactly like a missing or wrong .desktop file.
|
||||
#
|
||||
# TWO CONSEQUENCES worth knowing before you debug something odd on this host:
|
||||
# * a file capability makes the process AT_SECURE, so the dynamic loader IGNORES LD_LIBRARY_PATH
|
||||
# and LD_PRELOAD for it. A library-path shim that used to work will silently stop.
|
||||
# * core dumps are suppressed for capability-carrying binaries by default (fs.suid_dumpable).
|
||||
# Verified on CachyOS (kernel 7.1.6), same-uid reader, cap_sys_nice=ep on the target:
|
||||
# no capability .............................. readlink /proc/<pid>/exe OK
|
||||
# capability ................................. EPERM
|
||||
# capability + prctl(PR_SET_DUMPABLE, 1) ..... EPERM <- dumpable is NOT the gate
|
||||
# capability dropped + PR_SET_DUMPABLE(1) .... OK <- only a capability-free process works
|
||||
#
|
||||
# Never fails the install: a box without libcap, or a filesystem that cannot store capabilities
|
||||
# (some overlay/NFS setups), just runs at default priority exactly as before.
|
||||
_grant_sched_capability() {
|
||||
setcap 'cap_sys_nice=ep' usr/bin/punktfunk-host 2>/dev/null || true
|
||||
# The third row also rules out the obvious "move it to the systemd unit": AmbientCapabilities= puts
|
||||
# CAP_SYS_NICE in exactly the same permitted set and fails identically. Nothing short of not having
|
||||
# the capability restores identification, so the host does not get one. The encoder already walks
|
||||
# REALTIME -> HIGH -> default when the class is refused (pf-zerocopy vulkan.rs), so this costs
|
||||
# pacing under a GPU-bound game and nothing else — 0.25.0's behaviour exactly.
|
||||
#
|
||||
# The removal below heals boxes that ran 0.26.0-1's scriptlet. A pacman upgrade writes a new inode
|
||||
# and file capabilities do not survive that, so this is belt-and-braces for reinstall/downgrade
|
||||
# paths — cheap, and the failure it prevents is an 8-retry session death with a misleading message.
|
||||
_revoke_sched_capability() {
|
||||
setcap -r usr/bin/punktfunk-host 2>/dev/null || true
|
||||
}
|
||||
|
||||
post_install() {
|
||||
_ensure_update_group
|
||||
_ensure_punktfunk_group
|
||||
_grant_sched_capability
|
||||
_revoke_sched_capability
|
||||
udevadm control --reload-rules 2>/dev/null || true
|
||||
udevadm trigger --subsystem-match=misc 2>/dev/null || true
|
||||
# Apply the UDP socket-buffer tuning now (also auto-applied at boot by systemd-sysctl).
|
||||
@@ -97,8 +112,8 @@ post_upgrade() {
|
||||
# root-only, and the virtual Steam Deck pad silently unable to attach. groupadd is idempotent, so
|
||||
# this is a no-op on boxes that installed fresh.
|
||||
_ensure_punktfunk_group
|
||||
# A replaced binary is a NEW inode — file capabilities do not survive the upgrade, so re-grant.
|
||||
_grant_sched_capability
|
||||
# Strip the cap_sys_nice 0.26.0-1 granted: it makes the host unidentifiable to KWin (see above).
|
||||
_revoke_sched_capability
|
||||
udevadm control --reload-rules 2>/dev/null || true
|
||||
sysctl -p /usr/lib/sysctl.d/99-punktfunk-net.conf >/dev/null 2>&1 || true
|
||||
_warn_stale_firewall_ports
|
||||
|
||||
@@ -421,11 +421,23 @@ bash /usr/share/punktfunk/bazzite/kde-desktop-setup.sh
|
||||
# then log out + back into the KDE Desktop session once (or reboot) so KWin restarts with the flag
|
||||
```
|
||||
|
||||
That writes `~/.config/environment.d/10-punktfunk-kwin.conf`
|
||||
(`KWIN_WAYLAND_NO_PERMISSION_CHECKS=1`) and seeds the `kde-authorized` RemoteDesktop grant into
|
||||
`~/.local/share/flatpak/db/`. Gaming Mode is unaffected. To connect from Desktop Mode, switch to it
|
||||
(Steam → Power → Switch to Desktop), then connect the client; switching **mid-stream** requires a
|
||||
reconnect (the host resolves the backend per connect).
|
||||
That seeds the `kde-authorized` RemoteDesktop grant into `~/.local/share/flatpak/db/` — the input
|
||||
half. The **video** half needs no session-wide override: the image ships
|
||||
`io.unom.Punktfunk.Host.desktop`, whose `X-KDE-Wayland-Interfaces` grants the host KWin's
|
||||
`zkde_screencast` protocol on a normal Plasma login (least-privilege — only this binary, only that
|
||||
interface). Older versions of the script wrote a session-wide
|
||||
`KWIN_WAYLAND_NO_PERMISSION_CHECKS=1` into `~/.config/environment.d/10-punktfunk-kwin.conf`; it now
|
||||
*removes* that file as an over-broad leftover. Gaming Mode is unaffected. To connect from Desktop
|
||||
Mode, switch to it (Steam → Power → Switch to Desktop), then connect the client; switching
|
||||
**mid-stream** requires a reconnect (the host resolves the backend per connect).
|
||||
|
||||
> **On 0.26.0-1 specifically, Desktop mode is broken and no amount of this setup fixes it.** That
|
||||
> image shipped `cap_sys_nice=ep` on `/usr/bin/punktfunk-host`, and a capability-carrying process is
|
||||
> one KWin cannot identify (it resolves `/proc/<pid>/exe` to match the `.desktop`, and the kernel
|
||||
> refuses that read), so the session dies with `KWin does not expose zkde_screencast_unstable_v1 to
|
||||
> this client`. A merged sysext's `/usr` is read-only, so it cannot be repaired in place — take the
|
||||
> next image (`sudo punktfunk-sysext update`). `KWIN_WAYLAND_NO_PERMISSION_CHECKS=1` works around it
|
||||
> meanwhile by disabling the check that needs the identification.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -130,28 +130,35 @@ SYSEXT_VERSION_ID=$PF_VR
|
||||
EXTENSION_RELOAD_MANAGER=1
|
||||
EOF
|
||||
|
||||
# CAP_SYS_NICE on the host binary — the GPU-scheduling grant. PyroWave encodes on the GPU shader
|
||||
# cores a game saturates, and the driver gates the elevated global-priority Vulkan queue that fixes
|
||||
# it on this capability (measured 2026-08-08 on an RTX 5070 Ti: refused without it, granted REALTIME
|
||||
# with it; RADV the same). Narrow — scheduling priority only, no filesystem/network privilege, not
|
||||
# setuid.
|
||||
# NO CAP_SYS_NICE in the image — and an assertion that none crept back in.
|
||||
#
|
||||
# It has to be applied HERE, not in the merge hook: a merged sysext's /usr is a read-only squashfs,
|
||||
# so nothing can setcap it afterwards. And it cannot ride in from the RPM either — the spec declares
|
||||
# it with %caps, but rpm stores capabilities in its own header and `rpm2cpio | cpio` carries only
|
||||
# the payload, so the staged file arrives with no capability at all. mksquashfs DOES record
|
||||
# security.capability (only security.selinux is excluded below), so a setcap on the staging tree is
|
||||
# what ends up in the image.
|
||||
# 0.26.0-1 setcap'd the staged binary here for the GPU-priority lever. mksquashfs records
|
||||
# security.capability, so the capability really did ship: verified by mounting the published
|
||||
# punktfunk-0.26.0-1-x86-64.raw, where `getcap usr/bin/punktfunk-host` reports `cap_sys_nice=ep`.
|
||||
# That broke desktop streaming on every Bazzite KDE box, field-reported as
|
||||
# "KWin does not expose zkde_screencast_unstable_v1 to this client".
|
||||
#
|
||||
# Needs CAP_SETFCAP, i.e. root (or fakeroot) — a plain-user CI build cannot do it. That is not fatal:
|
||||
# the image just ships as it does today and the encode runs at default GPU priority, so warn and
|
||||
# carry on rather than fail a release build over a performance lever.
|
||||
if [ -f "$STAGE/usr/bin/punktfunk-host" ]; then
|
||||
if setcap 'cap_sys_nice=ep' "$STAGE/usr/bin/punktfunk-host" 2>/dev/null; then
|
||||
echo "granted CAP_SYS_NICE to usr/bin/punktfunk-host (GPU-priority lever active)"
|
||||
else
|
||||
echo "WARNING: could not setcap CAP_SYS_NICE (need root/CAP_SETFCAP) — the image will ship" >&2
|
||||
echo " without it and PyroWave will encode at default GPU priority." >&2
|
||||
# KWin advertises its restricted protocols (zkde_screencast_unstable_v1 for the virtual output,
|
||||
# org_kde_kwin_fake_input for input) only to a client it can IDENTIFY, by resolving that client's
|
||||
# /proc/<pid>/exe and matching it against an installed .desktop's Exec= — the image ships
|
||||
# usr/share/applications/io.unom.Punktfunk.Host.desktop for exactly that. The kernel refuses that
|
||||
# readlink to any reader whose effective set is not a superset of the target's PERMITTED set
|
||||
# (cap_ptrace_access_check), and KWin holds no capabilities. So a capability in this image makes the
|
||||
# host unidentifiable and every Desktop-mode session dies. Full matrix, including why neither
|
||||
# prctl(PR_SET_DUMPABLE, 1) nor systemd AmbientCapabilities= rescues it, in
|
||||
# packaging/arch/punktfunk-host.install.
|
||||
#
|
||||
# A merged sysext's /usr is a read-only squashfs, so this cannot be repaired on the box — the image
|
||||
# is the only place it can be got right. Assert it rather than trust it: the RPM payload arrives via
|
||||
# `rpm2cpio | cpio`, which carries no capabilities today, but the spec is one `%caps()` away from
|
||||
# changing that and this build would silently bake it in.
|
||||
if [ -f "$STAGE/usr/bin/punktfunk-host" ] && command -v getcap >/dev/null 2>&1; then
|
||||
staged_caps="$(getcap "$STAGE/usr/bin/punktfunk-host" 2>/dev/null || true)"
|
||||
if [ -n "$staged_caps" ]; then
|
||||
echo "ERROR: staged usr/bin/punktfunk-host carries capabilities: $staged_caps" >&2
|
||||
echo " A capability makes the host unidentifiable to KWin and breaks every Desktop-mode" >&2
|
||||
echo " session on a merged image, which cannot be repaired on the box (read-only /usr)." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -294,16 +294,23 @@ if [ "$1" = "configure" ]; then
|
||||
# primitive that must not ride on the group users are told to join for gamepads
|
||||
# (security-review 2026-08-05 M-4).
|
||||
getent group punktfunk >/dev/null 2>&1 || addgroup --system punktfunk 2>/dev/null || true
|
||||
# CAP_SYS_NICE — the GPU-scheduling grant. PyroWave encodes on the shader cores a game
|
||||
# saturates, and the driver gates the elevated global-priority Vulkan queue that fixes it on
|
||||
# this capability: measured 2026-08-08 on an RTX 5070 Ti, WITHOUT it every priority class is
|
||||
# refused and WITH it the encoder is granted REALTIME first try (RADV behaves the same).
|
||||
# Without this line the knob exists and does nothing. Narrow: it permits raising scheduling
|
||||
# priority only — no filesystem, network or user-switching privilege, and no setuid. Note a
|
||||
# capability-carrying binary is AT_SECURE, so the loader ignores LD_LIBRARY_PATH/LD_PRELOAD for
|
||||
# it and core dumps are suppressed by default. Best-effort: a box without libcap, or a
|
||||
# filesystem that cannot store capabilities, just runs at default priority as before.
|
||||
setcap 'cap_sys_nice=ep' /usr/bin/punktfunk-host 2>/dev/null || true
|
||||
# NO capability on the host binary — and an active removal of the one 0.26.0-1 granted here.
|
||||
#
|
||||
# 0.26.0-1 ran `setcap cap_sys_nice=ep` at this point for the GPU-priority lever, and that broke
|
||||
# desktop streaming on every KDE box. KWin advertises its restricted protocols
|
||||
# (zkde_screencast_unstable_v1 for the virtual output, org_kde_kwin_fake_input for input) only
|
||||
# to a client it can IDENTIFY, by resolving that client's /proc/<pid>/exe and matching it
|
||||
# against an installed .desktop's Exec=. The kernel refuses that readlink to any reader whose
|
||||
# effective set is not a superset of the target's PERMITTED set (cap_ptrace_access_check), and
|
||||
# KWin holds no capabilities — so a capability here makes the host unidentifiable and the
|
||||
# session dies with "KWin does not expose zkde_screencast_unstable_v1 to this client". Full
|
||||
# matrix (and why PR_SET_DUMPABLE and AmbientCapabilities= both fail to rescue it) in
|
||||
# packaging/arch/punktfunk-host.install.
|
||||
#
|
||||
# Costs pacing only: pf-zerocopy walks REALTIME -> HIGH -> default when a class is refused.
|
||||
# postinst runs on upgrade too, so this heals boxes that installed 0.26.0-1. `setcap -r` exits
|
||||
# non-zero on a file that has no capability, hence the redirect and `|| true`.
|
||||
setcap -r /usr/bin/punktfunk-host 2>/dev/null || true
|
||||
# Pick up the /dev/uinput rule without a reboot (best-effort, no-op in containers).
|
||||
udevadm control --reload-rules 2>/dev/null || true
|
||||
udevadm trigger --subsystem-match=misc 2>/dev/null || true
|
||||
|
||||
@@ -356,25 +356,25 @@ in
|
||||
allowedUDPPorts = nativeUDP ++ optionals cfg.host.gamestream gamestreamUDP;
|
||||
};
|
||||
|
||||
# CAP_SYS_NICE — the GPU-scheduling grant. PyroWave encodes on the GPU shader cores a game
|
||||
# saturates; the elevated global-priority Vulkan queue that fixes it is gated on this
|
||||
# capability (measured 2026-08-08, RTX 5070 Ti: without it EVERY priority class is refused,
|
||||
# with it the encoder gets REALTIME on the first attempt; RADV behaves the same).
|
||||
# NO CAP_SYS_NICE wrapper here — deliberately. 0.26.0-1 gave the host a
|
||||
# `security.wrappers.punktfunk-host` carrying `cap_sys_nice=ep` for the GPU-priority lever,
|
||||
# and that broke desktop streaming on every KDE box.
|
||||
#
|
||||
# NixOS cannot `setcap` a store path — it is read-only and shared — so this goes through
|
||||
# `security.wrappers`, which builds a small setcap'd wrapper in /run/wrappers/bin. The unit's
|
||||
# ExecStart points at the wrapper below; everything else about the host is unchanged.
|
||||
# KWin advertises its restricted Wayland protocols (zkde_screencast_unstable_v1 for the
|
||||
# virtual output, org_kde_kwin_fake_input for input) only to a client it can IDENTIFY, by
|
||||
# resolving that client's /proc/<pid>/exe and matching it against an installed .desktop's
|
||||
# Exec= (packages.nix substitutes ours to the store path). The kernel refuses that readlink to
|
||||
# any reader whose effective set is not a superset of the target's PERMITTED set
|
||||
# (cap_ptrace_access_check), and KWin holds no capabilities.
|
||||
#
|
||||
# Narrow: CAP_SYS_NICE permits raising scheduling priority only — no filesystem, network or
|
||||
# user-switching privilege, and the wrapper is capability-based, NOT setuid. Two side effects
|
||||
# to know: the wrapped binary is AT_SECURE (the loader ignores LD_LIBRARY_PATH/LD_PRELOAD for
|
||||
# it) and core dumps are suppressed by default.
|
||||
security.wrappers.punktfunk-host = {
|
||||
source = "${cfg.host.package}/bin/punktfunk-host";
|
||||
capabilities = "cap_sys_nice=ep";
|
||||
owner = "root";
|
||||
group = "root";
|
||||
};
|
||||
# A NixOS wrapper does not dodge this. It raises the capability into its AMBIENT set before
|
||||
# exec'ing the store binary, precisely so the capability survives — which lands CAP_SYS_NICE
|
||||
# in the exec'd process's permitted set and fails the readlink identically. Measured: an
|
||||
# ambient-only grant (dumpable=1, CapPrm set) is refused exactly like a file capability. See
|
||||
# packaging/arch/punktfunk-host.install for the full matrix.
|
||||
#
|
||||
# Costs pacing only: pf-zerocopy walks REALTIME -> HIGH -> default when a priority class is
|
||||
# refused, and pf-frame's thread nice is a best-effort no-op — 0.25.0's behaviour exactly.
|
||||
|
||||
systemd.user.services.punktfunk-host = {
|
||||
description = "punktfunk GameStream + punktfunk/1 streaming host";
|
||||
@@ -394,12 +394,12 @@ in
|
||||
# PUNKTFUNK_GAMESCOPE_BIN so an operator's own override of that env still wins.
|
||||
++ optional cfg.host.gamescopeHdr cfg.host.gamescopePackage;
|
||||
serviceConfig = {
|
||||
# Through the wrapper (see `security.wrappers.punktfunk-host` above), NOT the store path
|
||||
# directly — the store path carries no capability and the GPU-priority lever would be
|
||||
# inert. `config.security.wrapperDir` rather than a hard-coded /run/wrappers/bin so an
|
||||
# operator who has moved it is still correct.
|
||||
# The store path DIRECTLY — not a capability wrapper. /proc/<pid>/exe then resolves to the
|
||||
# very path packages.nix substituted into io.unom.Punktfunk.Host.desktop's Exec=, which is
|
||||
# what lets KWin identify the host and grant it the screencast/fake-input protocols (see
|
||||
# the note above the firewall block).
|
||||
ExecStart =
|
||||
"${config.security.wrapperDir}/punktfunk-host serve" + optionalString cfg.host.gamestream " --gamestream";
|
||||
"${cfg.host.package}/bin/punktfunk-host serve" + optionalString cfg.host.gamestream " --gamestream";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 2;
|
||||
EnvironmentFile =
|
||||
|
||||
@@ -477,15 +477,28 @@ install -Dm0644 scripts/punktfunk-scripting.service %{buildroot}%{_userunitdir}/
|
||||
%files
|
||||
%license LICENSE-MIT LICENSE-APACHE THIRD-PARTY-NOTICES.txt
|
||||
%doc README.md packaging/README.md
|
||||
# CAP_SYS_NICE — the GPU-scheduling grant, declared the RPM-native way so rpm applies it at
|
||||
# install, restores it on upgrade, and VERIFIES it (a plain %post setcap does none of those).
|
||||
# PyroWave encodes on the shader cores a game saturates; the elevated global-priority Vulkan queue
|
||||
# that fixes it is gated on this capability. Measured 2026-08-08 on an RTX 5070 Ti: without it
|
||||
# every priority class is refused, with it the encoder gets REALTIME first try (RADV the same).
|
||||
# Narrow — scheduling priority only, no filesystem/network/user-switching privilege, not setuid.
|
||||
# Consequences: the binary becomes AT_SECURE, so the loader ignores LD_LIBRARY_PATH/LD_PRELOAD for
|
||||
# it, and core dumps are suppressed by default.
|
||||
%caps(cap_sys_nice=ep) %{_bindir}/punktfunk-host
|
||||
# NO %caps() on the host binary. 0.26.0-1 declared `%caps(cap_sys_nice=ep)` here for the
|
||||
# GPU-priority lever and that BROKE DESKTOP STREAMING ON EVERY KDE BOX — on Fedora and, via
|
||||
# rpm-ostree layering, on Bazzite, where it was field-reported as
|
||||
# "KWin does not expose zkde_screencast_unstable_v1 to this client".
|
||||
#
|
||||
# KWin hands out its restricted Wayland protocols (zkde_screencast_unstable_v1 for the virtual
|
||||
# output, org_kde_kwin_fake_input for input) only to a client it can IDENTIFY, by resolving that
|
||||
# client's /proc/<pid>/exe and matching it against an installed .desktop's Exec= — ours is the
|
||||
# io.unom.Punktfunk.Host.desktop installed below. The kernel refuses that readlink to any reader
|
||||
# whose effective set is not a superset of the target's PERMITTED set (cap_ptrace_access_check),
|
||||
# and KWin holds no capabilities. So a capability here makes the host unidentifiable: KWin's
|
||||
# executablePath() is empty, no .desktop can match, and the globals are never advertised.
|
||||
# Measured on kernel 7.1.6 — see packaging/arch/punktfunk-host.install for the full matrix, incl.
|
||||
# why neither prctl(PR_SET_DUMPABLE, 1) nor systemd AmbientCapabilities= rescues it.
|
||||
#
|
||||
# The cost of not having it is pacing only: pf-zerocopy walks REALTIME -> HIGH -> default when a
|
||||
# priority class is refused, and pf-frame's thread nice is a best-effort no-op. That is exactly
|
||||
# how 0.25.0 behaved, which is the behaviour that worked.
|
||||
#
|
||||
# rpm applies file capabilities from package metadata, so a package built WITHOUT %caps() installs
|
||||
# the binary with none and an upgrade from 0.26.0-1 clears it — no scriptlet needed.
|
||||
%{_bindir}/punktfunk-host
|
||||
%{_bindir}/punktfunk-tray
|
||||
%{_udevrulesdir}/60-punktfunk.rules
|
||||
%dir %{_libexecdir}/punktfunk
|
||||
|
||||
@@ -344,24 +344,23 @@ if [ "$SUDO_OK" = 1 ]; then
|
||||
warn "(everything else works; the pad arrives as a generic Xbox 360 controller). By hand:"
|
||||
warn " sudo groupadd --system punktfunk; sudo usermod -aG punktfunk $USER"
|
||||
fi
|
||||
# CAP_SYS_NICE on the host binary — the GPU-scheduling grant, and the Deck is the box that
|
||||
# needs it most: a Van Gogh APU shares one small GPU between the game and PyroWave's encode
|
||||
# dispatch. The driver gates the elevated global-priority Vulkan queue on this capability
|
||||
# (measured 2026-08-08 on an RTX 5070 Ti: refused without it, granted REALTIME with it; RADV
|
||||
# behaves the same), so without this the knob exists and does nothing.
|
||||
# NO CAP_SYS_NICE on the host binary — and a removal of the one 0.26.0-1 granted here.
|
||||
#
|
||||
# The binary lives under $HOME, not /usr — so unlike the /etc drop-ins above this survives a
|
||||
# SteamOS A/B update on its own and needs no atomic-keep entry. It DOES need re-applying after
|
||||
# every rebuild, because a fresh binary is a new inode; re-running this installer does that.
|
||||
# 0.26.0-1 setcap'd this binary for the GPU-priority lever, which on a Van Gogh APU is a real
|
||||
# win. It also broke Desktop-mode streaming outright. Just above, this installer writes
|
||||
# ~/.local/share/applications/io.unom.Punktfunk.Host.desktop with Exec=$BIN so KWin will grant
|
||||
# the host its restricted protocols — and KWin makes that grant by resolving the client's
|
||||
# /proc/<pid>/exe and matching it against that Exec=. The kernel refuses that readlink to any
|
||||
# reader whose effective set is not a superset of the target's PERMITTED set
|
||||
# (cap_ptrace_access_check), and KWin holds no capabilities. So the capability silently voided
|
||||
# the .desktop written six lines earlier, and every Desktop-mode session died with
|
||||
# "KWin does not expose zkde_screencast_unstable_v1 to this client". Gaming Mode (gamescope) is
|
||||
# unaffected — it has no such gate. Full matrix in packaging/arch/punktfunk-host.install.
|
||||
#
|
||||
# Narrow (scheduling priority only, no filesystem/network privilege, not setuid) and
|
||||
# best-effort — a failure just means the encode runs at default priority as it does today.
|
||||
# Costs pacing only: pf-zerocopy walks REALTIME -> HIGH -> default when a class is refused.
|
||||
# `setcap -r` exits non-zero on a file that has no capability, hence the redirect.
|
||||
if [ -x "$BIN" ]; then
|
||||
if sudo setcap 'cap_sys_nice=ep' "$BIN" 2>/dev/null; then
|
||||
ok "granted CAP_SYS_NICE (PyroWave encode can outrank a GPU-bound game)"
|
||||
else
|
||||
warn "could not grant CAP_SYS_NICE to $BIN — PyroWave encode stays at default GPU priority"
|
||||
fi
|
||||
sudo setcap -r "$BIN" 2>/dev/null || true
|
||||
fi
|
||||
# SteamOS A/B updates rebuild /etc and DROP everything not on Valve's keep list — verified
|
||||
# live: an OS update stripped the udev rule + vhci autoload + UDP sysctl (gamepads silently
|
||||
|
||||
Reference in New Issue
Block a user