fix(stats): decode stage measures GPU completion; Alt+Enter fullscreen alias

The Vulkan path's receive_frame returns at SUBMISSION (~0.1 ms) — the
hardware decodes asynchronously, so the decode stat was truthful but
measured the wrong boundary. The pump now ships the frame to the
presenter FIRST, then waits the frame's timeline fence (vkWaitSemaphores
resolved through the shared device's proc chain) and stamps
received→decode-COMPLETE — true NVDEC time at zero pipeline cost, since
the presenter's own GPU wait is what actually gates sampling. Software/
VAAPI keep their synchronous stamps.

Also: Alt+Enter joins F11 as the fullscreen toggle (some keyboards' Fn
layer sends a media key for plain F11 — observed on glass as
'F11 only works with shift'); the shell's shortcuts panel lists both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 22:11:23 +02:00
parent a5bb5ec4d5
commit 349d16382e
14 changed files with 276 additions and 26 deletions
+2 -2
View File
@@ -34,10 +34,10 @@ On Linux the host **rewrites `WAYLAND_DISPLAY` / `XDG_CURRENT_DESKTOP` / `XDG_RU
| Setting | Values | Meaning |
|---|---|---|
| `PUNKTFUNK_COMPOSITOR` | `kwin` · `mutter` · `gamescope` · `wlroots` (aliases: `kde`/`plasma`, `gnome`, `sway`/`hyprland`) | Which backend creates the virtual display. **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 to auto-detect;** set only to force one. |
| `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. Auto-detected with the compositor. |
| `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. |
| `PUNKTFUNK_ENCODER` | `auto` · `nvenc` · `vaapi` (Linux) · `amf` · `qsv` (Windows) · `software` | Encoder backend. `auto` (default) detects the GPU vendor: NVIDIA→NVENC, AMD→VAAPI/AMF, Intel→VAAPI/QSV. `software` (aliases `sw`/`openh264`) is the GPU-less H.264 path on both platforms — on Windows `auto` falls back to it when no GPU is found; on Linux it is **explicit-only** (`auto` never picks it). |
| `PUNKTFUNK_RENDER_NODE` | path | Linux DRM render node for zero-copy (default `/dev/dri/renderD128`). Set on multi-GPU boxes to pick the right GPU. |
+90
View File
@@ -0,0 +1,90 @@
---
title: Hyprland
description: Configure a punktfunk host on a Hyprland session — headless output via hyprctl, capture via xdg-desktop-portal-hyprland.
---
Hyprland is a **first-class backend.** The host adds a per-client headless output at the client's
exact mode with `hyprctl`, captures it through the **xdg-desktop-portal-hyprland (xdph)** ScreenCast
portal (zero-copy dmabuf), and injects input via the wlroots virtual pointer/keyboard protocols —
which Hyprland still implements even after dropping wlroots in v0.42.
This is a distinct backend from [Sway / wlroots](/docs/sway): Hyprland has its own IPC (`hyprctl`)
and its own portal (xdph), so it is auto-detected and driven separately.
This page assumes the package is already installed — see [Arch](/docs/arch), [Ubuntu](/docs/ubuntu),
or [Fedora](/docs/fedora).
> New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of
> the machine, so keep it on a trusted LAN or VPN and require pairing.
## 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`:
```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.
## How it works
- **Video** — the host runs `hyprctl output create headless PF-1` and applies a monitor rule for the
client's exact mode. Outputs are **named**, so there's no before/after diffing. Both config eras
are supported: `hyprctl keyword monitor …` (≤ 0.54) and the Lua `hyprctl eval 'hl.monitor{…}'`
(≥ 0.55), selected from `hyprctl version`.
- **Capture** — it captures that output through the **xdg-desktop-portal-hyprland (xdph)** ScreenCast
portal. To pick the output without a GUI on a headless host, the host writes a managed
`~/.config/hypr/xdph.conf` pointing xdph's `custom_picker_binary` at a small shim that selects the
new output automatically — no interactive picker dialog to answer.
- **Input** — mouse and keyboard are injected via the wlroots **virtual pointer** and **virtual
keyboard** protocols (Hyprland kept them). Gamepads and audio are compositor-independent.
For how long the virtual output lives, and extend-vs-exclusive topology, see
[Virtual displays](/docs/virtual-displays).
## Requirements
- A running Hyprland session (any recent release; validate on both a ≤ 0.54 and a ≥ 0.55 install).
- **xdg-desktop-portal-hyprland (xdph)** installed and running — the host captures through its
ScreenCast portal, and steers its custom picker. Without it there is no video.
- The ScreenCast interface routed to xdph — see `scripts/headless/portals.conf` (a `[Hyprland]`
section pins `org.freedesktop.impl.portal.ScreenCast=hyprland`).
## Permission system
Hyprland's permission system (`ecosystem.enforce_permissions`, 0.49+, **off by default**) can deny
direct screencopy and virtual-input clients — and denial is **silent**: capture goes to *black
frames* and input is *dropped*, with no error. If you've enabled it, grant the host explicitly in
your Hyprland config:
```ini
ecosystem {
enforce_permissions = true
}
permission = /usr/bin/punktfunk-host, screencopy, allow
permission = /usr/bin/punktfunk-host, virtual-pointer, allow
permission = /usr/bin/punktfunk-host, virtual-keyboard, allow
```
The host logs a warning at startup when it detects enforcement is on. (Adjust the binary path to
where your package installed `punktfunk-host`.)
## Start the host
With the backend selected, start the host from **inside your Hyprland session**:
```sh
systemctl --user enable --now punktfunk-host
journalctl --user -u punktfunk-host -f
```
## Bring up the console and pair
Enable the web console, read its login password, and arm PIN pairing — see
[The Web Console](/docs/web-console). Then [connect a client](/docs/clients).
+1
View File
@@ -20,6 +20,7 @@
"kde",
"gnome",
"gamescope",
"hyprland",
"sway",
"running-as-a-service",
"virtual-displays",
+3 -3
View File
@@ -26,11 +26,11 @@ is also available. Setup splits along two axes: you **install** the package per
- [KDE Plasma (KWin)](/docs/kde)
- [GNOME (Mutter)](/docs/gnome)
- [Steam / gamescope](/docs/gamescope)
- [Hyprland](/docs/hyprland)
- [Sway / wlroots](/docs/sway)
Pick your distro to install, then your desktop to configure — the two are independent. Other
wlroots compositors (Hyprland) work but aren't a primary target; the host still needs one of these
compositor backends to create a virtual display.
Pick your distro to install, then your desktop to configure — the two are independent. The host
needs one of these compositor backends to create a virtual display.
> **Windows host:** punktfunk also runs as a native host on **Windows 11 22H2 or newer (x64)** — a
> signed installer that registers a service and bundles a virtual-display driver (whose driver-
+13 -10
View File
@@ -1,12 +1,15 @@
---
title: Sway / wlroots
description: Configure a punktfunk host on a wlroots compositor (Sway, Hyprland).
description: Configure a punktfunk host on a wlroots compositor (Sway, River).
---
The wlroots family can host — but **Sway is the only validated path.** The host adds a per-client
headless output at the client's exact mode and captures it through the xdg-desktop-portal-wlr (xdpw)
ScreenCast portal, injecting input via the wlroots virtual pointer/keyboard protocols. Hyprland and
other wlroots compositors are best-effort (see [How it works](#how-it-works) for the caveat).
Sway (and other wlroots-proper compositors like River) can host: the host adds a per-client headless
output at the client's exact mode with `swaymsg create_output` and captures it through the
xdg-desktop-portal-wlr (xdpw) ScreenCast portal, injecting input via the wlroots virtual
pointer/keyboard protocols.
> On **Hyprland**? It's a separate first-class backend (its own `hyprctl` IPC and xdph portal) —
> see [Hyprland](/docs/hyprland). This page is for sway and other wlroots-proper compositors.
This is **not a primary target.** It works and is validated live on **sway 1.11** (zero-copy), but it
sees far less testing than the KDE and GNOME paths — expect rougher edges. If you have a choice,
@@ -24,7 +27,7 @@ The host auto-detects a wlroots session, so you usually need nothing here. To fo
these in `~/.config/punktfunk/host.env`:
```ini
PUNKTFUNK_COMPOSITOR=wlroots # aliases: sway, hyprland
PUNKTFUNK_COMPOSITOR=wlroots # aliases: sway, wlr (Hyprland has its own: 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.
@@ -35,9 +38,8 @@ See [Configuration](/docs/configuration) for the full reference.
## How it works
- **Video** — the host adds a headless output at the client's exact mode with `swaymsg create_output`.
This uses Sway's IPC specifically; other wlroots compositors (Hyprland, …) don't expose an
equivalent, so virtual-output creation isn't wired up for them yet — Sway is the supported wlroots
path today.
This uses Sway's IPC specifically; other wlroots-proper compositors (River, …) are best-effort on
this path. (Hyprland is driven by its own [backend](/docs/hyprland), not this one.)
- **Capture** — it captures that output through the **xdg-desktop-portal-wlr (xdpw)** ScreenCast
portal. The host writes a managed chooser config so the output pick is automatic — no interactive
picker dialog to answer.
@@ -49,7 +51,8 @@ For how long the virtual output lives, and extend-vs-exclusive topology, see
## Requirements
- A running wlroots session (Sway, Hyprland, …).
- A running wlroots-proper session (Sway, River, …). On Hyprland, use the
[Hyprland backend](/docs/hyprland) instead.
- **xdg-desktop-portal-wlr (xdpw)** installed and running — the host captures through its ScreenCast
portal. Without it there is no video.
+1 -1
View File
@@ -101,7 +101,7 @@ What punktfunk does with your monitor layout while it streams.
Per-backend support:
| | KWin | Mutter/GNOME | Sway/wlroots | Windows |
| | KWin | Mutter/GNOME | Sway/wlroots · Hyprland | Windows |
|---|---|---|---|---|
| Extend | ✅ | ✅ | ✅ | ✅ |
| Primary | ✅ | ✅ | ⚠️ treated as Extend | ✅ |