Files
punktfunk/docs/releases/v0.21.0.md
T
enricobuehlerandClaude Opus 5 4065b5bd03
ci / rust (push) Failing after 12s
ci / web (push) Successful in 58s
ci / docs-site (push) Successful in 1m7s
apple / swift (push) Successful in 5m29s
ci / bench (push) Successful in 7m56s
windows-host / package (push) Failing after 8m13s
windows-host / winget-source (push) Skipped
decky / build-publish (push) Successful in 1m1s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
deb / build-publish (push) Successful in 9m28s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 1m7s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m0s
deb / build-publish-host (push) Successful in 10m49s
android / android (push) Successful in 12m19s
deb / build-publish-client-arm64 (push) Successful in 11m37s
ci / rust-arm64 (push) Successful in 13m59s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6m36s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 7m45s
arch / build-publish (push) Successful in 21m57s
apple / screenshots (push) Successful in 25m13s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m15s
docker / build-push-arm64cross (push) Successful in 8s
docker / deploy-docs (push) Successful in 30s
docs(gamescope): the missing cursor was never the whole story
"The mouse cursor isn't included in the captured image" sat in Known Limits
contradicting the section above it, which already explained that the host draws
the pointer back in. Both are half-true and the difference matters to a reader
choosing whether to install anything: gamescope does leave the pointer out, you
do still see one, and what it costs is a full pass over every frame — plus, on
the fastest encode paths, the pointer itself, because a fixed-function front
end has nowhere to blend it.

Which is the real argument for `punktfunk-gamescope` on a box that will never
turn HDR on, and the page never made it.

Release notes gain the spawn-flag verification and the packaging wiring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 18:03:30 +02:00

58 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Update whenever it suits you — the app and the machine you stream from can be updated one at a time, and everything already paired keeps working.
The headline: on Linux, Punktfunk's host can now stream one of your machine's own physical monitors instead of always creating a virtual one. This release also carries a KDE Plasma fix found while building it: on Plasma 6.7 and newer, the host had silently been falling back to a slower, less reliable way of arranging displays on every single session.
## New: stream one of your actual monitors, not just a virtual screen
Until now, Punktfunk's Linux host always created its own virtual display to stream from — even on a machine you're sitting in front of, with real monitors already showing something. You can now point it at one of your machine's real monitors instead: pick it from a new **Streamed screen** card in the console, or pin it once in the host's configuration for an unattended box.
The monitor keeps working exactly as it always did — Punktfunk mirrors it rather than taking it over, so nothing about your own setup changes. It streams at that monitor's own resolution and refresh rate; your device scales to fit. Works with **KWin, GNOME, sway and Hyprland**, in both the Punktfunk app and Moonlight, and applies whether you connect straight away or pick up a saved display preset later.
A machine you pin from the host's own configuration shows the choice in the console as locked, with the reason, rather than offering a control that would silently lose to it. And on a machine you actually log into day to day (rather than a dedicated streaming box), the host now restarts itself when your desktop session does, so it recovers on its own instead of quietly going stale until the next stream fails to start.
For headless and unattended setups, two new commands help you find and verify the right monitor without a screen attached: `punktfunk-host list-monitors` lists what the machine has (names, geometry, which one is pinned), and `punktfunk-host mirror-test` confirms frames are actually flowing from the one you picked.
## New: HDR on the gamescope path (opt-in)
Streaming Steam Gaming Mode from a Linux box has always been SDR — not because the encoder couldn't
do better, but because gamescope hands its picture to Punktfunk already tone-mapped down to 8-bit.
That gap is now closed, with a small companion package: install **`punktfunk-gamescope`** (gamescope
plus a patch that adds the 10-bit BT.2020 PQ formats to its capture output — offered upstream) and
set `PUNKTFUNK_GAMESCOPE_HDR=1`, and games render in real HDR while the stream carries HDR10 to an
HDR-capable client.
It sits *beside* your system's gamescope rather than replacing it — your own Gaming Mode is
untouched — and Punktfunk only uses it for the sessions it starts itself. On Bazzite it rides in
the Punktfunk sysext; there's an Arch package, a NixOS option, and a build script for everything
else. `punktfunk-host hdr-probe` tells you exactly which pieces are in place.
Opt-in for this release while it soaks: without the knob, or without the extra package, the
gamescope path streams SDR exactly as before.
## Fixed: newer KDE Plasma silently fell back to a slower, less reliable way to arrange displays
On current KDE Plasma (6.7 and newer), Punktfunk's direct way of talking to the desktop about display layout stopped seeing any displays at all, with no visible error — every session quietly fell back to an external helper tool that's known to hang under load, exactly the thing the previous release's Plasma fix was meant to stop relying on. Fixed: Punktfunk now recognizes both the way older and newer Plasma releases announce their displays.
## Under the hood (for developers)
- **Nothing changed on the wire.** Streaming protocol stays at version **2**, the embeddable C ABI at **13**, the Windows virtual-display driver protocol at **6** — 0.180.21 hosts and clients keep mixing freely. `GET /display/monitors` is a new endpoint (always 200, an explained empty list on a compositor-less host or a nested gamescope session); the display policy gains an optional `capture_monitor` field, resolved env-over-policy so a host.env pin outranks a console change. Both are additive.
- **Monitor enumeration is one read per compositor**, beside the code that already speaks its dialect: KWin's `kde_output_device_v2`, Mutter's `DisplayConfig.GetCurrentState`, `swaymsg get_outputs` (through a `swaymsg_query` helper after the bare command form mis-parsed `-t get_outputs` as an unknown command), `hyprctl monitors`. Logical geometry throughout — origin, not size, is what identifies a head.
- **Mirroring routes through `vdisplay::open` on all four backends**, arriving as a `VirtualDisplay` with `DisplayOwnership::External`, which by construction gets no keep-alive, no topology participation and no reuse, and ignores the client's requested mode — a real monitor runs at the mode its owner set. KWin uses `zkde_screencast`'s `stream_output` (the connector name is the selection, no dialog); Mutter uses the same private ScreenCast session the virtual path drives, `RecordMonitor` instead of `RecordVirtual`, deliberately outside the topology lock since mirroring adds or removes nothing; sway and Hyprland reuse their existing portal-chooser config (`xdpw`'s `Monitor: NAME`, `xdph`'s `[SELECTION]screen:NAME`), now serialized against the handshake that reads it. A mirrored display refuses a mid-stream reconfigure (Linux only — a pin on Windows streams nothing different).
- **The GameStream/Moonlight portal source now honors the pin too** — it previously always went straight to the portal chooser regardless — reusing the same `MirrorDisplay` path, with which screen a pooled capturer is showing added as a reuse key beside HDR-ness and cursor mode, so a live pin change between two connects can't leave a client streaming the previous screen.
- **libei absolute-coordinate resolution changed from matching by mode size to `mapping_id` → origin → size → first**, since two outputs can share a size but never a top-left, and a mirrored head's region is not the client's stream size at all. A named anchor that matches nothing falls back down the ladder rather than stranding input, and both a miss and a match now log once per distinct answer. `punktfunk-host anchor-test` exercises the ladder against a live compositor with two same-sized outputs — the one case a unit test can only simulate.
- **`SWAYSOCK` is now derived rather than required to be inherited** — by the compositor's known PID, then the newest socket owned by the host's user — closing the last session variable a `systemd --user` host lacked for sway. A new drop-in (`PartOf`/`WantedBy` on `graphical-session.target`, opt-in, shipped under `/usr/share`) restarts a desktop-login host with its desktop, so a Wayland socket and portal connection that die with a Plasma/GNOME restart don't leave the daemon silently unable to recover.
- **KWin's in-process output-management path now also binds `kde_output_device_registry_v2`**, not just the per-output `kde_output_device_v2` globals it originally shipped with — KWin 6.7 stopped advertising the latter, so the module written specifically to avoid shelling out to `kscreen-doctor` (0.19.x) saw zero devices and silently degraded to it on every session. Both models are supported now; registry-sourced devices arrive one Wayland round-trip later, so the handshake gains one conditional extra barrier.
- **gamescope HDR is decided statically, before the display exists.** The punktfunk/1 Welcome fixes a session's bit depth up front and cannot take it back (PQ frames on an 8-bit encoder are a deliberate hard error), so the capability answer is the identity of the gamescope binary the host will spawn — a `+pfhdr` marker in its `--version` banner, probed once per boot — never an optimistic negotiation. The capture-side gate became source-aware (`capturer_supports_hdr_for(compositor)`), the HDR negotiation-failure latch became per-source (a wedged monitor mirror no longer disables a gamescope session's HDR, or vice versa), and the keep-alive reuse key gained `hdr` so a display brought up SDR can never be handed to an HDR session. The GameStream plane's live BT.2100 monitor probe is now scoped to the portal source — a headless gamescope box has no monitor to be in HDR mode.
- **The gamescope patch mirrors code already in gamescope's tree.** Its PipeWire node additionally offers `xRGB_210LE`/`xBGR_210LE` with MANDATORY SMPTE ST.2084 + BT.2020 properties, mapped to `DRM_FORMAT_XRGB2101010`/`XBGR2101010` — the same 10-bit capture texture the HDR AVIF screenshot path allocates — and `paint_pipewire()` composites into them with the HDR screenshot LUT set and `EOTF_PQ`, which is exactly the `bHDRScreenshot` branch. The new formats are listed last, so every existing consumer keeps negotiating the 8-bit stream bit-for-bit, and the fixed PQ container is deliberately *not* conditional on the focused app being HDR (a stream's colourimetry must not follow what the game happens to render).
- **Vulkan Video learned 10-bit**, which is what keeps AMD/Intel HDR on the good path: an HDR session opens a 10-bit video profile (HEVC Main10 / AV1 Main at 10 bits) with a `G10X6…3PACK16` picture and DPB, headers carrying the depth and the BT.2020/PQ CICP triplet — an SPS `bit_depth_*_minus8 = 2` for HEVC, `high_bitdepth` plus the matching sequence-header OBU bits for AV1 — and a new `rgb2yuv10.comp`: the 8-bit BT.709 shader's twin, doing a pure BT.2020 NCL matrix on the already-PQ-encoded samples (there is no transfer function to apply, and applying one would be wrong) and writing 10-bit values into the high bits of `R16`/`RG16` scratch planes that are size-compatible with the picture's. That keeps the two things HDR would otherwise cost on this vendor: real RFI loss recovery, and the compute CSC's cursor blend — the only way a gamescope pointer reaches the stream at all.
- **GameStream advertises its 10-bit codec bits per codec.** `ServerCodecModeSupport` layered `SCM_HEVC_MAIN10` and never `SCM_AV1_MAIN10` — a blanket omission on the theory that the GameStream AV1 path was unconfirmed, even though the SDR baseline has always offered AV1 Main8, so the depth was never the uncertain part. Each 10-bit bit is now gated on that codec's own `can_encode_10bit` probe AND the baseline already advertising it, and the RTSP honor degrades a session whose NEGOTIATED codec can't carry 10 bits rather than labelling an 8-bit stream PQ. `host_hdr_capable` became codec-agnostic to match (any 10-bit-capable codec makes the host HDR-capable; which one a session gets is the session's question).
- **The Vulkan encode backend is now capability-probed per codec AND depth**, mirroring what the direct-SDK NVENC path already does with its GUID probe. `vkGetPhysicalDeviceVideoCapabilitiesKHR` is asked against the very profile the session open will build, so the dispatcher's prediction cannot disagree with reality: a device that can encode 10-bit keeps the Vulkan path, one that can't routes to libav VAAPI *before* burning a failed session open, and `can_encode_10bit` reports the union of what VAAPI and Vulkan Video can do rather than VAAPI's answer alone (which was under-reporting 10-bit on hardware that could do it).
- **A gamescope session can now be zero-copy end to end.** gamescope keeps the pointer out of its PipeWire node (it lives on a hardware plane for scanout), so the host always reconstructed it from XFixes and blended it into every frame — and *that blend* is what forced the encode path onto its compute colour-conversion arm, since the zero-copy RGB-direct source hands the captured buffer to a fixed-function front end with no blend stage. A second carried gamescope patch adds `--pipewire-composite-cursor`, which paints it in using the same `MouseCursor::paint` call the scanout composite uses; the repaint test grows the cursor's state alongside the commit ids, so a pointer-only move still produces a frame and a hidden cursor is erased. The host reads a monotonic `+pfhdr<N>` patch level from the `--version` banner — one probe now answering both "can it do HDR" and "does it paint the cursor" — and stops attaching the XFixes reader and blending when the answer is yes.
- **The two indirect spawn modes now verify that their flags arrived.** A host-managed `gamescope-session-plus` receives the HDR and cursor flags through `GAMESCOPE_BIN` + `PF_HDR_ARGS`, and SteamOS through a PATH shim — conventions, not guarantees. A session that ignored either would exec the distro's gamescope with none of them, and while the HDR half fails loudly (capture negotiation times out against the bit depth the Welcome already fixed), a lost `--pipewire-composite-cursor` was **silent**: the host had been told the compositor would paint the pointer, so it painted none, and the stream simply had no cursor. Both managed paths now read the running compositor's `/proc/<pid>/cmdline` once its node appears and refuse the session on a missing flag; since the plan is fixed by then (`cursor_blend` feeds the encoder open, which precedes the display), the capability is latched off for the process and the retry resolves a correct SDR host-composited session — one rejected attempt per boot, then it converges. The check fails **open** at every ambiguity: no flags expected, or no readable gamescope in `/proc`, says nothing.
- **`punktfunk-gamescope` is now built by CI on the channels that ship it.** `rpm.yml` builds it in the matching Fedora container and hands it to `build-sysext.sh --gamescope`; `arch.yml` builds `packaging/gamescope/PKGBUILD` into the same pacman repo. Both are cached on `packaging/gamescope/**` (that tree depends on nothing else in the repo, so a normal push restores a binary rather than spending ten minutes on someone else's C++) and both are best-effort, because the packages those workflows exist to publish must not hinge on a gamescope build. The Arch PKGBUILD and the nix derivation had each drifted to a stale patch list — the PKGBUILD naming two patches when there are three, the nix override naming the level-1 banner patch after level 2 landed — so both now read the patch *directory*, and the PKGBUILD delegates the whole build to the shared script (asserting its own pinned rev matches) instead of re-deriving the meson invocation, which had already lost the `force_fallback_for=wlroots` that keeps the binary startable off the build host. The nix override's `gamescope.unwrapped` requirement was wrong on current nixpkgs, where `gamescope` *is* the buildable derivation; it now prefers `.unwrapped` where it exists and checks the result is something `overrideAttrs` can actually patch.
- **The zero-CSC RGB-direct (EFC) source works in HDR too.** The `VK_VALVE_video_encode_rgb_conversion` probe now asks for the BT.2020 model and the captured 10-bit packed-RGB format instead of assuming BT.709/BGRA, and the session selects the matching model — so an HDR session with no pointer to composite (the GameStream desktop mirror) hands the captured buffer straight to the encoder's fixed-function front end and runs no host CSC at all. Sessions that DO composite a pointer keep the compute CSC, as before: the EFC cannot blend.
- **NVIDIA gained a zero-copy HDR leg**, and the VAAPI fallback needed no new encoder code. The VAAPI path already ingested XR30 dmabufs into `format=p010:out_color_matrix=bt2020`. On NVIDIA the packed 10-bit frame now travels LINEAR dmabuf → Vulkan bridge → CUDA → NVENC `ARGB10`/`ABGR10`, letting NVENC do the BT.2020 CSC itself: no host CSC pass, no depth loss, and the cursor-blend compute shader gained two 10-bit modes so the pointer survives. The tiled EGL de-tile blit is still 8-bit and HDR never routes through it. A host without the direct-SDK NVENC backend keeps HDR on the CPU path, since libav's HDR route swscales into a P010 hardware frame that a packed-10-bit CUDA buffer cannot fill.
- **CI-only fix:** the winget release-verification step's `envs:` allow-list was a step-level sibling of `with:`/`env:` instead of nested inside `with:`, so `appleboy/ssh-action` never actually received it as an input and the step kept failing on every tag. Moved to match how `REGISTRY_TOKEN` is already forwarded elsewhere.
See [v0.20.1](v0.20.1.md) for the fixes carried by that release (Windows install/winget, GameStream opt-in default, the gamescope Game Mode takeover hardening, a laptop-panel stall fix, and the PyroWave latency-creep bundle) — all included here too, since this release supersedes it.