Files
punktfunk/docs/releases/v0.21.0.md
T
enricobuehler 479f0965ee feat(encode/vulkan): Vulkan Video encodes 10-bit, so AMD/Intel HDR keeps the good path
The Vulkan Video backend was 8-bit for no structural reason — the API has
`VK_VIDEO_COMPONENT_BIT_DEPTH_10_BIT` and `PROFILE_IDC_MAIN_10` in the very
fields this pinned to 8 and MAIN, and AMD VCN and Intel both encode Main10.
It was six hardcoded sites, and the cost of leaving them was paid twice over:
an HDR session had to take libav VAAPI, losing real RFI loss recovery AND the
compute CSC's cursor blend — which on gamescope is the only way the pointer
reaches the stream at all, since gamescope has no embedded-cursor mode.

An HDR session now opens a Main10 profile with 10-bit component depths, a
`G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16` picture + DPB, and an SPS carrying
`bit_depth_*_minus8 = 2` with the BT.2020/PQ CICP triplet instead of BT.709.

`rgb2yuv10.comp` is the CSC's twin, and the two interesting parts of it are:

* it is a PURE 3x3 matrix. The samples arrive already PQ-encoded (gamescope
  composites into the PQ container), so BT.2020 NCL applies to the code values
  as they are — there is no transfer function to apply here and applying one
  would be wrong;
* the scratch planes are `R16`/`RG16`, not the picture's plane formats. The
  10-bit ycbcr plane formats are not storage-image formats, so the shader
  writes the value into the HIGH bits by hand (`code10 << 6`, hence the
  `64/65535` factor and not `1/1023`) into planes that are merely
  SIZE-compatible with the picture's — which is all `vkCmdCopyImage` requires.

Scope and safety:

* HEVC only. AV1 10-bit encode has far thinner driver coverage, and a session
  open is not the place to gamble on it — those stay on VAAPI, as does a device
  that fails the Main10 profile query inside the open (the pre-existing "failed
  Vulkan open falls back to VAAPI" net, no new probe needed).
* HDR pins the compute-CSC arm over the EFC RGB-direct one, which the EFC could
  not serve anyway: its fixed-function conversion is 8-bit BT.709 narrow with
  no knob for BT.2020.
* `open_inner` binds `hdr` to the parameter-set HEADER bytes, so the depth flag
  is `ten_bit` there — the one name collision this change had to route around.
2026-07-28 18:03:30 +02:00

11 KiB
Raw Blame History

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. One thing to know on AMD and Intel: keep the codec on HEVC — a 10-bit AV1 session falls back to a slower encode path that also loses the on-screen mouse pointer. NVIDIA is unaffected either way.

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 (HEVC Main10), which is what keeps AMD/Intel HDR on the good path: an HDR session opens a Main10 video profile with 10-bit component depths, a G10X6…3PACK16 picture and DPB, an SPS carrying bit_depth_*_minus8 = 2 and the BT.2020/PQ CICP triplet, 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. 10-bit AV1 still routes to libav VAAPI: encode-side driver coverage for it is far thinner than Main10. A device that can't do Main10 fails the profile query inside the open and lands on the same VAAPI fallback as any other unsupported config.
  • AMD/Intel HDR needs no new encoder code on the VAAPI fallback; NVIDIA gained a zero-copy leg. 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 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.