docs(env): stop teaching the compositor pin + uid-1000 anchors in starters
ci / docs-site (push) Successful in 54s
ci / web (push) Successful in 59s
apple / swift (push) Successful in 1m21s
decky / build-publish (push) Successful in 28s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 25s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 14s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
ci / bench (push) Successful in 6m23s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 4m43s
apple / screenshots (push) Successful in 6m18s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m9s
arch / build-publish (push) Successful in 13m44s
deb / build-publish (push) Successful in 13m48s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7m39s
deb / build-publish-host (push) Successful in 13m11s
docker / deploy-docs (push) Successful in 32s
android / android (push) Successful in 16m23s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m15s
ci / rust (push) Successful in 24m12s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m55s

Field triage (Nobara, Discord): the kde.md starter host.env told desktop
users to set PUNKTFUNK_COMPOSITOR=kwin, which PINS the backend — detect()
short-circuits and the capture-loss rebuild never re-detects — so a
mid-stream switch to Game Mode killed the stream instead of following it.
A follow-up hardcoded XDG_RUNTIME_DIR=/run/user/1000 anchor broke PipeWire
for any non-1000 uid (pw audio connect: Creation failed).

Revamp across every starter/example/reference:
- Desktop starters (kde/gnome/hyprland/sway) shrink to
  PUNKTFUNK_VIDEO_SOURCE=virtual + an explicit warning that pinning
  disables session-following; forcing a backend is CI/appliance-only.
- host.env.example: rewritten around auto-detection; anchors demoted to a
  commented ssh/cron-only block with the uid trap spelled out; the
  gamescope ATTACH/MANAGED knobs documented (previously missing);
  case-sensitivity called out.
- packaging/bazzite/host.env + README: drop the uid-1000 anchors (a
  systemctl --user service inherits/derives them); README's stale
  PUNKTFUNK_COMPOSITOR=gamescope-era template synced to the real one.
- packaging/kde/host.env: loud APPLIANCE-ONLY header (it pins on purpose).
- configuration.md: session-anchors section inverted to "leave unset",
  compositor row states the pin consequence, case-sensitivity note.
- troubleshooting.md: new "session fails right after editing host.env"
  section (case, wrong-uid anchors, stale pin, restart-to-apply).
- gamescope.md/bazzite.md: attach/managed descriptions match current
  behavior (managed is the infra-detected default; attach re-modes a
  box-owned session to the client's resolution).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 22:42:59 +02:00
co-authored by Claude Fable 5
parent bda015b101
commit 6617275387
14 changed files with 189 additions and 137 deletions
+9 -9
View File
@@ -85,11 +85,10 @@ cp /usr/share/punktfunk/host.env.bazzite ~/.config/punktfunk/host.env
The template is deliberately minimal — it does **not** force a compositor, because the host
auto-detects Gaming Mode (gamescope) vs Desktop (KWin) on every connect and follows the switch
mid-stream. The only settings that matter are the session anchors (GPU zero-copy is on by default):
mid-stream. No session anchors are needed either (a user service inherits the right runtime dir).
The only settings that matter (GPU zero-copy is on by default):
```sh
XDG_RUNTIME_DIR=/run/user/1000
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
PUNKTFUNK_VIDEO_SOURCE=virtual
# GPU zero-copy (dmabuf → CUDA → NVENC) is ON by default; auto-falls back to CPU. Set =0 to force CPU.
PUNKTFUNK_GAMESCOPE_ATTACH=1 # Gaming Mode = attach to the box's own session (see below)
@@ -99,12 +98,13 @@ PUNKTFUNK_GAMESCOPE_ATTACH=1 # Gaming Mode = attach to the box's own session
For Gaming Mode there are two models (pick one; the shipped default is **attach**):
- **Attach** (`PUNKTFUNK_GAMESCOPE_ATTACH=1`, the default) — the **box** owns its gamescope session,
the host attaches to whatever's live and never tears it down, and the streamed game-mode resolution
is the box's own gamescope mode. Switching Desktop ↔ Game is rock-solid.
- **Managed** (`PUNKTFUNK_GAMESCOPE_MANAGED=1`, and remove the attach line) — the host launches its
**own** gamescope at the *client's* exact resolution and refresh. Client-mode-following, but there
must be no physical gaming session already running.
- **Attach** (`PUNKTFUNK_GAMESCOPE_ATTACH=1`, the template's default) — the **box** owns its
gamescope session on its own display, and the host attaches to whatever's live without ever
tearing it down (a box-owned autologin session is restarted at the client's resolution on a
mismatch). Switching Desktop ↔ Game is rock-solid.
- **Managed** (`PUNKTFUNK_GAMESCOPE_MANAGED=1`, and remove the attach line) — the host takes the
box's gamescope over and relaunches it **headless** at the *client's* exact resolution and
refresh — Game Mode on the virtual screen — restoring the box on idle.
Full treatment: [Steam / gamescope → Attach vs managed](/docs/gamescope#attach-vs-managed).
+15 -15
View File
@@ -4,7 +4,8 @@ description: Every host.env setting and PUNKTFUNK_* environment variable — com
---
The host reads its settings from **`~/.config/punktfunk/host.env`** (a simple `KEY=value` file, `#`
starts a comment). On Windows the service reads **`%ProgramData%\punktfunk\host.env`** instead. Your
starts a comment; keys are **case-sensitive**`punktfunk_compositor` sets nothing, use the exact
uppercase names). On Windows the service reads **`%ProgramData%\punktfunk\host.env`** instead. Your
[setup guide](/docs/requirements) gives you a starting `host.env` for your desktop; this page is the
full reference for every setting.
@@ -16,25 +17,24 @@ full reference for every setting.
## Session anchors
These tell the host which desktop session to attach to. Your setup guide sets them for you; they're
required when the host runs outside your interactive session (e.g. as a service).
**Leave these unset on a normal setup.** Running as a `systemctl --user` service the host inherits
the correct `XDG_RUNTIME_DIR` from systemd, derives the session bus from it, and **rewrites
`WAYLAND_DISPLAY` / `XDG_CURRENT_DESKTOP` / `XDG_RUNTIME_DIR` / `DBUS_SESSION_BUS_ADDRESS` on every
connect** to follow the active session (Gaming ↔ Desktop) — a value written here can only be
redundant or stale.
| Setting | What it does |
| Setting | When to set it |
|---|---|
| `XDG_RUNTIME_DIR` | Your session's runtime dir (e.g. `/run/user/1000`). Always needed for a service. |
| `DBUS_SESSION_BUS_ADDRESS` | Your session bus (e.g. `unix:path=/run/user/1000/bus`). Always needed for a service. |
| `WAYLAND_DISPLAY` | The Wayland socket of your session (`wayland-0` for a normal desktop, `wayland-kde` for the headless-KDE unit). |
| `XDG_CURRENT_DESKTOP` | Your desktop (`GNOME`, `KDE`). |
On Linux the host **rewrites `WAYLAND_DISPLAY` / `XDG_CURRENT_DESKTOP` / `XDG_RUNTIME_DIR` /
`DBUS_SESSION_BUS_ADDRESS` on every connect** to follow the active session (Gaming ↔ Desktop). Only
`XDG_RUNTIME_DIR` and `DBUS_SESSION_BUS_ADDRESS` need to be pinned as trustworthy anchors.
| `XDG_RUNTIME_DIR` | Only when the host runs **outside** a user service (ssh, cron): `/run/user/<your uid>` — check `id -u`. A copy-pasted `1000` on a box where that isn't your uid points the host at another user's (nonexistent) PipeWire/D-Bus, and **everything** fails (audio `Creation failed`, no capture, clients report the host unreachable). |
| `DBUS_SESSION_BUS_ADDRESS` | Same cases only: `unix:path=/run/user/<your uid>/bus`. Otherwise derived automatically. |
| `WAYLAND_DISPLAY` | Only the dedicated [headless-KDE appliance](/docs/kde#headless-session) (`wayland-kde`, set by its shipped `host.env.kde`). |
| `XDG_CURRENT_DESKTOP` | Same — appliance-only. |
## Core
| Setting | Values | Meaning |
|---|---|---|
| `PUNKTFUNK_COMPOSITOR` | `kwin` · `mutter` · `gamescope` · `wlroots` · `hyprland` (aliases: `kde`/`plasma`, `gnome`, `sway`/`wlr`) | Which backend creates the virtual display. `wlroots` is sway/River; `hyprland` is its own backend. **Leave unset to auto-detect;** set only to force one. |
| `PUNKTFUNK_COMPOSITOR` | `kwin` · `mutter` · `gamescope` · `wlroots` · `hyprland` (aliases: `kde`/`plasma`, `gnome`, `sway`/`wlr`) | Which backend creates the virtual display. `wlroots` is sway/River; `hyprland` is its own backend. **Leave unset.** Setting it **pins** the backend and turns session-following **off** — per connect *and* mid-stream, so a Desktop ↔ Gaming switch kills the stream instead of being followed. For CI/tests and dedicated single-session appliances only. |
| `PUNKTFUNK_VIDEO_SOURCE` | `virtual` · `portal` | `virtual` creates a per-client display at the client's exact mode (the normal choice). `portal` captures an existing monitor instead. |
| `PUNKTFUNK_ZEROCOPY` | `1` · `0` *(default on)* | GPU zero-copy capture→encode (dmabuf → CUDA → NVENC, or D3D11 on Windows). **On by default** — no need to set it; it falls back to a CPU path automatically. Set `0` to force the CPU path. One exception: Windows **Intel/QSV** keeps the CPU path by default until zero-copy is validated on Intel hardware — set `1` to try it there. |
| `PUNKTFUNK_INPUT_BACKEND` | `libei` · `gamescope` · `wlr` · `uinput` | How input is injected. `libei` for GNOME/KDE, `gamescope` for Bazzite/gamescope, `wlr` for Sway/wlroots **and Hyprland**. Auto-detected with the compositor. |
@@ -53,8 +53,8 @@ the full picture (and [Bazzite](/docs/bazzite) for that distro's specifics).
| Setting | Values | Meaning |
|---|---|---|
| `PUNKTFUNK_GAMESCOPE_ATTACH` | `1` | **Attach** model: the box owns its gamescope session (you switch Gaming ↔ Desktop with the Steam UI); the host just captures whatever's live and never tears it down. Rock-solid; streamed resolution is the box's gamescope mode. |
| `PUNKTFUNK_GAMESCOPE_MANAGED` | `1` | **Managed** model: the host tears the box's gamescope down on connect and launches its **own** at the *client's* exact resolution, restoring on idle. Client-mode-following, but doesn't coexist with a box-owned game-mode session. |
| `PUNKTFUNK_GAMESCOPE_ATTACH` | `1` | **Attach** model: the box owns its gamescope session on its own display (you switch Gaming ↔ Desktop with the Steam UI); the host just captures whatever's live and never tears it down. A box-owned autologin session is restarted at the client's resolution on a mismatch; a foreign/bare gamescope streams at its own mode. |
| `PUNKTFUNK_GAMESCOPE_MANAGED` | `1` | **Managed** model (the default where session infra is detected): the host takes the box's gamescope over and relaunches it **headless** at the *client's* exact resolution — Game Mode on the virtual screen — restoring the box on idle. |
| `PUNKTFUNK_GAMESCOPE_SESSION` | `steam` | The host owns a `gamescope-session-plus` (Steam) session at the client's mode (headless appliance; no physical session running). |
| `PUNKTFUNK_GAMESCOPE_NODE` | `auto` · node id | Discover + capture a **running** gamescope's PipeWire node at a fixed mode. Do **not** combine with `SESSION`. |
| `PUNKTFUNK_GAMESCOPE_APP` | command | For an ad-hoc bare-gamescope session, the nested command to run (e.g. `vkcube`). |
+17 -15
View File
@@ -17,18 +17,20 @@ from the install guide for your OS: [Bazzite](/docs/bazzite) or [SteamOS (Host)]
## Attach vs managed
There are two mutually-exclusive models for a gamescope box; pick one. The shipped default is
**attach**.
There are two mutually-exclusive models for a gamescope box; pick one. With **nothing set**, a box
that has gamescope session infrastructure (Bazzite, SteamOS, Nobara) gets **managed**; the
[Bazzite template](/docs/bazzite) ships with **attach** chosen instead.
- **Attach** (`PUNKTFUNK_GAMESCOPE_ATTACH=1`, the default) — the **box** owns its gamescope session
and decides Gaming vs Desktop via the normal Steam UI. The host just attaches to whatever's live
and never tears it down, so switching Desktop ↔ Game is rock-solid and disconnecting leaves the box
where it was. The streamed game-mode resolution is the box's gamescope mode
(`SCREEN_WIDTH/HEIGHT` in `/etc/gamescope-session-plus/sessions.d/steam`), not the client's.
- **Managed** (`PUNKTFUNK_GAMESCOPE_MANAGED=1`, and remove the attach line) — the host tears the
box's gamescope down on connect and launches its **own** at the *client's* exact resolution and
refresh, restoring on idle. Client-mode-following, but it can't coexist with a box-owned game-mode
session, and there must be **no physical gaming session already running**.
- **Attach** (`PUNKTFUNK_GAMESCOPE_ATTACH=1`) — the **box** owns its gamescope session and decides
Gaming vs Desktop via the normal Steam UI. Game Mode stays on the box's own (physical) display;
the host attaches to whatever's live and never tears it down, so switching Desktop ↔ Game is
rock-solid and disconnecting leaves the box where it was. When the session is the box's own
autologin unit, the host restarts it at the **client's** resolution on a mismatch; a foreign or
bare gamescope is streamed at its own mode.
- **Managed** (the infra-detected default; force with `PUNKTFUNK_GAMESCOPE_MANAGED=1`) — the host
takes the box's gamescope session over and relaunches it **headless** at the *client's* exact
resolution and refresh — Game Mode runs on the virtual screen, physical displays drop out of it —
restoring the box on idle after disconnect.
## Session following
@@ -40,8 +42,8 @@ over its own compositor, and re-targets whichever is live on each switch.
## Start the host
On an appliance box (Bazzite, SteamOS) the install guide already enables the host service for you. On
any other distro running a gamescope session, start it from your session — the default attach model
just latches onto whatever gamescope session is live:
any other distro running a gamescope session, just start it — the host auto-detects the live
gamescope session and picks the model for it:
```sh
systemctl --user enable --now punktfunk-host
@@ -56,8 +58,8 @@ a model. See the full [Configuration reference](/docs/configuration) for every o
| Setting | Values | Meaning |
|---|---|---|
| `PUNKTFUNK_GAMESCOPE_ATTACH` | `1` | **Attach** model: the box owns its gamescope session; the host captures whatever's live and never tears it down. Streamed resolution is the box's gamescope mode. The default. |
| `PUNKTFUNK_GAMESCOPE_MANAGED` | `1` | **Managed** model: the host tears the box's gamescope down on connect and launches its own at the client's exact mode, restoring on idle. Doesn't coexist with a box-owned game-mode session. |
| `PUNKTFUNK_GAMESCOPE_ATTACH` | `1` | **Attach** model: the box owns its gamescope session (on its own display); the host captures whatever's live and never tears it down. A box-owned autologin session is restarted at the client's resolution on a mismatch; a foreign/bare gamescope streams at its own mode. |
| `PUNKTFUNK_GAMESCOPE_MANAGED` | `1` | **Managed** model (the default where session infra is detected): the host takes the box's gamescope over and relaunches it headless at the client's exact mode, restoring on idle. |
| `PUNKTFUNK_GAMESCOPE_SESSION` | `steam` | The host owns a `gamescope-session-plus` (Steam) session at the client's mode — a headless appliance with no physical session running. |
| `PUNKTFUNK_GAMESCOPE_NODE` | `auto` · node id | Discover and capture a **running** gamescope's PipeWire node at a fixed mode. Do **not** combine with `SESSION`. |
| `PUNKTFUNK_GAMESCOPE_APP` | command | For an ad-hoc bare-gamescope session, the nested command to run (e.g. `vkcube`). |
+8 -7
View File
@@ -12,19 +12,20 @@ installed — see [Ubuntu](/docs/ubuntu), [Fedora](/docs/fedora), or [Arch](/doc
## host.env
Write `~/.config/punktfunk/host.env` with the GNOME settings. The host auto-detects the compositor
from your session, so the explicit `PUNKTFUNK_COMPOSITOR` is belt-and-braces:
The host auto-detects the compositor from your live session on every connect, so the starter
`~/.config/punktfunk/host.env` is one line:
```ini
# ~/.config/punktfunk/host.env
WAYLAND_DISPLAY=wayland-0
XDG_CURRENT_DESKTOP=GNOME
PUNKTFUNK_COMPOSITOR=mutter
# ~/.config/punktfunk/host.env (keys are case-sensitive)
PUNKTFUNK_VIDEO_SOURCE=virtual
# GPU zero-copy (dmabuf → CUDA → NVENC) is ON by default; auto-falls back to CPU. Set =0 to force CPU.
PUNKTFUNK_INPUT_BACKEND=libei
```
> **Don't set `PUNKTFUNK_COMPOSITOR`, `WAYLAND_DISPLAY`, or `XDG_CURRENT_DESKTOP` here.** Pinning
> the compositor turns auto-detection **off** — per connect *and* mid-stream — so the host stops
> following session switches, and stale session values point it at dead sockets. Forcing a backend
> is a CI / dedicated-appliance posture, not desktop configuration.
You must be on a **Wayland** session (not X11), and Mutter must be **≥ 48**. See the
[Configuration reference](/docs/configuration) for every option.
+9 -4
View File
@@ -19,14 +19,19 @@ or [Fedora](/docs/fedora).
## host.env
The host auto-detects a Hyprland session, so you usually need nothing here. To force the backend, set
these in `~/.config/punktfunk/host.env`:
The host auto-detects a Hyprland session, so the starter `~/.config/punktfunk/host.env` is one line:
```ini
PUNKTFUNK_VIDEO_SOURCE=virtual
# GPU zero-copy capture→encode is ON by default; auto-falls back to CPU. Set PUNKTFUNK_ZEROCOPY=0 to force CPU.
```
To force the backend (CI/testing — note that pinning turns live-session auto-detection **off**, so
the host stops following session switches):
```ini
PUNKTFUNK_COMPOSITOR=hyprland
PUNKTFUNK_INPUT_BACKEND=wlr
PUNKTFUNK_VIDEO_SOURCE=virtual
# GPU zero-copy capture→encode is ON by default; auto-falls back to CPU. Set PUNKTFUNK_ZEROCOPY=0 to force CPU.
```
See [Configuration](/docs/configuration) for the full reference.
+18 -8
View File
@@ -13,20 +13,30 @@ installed — see [Ubuntu](/docs/ubuntu), [Fedora](/docs/fedora), [Arch](/docs/a
## host.env
A KDE starter `~/.config/punktfunk/host.env`:
The host auto-detects your KWin session on every connect — including a box that switches between
the Plasma desktop and Steam Game Mode — so the starter `~/.config/punktfunk/host.env` is one line:
```ini
WAYLAND_DISPLAY=wayland-0
XDG_CURRENT_DESKTOP=KDE
PUNKTFUNK_COMPOSITOR=kwin
# ~/.config/punktfunk/host.env (keys are case-sensitive)
PUNKTFUNK_VIDEO_SOURCE=virtual
# GPU zero-copy (dmabuf → CUDA → NVENC) is ON by default; auto-falls back to CPU. Set =0 to force CPU.
PUNKTFUNK_INPUT_BACKEND=libei
```
The host auto-detects the running compositor on every connect, so most of this is optional — the
values above are just what it resolves to on a KWin session. See the
[Configuration reference](/docs/configuration) for every option.
> **Don't set `PUNKTFUNK_COMPOSITOR`, `WAYLAND_DISPLAY`, or `XDG_CURRENT_DESKTOP` here.** Pinning
> the compositor turns auto-detection **off** — per connect *and* mid-stream — so a switch to Game
> Mode then kills the stream instead of being followed, and stale session values point the host at
> dead sockets. Forcing a backend is for CI and dedicated appliances (the
> [headless session](#headless-session) below ships a `host.env.kde` that pins on purpose).
If the box switches between the desktop and Game Mode, also enable lingering — the host is a user
service, and without linger the logout moment of a session switch tears it (and PipeWire) down
mid-stream:
```sh
sudo loginctl enable-linger "$USER"
```
See the [Configuration reference](/docs/configuration) for every option.
## Use a Wayland session
+9 -4
View File
@@ -23,16 +23,21 @@ or [Fedora](/docs/fedora).
## host.env
The host auto-detects a wlroots session, so you usually need nothing here. To force the backend, set
these in `~/.config/punktfunk/host.env`:
The host auto-detects a wlroots session, so the starter `~/.config/punktfunk/host.env` is one line:
```ini
PUNKTFUNK_COMPOSITOR=wlroots # aliases: sway, wlr, hyprland (all the wlroots family; the exact backend is auto-detected)
PUNKTFUNK_INPUT_BACKEND=wlr
PUNKTFUNK_VIDEO_SOURCE=virtual
# GPU zero-copy capture→encode is ON by default; auto-falls back to CPU. Set PUNKTFUNK_ZEROCOPY=0 to force CPU.
```
To force the backend (CI/testing — note that pinning turns live-session auto-detection **off**, so
the host stops following session switches):
```ini
PUNKTFUNK_COMPOSITOR=wlroots # aliases: sway, wlr (the wlroots-proper family)
PUNKTFUNK_INPUT_BACKEND=wlr
```
See [Configuration](/docs/configuration) for the full reference.
## How it works
+16 -1
View File
@@ -103,7 +103,22 @@ See [GNOME](/docs/gnome) for the GL/EGL userspace details.
- KWin must be **≥ 6.5.6** (`kwin_wayland --version`); GNOME **≥ 48**; gamescope **≥ 3.16.22**. See
[KDE](/docs/kde) for the KWin/Wayland requirement and [gamescope](/docs/gamescope) for the
gamescope one.
- Confirm `PUNKTFUNK_COMPOSITOR` in [`host.env`](/docs/configuration) matches your desktop.
- If [`host.env`](/docs/configuration) sets `PUNKTFUNK_COMPOSITOR`, **remove it** — the host
auto-detects the live compositor, and the pin points it at one backend even when a different
session is live (it also disables Gaming ↔ Desktop following).
## Session fails right after editing host.env
- Keys are **case-sensitive**: `punktfunk_gamescope_attach=1` sets nothing — use the exact
uppercase names.
- Hardcoded session anchors with the wrong uid (`XDG_RUNTIME_DIR=/run/user/1000` when `id -u`
isn't 1000) point the host at another user's PipeWire/D-Bus: audio errors like
`pw audio connect … Creation failed`, no capture, and clients reporting the host as
unreachable or asleep. **Delete both anchor lines** — a `systemctl --user` service doesn't need
them — or fix the uid.
- `PUNKTFUNK_COMPOSITOR` pins the backend and disables Gaming ↔ Desktop following — remove it on
any box that switches sessions.
- The env file is read at service start: `systemctl --user restart punktfunk-host` after edits.
## Capture fails: "Session creation inhibited" (GNOME)
+17 -24
View File
@@ -234,33 +234,25 @@ cp /usr/share/punktfunk/host.env.bazzite ~/.config/punktfunk/host.env
The Bazzite template (`packaging/bazzite/host.env`) contains:
```sh
XDG_RUNTIME_DIR=/run/user/1000
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
# gamescope backend: spawned per session, no compositor login required.
PUNKTFUNK_COMPOSITOR=gamescope
PUNKTFUNK_VIDEO_SOURCE=virtual
PUNKTFUNK_GAMESCOPE_APP=steam -gamepadui
# gamescope hosts its own EIS input socket — input lands in the nested session.
PUNKTFUNK_INPUT_BACKEND=gamescope
# GPU zero-copy capture (dmabuf -> CUDA -> NVENC) is ON by default and auto-falls back to CPU if
# unavailable. No need to set it. Set to 0 only to force the CPU path.
# PUNKTFUNK_ZEROCOPY=0
#RUST_LOG=info
# Gaming Mode = ATTACH: the box owns its gamescope session; the host captures + follows it.
PUNKTFUNK_GAMESCOPE_ATTACH=1
```
**What each knob means and why these are the Bazzite defaults:**
| Knob | Value | Meaning |
|---|---|---|
| `XDG_RUNTIME_DIR` / `DBUS_SESSION_BUS_ADDRESS` | `…/user/1000` | Session bus / runtime dir. **`1000` assumes your user is UID 1000** — change both if `id -u` says otherwise. |
| `PUNKTFUNK_COMPOSITOR` | `gamescope` | **The Bazzite default.** The host spawns a **headless gamescope per session** at the client's exact resolution/refresh and captures its PipeWire node — so you need **no graphical desktop login** to stream. Bazzite ships gamescope, so this "just works." |
| *(no compositor / no anchors)* | — | The host **auto-detects** the live session per connect (Gaming Mode gamescope vs the KDE desktop) and follows switches mid-stream; a `systemctl --user` service inherits the right `XDG_RUNTIME_DIR` and the host derives the bus itself. Pinning `PUNKTFUNK_COMPOSITOR` or hardcoding uid-1000 anchors only breaks this — leave them out. |
| `PUNKTFUNK_VIDEO_SOURCE` | `virtual` | Create a per-client virtual output at the client's exact WxH@Hz (the flagship "native resolution, no scaling" mode), vs. `portal` which captures an existing monitor. |
| `PUNKTFUNK_GAMESCOPE_APP` | `steam -gamepadui` | The command launched **inside** the nested gamescope — here, a SteamOS-style couch UI. Set it to whatever you want the session to run. |
| `PUNKTFUNK_INPUT_BACKEND` | `gamescope` | Inject mouse/keyboard/gamepad into the nested gamescope via its own EIS socket. |
| `PUNKTFUNK_GAMESCOPE_ATTACH` | `1` | Gaming Mode model: the **box** owns its gamescope session; the host attaches to whatever's live and never tears it down. Swap for `PUNKTFUNK_GAMESCOPE_MANAGED=1` to have the host relaunch the gaming session headless at the **client's** exact mode instead (see the template's comments). |
| `PUNKTFUNK_ZEROCOPY` | `on` *(default)* | GPU zero-copy capture (dmabuf → CUDA → NVENC), on by default. Falls back to CPU automatically if unavailable; set `0` to force the CPU path. |
| `RUST_LOG` | (commented) | Uncomment `RUST_LOG=info` for verbose logs while debugging. |
@@ -268,16 +260,14 @@ PUNKTFUNK_INPUT_BACKEND=gamescope
games a virtual Sony DualSense (lightbar, adaptive triggers, touchpad, motion) instead of the
default X-Box-360 pad. The feedback flows back to a real DualSense on the client.
**Alternative — drive the full Plasma/GNOME desktop** instead of a nested gamescope (per the
template's footer comment): switch to `PUNKTFUNK_COMPOSITOR=kwin` and
`PUNKTFUNK_INPUT_BACKEND=libei`, and run the host **inside** a KDE session with `WAYLAND_DISPLAY` /
`XDG_CURRENT_DESKTOP` set. The full knob list (FEC %, per-stage timing, etc.) is in
`scripts/host.env.example` / `/usr/share/punktfunk/host.env.example`.
**The Plasma desktop needs no extra config:** the same auto-detection streams the KDE Desktop
session whenever that's what's live — no compositor pin, no `WAYLAND_DISPLAY` /
`XDG_CURRENT_DESKTOP` (the host retargets those per connect). The full knob list (FEC %, per-stage
timing, etc.) is in `scripts/host.env.example` / `/usr/share/punktfunk/host.env.example`.
> The gamescope default is what makes Bazzite the easy path: it's a **headless, per-session**
> compositor — no desktop login, no display manager, no `--drm` scanout. You don't need any of the
> headless-KDE bring-up scripts (`scripts/headless/run-headless-kde.sh`) on Bazzite unless you
> deliberately switch to the KWin backend.
> Auto-detection is what makes Bazzite the easy path: the host follows the box between Gaming Mode
> and the Desktop — even mid-stream — with a one-line config. You don't need any of the
> headless-KDE bring-up scripts (`scripts/headless/run-headless-kde.sh`) on Bazzite.
---
@@ -474,8 +464,11 @@ desktop viewer.
NVIDIA driver. The code falls back to CPU automatically; check the log for the fallback line and
verify the `-nvidia` image / driver is healthy.
- **Wrong UID in `host.env`.** `XDG_RUNTIME_DIR=/run/user/1000` and the bus path assume UID 1000. Run
`id -u`; if it's different, fix both lines or the host can't reach your session's PipeWire/D-Bus.
- **Session anchors in `host.env`.** The template no longer sets `XDG_RUNTIME_DIR` /
`DBUS_SESSION_BUS_ADDRESS` — a `systemctl --user` service inherits the right values. If an older
config hardcodes them with the wrong uid (`/run/user/1000` when `id -u` isn't 1000), the host
points at another user's PipeWire/D-Bus and everything fails (`pw audio connect … Creation
failed`, no capture). Delete both lines, or fix the uid.
- **Service `ExecStart` points at a missing path in `$HOME`.** The dev unit references
`%h/punktfunk/target/release/...`. The RPM binary is `/usr/bin/punktfunk-host`. Override
+8 -8
View File
@@ -3,10 +3,9 @@
# The compositor + input backend are AUTO-DETECTED per connect from the ACTIVE session: the host
# follows the box as you flip between Steam Gaming Mode (gamescope — a managed session at the
# CLIENT's resolution) and a KDE/GNOME Desktop (KWin/Mutter virtual output at the client's mode).
# So nothing here forces a backend — only the trustworthy anchors stay.
XDG_RUNTIME_DIR=/run/user/1000
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
# So nothing here forces a backend, and no session anchors are needed: a `systemctl --user`
# service inherits the correct XDG_RUNTIME_DIR and the host derives the bus from it. (Keys are
# CASE-SENSITIVE — use the exact uppercase names.)
PUNKTFUNK_VIDEO_SOURCE=virtual
@@ -23,10 +22,11 @@ PUNKTFUNK_VIDEO_SOURCE=virtual
#
# GAME MODE = ATTACH (the box owns its session; the host follows). The box decides whether it's in
# Steam Gaming Mode or a Desktop — you switch with the normal Steam UI / "Switch to Desktop". The
# host just ATTACHES to whatever's live and captures it; it never tears the session down or relaunches
# it. So switching Desktop<->Game is rock-solid, and when you disconnect the box STAYS in its current
# mode — reconnecting drops you right back where you were. The streamed resolution in game mode is the
# box's gamescope mode (see SCREEN_WIDTH/HEIGHT in /etc/gamescope-session-plus/sessions.d/steam).
# host just ATTACHES to whatever's live and captures it; it never tears the session down. So
# switching Desktop<->Game is rock-solid, and when you disconnect the box STAYS in its current
# mode — reconnecting drops you right back where you were. On a resolution mismatch the host
# restarts the box's own game-mode session at the CLIENT's resolution (a foreign/bare gamescope
# instead streams at its own mode).
PUNKTFUNK_GAMESCOPE_ATTACH=1
#
# Opt OUT to the MANAGED model instead (host tears the box's gamescope down on connect and launches
+7
View File
@@ -1,4 +1,11 @@
# punktfunk host config for a Fedora/Ubuntu KDE Plasma appliance (kwin backend).
#
# APPLIANCE-ONLY: this file deliberately PINS the backend (PUNKTFUNK_COMPOSITOR) and the session
# env (WAYLAND_DISPLAY/XDG_CURRENT_DESKTOP) at the dedicated headless KWin session — which also
# turns OFF the host's live-session auto-detection and Desktop<->Game following. On a normal
# desktop (or any box that switches to Steam Game Mode) do NOT use this file; start from
# host.env.example instead, whose defaults auto-detect and follow the live session.
#
# Copy to ~/.config/punktfunk/host.env. Pairs with punktfunk-kde-session.service, which brings
# up a headless `kwin --virtual` on wayland-kde (with KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 so the
# host can bind KWin's privileged zkde_screencast protocol — an interactive Plasma session will
+4 -2
View File
@@ -168,14 +168,16 @@ Everything the RPM's `%install` + `%post` do, declaratively:
### Headless / appliance
Set `autoStart = true`, enable lingering, and pick a backend in `settings`:
Set `autoStart = true`, enable lingering, and — for a **dedicated single-session appliance**
pin a backend in `settings` (pinning `PUNKTFUNK_COMPOSITOR` disables live-session auto-detection,
so leave it out on any box that switches between a desktop and Game Mode):
```nix
services.punktfunk.host = {
enable = true;
autoStart = true;
users = [ "streamer" ];
settings = { PUNKTFUNK_COMPOSITOR = "gamescope"; }; # or kwin/mutter/wlroots
settings = { PUNKTFUNK_COMPOSITOR = "gamescope"; }; # appliance-only; omit to auto-detect
};
users.users.streamer.linger = true;
# For the gamescope/KWin backends extend the service PATH, e.g.:
+44 -35
View File
@@ -1,16 +1,17 @@
# punktfunk host configuration (~/.config/punktfunk/host.env) — consumed by punktfunk-host.service.
#
# The compositor + input backend are AUTO-DETECTED per connect from the live session (the host
# probes which compositor is actually running and retargets WAYLAND_DISPLAY/XDG_CURRENT_DESKTOP/
# DBUS at it), so a box that flips between Steam Gaming Mode and a KDE/GNOME desktop is followed
# automatically. The blocks below are OPTIONAL OVERRIDES — uncomment one only to force a backend
# (this also skips the per-connect env retargeting). The anchors XDG_RUNTIME_DIR + DBUS stay.
# Session / compositor environment (headless KWin example).
XDG_RUNTIME_DIR=/run/user/1000
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
WAYLAND_DISPLAY=wayland-kde
XDG_CURRENT_DESKTOP=KDE
# YOU BARELY NEED THIS FILE. The host AUTO-DETECTS the live session per connect — which compositor
# is running (KWin / Mutter / sway / Hyprland / gamescope), its Wayland socket, session bus, and the
# matching input backend — and FOLLOWS the box when it switches between a desktop and Steam Gaming
# Mode, even mid-stream. Everything below except PUNKTFUNK_VIDEO_SOURCE is an optional override.
#
# Two rules that save debugging sessions:
# * Keys are CASE-SENSITIVE. `punktfunk_gamescope_attach=1` sets nothing — use the exact
# uppercase names.
# * On a desktop you actually use, do NOT set PUNKTFUNK_COMPOSITOR / WAYLAND_DISPLAY /
# XDG_CURRENT_DESKTOP. Pinning the compositor DISABLES session-following (a switch to Game
# Mode mid-stream then kills the stream instead of being followed), and stale session vars
# point detection at dead sockets. Those knobs are for CI and dedicated appliances (below).
# Video source: `virtual` creates a per-client virtual output at the client's exact
# resolution+refresh (the flagship mode); `portal` captures an existing monitor.
@@ -20,33 +21,41 @@ PUNKTFUNK_VIDEO_SOURCE=virtual
# CPU automatically. No need to set it. Set to 0 only to force the CPU path.
# PUNKTFUNK_ZEROCOPY=0
# --- Bazzite / SteamOS-like host: host-managed Steam-Deck-UI session -----------------------
# The host LAUNCHES gamescope-session-plus headless AT THE CLIENT'S mode (so games see the
# client's exact resolution + refresh, not the box's TV), and relaunches it when the mode
# changes. Requires the headless-appliance prereqs (linger + multi-user.target — see
# punktfunk-steam-session.service header) and NO physical gaming session running.
#PUNKTFUNK_COMPOSITOR=gamescope
#PUNKTFUNK_GAMESCOPE_SESSION=steam # host owns a gamescope-session-plus session at the client mode
#PUNKTFUNK_INPUT_BACKEND=gamescope
# Mutually exclusive with the above: ATTACH to a gamescope session something ELSE owns (fixed mode):
#PUNKTFUNK_GAMESCOPE_NODE=auto # discover + capture a running gamescope (do NOT combine with SESSION)
# --- Session anchors (rarely needed) -------------------------------------------------------
# As a `systemctl --user` service the host inherits the correct XDG_RUNTIME_DIR from systemd and
# derives the bus (`unix:path=$XDG_RUNTIME_DIR/bus`) itself. Set these ONLY when running the host
# outside a user service (ssh, cron) — and with YOUR uid (`id -u`), never a copy-pasted 1000: a
# wrong uid points the host at another user's (nonexistent) PipeWire/D-Bus, and every session
# fails with errors like "pw audio connect … Creation failed".
#XDG_RUNTIME_DIR=/run/user/<uid>
#DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/<uid>/bus
# --- GNOME / Mutter host (e.g. an Ubuntu desktop) -----------------------------------------
# Attach to a running GNOME (Wayland) session — its default socket is wayland-0, not wayland-kde.
# Mutter creates the per-client virtual output via its `RecordVirtual` D-Bus API (a virtual
# monitor alongside any real one), and input goes through the RemoteDesktop portal (libei). On a
# real desktop the host runs as the logged-in user; headless GNOME also works (gnome-shell
# --headless). Needs GNOME ≥ 48 for the zero-copy RecordVirtual path.
#WAYLAND_DISPLAY=wayland-0
#XDG_CURRENT_DESKTOP=GNOME
#PUNKTFUNK_COMPOSITOR=mutter
#PUNKTFUNK_VIDEO_SOURCE=virtual
#PUNKTFUNK_INPUT_BACKEND=libei
# --- Steam Gaming Mode (Linux boxes with gamescope session infra: Bazzite/SteamOS/Nobara) ---
# Game Mode is auto-handled; two models decide WHERE it runs when a client streams:
# * MANAGED (the default where session infra is detected) — the host relaunches the gaming
# session HEADLESS at the CLIENT's exact mode ("game mode on the virtual screen"); physical
# displays drop out of it, and the box is restored on a debounced idle after disconnect.
# * ATTACH — the BOX owns its session: Game Mode stays on the physical screen and the host
# captures/follows it, never tearing it down. Reconnects land wherever the box is.
#PUNKTFUNK_GAMESCOPE_ATTACH=1 # pick the ATTACH model
#PUNKTFUNK_GAMESCOPE_MANAGED=1 # force MANAGED even where infra detection wouldn't pick it
#PUNKTFUNK_GAMESCOPE_SESSION=steam # host owns a gamescope-session-plus session at the client mode
#PUNKTFUNK_GAMESCOPE_NODE=auto # raw attach: discover + capture a running gamescope's node
# # (do NOT combine with SESSION)
#PUNKTFUNK_GAMESCOPE_APP=vkcube # nested command for ad-hoc bare-gamescope sessions
#PUNKTFUNK_SESSION_WATCH=0 # disable mid-stream Desktop<->Game following (on by default
# # on gamescope-infra boxes)
# --- Force a backend (CI / tests / dedicated single-session appliances ONLY) ---------------
# PINS the backend: the host stops following the live session entirely — per connect AND
# mid-stream. Fine for a dedicated headless appliance (punktfunk-kde-session.service, a pure
# gamescope box) or a CI run; wrong for any box that switches sessions.
#PUNKTFUNK_COMPOSITOR=kwin # kwin | mutter | gamescope | wlroots | hyprland
#PUNKTFUNK_INPUT_BACKEND=libei # wlr | libei | gamescope | uinput (auto-routed per connect)
#WAYLAND_DISPLAY=wayland-kde # headless-KDE appliance socket; retargeted per connect otherwise
#XDG_CURRENT_DESKTOP=KDE
# Optional overrides (apps.json is the primary mechanism for per-app settings):
#PUNKTFUNK_COMPOSITOR=kwin # kwin | mutter | gamescope | wlroots
#PUNKTFUNK_GAMESCOPE_APP=vkcube # nested command for ad-hoc bare-gamescope sessions
#PUNKTFUNK_INPUT_BACKEND=libei # wlr | libei | gamescope | uinput
#PUNKTFUNK_FEC_PCT=20 # video FEC overhead percent
#PUNKTFUNK_PERF=1 # per-stage timing logs
#PUNKTFUNK_MDNS=0 # disable the mDNS adverts (native + GameStream) — for multicast-
+8 -5
View File
@@ -2,15 +2,18 @@
# GameStream/Moonlight-compat planes). For a SECURE native-only host (no plain-HTTP pairing / legacy
# GCM nonce reuse — security-review #5/#9; native clients only), drop `--gamestream` from ExecStart.
#
# Install (against an already-running compositor session):
# Install (against an already-running compositor session — the host auto-detects and follows it,
# so host.env needs no backend config):
# mkdir -p ~/.config/systemd/user && cp scripts/punktfunk-host.service ~/.config/systemd/user/
# cp scripts/host.env.example ~/.config/punktfunk/host.env # then edit for your backend
# cp scripts/host.env.example ~/.config/punktfunk/host.env # defaults are right for a desktop
# systemctl --user daemon-reload && systemctl --user enable --now punktfunk-host
#
# Self-contained boot appliance (no login, no manual steps after boot):
# Self-contained boot appliance (no login, no manual steps after boot). These routes PIN the
# backend via PUNKTFUNK_COMPOSITOR — correct for a dedicated single-session box, but it turns off
# live-session auto-detection, so never do it on a desktop that switches sessions (Game Mode etc.):
# - kwin backend (stream the Plasma desktop): also install + enable
# punktfunk-kde-session.service (it brings up the headless KWin session this After=s), and set
# PUNKTFUNK_COMPOSITOR=kwin + WAYLAND_DISPLAY=wayland-kde in host.env.
# punktfunk-kde-session.service (it brings up the headless KWin session this After=s), and use
# the shipped packaging/kde/host.env (pins kwin + WAYLAND_DISPLAY=wayland-kde on purpose).
# - gamescope backend (stream a nested app, no desktop): set PUNKTFUNK_COMPOSITOR=gamescope in
# host.env — the host spawns gamescope per session, so no kde-session unit is needed.
# Then `sudo loginctl enable-linger "$USER"` so user units start at boot, and reboot.