feat(vdisplay): topology decoupling — distinct primary level (Stage 2)

The three topology levels become distinct behaviors (Stage 0 only did
extend-vs-exclusive, faking primary):
- vdisplay::effective_topology() -> the concrete level (console policy > legacy
  *_VIRTUAL_PRIMARY env > Auto default). Backends read it directly at create
  time; apply_session_env no longer writes the boolean env (one fewer connect-
  path env mutation).
- Mutter: extend (no config), primary (virtual primary + physicals kept as
  secondaries — build_primary_keeping_physicals), exclusive (sole, physicals
  disabled). KWin: extend (no-op), primary (kscreen primary only), exclusive
  (primary + disable others).
- Windows should_isolate treats primary as isolate (the primary-only CCD variant
  is a follow-up); wlroots exclusive + the physical-keep effect need a
  display-attached box (headless lab boxes can't observe primary vs exclusive).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 00:18:46 +00:00
parent 60816709c4
commit cb7ddc0411
3 changed files with 143 additions and 82 deletions
@@ -111,14 +111,19 @@ impl VirtualDisplay for KwinDisplay {
} else {
mode.refresh_hz
};
// Make our streamed output the SOLE desktop: plasmashell + windows land on the surface we
// stream, not on the headless session's `kwin --virtual` bootstrap output (otherwise the
// client sees only the wallpaper of an empty extended output). Opt-in
// (PUNKTFUNK_KWIN_VIRTUAL_PRIMARY), mirroring the Mutter backend's PUNKTFUNK_MUTTER_VIRTUAL_PRIMARY.
let restore = if virtual_primary_enabled() {
apply_virtual_primary()
} else {
Vec::new()
// Display-management topology (Stage 2): `Extend` leaves the streamed output an extension;
// `Primary` makes it the primary output but keeps the bootstrap/physical outputs enabled;
// `Exclusive` makes it the SOLE desktop (others disabled, restored on teardown) — so
// plasmashell + windows land on the streamed surface, not the headless `kwin --virtual`
// bootstrap output. Read from the policy (replacing the PUNKTFUNK_KWIN_VIRTUAL_PRIMARY boolean).
use crate::vdisplay::policy::Topology;
let restore = match crate::vdisplay::effective_topology() {
Topology::Exclusive => apply_virtual_primary(),
Topology::Primary => {
apply_virtual_primary_only();
Vec::new() // nothing disabled → nothing to restore
}
Topology::Extend | Topology::Auto => Vec::new(),
};
Ok(VirtualOutput {
node_id,
@@ -213,21 +218,6 @@ fn read_active_refresh(output: &str) -> Option<u32> {
Some(hz.round() as u32)
}
/// Opt-in: make the per-session virtual output the sole desktop. Off by default — a host with no
/// competing output (or one that wants the bootstrap kept) is unaffected; the headless KDE appliance
/// (run-headless-kde.sh's `kwin --virtual` bootstrap + our streamed output) sets it so the desktop
/// renders on the streamed surface, not the bootstrap. Mirrors the Mutter backend's gate.
fn virtual_primary_enabled() -> bool {
std::env::var("PUNKTFUNK_KWIN_VIRTUAL_PRIMARY")
.map(|v| {
matches!(
v.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
})
.unwrap_or(false)
}
/// Names of currently-ENABLED outputs other than our `Virtual-punktfunk` — i.e. the headless
/// session's bootstrap output(s), which hold the desktop by default. Parsed from `kscreen-doctor -j`
/// (same source as [`read_active_refresh`]).
@@ -292,6 +282,23 @@ fn apply_virtual_primary() -> Vec<String> {
others
}
/// **Primary** (Stage 2): make the streamed output the primary but KEEP the other outputs enabled
/// (don't disable the bootstrap/physical) — so the shell re-homes onto the streamed surface while a
/// physical screen stays usable. Nothing to restore on teardown (we disabled nothing).
fn apply_virtual_primary_only() {
let ours = format!("Virtual-{VOUT_NAME}");
let ok = std::process::Command::new("kscreen-doctor")
.arg(format!("output.{ours}.primary"))
.status()
.map(|s| s.success())
.unwrap_or(false);
if ok {
tracing::info!("KWin: streamed output set primary (physical outputs kept)");
} else {
tracing::warn!("KWin: could not set the virtual output primary");
}
}
/// Dropping this releases the KWin virtual output: it flips the keepalive thread's `stop`, which
/// drops the Wayland connection and makes KWin reclaim the output.
struct StopGuard {