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

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:
2026-07-29 17:59:24 +02:00
co-authored by Claude Fable 5
parent a8744b98d1
commit ddd0e5cc5d
@@ -79,9 +79,10 @@ use management::kde_output_configuration_v2::{
use management::kde_output_management_v2::KdeOutputManagementV2 as OutputManagement; use management::kde_output_management_v2::KdeOutputManagementV2 as OutputManagement;
/// Highest interface versions we drive; we bind `min(advertised, MAX)`. Every request we issue is /// 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` /// `since ≤ 3` (`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 /// v2, `set_priority` v3 — version-gated at its call site) and every event we read is `since ≤ 18`
/// always in range on any KWin that advertises the globals. /// (`priority`), so binding high and calling low is always in range on any KWin that advertises the
/// globals.
const MGMT_MAX: u32 = 22; const MGMT_MAX: u32 = 22;
const DEVICE_MAX: u32 = 24; const DEVICE_MAX: u32 = 24;
/// `kde_output_device_registry_v2` — the newer way KWin hands out output devices (verified live on /// `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()); 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 // 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| { let sibling_is_primary = sess.state.devices.values().any(|d| {
d.enabled d.enabled
&& d.priority == Some(1) && d.priority == Some(1)
&& d.proxy.as_ref().map(|p| p.id()) != our_id && d.proxy.as_ref().map(|p| p.id()) != our_id
&& d.name != ours.name
&& d.name && d.name
.as_deref() .as_deref()
.is_some_and(|n| n.starts_with(MANAGED_PREFIX)) .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), // 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. // 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(); let config = sess.new_config();
if let Some(proxy) = ours.proxy.as_ref() { if let Some(proxy) = ours.proxy.as_ref() {
config.enable(proxy, 1); config.enable(proxy, 1);
if !sibling_is_primary { if !sibling_is_primary {
config.set_primary_output(proxy); 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 { for (proxy, _, _) in &to_disable {
@@ -679,11 +723,46 @@ pub(crate) fn apply_topology(
.map(|(_, name, spec)| (name, spec)) .map(|(_, name, spec)| (name, spec))
.collect(); .collect();
if applied { if applied {
tracing::info!( // Read the outcome BACK instead of echoing the request: KWin emits fresh `priority`
also_disabled = ?disabled, // events after the apply; one sync barrier drains them, and "primary" is then a fact —
primary_taken = !sibling_is_primary, // ours holds the lowest priority among enabled outputs. The old log's `primary_taken`
"KWin output management: streamed output set as the desktop (in-process)" // 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 { } else {
tracing::warn!( tracing::warn!(
reason = ?sess.state.failure_reason, reason = ?sess.state.failure_reason,