KWin creates our virtual output disabled and refuses to stream it — enable it and retry #344

Merged
enricobuehler merged 2 commits from worktree-kwin-vout-enable-repair into main 2026-08-19 20:48:48 +00:00
Owner

Follow-up to #331/#342, from the same field report: a KDE box where every session dies with

create virtual output: KWin virtual output failed: stream_virtual_output failed:
Não foi possível encontrar saída

The mechanism

On KWin ≥ 6.6, ScreencastManager::streamVirtualOutput creates the output on the backend and then hands workspace()->findOutput(output) to the stream. That returns null for an output the workspace does not manage — wantsToManage() is isEnabled() && !isNonDesktop() — so an output KWin creates disabled is refused with i18n("Could not find output"). 6.4/6.5 passed the backend output straight through and streamed it either way.

The reporter's box narrowed it to exactly this: their journal shows automatically choosing drm (so createVirtualOutput cannot return null) and complete silence at the moment of refusal — no Applying output configuration failed!, which means the apply succeeded. A successful apply that still ends in "Could not find output" leaves only "the config it applied has our output disabled".

And it repeats forever. The host asks for a stable per-client output name on purpose, so KWin persists that client's scale and mode against it — which means a stored setup naming it enabled: false gets reapplied to every future session for that client. The user can't fix it in System Settings either: the output only exists for the few milliseconds the request is alive.

The repair

On a refusal, enable the head over kde_output_management_v2, then let the existing retry go again. Two properties of KWin make this possible, both read off Plasma/6.7 rather than assumed:

  • sendFailed only sends the event. It does not emit finished, and removeVirtualOutput is wired to finished — which fires when we destroy the stream. So the disabled output stays alive as long as we hold the failed stream open. That is the window the repair runs in, and why it must run inside the worker thread, before the connection drops.
  • WaylandServer::handleOutputAdded offers every backend output to the output-device registry, gating only placeholders and non-desktop ones. Only wl_output is gated on being enabled — so a disabled output is invisible to stream_output but fully addressable over output management.

Enabling it is a user-applied configuration, so KWin persists it against that output's identity: the retry's fresh request finds a stored setup that enables it. The repair fixes attempt N+1, never attempt N — which is why it must not be classified permanent.

Why REPAIRED_HINT exists

spawn_vout wraps every worker error with "KWin virtual output failed" — the exact phrase is_permanent_build_error matches to short-circuit the retry loop (added in #342). A repaired refusal inheriting that wrapper would be classified permanent, the retry would never run, and this entire path would be dead code. So a repaired refusal is reported verbatim without the wrapper; an unrepairable one keeps it and still fails fast, because nothing about the box changed. permanent_errors_short_circuit_retry pins both halves.

enable_disabled_output returns Some only when a head actually matched, was disabled, and the apply landed — an already-enabled or unmatched head returns None, so an empty config that would applied successfully can never read as a fix (the same trap reenable_outputs documents).

Verification

  • permanent_errors_short_circuit_retry, extended with the repaired chain, extracted and run standalone: passes.
  • cargo fmt clean.
  • ⚠️ The Linux compile is on CI, not local. Docker on the dev box wedged mid-session (docker info hangs past 120 s), and a local --target x86_64-unknown-linux-gnu cross-check dies in audiopus_sys for want of a Linux C toolchain. This backend is cfg(linux), so none of it compiles on macOS — the CI Rust job is the real gate here.

Not merging until CI is green.

Follow-up to #331/#342, from the same field report: a KDE box where every session dies with ``` create virtual output: KWin virtual output failed: stream_virtual_output failed: Não foi possível encontrar saída ``` ## The mechanism On KWin **≥ 6.6**, `ScreencastManager::streamVirtualOutput` creates the output on the backend and then hands `workspace()->findOutput(output)` to the stream. That returns null for an output the workspace does not manage — `wantsToManage()` is `isEnabled() && !isNonDesktop()` — so an output KWin creates **disabled** is refused with `i18n("Could not find output")`. 6.4/6.5 passed the backend output straight through and streamed it either way. The reporter's box narrowed it to exactly this: their journal shows `automatically choosing drm` (so `createVirtualOutput` cannot return null) and **complete silence** at the moment of refusal — no `Applying output configuration failed!`, which means the apply *succeeded*. A successful apply that still ends in "Could not find output" leaves only "the config it applied has our output disabled". And it repeats forever. The host asks for a **stable per-client output name** on purpose, so KWin persists that client's scale and mode against it — which means a stored setup naming it `enabled: false` gets reapplied to every future session for that client. The user can't fix it in System Settings either: the output only exists for the few milliseconds the request is alive. ## The repair On a refusal, enable the head over `kde_output_management_v2`, then let the existing retry go again. Two properties of KWin make this possible, both read off Plasma/6.7 rather than assumed: - **`sendFailed` only sends the event.** It does not emit `finished`, and `removeVirtualOutput` is wired to `finished` — which fires when *we* destroy the stream. So the disabled output stays alive as long as we hold the failed stream open. That is the window the repair runs in, and why it must run inside the worker thread, before the connection drops. - **`WaylandServer::handleOutputAdded` offers every backend output** to the output-device registry, gating only placeholders and non-desktop ones. Only `wl_output` is gated on being enabled — so a disabled output is invisible to `stream_output` but fully addressable over output management. Enabling it is a user-applied configuration, so KWin **persists** it against that output's identity: the retry's fresh request finds a stored setup that enables it. The repair fixes attempt N+1, never attempt N — which is why it must not be classified permanent. ## Why `REPAIRED_HINT` exists `spawn_vout` wraps every worker error with `"KWin virtual output failed"` — the exact phrase `is_permanent_build_error` matches to short-circuit the retry loop (added in #342). A repaired refusal inheriting that wrapper would be classified permanent, the retry would never run, and this entire path would be dead code. So a repaired refusal is reported verbatim without the wrapper; an unrepairable one keeps it and still fails fast, because nothing about the box changed. `permanent_errors_short_circuit_retry` pins both halves. `enable_disabled_output` returns `Some` **only** when a head actually matched, was disabled, and the apply landed — an already-enabled or unmatched head returns `None`, so an empty config that would `applied` successfully can never read as a fix (the same trap `reenable_outputs` documents). ## Verification - `permanent_errors_short_circuit_retry`, extended with the repaired chain, extracted and run standalone: passes. - `cargo fmt` clean. - ⚠️ **The Linux compile is on CI, not local.** Docker on the dev box wedged mid-session (`docker info` hangs past 120 s), and a local `--target x86_64-unknown-linux-gnu` cross-check dies in `audiopus_sys` for want of a Linux C toolchain. This backend is `cfg(linux)`, so none of it compiles on macOS — the CI Rust job is the real gate here. Not merging until CI is green.
enricobuehler added 1 commit 2026-08-19 18:29:59 +00:00
fix(kwin): KWin creates our virtual output disabled and refuses to stream it — enable it and retry
ci / web (pull_request) Successful in 1m29s
ci / rust-arm64 (pull_request) Successful in 2m30s
ci / docs-site (pull_request) Successful in 1m14s
ci / docs-drift (pull_request) Failing after 19s
ci / bun-nix (pull_request) Successful in 1m42s
android / android (pull_request) Successful in 10m8s
ci / rust (pull_request) Failing after 10m48s
20568d988f
On KWin >= 6.6 `streamVirtualOutput` creates the output on the backend and then
passes `workspace()->findOutput(output)` to the stream, which is null for an
output the workspace does not manage (`wantsToManage` = `isEnabled() &&
!isNonDesktop()`). So an output KWin creates DISABLED is refused with
"Could not find output" — translated into the session's language, and logged
nowhere, because disabling an output is a perfectly valid configuration that
applies successfully. 6.4/6.5 passed the backend output straight through and
streamed it either way.

It repeats forever. The host asks for a STABLE per-client output name precisely
so KWin persists that client's scale and mode against it, so a stored setup
naming it `enabled: false` is reapplied to every future session for that client
— and the user cannot fix it in System Settings, because the output only exists
for the few milliseconds the request is alive.

Repair it instead. On a refusal, enable the head over kde_output_management_v2
and let the retry go again. Two properties of KWin make that possible, both read
off Plasma/6.7 rather than assumed:

  * `sendFailed` only sends the event — it does not emit `finished`, and
    `removeVirtualOutput` is wired to `finished`. The disabled output therefore
    stays alive for as long as we hold the failed stream open, which is the
    window the repair runs in (and why it must run inside the worker thread,
    before the connection drops).
  * `WaylandServer::handleOutputAdded` offers EVERY backend output to the
    output-device registry, gating only placeholders and non-desktop ones. Only
    `wl_output` is gated on being enabled, so a disabled output is invisible to
    `stream_output` but fully addressable over output management.

Enabling it is a user-applied configuration, so KWin persists it against that
output's identity: the retry's fresh request finds a stored setup that enables
it. The repair therefore fixes attempt N+1, never attempt N.

`REPAIRED_HINT` keeps that retry reachable. The opener wraps every worker error
with "KWin virtual output failed", which is the phrase the host's
`is_permanent_build_error` matches to short-circuit the retry loop — so a
repaired refusal carrying it would be classified permanent and the retry that
consumes the repair would never run, making the whole path dead code. A repaired
refusal is reported verbatim without that wrapper; an unrepairable one keeps it
and still fails fast, because nothing about the box changed. The host-side test
pins both halves.
enricobuehler added 1 commit 2026-08-19 20:39:39 +00:00
Merge remote-tracking branch 'origin/main' into worktree-kwin-vout-enable-repair
ci / bun-nix (pull_request) Successful in 32s
ci / docs-drift (pull_request) Successful in 35s
ci / web (pull_request) Successful in 1m13s
ci / docs-site (pull_request) Successful in 1m43s
ci / rust-arm64 (pull_request) Successful in 2m20s
android / android (pull_request) Successful in 5m39s
ci / rust (pull_request) Successful in 6m20s
cdacd5636e
enricobuehler merged commit 6b5307618f into main 2026-08-19 20:48:48 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#344