Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc5f6a3881 | ||
|
|
6617275387 | ||
|
|
bda015b101 |
@@ -0,0 +1,97 @@
|
||||
// Keeps the local display awake for the duration of a streaming session.
|
||||
//
|
||||
// A stream is not "user activity" to the OS: the pixels arrive over the network and the input that
|
||||
// drives them is often a game controller, which does NOT feed the HID idle timer on any Apple
|
||||
// platform. So a controller-only session reliably idles the panel out from under the user — the
|
||||
// same reason the Android client holds FLAG_KEEP_SCREEN_ON while streaming (StreamScreen.kt).
|
||||
//
|
||||
// Held by SessionModel from `beginStreaming` to `disconnect`, so it is scoped to the session and
|
||||
// never leaks past it (including a host-ended or timed-out background session, which both land in
|
||||
// `disconnect`).
|
||||
|
||||
import Foundation
|
||||
|
||||
#if os(macOS)
|
||||
import IOKit.pwr_mgt
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
final class DisplaySleepGuard {
|
||||
#if os(macOS)
|
||||
/// The `beginActivity` token; non-nil exactly while held.
|
||||
private var activity: NSObjectProtocol?
|
||||
/// Re-used across heartbeats so the whole session shares one assertion instead of
|
||||
/// accumulating one per tick.
|
||||
private var userActivityAssertion: IOPMAssertionID = IOPMAssertionID(0)
|
||||
private var heartbeat: Timer?
|
||||
|
||||
/// The power assertion defers DISPLAY SLEEP but not the screen saver — that runs off the
|
||||
/// HID idle timer, which a controller-only session never touches. Declaring user activity
|
||||
/// on an interval well under the shortest selectable screen-saver delay (1 minute) keeps
|
||||
/// that timer from ever reaching it. Side effect, and the intended one: an idle-lock
|
||||
/// configured to follow the screen saver is deferred too, for the session only.
|
||||
private static let heartbeatInterval: TimeInterval = 30
|
||||
#endif
|
||||
|
||||
private(set) var isHeld = false
|
||||
|
||||
/// Idempotent — a second acquire while held is a no-op.
|
||||
func acquire() {
|
||||
guard !isHeld else { return }
|
||||
isHeld = true
|
||||
#if os(macOS)
|
||||
// The high-level Foundation API over IOKit power assertions: `.idleDisplaySleepDisabled`
|
||||
// is the panel, `.userInitiated` also holds off idle SYSTEM sleep and sudden termination
|
||||
// for a session the user is watching in real time.
|
||||
activity = ProcessInfo.processInfo.beginActivity(
|
||||
options: [.userInitiated, .idleDisplaySleepDisabled],
|
||||
reason: "Punktfunk streaming session")
|
||||
declareUserActivity()
|
||||
let timer = Timer.scheduledTimer(withTimeInterval: Self.heartbeatInterval, repeats: true) {
|
||||
[weak self] _ in
|
||||
MainActor.assumeIsolated { self?.declareUserActivity() }
|
||||
}
|
||||
// The stream runs under a tracking run-loop mode while a menu or a window resize is up;
|
||||
// .common keeps the heartbeat ticking through those.
|
||||
RunLoop.main.add(timer, forMode: .common)
|
||||
heartbeat = timer
|
||||
#else
|
||||
// iOS/iPadOS/tvOS: app-wide, and ignored while backgrounded — the background keep-alive
|
||||
// (audio-only, video dropped) correctly lets the device sleep without touching this.
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Idempotent — safe to call when not held (`disconnect` runs on paths that never streamed).
|
||||
func release() {
|
||||
guard isHeld else { return }
|
||||
isHeld = false
|
||||
#if os(macOS)
|
||||
heartbeat?.invalidate()
|
||||
heartbeat = nil
|
||||
if let activity {
|
||||
ProcessInfo.processInfo.endActivity(activity)
|
||||
self.activity = nil
|
||||
}
|
||||
if userActivityAssertion != IOPMAssertionID(0) {
|
||||
IOPMAssertionRelease(userActivityAssertion)
|
||||
userActivityAssertion = IOPMAssertionID(0)
|
||||
}
|
||||
#else
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
/// Resets the HID idle timer (see `heartbeatInterval`). `kIOPMUserActiveLocal` = activity at
|
||||
/// this Mac's own display, which is what a stream being watched here is.
|
||||
private func declareUserActivity() {
|
||||
IOPMAssertionDeclareUserActivity(
|
||||
"Punktfunk streaming session" as CFString,
|
||||
kIOPMUserActiveLocal,
|
||||
&userActivityAssertion)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -196,6 +196,11 @@ final class SessionModel: ObservableObject {
|
||||
/// Bounded auto-disconnect for a backgrounded keep-alive session. Fires on `.main`.
|
||||
private var backgroundTimer: DispatchSourceTimer?
|
||||
|
||||
/// Holds off display sleep (and, on macOS, the screen saver) for the life of a session —
|
||||
/// nothing about watching a stream looks like user activity to the OS, least of all a
|
||||
/// controller-only session. Acquired in `beginStreaming`, released in `disconnect`.
|
||||
private let displaySleepGuard = DisplaySleepGuard()
|
||||
|
||||
/// `allowTofu` gates the trust-on-first-use prompt for an unpinned host: it is only true
|
||||
/// when the host EXPLICITLY advertised `pair=optional` (rule 3a). For any other unpinned host
|
||||
/// — `pair=required`, a manually-typed host, or a discovered host with no/unknown `pair`
|
||||
@@ -455,6 +460,8 @@ final class SessionModel: ObservableObject {
|
||||
func disconnect(deliberate: Bool = true) {
|
||||
statsTimer?.invalidate()
|
||||
statsTimer = nil
|
||||
// No-op when this session never reached `.streaming` (a refused/aborted connect).
|
||||
displaySleepGuard.release()
|
||||
// Drop any armed background keep-alive (incl. the timeout that just fired us).
|
||||
backgroundTimer?.cancel()
|
||||
backgroundTimer = nil
|
||||
@@ -550,6 +557,7 @@ final class SessionModel: ObservableObject {
|
||||
// Input capture itself is owned by StreamView (engaged by the captureEnabled
|
||||
// flip this phase change causes, released/re-engaged by the user from there).
|
||||
phase = .streaming
|
||||
displaySleepGuard.acquire()
|
||||
// Audio starts with streaming, not during the trust prompt — no host sound (or
|
||||
// mic uplink!) before the user trusted the host. Devices come from Settings;
|
||||
// "" = system default.
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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,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`). |
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+20
-7
@@ -31,15 +31,28 @@ even across a tag re-point.
|
||||
Canary / `-rc` builds have **no** file here on purpose: they get no curated body and are not
|
||||
announced.
|
||||
|
||||
## Format
|
||||
## Voice & format
|
||||
|
||||
Match the house style (see any recent `vX.Y.Z.md`):
|
||||
**Write for the people who USE Punktfunk to stream their games and desktops — not for the people who
|
||||
build it.** A non-engineer should finish knowing what's new and whether it affects them; an engineer
|
||||
should never be confused or forced to decode internals. (See any recent `vX.Y.Z.md` for the target.)
|
||||
|
||||
- Open with a **wire-compatibility** line — *"Wire-compatible with X.Y.x — existing pairings and
|
||||
clients keep working."* — plus a one-sentence fallback/negotiation note. This lead-in (all text
|
||||
before the first `##` header) is what the Discord embed shows, so make it a real summary.
|
||||
- Then `## Section` headers grouping the changes, with **bold lead-in** bullets.
|
||||
- Be concrete: env vars, ABI/protocol versions, on-glass-verified hardware, platform scope.
|
||||
1. **Lead with the benefit.** Each entry = what the user can now *do*, what now *works*, or what
|
||||
stopped *going wrong* — in their words. Implementation is not the story.
|
||||
2. **No internal vocabulary in the body.** No protocol/message names, code type names, hex codes or
|
||||
hardware IDs, crate/component names, or API symbols. Translate any essential detail to plain
|
||||
language. Name things users recognize (iPad, Apple Pencil, Steam Deck, Android TV, the Windows
|
||||
sign-in screen) — not subsystems.
|
||||
3. **Group as New / Improved / Fixed**, each a bold one-line lead-in + a tight plain explanation.
|
||||
Skimmable. The lead-in text before the first `##` is what the Discord announcement shows, so make
|
||||
it a real, plain-language summary.
|
||||
4. **Be specific and honest** — no vague "various improvements"; a reader should know exactly what
|
||||
changed.
|
||||
5. **Compatibility line up top, in plain terms:** can they update one side at a time? does their
|
||||
existing setup keep working? No version numbers in the lead.
|
||||
6. **All protocol / ABI / driver / embedder detail goes in ONE `## Under the hood (for developers)`
|
||||
section at the very bottom** — the only place internal names and version numbers belong, clearly
|
||||
optional. The old dense engineering style survives only there.
|
||||
|
||||
The short annotated-**tag** message stays separate and short (a headline + a paragraph); it is the
|
||||
tag object's message, not this file.
|
||||
|
||||
+20
-34
@@ -1,47 +1,33 @@
|
||||
Wire-compatible with 0.18.x — existing pairings and clients keep working. The new stylus plane is capability-negotiated (`HOST_CAP_PEN`) and rides an additive datagram that older peers never send and never have to parse, so a 0.18 host and a 0.19 client (or the reverse) pair exactly as before and simply fall back to pen-as-touch. WIRE_VERSION stays **2**; the embeddable C ABI moves to **13** for one new entry point (`punktfunk_connection_send_pen`); the Windows display-driver protocol is unchanged at **6** (compat floor **3**).
|
||||
Update whenever it suits you — you can update your app and the machine you stream from one at a time, the new and old versions work together, and everything you've already paired stays paired. The new pen features simply switch on once both ends are updated. Nothing you already use changes.
|
||||
|
||||
## Pen, stylus & tablet — a whole new input plane
|
||||
## New: draw and write with a pen or stylus
|
||||
|
||||
Punktfunk streams now carry a first-class, pressure-sensitive stylus, end to end and on every platform. It is its own wire plane — not mouse events in disguise — so pressure, tilt, hover and barrel buttons survive the trip from glass to host.
|
||||
You can now use a real stylus while streaming, and it behaves like a real pen on the machine you're controlling — **pressure, tilt, and the pen's side buttons all come through**, not just a plain tap. Use your **iPad's Apple Pencil** (including Pencil Pro), an **Android phone or tablet's S Pen** or other active stylus, or a pen from a Moonlight app.
|
||||
|
||||
- **The wire (P0).** A state-full `RICH_PEN` datagram carries batches of up to 8 samples — sub-pixel `f32` position, `u16` pressure, hover distance, tilt + azimuth, an eraser tool, and two barrel buttons — ordered by a wrapping sequence number and dropped whole when stale, so a late batch never rewrites the present. A `PenTracker` coalesces the stream and `HOST_CAP_PEN` advertises the plane; toward a host without the bit the client keeps its pen-as-touch fallback.
|
||||
- **Linux host (P1).** A per-session uinput virtual graphics tablet injects the full stylus — pressure and tilt included — and tears down with the session. `HOST_CAP_PEN` goes live wherever uinput is available.
|
||||
- **GameStream / Moonlight (P2).** The host ingests Moonlight's `SS_PEN`/`SS_TOUCH` pointer packets and advertises the matching `featureFlags`, so a capable Moonlight client sends real stylus onto the same plane.
|
||||
- **Windows host (P3).** `PT_PEN` + `PT_TOUCH` synthetic pointer injection drives the Windows pen stack directly — pressure and barrel buttons reach applications as genuine pen input.
|
||||
- **iPad (P4).** Apple Pencil (including Pencil Pro) capture maps onto the stylus plane — pressure and tilt from the panel, with the cursor-capability fix so the pencil and the pointer coexist.
|
||||
- **Android (P5).** Active-stylus capture (S Pen and friends) forwards onto the pen plane with pressure and tilt.
|
||||
It's great for drawing apps, handwriting and note-taking, signing documents, and photo editing over the stream. Windows and Linux hosts receive it as genuine pen input. If the machine you're streaming from is still on an older version, your pen keeps working as an ordinary touch.
|
||||
|
||||
## Touch injection, fixed
|
||||
## Fixed: your Steam Deck controller shows up as the right controller
|
||||
|
||||
- **Windows native touch actually injects now.** `PT_TOUCH` pointer ids must be contiguous injector slots, but the host was passing the raw wire touch ids straight through — Windows rejected them **silently** and Moonlight-native multitouch landed nothing. Wire ids are now compacted into slots, and the first rejection is surfaced with a warning instead of vanishing.
|
||||
- **NaN pressure no longer drops a finger.** VoidLink's finger touches arrive with a NaN `pressureOrDistance`; the GameStream path discarded the whole packet. It now tolerates NaN and injects the touch.
|
||||
If a Steam controller was plugged into the computer you stream from, a streamed **Steam Deck** controller could be misread by games as a **PlayStation (DualSense)** controller — so button prompts and layouts came out wrong. It now stays a Steam Deck. Punktfunk only steps in to avoid a clash when two genuinely identical controllers are present.
|
||||
|
||||
## Cursor & pointer polish
|
||||
## Improved: a sharper, correctly-sized mouse pointer
|
||||
|
||||
- **The forwarded cursor is scaled to the video fit** on the SDL/Linux and Apple/macOS clients. A high-DPI host pointer was drawn against the client's own backing scale and came out roughly 2× too large; it now renders true-size against the streamed video.
|
||||
- **Right-sized pointer on high-resolution screens.** On Linux and Mac, a pointer coming from a high-resolution host could show up about twice as big as it should. It now matches the video exactly.
|
||||
- **Cleaner pointer on Windows security screens.** The mouse pointer no longer duplicates or gets stuck on Windows sign-in and User Account Control (admin permission) prompts.
|
||||
- **More reliable multi-monitor streaming on Windows.** Switching between monitor layouts on the host could occasionally drop the stream — that's fixed.
|
||||
|
||||
## Windows host fixes
|
||||
## Fixed: touch input
|
||||
|
||||
- **No more `0x57` on display-config isolate.** When a supplied CCD path is doomed (a Steam Deck's live-deactivate always is), the isolate escalates to a keep-only supplied config instead of failing the whole apply with `0x57`.
|
||||
- **Cursor exclusion is reported adapter-wide.** The IddCx declare's cursor exclusion is not a per-target property; the virtual-display driver now reports `cursor_excluded` across the adapter.
|
||||
- **Secure-desktop cursor stood down.** The IddCx hardware cursor is held down while the Windows secure desktop (UAC / Winlogon) is up, ending the duplicated/stuck pointer there.
|
||||
- **Multi-touch from Moonlight apps now works on Windows hosts.** It previously registered nothing; pinch, zoom, and multi-finger gestures now come through.
|
||||
- **Touch is no longer silently dropped** from some devices that report finger pressure in an unusual way.
|
||||
|
||||
## Gamepad
|
||||
## Fixed: Android (and Android TV)
|
||||
|
||||
- **A virtual Steam Deck pad no longer degrades to a DualSense because of an unrelated Steam controller.** To stop Steam Input from double-driving two *identical* controllers, the host downgrades a requested virtual Steam pad to a DualSense when a matching physical Valve controller is present — but the gate matched *any* `28DE` (Valve) device. So plugging a physical Steam Controller 2 (`28DE:1302`) into the host dropped a client's virtual Steam Deck (`28DE:1205`) to the wrong pad — the passthrough came up as a DualSense. The gate now keys on the exact VID+PID: distinct Steam controllers coexist (Steam Input drives them side by side fine), and only a genuine same-identity duplicate — a physical Deck alongside a virtual Deck — still degrades.
|
||||
- **Your mouse's back/forward buttons stay in the stream** instead of bouncing you out to the Android home screen — a relief on Android TV in particular.
|
||||
- **The on-screen keyboard stays out of the way** when you're typing on a physical keyboard.
|
||||
|
||||
## Android input — mouse & keyboard regressions
|
||||
## Under the hood (for developers)
|
||||
|
||||
Two regressions from 0.18.0's mouse-&-keyboard overhaul, felt most on Android TV boxes:
|
||||
|
||||
- **Mouse back/forward stays in the stream.** A mouse's back/forward buttons were synthesized by the reader as `SOURCE_MOUSE` key events and leaked into Android navigation (yanking you out of the stream on a TV); they're now swallowed while streaming.
|
||||
- **Hardware typing no longer pops the IME.** The soft keyboard is gated on `imeShown`, so typing on a physical keyboard against a text-input host doesn't summon the on-screen IME.
|
||||
|
||||
## Release & CI
|
||||
|
||||
- **Release notes now ship with the release.** Notes are authored in-repo at `docs/releases/vX.Y.Z.md` and seeded into the release body at creation, and stable releases announce to the Discord `#releases` channel. This file is the first of them.
|
||||
- **iOS `.ipa` attached.** The iOS build exports an App Store-signed `.ipa` to the unified release and to the run artifacts (archival/TestFlight, not a direct sideload).
|
||||
|
||||
## Versions
|
||||
|
||||
WIRE_VERSION **2** (unchanged — 0.18.x hosts and clients interoperate) · C ABI **13** (adds `punktfunk_connection_send_pen`; embedders recompile) · Windows display driver protocol **6**, compat floor **3** (unchanged — pen injects through the Windows pen stack, not the display driver). Windows drivers ship separately, as always.
|
||||
- The streaming protocol is unchanged, so 0.18 and 0.19 hosts and apps mix freely; the pen is negotiated and older peers simply fall back to touch.
|
||||
- The embeddable core library adds one new call for sending pen input (the C ABI moves to **13**) — rebuild any embedders against 0.19. The Windows virtual-display driver is unchanged: pen goes through Windows' normal pen system, not the display driver.
|
||||
- iOS builds are now attached to each release as a downloadable file (for TestFlight/archival).
|
||||
|
||||
+17
-24
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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-
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user