fix(linux/vdisplay): KWin virtual outputs stream above 60 Hz via sacrificial-birth renegotiation

KWin's ScreenCastStream builds its PipeWire format offer — including the
maxFramerate cap it actively throttles delivery to — ONCE at stream
creation, when the virtual output sits at its hardcoded birth 60 Hz. A
kscreen custom-mode change afterwards updates the OUTPUT (readback says
240) but never the offer, so every consumer negotiates max=60 and the
stream delivers 60 (pw-dump-verified on KWin 6.6.4). The only path that
rebuilds the offer is the stream's own resize handling: a source SIZE
change while recording re-runs buildFormats — picking up the output's
current refresh — and renegotiates the live stream.

So above 60 Hz, birth the output at a sacrificial height (+16), then
install + select the real WxH@hz custom mode: the first frame recorded
after the consumer connects triggers KWin's resize path, which renegotiates
the live stream to WxH@hz. The capturer holds (requeues) buffers until the
negotiated size matches — new expect_exact_dims plumbing VirtualOutput →
registry (fresh creates only) → open_virtual_output → a self-disarming
gate in the process callback — bounded by a 3 s deadline that accepts the
producer's dims rather than wedging the session into the first-frame
retry loop. set_custom_refresh reads back the full active mode; a size
reject (pre-6.6 KWin) recreates plain at the real size @60.

Every kscreen operation now addresses the output by its NUMERIC id,
resolved by matching the managed name-prefix AND the just-created birth
size (newest id wins): a mode-switch supersede reuses the per-slot output
NAME (deliberately, for KWin's per-name config persistence) while the
superseded sibling is still alive, and name addressing hit the FIRST
match = the OLD output — on-glass that resized the live session's display
out from under it (wrong-res/black), read the old output back as 'mode
applied', and set the old output primary while the replacement starved at
its birth mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 23:47:53 +02:00
co-authored by Claude Fable 5
parent 09113c9899
commit af87549052
9 changed files with 339 additions and 89 deletions
@@ -70,6 +70,12 @@ pub struct VirtualOutput {
/// Linux-only (the keep-alive pool is Linux).
#[cfg(target_os = "linux")]
pub pool_gen: Option<u64>,
/// The backend created the output at a SACRIFICIAL mode and the producer will renegotiate the
/// live stream to `preferred_mode`'s dims (KWin's screencast only rebuilds its format offer —
/// and thus its refresh cap — on a size change while recording; see kwin.rs `create`). The
/// capturer must hold frames until that renegotiation lands. Linux-only.
#[cfg(target_os = "linux")]
pub expect_exact_dims: bool,
}
impl VirtualOutput {
@@ -93,6 +99,8 @@ impl VirtualOutput {
reused_gen: None,
#[cfg(target_os = "linux")]
pool_gen: None,
#[cfg(target_os = "linux")]
expect_exact_dims: false,
}
}
}