fix(vdisplay/kwin): primary is a PRIORITY, and the log now reads the answer back
audit / bun-audit (plugin-kit) (push) Successful in 19s
audit / bun-audit (sdk) (push) Successful in 17s
audit / pnpm-audit (push) Successful in 10s
audit / docs-site-audit (push) Successful in 28s
audit / bun-audit (web) (push) Successful in 30s
ci / web (push) Successful in 1m3s
ci / docs-site (push) Successful in 2m26s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Failing after 3m35s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Failing after 2m50s
ci / bench (push) Successful in 6m47s
deb / build-publish (push) Failing after 6m36s
audit / license-gate (push) Successful in 7m54s
decky / build-publish (push) Successful in 32s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 55s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 46s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 28s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 17s
ci / rust-arm64 (push) Successful in 10m48s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m55s
apple / swift (push) Successful in 4m50s
android / android (push) Successful in 12m33s
deb / build-publish-client-arm64 (push) Failing after 5m44s
deb / build-publish-host (push) Failing after 10m5s
ci / rust (push) Failing after 13m19s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 5m4s
docker / deploy-docs (push) Successful in 29s
docker / build-push-arm64cross (push) Successful in 37s
audit / cargo-audit (push) Successful in 2m46s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 6m2s
flatpak / build-publish (push) Failing after 8m44s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 12m5s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 12m18s
arch / build-publish (push) Successful in 15m43s
apple / screenshots (push) Canceled after 0s
release / apple (push) Canceled after 18m42s
windows-host / package (push) Canceled after 13m31s
windows-host / winget-source (push) Canceled after 0s
audit / bun-audit (plugin-kit) (push) Successful in 19s
audit / bun-audit (sdk) (push) Successful in 17s
audit / pnpm-audit (push) Successful in 10s
audit / docs-site-audit (push) Successful in 28s
audit / bun-audit (web) (push) Successful in 30s
ci / web (push) Successful in 1m3s
ci / docs-site (push) Successful in 2m26s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Failing after 3m35s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Failing after 2m50s
ci / bench (push) Successful in 6m47s
deb / build-publish (push) Failing after 6m36s
audit / license-gate (push) Successful in 7m54s
decky / build-publish (push) Successful in 32s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 55s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 46s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 28s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 17s
ci / rust-arm64 (push) Successful in 10m48s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m55s
apple / swift (push) Successful in 4m50s
android / android (push) Successful in 12m33s
deb / build-publish-client-arm64 (push) Failing after 5m44s
deb / build-publish-host (push) Failing after 10m5s
ci / rust (push) Failing after 13m19s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 5m4s
docker / deploy-docs (push) Successful in 29s
docker / build-push-arm64cross (push) Successful in 37s
audit / cargo-audit (push) Successful in 2m46s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 6m2s
flatpak / build-publish (push) Failing after 8m44s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 12m5s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 12m18s
arch / build-publish (push) Successful in 15m43s
apple / screenshots (push) Canceled after 0s
release / apple (push) Canceled after 18m42s
windows-host / package (push) Canceled after 13m31s
windows-host / winget-source (push) Canceled after 0s
A field report (CachyOS, KWin 6.7.3, Plasma): topology primary streams the new virtual display but the desktop stays on the physical — while the host logs primary_taken=true on every apply. Both halves were real: * KWin's handler for kde_output_configuration_v2.set_primary_output is literally '// intentionally ignored' (verified in the 6.7.3 source). The output order is driven by per-output set_priority (management >= 3; we bind up to v22 and never called it). Exclusive only ever LOOKED right because disabling every other output leaves KWin nothing else to promote. Now: ours gets priority 1 and every other enabled output is renumbered uniquely behind it, exactly what kscreen-doctor does — the reporter proved that path applies and sticks on their box. set_primary_output is still sent for pre-v3 compositors. * primary_taken echoed the REQUEST. The log now waits one sync barrier for KWin's post-apply priority events and reports primary_verified from the read-back — a request the compositor ignores can never ship a green log again; the mismatch is a WARN naming the symptom. First-slot-wins refinement the working priorities made necessary: a SAME-NAMED output is this slot's own predecessor mid-supersede (mode switches create the replacement before dropping the old), not a sibling whose primary must be respected — deferring to it handed primary to whatever KWin promoted when the predecessor vanished a moment later. On-glass (nested KWin 6.7.3): fresh create, high-refresh custom-mode create, and the mode-switch supersede all read back verified=true; exclusive still disables + restores the other output in-process. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -79,9 +79,10 @@ use management::kde_output_configuration_v2::{
|
||||
use management::kde_output_management_v2::KdeOutputManagementV2 as OutputManagement;
|
||||
|
||||
/// Highest interface versions we drive; we bind `min(advertised, MAX)`. Every request we issue is
|
||||
/// `since ≤ 2` (`create_configuration`/`enable`/`mode`/`position`/`apply` are v1, `set_primary_output`
|
||||
/// is v2) and every event we read is `since ≤ 18` (`priority`), so binding high and calling low is
|
||||
/// always in range on any KWin that advertises the globals.
|
||||
/// `since ≤ 3` (`create_configuration`/`enable`/`mode`/`position`/`apply` are v1, `set_primary_output`
|
||||
/// v2, `set_priority` v3 — version-gated at its call site) and every event we read is `since ≤ 18`
|
||||
/// (`priority`), so binding high and calling low is always in range on any KWin that advertises the
|
||||
/// globals.
|
||||
const MGMT_MAX: u32 = 22;
|
||||
const DEVICE_MAX: u32 = 24;
|
||||
/// `kde_output_device_registry_v2` — the newer way KWin hands out output devices (verified live on
|
||||
@@ -627,11 +628,17 @@ pub(crate) fn apply_topology(
|
||||
let our_id = ours.proxy.as_ref().map(|p| p.id());
|
||||
|
||||
// First-slot-wins (§6.1): don't steal primary if another managed sibling already holds it
|
||||
// (priority 1) — a 2nd exclusive session joins as a secondary of the shared desktop.
|
||||
// (priority 1) — a 2nd exclusive session joins as a secondary of the shared desktop. A
|
||||
// SAME-NAMED output is not a sibling: it is this slot's own predecessor mid-supersede (a
|
||||
// mode switch creates the replacement before dropping the old), and deferring to it would
|
||||
// hand primary to whatever output KWin promotes when the predecessor disappears a moment
|
||||
// later — the physical, in the field report's layout. Now that primary is driven through
|
||||
// priorities that actually apply, the predecessor really does hold priority 1 here.
|
||||
let sibling_is_primary = sess.state.devices.values().any(|d| {
|
||||
d.enabled
|
||||
&& d.priority == Some(1)
|
||||
&& d.proxy.as_ref().map(|p| p.id()) != our_id
|
||||
&& d.name != ours.name
|
||||
&& d.name
|
||||
.as_deref()
|
||||
.is_some_and(|n| n.starts_with(MANAGED_PREFIX))
|
||||
@@ -658,11 +665,48 @@ pub(crate) fn apply_topology(
|
||||
|
||||
// Build one configuration: ensure ours is enabled, take primary (unless a sibling holds it),
|
||||
// disable the others. `apply()` is atomic — KWin re-homes the shell onto the remaining desktop.
|
||||
//
|
||||
// "Primary" is driven through the output PRIORITY list, not `set_primary_output`: KWin's
|
||||
// handler for the latter is literally `// intentionally ignored` (verified in the 6.7.3
|
||||
// source, and on-glass by a field report whose virtual output stayed at priority 2 while we
|
||||
// logged success). `set_priority` (management ≥ 3; we bind up to v22) is what kscreen-doctor
|
||||
// itself uses, so ours gets 1 and every other enabled output is renumbered behind it — the
|
||||
// protocol wants the values unique among enabled outputs. `set_primary_output` is still sent
|
||||
// for pre-`set_priority` compositors, where it was honored.
|
||||
let mgmt_version = sess
|
||||
.state
|
||||
.mgmt_name_version
|
||||
.map(|(_, v)| v)
|
||||
.unwrap_or_default();
|
||||
let config = sess.new_config();
|
||||
if let Some(proxy) = ours.proxy.as_ref() {
|
||||
config.enable(proxy, 1);
|
||||
if !sibling_is_primary {
|
||||
config.set_primary_output(proxy);
|
||||
if mgmt_version >= 3 {
|
||||
config.set_priority(proxy, 1);
|
||||
// The rest of the enabled outputs, in their existing order, from 2 — skipping
|
||||
// the ones this apply disables (a disabled output's priority is meaningless).
|
||||
let disabling: Vec<ObjectId> = to_disable.iter().map(|(p, _, _)| p.id()).collect();
|
||||
let mut others: Vec<&DeviceState> = sess
|
||||
.state
|
||||
.devices
|
||||
.values()
|
||||
.filter(|d| {
|
||||
d.enabled
|
||||
&& d.proxy.as_ref().map(|p| p.id()) != our_id
|
||||
&& d.proxy
|
||||
.as_ref()
|
||||
.is_some_and(|p| !disabling.contains(&p.id()))
|
||||
})
|
||||
.collect();
|
||||
others.sort_by_key(|d| d.priority.unwrap_or(u32::MAX));
|
||||
for (i, d) in others.iter().enumerate() {
|
||||
if let Some(proxy) = d.proxy.as_ref() {
|
||||
config.set_priority(proxy, 2 + i as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (proxy, _, _) in &to_disable {
|
||||
@@ -679,11 +723,46 @@ pub(crate) fn apply_topology(
|
||||
.map(|(_, name, spec)| (name, spec))
|
||||
.collect();
|
||||
if applied {
|
||||
tracing::info!(
|
||||
also_disabled = ?disabled,
|
||||
primary_taken = !sibling_is_primary,
|
||||
"KWin output management: streamed output set as the desktop (in-process)"
|
||||
);
|
||||
// Read the outcome BACK instead of echoing the request: KWin emits fresh `priority`
|
||||
// events after the apply; one sync barrier drains them, and "primary" is then a fact —
|
||||
// ours holds the lowest priority among enabled outputs. The old log's `primary_taken`
|
||||
// was the request, which is how an intentionally-ignored `set_primary_output` shipped a
|
||||
// green log over a non-primary output for a whole release line.
|
||||
let verified = {
|
||||
let vdeadline = Instant::now() + Duration::from_millis(500);
|
||||
sess.sync_barrier(vdeadline);
|
||||
let ours_now = sess
|
||||
.state
|
||||
.devices
|
||||
.values()
|
||||
.find(|d| d.proxy.as_ref().map(|p| p.id()) == our_id)
|
||||
.and_then(|d| d.priority);
|
||||
let min_enabled = sess
|
||||
.state
|
||||
.devices
|
||||
.values()
|
||||
.filter(|d| d.enabled)
|
||||
.filter_map(|d| d.priority)
|
||||
.min();
|
||||
match (ours_now, min_enabled) {
|
||||
(Some(p), Some(m)) => Some(p <= m),
|
||||
_ => None, // pre-v18 devices carry no priority event — nothing to verify against
|
||||
}
|
||||
};
|
||||
if sibling_is_primary || verified != Some(false) {
|
||||
tracing::info!(
|
||||
also_disabled = ?disabled,
|
||||
primary_requested = !sibling_is_primary,
|
||||
primary_verified = ?verified,
|
||||
"KWin output management: streamed output set as the desktop (in-process)"
|
||||
);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
also_disabled = ?disabled,
|
||||
"KWin output management: apply acked but the streamed output did NOT become the \
|
||||
primary (priority read-back) — the desktop stays on another output"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tracing::warn!(
|
||||
reason = ?sess.state.failure_reason,
|
||||
|
||||
Reference in New Issue
Block a user