From b551f7dae824c7ab571f54dbc1c927a5f820c339 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 19 Aug 2026 08:54:02 +0200 Subject: [PATCH 1/3] =?UTF-8?q?fix(host):=20a=20translated=20KWin=20refusa?= =?UTF-8?q?l=20burned=20all=208=20retries=20=E2=80=94=20match=20our=20own?= =?UTF-8?q?=20prefix,=20not=20KWin's=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KWin sends the `failed` reason on zkde_screencast localized. The retry short-circuit matched the English "could not find output", so a pt-BR session ("Não foi possível encontrar saída") fell through to 8 attempts over ~11 s and then reported "out of retries" — a config fact dressed up as a flaky one. Match "KWin virtual output failed" instead: our own prefix, never translated. Every `failed` KWin sends on this path is a backend/config fact (unsupported compositing type, a backend without createVirtualOutput, an output the workspace declined to enable), none of which a retry 500 ms later changes. The message itself now says what KWin's untranslatable reason means, and the KDE docs gain the KWin 6.6+ shape of it: KWin creates the output, leaves it disabled, and workspace()->findOutput() returns null — reported identically to a backend that cannot create one at all. --- crates/pf-vdisplay/src/vdisplay/linux/kwin.rs | 11 ++++++++++- crates/punktfunk-host/src/native/stream.rs | 15 ++++++++++++--- docs-site/content/docs/kde.md | 12 ++++++++++++ docs-site/content/docs/virtual-displays.md | 4 +++- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/crates/pf-vdisplay/src/vdisplay/linux/kwin.rs b/crates/pf-vdisplay/src/vdisplay/linux/kwin.rs index 3ea211a8..4af8a0a0 100644 --- a/crates/pf-vdisplay/src/vdisplay/linux/kwin.rs +++ b/crates/pf-vdisplay/src/vdisplay/linux/kwin.rs @@ -268,7 +268,16 @@ impl VirtualDisplay for KwinDisplay { .context("spawn KWin virtual-output thread")?; match setup_rx.recv_timeout(OPENER_BUDGET) { Ok(Ok(v)) => Ok((v, stop)), - Ok(Err(e)) => bail!("KWin virtual output failed: {e}"), + // KWin's reason is TRANSLATED into the session's language, so it is often + // unsearchable for the person reading the log. Say what it means once, here. + Ok(Err(e)) => bail!( + "KWin virtual output failed: {e} — KWin declined to create the output. It \ + needs a Plasma WAYLAND session on KWin's DRM backend; a nested or \ + `kwin_wayland --virtual` KWin can only do this since 6.5.6, and on KWin 6.6+ \ + an output KWin creates but leaves DISABLED (stored \ + ~/.config/kwinoutputconfig.json, or a display config it refused to apply) \ + reports the same. kwin_wayland's own journal says which" + ), Err(_) => { // Nothing else will ever flip this `stop`: it is dropped with the error, and // the `StopGuard` that normally owns it is only built on the success path. So diff --git a/crates/punktfunk-host/src/native/stream.rs b/crates/punktfunk-host/src/native/stream.rs index 9c8157b6..ca5cd643 100644 --- a/crates/punktfunk-host/src/native/stream.rs +++ b/crates/punktfunk-host/src/native/stream.rs @@ -4611,9 +4611,14 @@ fn is_permanent_build_error(chain: &str) -> bool { "virtual displays require linux", "unknown punktfunk_compositor", "could not detect compositor", - "could not find output", // KWin < 6.5.6: createVirtualOutput unsupported - "must be a node id", // PUNKTFUNK_GAMESCOPE_NODE not an integer - "is it installed", // gamescope / kscreen-doctor not on PATH + // KWin refused the virtual output. Its own reason arrives TRANSLATED (a field report read + // "Não foi possível encontrar saída" and burned all 8 retries), so match OUR English + // prefix, not KWin's payload. Every `failed` KWin sends on this path is a config/backend + // fact — unsupported compositing type, a backend without `createVirtualOutput`, an output + // the workspace declined to enable — none of which a retry 500 ms later changes. + "kwin virtual output failed", + "must be a node id", // PUNKTFUNK_GAMESCOPE_NODE not an integer + "is it installed", // gamescope / kscreen-doctor not on PATH // 4:4:4 NVENC got a CUDA frame — should never happen now the Linux capturer honors gpu=false, // but fail fast instead of 8× retry (~90 s) rather than wedge the session if it ever recurs. "capture/encoder negotiation mismatch", @@ -5329,6 +5334,10 @@ mod tests { assert!(is_permanent_build_error( "create virtual output: KWin virtual output failed: Could not find output" )); + // Same refusal from a localized KWin — the reason is translated, our prefix is not. + assert!(is_permanent_build_error( + "create virtual output: KWin virtual output failed: Não foi possível encontrar saída" + )); assert!(is_permanent_build_error( "unknown PUNKTFUNK_COMPOSITOR 'foo' (kwin|wlroots|mutter|gamescope)" )); diff --git a/docs-site/content/docs/kde.md b/docs-site/content/docs/kde.md index 89a8bb2d..244afdfc 100644 --- a/docs-site/content/docs/kde.md +++ b/docs-site/content/docs/kde.md @@ -144,6 +144,18 @@ The session unit brings up headless KWin; the host unit follows it and starts li print **nothing at all**. A host binary carrying a Linux capability cannot be identified by KWin and is never offered the protocol, however correctly its grant is installed — see [GPU scheduling priority](/docs/running-as-a-service#gpu-scheduling-priority). +- **"KWin virtual output failed: Could not find output"** (the message arrives translated — a + Brazilian session reads *"Não foi possível encontrar saída"*): KWin got the request and refused + it, so the grant above is fine and only the output creation is not. Two shapes. Its **backend** + can't create one — a nested KWin, or `kwin_wayland --virtual` below 6.5.6; a normal Plasma + session on the DRM backend always can. Or, on **KWin 6.6+**, KWin created the output and then + left it **disabled**, which reports identically: check `punktfunk-host list-monitors` for a + `Virtual-punktfunk-*` marked *disabled*, a stale entry in `~/.config/kwinoutputconfig.json`, and + `journalctl --user -b -t kwin_wayland` for *"Applying output configuration failed!"* — KWin logs + that when it declines to enable one more output next to your current monitors, and keeps the old + configuration instead. Streaming a real monitor + ([`PUNKTFUNK_CAPTURE_MONITOR`](/docs/configuration), or **Streamed screen** in the console) skips + virtual-output creation entirely and is the workaround while you sort the above out. - **Black screen / no picture:** confirm you're on a Wayland session (not X11) and, on NVIDIA, that the GL userspace is installed. More in [Troubleshooting](/docs/troubleshooting). diff --git a/docs-site/content/docs/virtual-displays.md b/docs-site/content/docs/virtual-displays.md index 5b9b149b..5ce038f0 100644 --- a/docs-site/content/docs/virtual-displays.md +++ b/docs-site/content/docs/virtual-displays.md @@ -376,7 +376,9 @@ an empty extension. Use **Primary** or **Exclusive** so your desktop actually la **KWin can't create the virtual output.** On a normal Plasma session KWin runs its **DRM backend**, which creates virtual outputs at any version. The 6.5.6 floor applies only to the **virtual backend** (`kwin_wayland --virtual`, used for headless and test sessions) — below that the request fails with -"Could not find output". See [requirements](/docs/requirements). +"Could not find output". On **KWin 6.6+** that same message also covers an output KWin *did* create +and then left disabled; [KDE Plasma](/docs/kde#troubleshooting) walks that one. See +[requirements](/docs/requirements). **Reconnecting into game mode reconnects cleanly now.** On a Steam Deck / Bazzite box, disconnecting and reconnecting within game mode reuses the still-warm session (or cleanly recreates it) instead of -- 2.54.0 From a75ed71428af21c06b9268431c05f807235ff433 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 19 Aug 2026 09:01:12 +0200 Subject: [PATCH 2/3] =?UTF-8?q?docs(kde):=20name=20the=206.6=20change=20be?= =?UTF-8?q?hind=20the=20KWin=20virtual-output=20failure=20=E2=80=94=20the?= =?UTF-8?q?=20output=20must=20now=20be=20enabled,=20not=20just=20created?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs-site/content/docs/kde.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs-site/content/docs/kde.md b/docs-site/content/docs/kde.md index 244afdfc..8c5c1b2b 100644 --- a/docs-site/content/docs/kde.md +++ b/docs-site/content/docs/kde.md @@ -149,7 +149,10 @@ The session unit brings up headless KWin; the host unit follows it and starts li it, so the grant above is fine and only the output creation is not. Two shapes. Its **backend** can't create one — a nested KWin, or `kwin_wayland --virtual` below 6.5.6; a normal Plasma session on the DRM backend always can. Or, on **KWin 6.6+**, KWin created the output and then - left it **disabled**, which reports identically: check `punktfunk-host list-monitors` for a + left it **disabled**. That reports identically, and it is new: 6.6 put a + `workspace()->findOutput()` hop in front of the stream, so from 6.6 on the output must also be + *enabled and workspace-managed*, where 6.5 and earlier streamed it either way. Check + `punktfunk-host list-monitors` for a `Virtual-punktfunk-*` marked *disabled*, a stale entry in `~/.config/kwinoutputconfig.json`, and `journalctl --user -b -t kwin_wayland` for *"Applying output configuration failed!"* — KWin logs that when it declines to enable one more output next to your current monitors, and keeps the old -- 2.54.0 From b6ca692c13642e365db2ae26faf72065ad069f0f Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 19 Aug 2026 11:54:45 +0200 Subject: [PATCH 3/3] =?UTF-8?q?docs(abi):=20ex11's=20summary=20still=20sta?= =?UTF-8?q?ted=20the=20pre-2026-08-16=20rule=20its=20own=20warning=20corre?= =?UTF-8?q?cts=20=E2=80=94=20a=20format=20at=20all=20sets=20the=20hi-res?= =?UTF-8?q?=20cap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/punktfunk-core/src/abi.rs | 15 +++++++++------ include/punktfunk_core.h | 15 +++++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/crates/punktfunk-core/src/abi.rs b/crates/punktfunk-core/src/abi.rs index 1f4fea20..4be11de0 100644 --- a/crates/punktfunk-core/src/abi.rs +++ b/crates/punktfunk-core/src/abi.rs @@ -2326,12 +2326,15 @@ pub unsafe extern "C" fn punktfunk_connect_ex10( /// `audio_rate_hz` — `48000`, `96000`, or the 44.1 kHz family `44100` / `88200` / `176400` — and /// `audio_bits` (`16` or `24`). /// -/// Passing anything other than `48000`/`16` sets `CLIENT_CAP_AUDIO_HIRES` in the `Hello` and asks -/// the host for the LOSSLESS `0xD3` plane — bit-exact PCM instead of Opus. That is an opt-in on -/// both ends, and it is meant to be: it costs **1.5–4.6 Mbps** taken off the top of the link -/// (audio rides QUIC datagrams outside the ABR loop, so ABR can neither see it nor reclaim it), -/// against the ~256 kbps Opus this replaces. Only call it with a non-default format when the -/// user turned the feature on AND this embedder can genuinely open an output device at it. +/// Passing a format AT ALL — any non-zero `audio_rate_hz`/`audio_bits`, `48000`/`16` included — +/// sets `CLIENT_CAP_AUDIO_HIRES` in the `Hello` and asks the host for the LOSSLESS `0xD3` plane, +/// bit-exact PCM instead of Opus. (This line once said "anything other than `48000`/`16`", which +/// was the rule until the cheapest rung turned out to be the one nobody could ask for; the ⚠ below +/// is the whole story.) That is an opt-in on both ends, and it is meant to be: it costs +/// **1.5–4.6 Mbps** taken off the top of the link (audio rides QUIC datagrams outside the ABR +/// loop, so ABR can neither see it nor reclaim it), against the ~256 kbps Opus this replaces. Only +/// pass a format when the user turned the feature on AND this embedder can genuinely open an +/// output device at it. /// /// **The request is not the answer.** The host runs a five-condition gate /// (`design/hi-res-audio.md` §8.4 — client asked, operator policy allows, stereo, the capture diff --git a/include/punktfunk_core.h b/include/punktfunk_core.h index de053f96..deb95a41 100644 --- a/include/punktfunk_core.h +++ b/include/punktfunk_core.h @@ -2983,12 +2983,15 @@ PunktfunkConnection *punktfunk_connect_ex10(const char *host, // `audio_rate_hz` — `48000`, `96000`, or the 44.1 kHz family `44100` / `88200` / `176400` — and // `audio_bits` (`16` or `24`). // -// Passing anything other than `48000`/`16` sets `CLIENT_CAP_AUDIO_HIRES` in the `Hello` and asks -// the host for the LOSSLESS `0xD3` plane — bit-exact PCM instead of Opus. That is an opt-in on -// both ends, and it is meant to be: it costs **1.5–4.6 Mbps** taken off the top of the link -// (audio rides QUIC datagrams outside the ABR loop, so ABR can neither see it nor reclaim it), -// against the ~256 kbps Opus this replaces. Only call it with a non-default format when the -// user turned the feature on AND this embedder can genuinely open an output device at it. +// Passing a format AT ALL — any non-zero `audio_rate_hz`/`audio_bits`, `48000`/`16` included — +// sets `CLIENT_CAP_AUDIO_HIRES` in the `Hello` and asks the host for the LOSSLESS `0xD3` plane, +// bit-exact PCM instead of Opus. (This line once said "anything other than `48000`/`16`", which +// was the rule until the cheapest rung turned out to be the one nobody could ask for; the ⚠ below +// is the whole story.) That is an opt-in on both ends, and it is meant to be: it costs +// **1.5–4.6 Mbps** taken off the top of the link (audio rides QUIC datagrams outside the ABR +// loop, so ABR can neither see it nor reclaim it), against the ~256 kbps Opus this replaces. Only +// pass a format when the user turned the feature on AND this embedder can genuinely open an +// output device at it. // // **The request is not the answer.** The host runs a five-condition gate // (`design/hi-res-audio.md` §8.4 — client asked, operator policy allows, stereo, the capture -- 2.54.0