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;
|
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,
|
||||||
|
|||||||
Reference in New Issue
Block a user