merge: display-management (Stages 0-5 §6A + keep-alive hardening + gaming-rig)
apple / swift (push) Successful in 1m15s
windows-host / package (push) Failing after 3m56s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m18s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m20s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 57s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m2s
release / apple (push) Successful in 8m43s
android / android (push) Successful in 4m17s
ci / rust (push) Failing after 29s
ci / web (push) Successful in 49s
arch / build-publish (push) Successful in 5m33s
ci / docs-site (push) Successful in 59s
apple / screenshots (push) Successful in 5m42s
deb / build-publish (push) Successful in 3m7s
decky / build-publish (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 28s
ci / bench (push) Successful in 4m41s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 44s
flatpak / build-publish (push) Successful in 4m30s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 10m21s
docker / deploy-docs (push) Successful in 20s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m25s
apple / swift (push) Successful in 1m15s
windows-host / package (push) Failing after 3m56s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m18s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m20s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 57s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m2s
release / apple (push) Successful in 8m43s
android / android (push) Successful in 4m17s
ci / rust (push) Failing after 29s
ci / web (push) Successful in 49s
arch / build-publish (push) Successful in 5m33s
ci / docs-site (push) Successful in 59s
apple / screenshots (push) Successful in 5m42s
deb / build-publish (push) Successful in 3m7s
decky / build-publish (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 28s
ci / bench (push) Successful in 4m41s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 44s
flatpak / build-publish (push) Successful in 4m30s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 10m21s
docker / deploy-docs (push) Successful in 20s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m25s
Merges display-mgmt-stage0 — the user-configurable virtual-display policy layer above the
per-compositor backends. On-glass validated (KWin .116 + Mutter .21; Windows compile-verified .173):
- Policy surface (keep_alive · topology · conflict · identity · layout · max) →
display-settings.json, console-editable via /api/v1/display/{settings,state,release,layout} + a
dedicated "Virtual displays" console section. All five axes enforced, not just stored.
- Lifecycle: pure state machine + Linux keep-alive pool (registry + DisplayLease ownership split),
incl. keep_alive=forever/Pinned (freed via /display/release); topology extend/primary/exclusive
(group-aware); per-client identity (KWin per-slot names → KDE scaling round-trips); mode_conflict
admission (Windows default reject, single-capturer IDD); §6A multi-monitor (display groups +
layout engine + console arrangement table — several clients as monitors of one desktop).
- Keep-alive reconnect hardened: same-client zombie preempt (never a 2nd display), deliberate-quit
skip-linger (QUIT_CLOSE_CODE), tunable idle timeout (PUNKTFUNK_IDLE_TIMEOUT_MS).
Conflicts (packaging/{arch,debian}/README.md firewall docs): kept main's ufw/nft port commands +
the branch's --data-port documentation. build + clippy -D warnings + cargo test --workspace
(18 suites, 0 failed) green on the merged tree.
This commit is contained in:
@@ -62,9 +62,15 @@ picture.
|
||||
|
||||
## Compositor-specific (Linux)
|
||||
|
||||
> **Managing virtual displays** — keep-alive after disconnect, exclusive vs. extend, and (on
|
||||
> Windows/KDE) persistent per-client scaling — now has its own settings surface in the web console
|
||||
> and `display-settings.json`. See [Virtual displays](/docs/virtual-displays). The two
|
||||
> `*_VIRTUAL_PRIMARY` knobs and `PUNKTFUNK_MONITOR_LINGER_MS` below still work but are superseded by
|
||||
> it (a settings file wins over them).
|
||||
|
||||
| Setting | Values | Meaning |
|
||||
|---|---|---|
|
||||
| `PUNKTFUNK_KWIN_VIRTUAL_PRIMARY` | `1` | Make the streamed per-session output the sole desktop so plasmashell + windows render on it (not on the headless bootstrap output). Set by the KDE appliance `host.env`. |
|
||||
| `PUNKTFUNK_KWIN_VIRTUAL_PRIMARY` | `1` | Make the streamed per-session output the sole desktop so plasmashell + windows render on it (not on the headless bootstrap output). Set by the KDE appliance `host.env`. Superseded by the console's **Topology** setting. |
|
||||
| `PUNKTFUNK_MUTTER_VIRTUAL_PRIMARY` | `1` | GNOME/Mutter equivalent of the above. |
|
||||
| `PUNKTFUNK_MUTTER_VIRTUAL_REFRESH` | `1` | Pin the client's exact WxH**@Hz** via `RecordVirtual`'s custom modes (needed for >60 Hz on Mutter). |
|
||||
|
||||
@@ -99,7 +105,7 @@ picture.
|
||||
|---|---|---|
|
||||
| `PUNKTFUNK_VDISPLAY` | `pf` | Virtual-display backend. The bundled pf-vdisplay IddCx driver is the only backend now — informational; leave as `pf`. |
|
||||
| `PUNKTFUNK_SECURE_DDA` | `1` | Capture the secure desktop (UAC / lock / login) so the stream survives those transitions. |
|
||||
| `PUNKTFUNK_MONITOR_LINGER_MS` | ms (default `10000`) | Defer tearing a per-client virtual display down after disconnect. A reconnect inside the window preempts it and creates a fresh one (a reused IddCx swap-chain is dead); the stable per-client monitor id keeps Windows' saved display config applying either way. |
|
||||
| `PUNKTFUNK_MONITOR_LINGER_MS` | ms (default `10000`) | Defer tearing a per-client virtual display down after disconnect. A reconnect inside the window preempts it and creates a fresh one (a reused IddCx swap-chain is dead); the stable per-client monitor id keeps Windows' saved display config applying either way. Superseded by the console's **Keep alive** setting — see [Virtual displays](/docs/virtual-displays). |
|
||||
| `PUNKTFUNK_RENDER_ADAPTER` | description substring | Multi-GPU boxes only: force the NVENC/capture GPU by adapter Description substring (e.g. `4090`). Leave unset on single-GPU machines. |
|
||||
| `PUNKTFUNK_HOST_CMD` | e.g. `serve --gamestream` | The host subcommand the service launches. Default `serve --gamestream`; use `serve` for a secure native-only host. |
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"pairing",
|
||||
"---Configuration---",
|
||||
"configuration",
|
||||
"virtual-displays",
|
||||
"host-cli",
|
||||
"---Troubleshooting---",
|
||||
"troubleshooting",
|
||||
|
||||
@@ -10,11 +10,52 @@ description: Common problems setting up or using a punktfunk host, and how to fi
|
||||
- Host and client must be on the **same network/subnet**. Discovery uses mDNS, which doesn't cross
|
||||
routed subnets or most VPNs-without-multicast. As a fallback, add the host by **IP address** in your
|
||||
client.
|
||||
- A firewall on the host can block it. The native protocol's control plane uses UDP port **9777**. The
|
||||
per-session **data plane** uses an *ephemeral* UDP port negotiated at connect time (currently
|
||||
random) — for a strict firewall, open a UDP range or move the data port. GameStream/Moonlight uses
|
||||
TCP **47984/47989/48010** + UDP **47998–48010** + ENet UDP **47999**. Allow them on the host's
|
||||
firewall.
|
||||
- A firewall on the host can block it. The native protocol's **control plane** is a fixed UDP port,
|
||||
**9777** — open this one. The per-session **data plane** rides a *separate, random* UDP port and
|
||||
usually needs **no** firewall rule (see [Video is slow to start, or fails across
|
||||
subnets](#video-is-slow-to-start-or-fails-across-subnets) for why, and the one case where opening it
|
||||
helps). GameStream/Moonlight (only with `--gamestream`) uses TCP **47984/47989/48010** + UDP
|
||||
**47998–48010** (video/FEC 47998, ENet control 47999, audio 48000) + mDNS UDP **5353**. Allow those
|
||||
on the host's firewall.
|
||||
|
||||
## Video is slow to start, or fails across subnets
|
||||
|
||||
The native **data plane** (the raw UDP that carries video, separate from the 9777 control plane) uses
|
||||
a **random, per-session UDP port** — the host binds `0.0.0.0:0`, then tells the client which port it
|
||||
got during the connect handshake. There is no fixed data port.
|
||||
|
||||
Video flows host → client, but the **client sends the first packet**: a small *hole-punch* datagram to
|
||||
that port. This is deliberate. It lets the host learn the client's real (possibly NAT-translated)
|
||||
source address and stream back to it, so a session can cross a NAT or a stateful inter-VLAN firewall
|
||||
**without** a forwarded data port. What it means for a host firewall:
|
||||
|
||||
- **Same LAN, no host firewall (or the port allowed):** the punch arrives immediately and video starts
|
||||
at once. Nothing to configure.
|
||||
- **Same LAN, host firewall that denies inbound** (ufw/nftables/firewalld default): the punch is
|
||||
dropped, so the host waits **~2.5 s**, then falls back to the address the client reported and streams
|
||||
anyway — a stateful firewall admits the return traffic because the host sent first. **Net effect: it
|
||||
works, but each session takes ~2.5 s longer to start.** That slow start is the symptom of a
|
||||
data-plane rule you're missing.
|
||||
- **Across subnets / NAT:** the same punch-then-fallback applies, as long as the host's outbound video
|
||||
can reach the client (the path's stateful firewall then admits the return). If the host itself is
|
||||
behind NAT reached only via a forwarded control port, the data path may not establish — this is the
|
||||
case a fixed, forwardable data port would solve.
|
||||
|
||||
To remove the ~2.5 s fallback delay, **pin the data port** with `--data-port` (or the
|
||||
`PUNKTFUNK_DATA_PORT` env in `host.env`) and open exactly that one port. The host then binds that
|
||||
fixed port, skips the punch-wait, and streams straight to the client — no timeout to pay:
|
||||
|
||||
```sh
|
||||
punktfunk-host serve --data-port 9778 # or PUNKTFUNK_DATA_PORT=9778 in host.env
|
||||
sudo ufw allow 9778/udp # open exactly that one port
|
||||
```
|
||||
|
||||
Two caveats. A fixed data port serves **one session at a time**; a second concurrent session finds it
|
||||
busy and transparently falls back to a random port + hole-punch (logged). And `--data-port` streams
|
||||
to the client's *reported* address, so use it only where that address is reachable — a flat LAN, or a
|
||||
port-forward that doesn't remap the client's source. Leave it **off** (the default) to keep the
|
||||
NAT-crossing hole-punch. On a normal single-LAN setup you can also just leave the data port closed and
|
||||
accept the one-time ~2.5 s punch-timeout, or not run a host firewall on a trusted LAN at all.
|
||||
|
||||
## `nvidia-smi` says it can't communicate with the driver
|
||||
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
title: Virtual displays
|
||||
description: Control how punktfunk creates, keeps alive, and arranges the virtual displays it streams — presets, keep-alive, exclusive vs. extend, and persistent per-client scaling.
|
||||
---
|
||||
|
||||
When a client connects, punktfunk creates a **virtual display** sized to exactly that client's
|
||||
resolution and refresh, renders your desktop or game onto it, and streams it. This page is about the
|
||||
**policy** for that display: how long it survives a disconnect, whether it takes over your physical
|
||||
monitors, what happens when a second client connects, and how desktop environments remember
|
||||
per-client settings like scaling.
|
||||
|
||||
You set this policy in the **web console** (Host → *Virtual displays*), or by editing
|
||||
`~/.config/punktfunk/display-settings.json` directly (`%ProgramData%\punktfunk\display-settings.json`
|
||||
on Windows). A change applies to the **next** connection — a running session keeps the display it
|
||||
opened on.
|
||||
|
||||
> **You rarely need to touch this.** The default behavior matches how punktfunk has always worked.
|
||||
> Reach for a preset when you want a specific experience — a dedicated couch/gaming box, a desktop
|
||||
> you also use in person, or a multi-monitor workstation.
|
||||
|
||||
> **What's live today:** **keep-alive** (linger, or **forever**), **topology** (extend / primary /
|
||||
> exclusive), **conflict handling**, **per-client identity + persistent scaling** (Windows *and*
|
||||
> KDE/KWin), and **multi-monitor layout** (several clients as monitors of one desktop) are all
|
||||
> enforced. A reconnect always resumes the kept display — even a fast one — instead of spawning a
|
||||
> second. The remaining gaps are noted inline: the Linux `primary` physical-keep *effect*, Sway
|
||||
> `exclusive`, and multi-display for a *single* client (that last is the next stage).
|
||||
|
||||
## Pick a preset
|
||||
|
||||
A preset is the easy way in — select one in the console and you're done. Each expands to a bundle of
|
||||
the individual options documented further down.
|
||||
|
||||
| Preset | What it's for |
|
||||
|---|---|
|
||||
| **Default** | Today's behavior. A short linger absorbs reconnects, the streamed output becomes the sole desktop, and extra clients each get their own view. |
|
||||
| **Gaming rig** | A dedicated couch/headless box. The game and its display survive disconnects indefinitely (keep-alive **forever**), and whoever connects takes the box over. Release it from the console when you're done. |
|
||||
| **Shared desktop** | A desktop you also use in person. punktfunk never blanks your real monitors and never leaves a ghost display behind; concurrent viewers each get a view. |
|
||||
| **Hot-desk** | One user at a time with fast reattach — roaming between your own devices. A second user is told the box is busy, and each device+resolution keeps its own scaling. |
|
||||
| **Workstation** | The multi-monitor daily driver. Your displays come back exactly where you arranged them, with per-client identity and an exclusive desktop. |
|
||||
|
||||
## Options reference
|
||||
|
||||
Choose **Custom** in the console to set these directly.
|
||||
|
||||
### Keep alive
|
||||
|
||||
How long the virtual display survives after your last session disconnects. On a gamescope game host,
|
||||
this also keeps the **game itself running** so you can reconnect straight back into it.
|
||||
|
||||
- **Off** — tear the display down at session end (nothing lingers).
|
||||
- **A duration** (seconds) — keep it for that long; a reconnect inside the window drops you straight
|
||||
back in, with no re-negotiation and no desktop reshuffle.
|
||||
- **Forever** — keep it until you stop the host or **release it** from the console (Host → *Virtual
|
||||
displays* → *Release*). This is the gaming-rig model.
|
||||
|
||||
Default: **10 seconds**. Windows has always lingered 10 s; the Linux backends previously tore down
|
||||
immediately — a short linger makes reconnects smoother on both.
|
||||
|
||||
**A reconnect always resumes the kept display** — the host recognises your device and hands back the
|
||||
same display, even if you reconnect a second or two after dropping (before it has noticed you left).
|
||||
**Deliberately quitting** (closing the client, not a network drop) tears the display down at once,
|
||||
skipping the linger, so you don't leave a ghost behind. How quickly a *dropped* client is noticed is
|
||||
the QUIC idle timeout — 8 s by default, tunable with `PUNKTFUNK_IDLE_TIMEOUT_MS` (see
|
||||
[Legacy environment knobs](#legacy-environment-knobs)) if you want kept displays freed sooner.
|
||||
|
||||
> **Keep-alive + Exclusive keeps your physical monitors dark after you disconnect**, until the
|
||||
> linger expires or you release the display. That's intentional for a dedicated gaming box, but
|
||||
> don't set a long/forever keep-alive together with Exclusive on a machine whose monitors you also
|
||||
> use in person — use **Shared desktop** there instead.
|
||||
|
||||
### Topology
|
||||
|
||||
What punktfunk does with your monitor layout while it streams.
|
||||
|
||||
- **Extend** — add the virtual display alongside your real monitors; touch nothing else.
|
||||
- **Primary** — make the virtual display your primary output; your physical monitors stay on.
|
||||
- **Exclusive** — the virtual display becomes your **only** enabled output (physical monitors are
|
||||
disabled, then restored when streaming ends). This is what makes the streamed surface *be* the
|
||||
desktop, so panels and windows land on it.
|
||||
- **Automatic** *(default)* — Exclusive on Windows and on an auto-detected KDE/GNOME desktop
|
||||
("stream this desktop" means the streamed output *is* the desktop); Extend when you've pinned a
|
||||
specific compositor with `PUNKTFUNK_COMPOSITOR` (a test/CI posture).
|
||||
|
||||
Per-backend support:
|
||||
|
||||
| | KWin | Mutter/GNOME | Sway/wlroots | Windows |
|
||||
|---|---|---|---|---|
|
||||
| Extend | ✅ | ✅ | ✅ | ✅ |
|
||||
| Primary | ✅ | ✅ | ⚠️ treated as Extend | ✅ |
|
||||
| Exclusive | ✅ | ✅ | ⏳ following release | ✅ |
|
||||
|
||||
### Conflict handling · identity · layout
|
||||
|
||||
- **Conflict handling** — what happens when a *different* client connects while one is already
|
||||
streaming and asks for a different resolution: give it its own display (**separate**), take the
|
||||
box over (**steal**), share the existing display at its current mode (**join**), or refuse it
|
||||
(**reject**). On Linux, `separate` gives each client its own display on the shared desktop. On
|
||||
**Windows** a second client is **rejected** (a clean "host busy") even under `separate` — two
|
||||
clients can't yet share one virtual display's capture there (that's a later stage), so the live
|
||||
session is protected instead. A same-client *reconnect* never conflicts — it resumes.
|
||||
- **Identity** — whether each client gets a **stable display identity** so your desktop environment
|
||||
remembers its settings (see [Persistent scaling](#persistent-scaling)): one shared identity, one
|
||||
**per client**, or one **per client + resolution**.
|
||||
- **Layout / max displays** — when several clients each become a monitor of one desktop, this places
|
||||
them side by side (**auto**) or exactly where you arrange them in the console (**manual**, keyed to
|
||||
each client), up to **max displays**. Arrange them under Host → *Virtual displays* once two or more
|
||||
are streaming.
|
||||
|
||||
## Persistent scaling
|
||||
|
||||
Set your display **scaling** once and have it stick across reconnects. This works by giving each
|
||||
client a *stable display identity*, so your desktop environment keys its per-monitor settings to it.
|
||||
|
||||
| Host | Supported | How |
|
||||
|---|---|---|
|
||||
| **Windows** | ✅ today | Connect, set scaling in Settings while streaming — Windows remembers it per client. |
|
||||
| **KDE / KWin** | ✅ today | Set scaling in System Settings while streaming; KWin keys it to a stable per-client output name and reapplies it on reconnect. Validated live (150 %/125 % survive a full disconnect + reconnect). |
|
||||
| **GNOME / Mutter** | ❌ | GNOME's virtual-monitor API exposes no stable identity to key config on. |
|
||||
| **Sway / wlroots** | ❌ | Headless outputs can't carry a stable identity; pin scale in your sway config instead. |
|
||||
|
||||
## Legacy environment knobs
|
||||
|
||||
These `PUNKTFUNK_*` variables still work, but the console (and `display-settings.json`) supersede
|
||||
them — when a settings file exists, it wins.
|
||||
|
||||
| Legacy knob | Now expressed as |
|
||||
|---|---|
|
||||
| `PUNKTFUNK_MONITOR_LINGER_MS` | **Keep alive** → duration *(Windows)* |
|
||||
| `PUNKTFUNK_NO_ISOLATE` | **Topology** → Extend *(Windows)* |
|
||||
| `PUNKTFUNK_KWIN_VIRTUAL_PRIMARY` / `PUNKTFUNK_MUTTER_VIRTUAL_PRIMARY` | **Topology** → Exclusive (when set) / Extend (when `0`) |
|
||||
|
||||
One knob has no console equivalent — it's a transport tuning, not display policy:
|
||||
|
||||
- **`PUNKTFUNK_IDLE_TIMEOUT_MS`** (host, default `8000`) — how long the host waits before declaring a
|
||||
*dropped* client gone, which is when a kept display starts its linger (or is freed). Lower it (e.g.
|
||||
`3000`) to reclaim kept displays sooner after an ungraceful drop; it's clamped to ≥1 s and its
|
||||
keep-alive ping scales with it, so a live session never false-disconnects. A deliberate quit is
|
||||
instant regardless. Also `--idle-timeout-ms` on `punktfunk1-host`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**My physical monitors stayed off after I disconnected.** You have keep-alive set together with
|
||||
Exclusive topology — the display (and your isolated desktop) is being kept for the linger window.
|
||||
Release it from the console (Host → *Virtual displays*), or switch to the **Shared desktop** preset
|
||||
so streaming never disables your real monitors.
|
||||
|
||||
**The virtual output shows only my wallpaper.** Your topology is Extend, so the streamed display is
|
||||
an empty extension. Use **Primary** or **Exclusive** so your desktop actually lands on it.
|
||||
|
||||
**KWin virtual outputs need KWin ≥ 6.5.6.** Older KWin can't create the virtual output at all —
|
||||
see [requirements](/docs/requirements).
|
||||
Reference in New Issue
Block a user