Files
enricobuehlerandClaude Fable 5 bfd0de8973
ci / rust-arm64 (pull_request) Successful in 1m30s
apple / swift (pull_request) Successful in 1m59s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 2m5s
ci / bun-nix (pull_request) Successful in 46s
ci / docs-site (pull_request) Successful in 1m25s
ci / docs-drift (pull_request) Successful in 46s
android / android (pull_request) Successful in 8m17s
ci / rust (pull_request) Successful in 22m6s
docs: two audiences, one home per fact — get-started rewrite, a Switching-from-Sunshine page, and install commands quoted from platforms.json
WP2 of the docs-and-onboarding overhaul (punktfunk-planning design/docs-and-onboarding-overhaul.md),
stacked on #337 (WP0+WP1).

Nav (meta.json) is now Get started / Guides / Switching from Sunshine / Reference / Troubleshooting.

Get-started track, rewritten for someone with no Linux expertise — one task per page, happy path,
under two minutes each: quickstart (5 steps), install (a pick-your-system hub), ubuntu, debian,
fedora, arch, bazzite (+ a firewall step it was missing), a new nixos page (moved out of install),
pairing. Every distro page quotes its install snippet through a new <Install platform="…"/> MDX
component that reads docs-site/src/data/platforms.json — a byte-identical snapshot of
data/platforms.json (the Docker build context is docs-site/ alone, same arrangement as
openapi.json) which scripts/ci/check-docs-drift.sh now gates. <Ports/> renders the port table the
same way on a new reference page, ports.mdx, so the four distro pages stop restating port lists.

platforms.json itself was fixed while wiring it up: the apt snippet lacked the keyring lines, the
winget one lacked `winget source add`, the Arch repo-add is now idempotent (grep guard — the
documented double-append gotcha disappears at the source), names match the real floors (Ubuntu
26.04+, Windows 11 22H2+), a web-console port entry and the firewall profile names were added, and
client platforms (Windows MSIX, macOS, TestFlight, Play, Steam Deck) so the website can render its
cards from the same file.

Dense material moved down rather than deleted: build-from-source.md (the three distro appendices),
the Mint/LMDE/Cinnamon analysis → requirements.md, TOFU/`--open` → security.md, Secure Boot MOK
paths and the Arch libavcodec soname refusal → troubleshooting, sysext channels/rollback/rebase →
updating.md, the 0.28.1 cert note → windows-host.

New page switching-from-sunshine.md: coexistence via the 47990 move (PUNKTFUNK_MGMT_BIND), the
Windows display-topology overlap, detect-conflicts exit semantics (1 only for an active host — the
troubleshooting page said "non-zero if any"), what maps to what, migration steps. The
troubleshooting Sunshine section is tightened and points there.

The three facts whose only home was a README now live in troubleshooting — the ffmpeg-libs weak
dependency (no NVENC on Fedora), the pacman double-append error, the ds_inhibit SELinux storm with
DualSense pads on Bazzite — and packaging/bazzite + packaging/arch READMEs are pointers.

Stale claims corrected against the tree: the packaged unit runs the native-only `serve` and
GameStream is opt-in on every route (kde, hyprland, security, steamos-host said otherwise);
host.env is optional (EnvironmentFile=-); Fedora 43 uses the `bazzite` group; the iOS clipboard
bridge exists; PUNKTFUNK_GAMESCOPE_SDR_NITS defaults to 203.

Guides trimmed for verbosity (~5% by words — they are fact-dense; every heading, command, number,
env var and link target is preserved, verified by token-inventory diffs). Tooling: docs-site README
and CONTRIBUTING describe the platforms snapshot; input/uninstall/support-matrix links retargeted.

Verified: check-docs-drift.sh and check-docs-links.sh green, docs-site `bun run build` + `bun run
lint` green, and the built site served locally to confirm every cross-page anchor the rewrite
links to (37) renders, plus the <Install/> blocks and the <Ports/> table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 19:08:38 +02:00

12 KiB

title, description
title description
HDR How an HDR10 stream is decided end to end — the four things that must all be true, what each host and client can actually do, and how to check which link is missing.

An HDR session carries a 10-bit BT.2020 PQ (HDR10) picture from the host's display to your screen. It is on by default wherever it works; otherwise the session streams 8-bit BT.709 SDR. The host decides before the first frame — it resolves every gate below and tells the client what it will really send — so nothing on this page takes effect mid-stream; reconnect after changing any of it.

The chain

Four things must all be true. If you got SDR when you expected HDR, one of these is why.

  1. The source. What the host captures must hand it 10-bit PQ pixels. This link fails most often and is entirely host-side — see Per host.
  2. The encoder. The host GPU must encode 10-bit for the session's codec. The host probes by opening a tiny real encoder once per GPU and codec, and believes the answer. (PyroWave skips the probe — its own rule is below.)
  3. The codec. Only HEVC, AV1 and PyroWave have a 10-bit path — see Codec rules.
  4. The client. It must advertise 10-bit and HDR (its HDR setting) and be able to present or tone-map PQ.

The host allows 10-bit by default (PUNKTFUNK_10BIT). It only ever allows — the client's setting is the real per-session switch.

Per host

Windows

The Windows host turns HDR on for the session's virtual display itself when 10-bit was negotiated — you don't have to enable "Use HDR" in Windows Settings. It enables advanced colour at capture open, waits for it to settle, then composes in FP16 and encodes P010 BT.2020 PQ. The reverse holds too: an SDR session forces advanced colour off on that display, so a client that asked for 8-bit is never handed PQ. If enabling it fails, the host logs a loud error and encodes 8-bit anyway — the client was already told HDR, so that is the one place a Punktfunk label can outrun the picture.

  • HDR and 4:4:4 compose on Windows, not on Linux. A Windows host carries both: the capture path writes full-resolution 10-bit chroma and NVENC encodes HEVC Main 4:4:4 10, so full chroma costs nothing on an HDR desktop; PyroWave does the same there, in 16-bit planes. On Linux the 4:4:4 route is 8-bit, so a session that negotiates both resolves back down to SDR — full chroma wins. AV1 never carries 4:4:4 anywhere: Range Extensions are HEVC-only.
  • Vulkan games need the bundled layer. NVIDIA and AMD Vulkan drivers refuse to advertise any HDR colour space for a surface on an indirect (virtual) display, so Vulkan games decide the device "does not support HDR" — though the driver happily presents an HDR swapchain there. The host installer ships an implicit Vulkan layer, VK_LAYER_PUNKTFUNK_hdr_inject, that adds those formats back (installer task Install the HDR Vulkan layer, ticked by default). It self-gates on the monitor's live advanced-colour state, so it does nothing on an SDR session, and it skips a built-in list of kernel-anti-cheat titles. DISABLE_PF_VKHDR=1 in a game's environment switches it off for that process; PF_VKHDR_EXCLUDE=foo.exe,bar.exe skips further executables by name. D3D11/D3D12 games need none of this.

Linux + gamescope

Stock gamescope tone-maps its composite down to 8 bits before handing it over, so its capture output is SDR whatever the game rendered. Real HDR needs punktfunk-gamescope, a build carrying a patch that adds the 10-bit PQ formats to its PipeWire node. It installs beside your system gamescope rather than replacing it; HDR on gamescope has the package for each distro.

Before spawning anything the host settles two facts: the gamescope binary it will run carries the patch (its --version banner contains +pfhdr), and this host is the one starting the session rather than attaching to a node someone else started.

Attach mode is the trap. The patched build only reaches sessions the host spawns itself — managed, PUNKTFUNK_GAMESCOPE_SESSION, or a bare spawn. A session started by your display manager runs the distro's own gamescope, which offers neither the 10-bit formats nor the in-node cursor, and the host cannot tell that from the outside unless you pinned PUNKTFUNK_GAMESCOPE_NODE. With PUNKTFUNK_GAMESCOPE_ATTACH=1 and the patched build installed it reads the binary, believes HDR is available, and offers it; the attached session can't answer that negotiation, so the connect fails with no picture, the host latches an SDR downgrade for the rest of its life, and the next connect streams — in SDR.

That bites on Bazzite, where the sysext installs punktfunk-gamescope alongside a stock session gamescope. No template pins attach any more, so the managed default gets you HDR and the compositor-drawn cursor — but an older template did, and an upgrade never rewrites a host.env you already have: check yours for PUNKTFUNK_GAMESCOPE_ATTACH=1 and delete the line. If you deliberately stay on attach, set PUNKTFUNK_GAMESCOPE_HDR=0 so the failed attempt never happens. Attach also leaves the stream with no cursor; HDR on gamescope has the fix for that half.

SDR content — the desktop, the Steam overlay, an SDR game — rides the same PQ container, mapped in at PUNKTFUNK_GAMESCOPE_SDR_NITS, default 203 nits. That is BT.2408 reference white and the level our clients decode against, so the two ends agree out of the box. gamescope's own default is 400, nearly a stop brighter; hosts that let it float showed a glaring, over-saturated Steam UI and washed-out HDR game content on the same stream. Moving the knob re-opens that gap.

Linux + GNOME

A host serves two protocols: its own punktfunk/1 (the Linux, Windows, Apple and Android apps) and GameStream (Moonlight). GNOME HDR is available on the GameStream side only.

GNOME 50 added HDR screencast for real monitors only, so this route mirrors a monitor instead of creating a virtual display: set PUNKTFUNK_VIDEO_SOURCE=portal, put the monitor in HDR mode in Settings → Displays, and connect an HDR-capable client. PUNKTFUNK_CAPTURE_MONITOR=<connector> pins which head; when it is set the host checks that monitor's colour mode rather than asking whether any monitor is in HDR. If none is, the session degrades to 8-bit SDR and says so in the log.

A Punktfunk app connecting to a GNOME host over punktfunk/1 gets SDR. On that protocol the only Linux HDR source is the gamescope virtual output.

Linux virtual displays on KWin, Mutter and wlroots

These are SDR. Mutter's RecordVirtual streams and the KWin and wlroots virtual outputs are 8-bit upstream, so there is nothing to capture in 10 bits — no setting changes this. Streaming a physical monitor with the Streamed screen setting is SDR to a Punktfunk app too, HDR panel or not; the GNOME/GameStream route above is the only Linux monitor mirror that can be HDR.

Per client

Client HDR10 present Advertises HDR when
Linux (GTK) · Windows (WinUI 3) Yes — the Vulkan presenter switches to an HDR10 swapchain when the surface offers one the setting is on. It does not check your display first
macOS · iPhone · iPad Yes — Metal, rgba16Float in BT.2100 PQ with EDR the setting is on and the display reports HDR capability
Apple TV Yes — PQ passthrough once the session's display-mode switch lands; before that, tone-mapped to SDR in-shader the setting is on and the TV is HDR-capable
Android (phone + TV) Yes — HDR10 via the Surface dataspace plus static metadata the setting is on and the panel reports HDR10 or HDR10+. On an SDR panel the toggle is disabled
Moonlight Its own HDR toggle, which appears only when the host advertises a 10-bit codec

The Linux and Windows clients are deliberately looser: they advertise HDR whenever the setting is on and let the presenter sort out the display — HDR10 swapchain where the compositor offers one, tone-mapped to SDR where it doesn't. The stats overlay says which happened: HDR versus HDR→SDR.

One exception: frames from software decode never take the HDR10 swapchain, whatever the surface offers, so a client with no hardware HEVC decode presents an HDR stream on the SDR swapchain without a tone-map — washed out. Turn the client's HDR setting off there. The Steam Deck plugin streams through this same client.

Codec rules

  • HEVC — Main10. The usual HDR codec.
  • AV1 — 10-bit, where the GPU encodes it. Advertised separately from HEVC, so a box that does one and not the other tells the truth about each.
  • H.264 — never. High10 is not an encode mode on the hardware Punktfunk targets, so negotiation never asks. Pinning H.264 in your client settings pins the session to SDR.
  • PyroWave — carries HDR in 16-bit planes, but only from a Windows host. The Linux PyroWave capture path has no HDR colour conversion, so a Linux-hosted PyroWave session is SDR. Use HEVC or AV1 for HDR from Linux.

With full chroma: a Linux host encodes 4:4:4 at 8 bits, so a session that negotiates both resolves back down to SDR before the stream starts — on Linux, 4:4:4 wins. A Windows host carries HDR and full chroma at once. Full chroma is off until you turn it on, so this only bites if you did.

Check it

On a Linux host, one subcommand answers every link in the chain:

punktfunk-host hdr-probe

It prints, line by line: whether a monitor is in BT.2100 colour mode, whether the resolved gamescope offers 10-bit PQ capture, the state of PUNKTFUNK_GAMESCOPE_HDR, whether gamescope will paint the cursor into the capture node, the encoder's Main10 answer for HEVC and for AV1, whether the resolved compositor can do HDR on Punktfunk's own plane, and the combined GameStream capability.

Run it with the environment the host service has. The service loads host.env itself; your shell does not, and PATH decides which gamescope binary gets probed:

set -a; . ~/.config/punktfunk/host.env; set +a
punktfunk-host hdr-probe

There is no hdr-probe on Windows. Windows has instead a GPU colour self-test for the capture conversion, which needs no display or session:

punktfunk-host hdr-p010-selftest 1920x1080 nvidia

Pass your real capture size (heights like 1080 are not 16-aligned and take a different driver path) and, on a dual-GPU box, the vendor that encodes — intel, nvidia or amd.

From the client side, set the stats overlay to Detailed. On Linux and Windows it adds an HDR tag — or HDR→SDR when a PQ stream is arriving but the local surface can't present it. Android prints the negotiated depth and colour outright at the same tier (HEVC · 10-bit · HDR (BT.2020 PQ) · 4:2:0).

The switches

Host, in host.env:

Setting Default Effect
PUNKTFUNK_10BIT on Allow 10-bit (HEVC Main10 / AV1 10-bit) at all. 0, false, off or no forces every session to 8-bit SDR.
PUNKTFUNK_GAMESCOPE_HDR on Allow HDR on the gamescope backend. It only decides whether HDR is attempted — a host without punktfunk-gamescope stays SDR either way. 0 is the escape hatch that puts the gamescope backend back on the old SDR path, spawn flags included.
PUNKTFUNK_GAMESCOPE_SDR_NITS 203 How bright SDR content is inside the PQ container of an HDR gamescope session.
PUNKTFUNK_VIDEO_SOURCE=portal unset Required for the GNOME 50+ monitor-mirror route. GameStream/Moonlight only — no effect on punktfunk/1 sessions.

Client: one toggle, in Settings under Quality with the rest of the video settings10-bit HDR on the Linux, macOS, iOS, iPadOS and tvOS apps, HDR (10-bit, BT.2020 PQ) on Windows, HDR on Android. It is on by default on all of them. Off means "never send me 10-bit", and the host then never upgrades the session. Like the other video settings it can be set per profile, so a Work profile can prefer 4:4:4 while a Couch profile prefers HDR.