From 1423b6333364e7f10109075b1415c6491e947003 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sun, 26 Jul 2026 12:07:04 +0200 Subject: [PATCH] fix(gamestream): the HDR SDR-downgrade latch gated the capture offer but not the negotiation (X6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pf_capture::hdr_capture_failed()` had exactly one consumer: `open_portal_monitor`, which uses it to drop the HDR offer (`want_hdr && !hdr_capture_failed()`). The RTSP negotiation consulted only `gnome_hdr_monitor_active()`. So once the latch was set — an HDR negotiation timed out, the monitor left HDR mode between probe and negotiation, NVIDIA EGL not listing LINEAR for XR30, a pre-50 Mutter — the host kept telling the client HDR while capturing and encoding SDR: `cfg.hdr` flowed to `open_portal_monitor`, which then silently opened the SDR offer. The client renders an SDR stream as PQ. Washed out, wrong gamut, and no error anywhere. The latch is sticky until host restart, so every reconnect repeated it. Consulted at RTSP honor time rather than folded into `host_hdr_capable()` for the same reason as the colour-mode probe next to it: that fn is the STATIC serverinfo capability (it decides whether to advertise SCM_HEVC_MAIN10 at all), and this is a live per-session fact. The native plane needs no equivalent — `capturer_supports_hdr()` is hard-false on Linux, and the latch is a fact about the portal capturer, which that plane never uses. Noted in handshake.rs so it isn't re-derived. Listed in the sweep's confirmed-defect register as cross-cutting/medium, then absent from every phase of its implementation plan. Verification is on-glass (force the latch, reconnect a client that requests HDR, confirm the Welcome/SDP reports SDR). Co-Authored-By: Claude Opus 5 (1M context) --- crates/punktfunk-host/src/gamestream/rtsp.rs | 19 +++++++++++++++++++ crates/punktfunk-host/src/native/handshake.rs | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/crates/punktfunk-host/src/gamestream/rtsp.rs b/crates/punktfunk-host/src/gamestream/rtsp.rs index 5f155178..8bf85dea 100644 --- a/crates/punktfunk-host/src/gamestream/rtsp.rs +++ b/crates/punktfunk-host/src/gamestream/rtsp.rs @@ -450,6 +450,25 @@ fn stream_config(map: &HashMap) -> Option { ); hdr = false; } + // Second half of the same live gate: the process-wide HDR-capture latch. The colour-mode probe + // above only says the monitor is in BT.2100 RIGHT NOW — it says nothing about whether the + // portal will actually hand us the 10-bit PQ formats. Once a `want_hdr` negotiation has failed + // (the monitor left HDR mode between probe and negotiation, NVIDIA EGL not listing LINEAR for + // XR30, a pre-50 Mutter), `pf_capture::open_portal_monitor` permanently drops the HDR OFFER and + // captures SDR. Without this check we'd keep telling the client HDR while capturing and + // encoding SDR: it renders an SDR picture through PQ — washed out, wrong gamut, no error + // anywhere — and because the latch is sticky until host restart, every reconnect repeats it. + // Consulted here rather than folded into `host_hdr_capable` for the same reason as the probe + // above: that fn is the STATIC serverinfo capability, and this is a live per-session fact. + #[cfg(target_os = "linux")] + if hdr && pf_capture::hdr_capture_failed() { + tracing::warn!( + "client requested HDR and a monitor is in BT.2100 (HDR) colour mode, but an earlier \ + HDR capture negotiation on this host failed — the capturer offers SDR only for the \ + rest of the process lifetime, so streaming 8-bit SDR (restart the host to retry HDR)" + ); + hdr = false; + } // The client's requested CSC (moonlight-common-c SdpGenerator.c: `encoderCscMode = // (colorspace << 1) | fullRange` — colorspace 0=Rec601, 1=Rec709, 2=Rec2020). Moonlight // renderers configure their YUV→RGB from this REQUESTED value (not the bitstream VUI), so a diff --git a/crates/punktfunk-host/src/native/handshake.rs b/crates/punktfunk-host/src/native/handshake.rs index 067d7146..2a9da219 100644 --- a/crates/punktfunk-host/src/native/handshake.rs +++ b/crates/punktfunk-host/src/native/handshake.rs @@ -267,6 +267,12 @@ pub(super) async fn negotiate( // *monitor* streams only — the GameStream portal-mirror path uses that; see // `gamestream::host_hdr_capable`), so a Linux native session honestly stays 8-bit SDR even // though `can_encode_10bit` now probes true on a Main10-capable GPU. + // + // That `false` is also why this plane needs no equivalent of the GameStream path's + // `pf_capture::hdr_capture_failed()` check (rtsp.rs): that latch is a fact about the PORTAL + // capturer's HDR offer, and the native plane captures a virtual output instead — it never + // reaches the portal path, and on Linux it never negotiates 10-bit at all. Revisit both halves + // together if Mutter ever gains HDR for RecordVirtual streams. let capture_supports_hdr = crate::capture::capturer_supports_hdr(); // The GPU probe may open a tiny encoder on first use, so run it off the reactor like the // 4:4:4 probe below (blocking probes → spawn_blocking), short-circuited behind the cheap