fix(kwin): install >60Hz custom modes in-process too — KWin path is now kscreen-free
audit / bun-audit (push) Successful in 35s
audit / cargo-audit (push) Successful in 2m34s
decky / build-publish (push) Successful in 17s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 47s
deb / build-publish-host (push) Successful in 9m56s
deb / build-publish (push) Successful in 11m17s
android / android (push) Successful in 12m31s
docker / deploy-docs (push) Successful in 29s
arch / build-publish (push) Successful in 15m14s
apple / swift (push) Successful in 1m21s
apple / screenshots (push) Successful in 4m50s
release / apple (push) Successful in 12m45s
flatpak / build-publish (push) Successful in 7m14s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m58s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m18s
windows-host / package (push) Successful in 16m32s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m19s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m14s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m28s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m25s
ci / rust (push) Successful in 17m55s
ci / web (push) Successful in 52s
ci / docs-site (push) Successful in 59s
ci / bench (push) Successful in 5m45s

Follow-up to the previous commit: the only KWin topology op still shelling out to
kscreen-doctor was the >60 Hz custom-mode INSTALL (`set_custom_refresh`). Port it
to `kde_output_management_v2` so the whole KWin path is kscreen-free on modern KWin.

`kwin_output_mgmt::set_custom_mode` builds a one-entry `kde_mode_list_v2`
(set_resolution / set_refresh_rate / set_reduced_blanking=full / add_mode), applies
it via `kde_output_configuration_v2.set_custom_modes` (since 18), waits for KWin to
generate the mode (its CVT generator may align the width down — matched with the
same height-exact / width-within-8 / refresh-within-1Hz gate as before), then
selects it — which changes the output size and drives the sacrificial-birth stream
renegotiation. `kwin::create`'s want_high branch tries this first and only resolves
a kscreen id + calls `set_custom_refresh` if it returns None (pre-6.6 KWin without
set_custom_modes, or the compositor not answering) — so a >60 Hz session on a box
where kscreen-doctor wedges no longer eats a 5 s resolve timeout either.

Bonus: `set_custom_modes` REPLACES the custom list, so reconnects are idempotent —
no more one-custom-mode-per-connect growth of the user's display list.

Verified: cargo test -p pf-vdisplay (73 pass), clippy -D warnings clean, fmt clean.
The set_custom_modes round-trip is proven live on KWin 6.6.4: apply=applied, and a
1648x928@75 request generates 1648x928@74.901 (CVT fractional refresh, caught by the
1 Hz tolerance). The mode SELECT + full >60 Hz stream is still owed on-glass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-24 12:47:34 +02:00
co-authored by Claude Opus 4.8
parent 86d5d66660
commit 64511af4d3
2 changed files with 191 additions and 18 deletions
+29 -18
View File
@@ -264,12 +264,12 @@ impl VirtualDisplay for KwinDisplay {
// on virtual outputs since KWin 6.6) then changes the SIZE, and the first buffers recorded
// after the consumer connects trigger KWin's resize → a renegotiation to that mode. The
// capturer holds frames until that lands (`expect_exact_dims`), so the pipeline never
// builds against the birth mode. First cut shells out to kscreen-doctor; the in-process
// kde_output_management_v2 client is a follow-up. `set_custom_refresh` reads back what
// KWin *actually* gave — both the rate (so the encoder paces to the real source) and the
// size, which KWin's CVT generator may have aligned down (see `CVT_H_GRANULARITY`). At
// ≤60 Hz there's nothing to install — the output is born at the real size and 60 Hz is the
// offer anyway.
// builds against the birth mode. The install/select runs in-process over
// kde_output_management_v2 (`kwin_output_mgmt::set_custom_mode`), with kscreen-doctor
// (`set_custom_refresh`) as the fallback; either reads back what KWin *actually* gave — both
// the rate (so the encoder paces to the real source) and the size, which KWin's CVT
// generator may have aligned down (see `CVT_H_GRANULARITY`). At ≤60 Hz there's nothing to
// install — the output is born at the real size and 60 Hz is the offer anyway.
let want_high = mode.refresh_hz > 60;
let birth_h = if want_high { height + 16 } else { height };
let (mut node_id, mut stop) = spawn_vout(width, birth_h)?;
@@ -283,8 +283,8 @@ impl VirtualDisplay for KwinDisplay {
);
// Topology + positioning address OUR output by its kde_output_management UUID (resolved
// in-process in `apply_topology`, supersede-robust) — no early kscreen-doctor resolve, so
// the 60 Hz path never shells out. Only the >60 Hz custom-mode install below still uses
// kscreen-doctor (its in-process port — set_custom_modes/add_cvt — is a follow-up).
// the path never shells out. `Virtual-<name>` is the name KWin exposes our output as.
let our_prefix = format!("Virtual-{name}");
let mut expect_exact_dims = false;
// The size the output actually ENDS UP at — the request, unless KWin's CVT generator had to
// shrink the width to the cell grain (see `CVT_H_GRANULARITY`). Reported as the output's
@@ -292,15 +292,27 @@ impl VirtualDisplay for KwinDisplay {
// encoder opens against, so a CVT-aligned mode flows end-to-end instead of starving.
let mut final_dims = (width, height);
let achieved_hz = if want_high {
// ⚠️ ADDRESS BY NUMERIC KSCREEN ID, NEVER BY NAME: a supersede (mode switch) creates the
// replacement output — SAME per-slot name, deliberately, for KWin's per-name config
// persistence — while the superseded sibling is still alive (create-before-drop). A
// name-addressed kscreen command would hit the FIRST match = the OLD output. Resolve OUR
// output's kscreen id (managed-prefix name AND current mode == the birth size; newest id
// wins) for the custom-mode install, and keep it as the kscreen-doctor position fallback.
let addr = resolve_kscreen_addr(&name, width, birth_h);
self.last_name = Some(addr.clone());
let active = set_custom_refresh(width, height, mode.refresh_hz, &addr);
// >60 Hz needs the real high-refresh custom mode installed + selected (sacrificial-birth,
// see above). In-process over kde_output_management_v2 first (no kscreen-doctor); fall
// back to the kscreen-doctor shell-out on pre-6.6 KWin (no `set_custom_modes`) or if the
// compositor doesn't answer in budget.
let active = crate::kwin_output_mgmt::set_custom_mode(
&our_prefix,
width,
birth_h,
width,
height,
mode.refresh_hz,
)
.or_else(|| {
// ⚠️ ADDRESS BY NUMERIC KSCREEN ID, NEVER BY NAME: a supersede reuses the per-slot
// name while the superseded sibling is still alive, so a name-addressed kscreen
// command hits the OLD output. Resolve our kscreen id for the shell-out install +
// keep it as apply_position's kscreen fallback.
let addr = resolve_kscreen_addr(&name, width, birth_h);
self.last_name = Some(addr.clone());
set_custom_refresh(width, height, mode.refresh_hz, &addr)
});
// Accept only an active mode that IS our custom one: the exact requested height, and a
// width at or just below the request (a CVT alignment). That also proves the output
// left the sacrificial birth size, so the recording stream will renegotiate to it.
@@ -349,7 +361,6 @@ impl VirtualDisplay for KwinDisplay {
// bootstrap output. Applied over kde_output_management_v2 in-process (immune to a wedged
// kscreen-doctor backend; see `apply_topology`), with a kscreen-doctor fallback. `disabled`
// is the physical/bootstrap outputs, each `(name, "WxH@Hz")`, to restore on teardown.
let our_prefix = format!("Virtual-{name}");
let disabled = self.apply_topology(&name, &our_prefix, final_dims);
// A plain managed name is enough for apply_position's kscreen-doctor fallback when the
// in-process UUID path isn't set (single-output sessions are unambiguous; a supersede uses