feat(host): KWin virtual output primary + settle portal env on switch
android / android (push) Failing after 22s
ci / web (push) Failing after 14s
ci / docs-site (push) Failing after 0s
ci / bench (push) Failing after 0s
deb / build-publish (push) Failing after 1s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Failing after 1s
decky / build-publish (push) Failing after 0s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Failing after 1s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Failing after 0s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 0s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 6s
docker / deploy-docs (push) Has been skipped
flatpak / build-publish (push) Failing after 3s
apple / swift (push) Successful in 54s
ci / rust (push) Failing after 1m42s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 52s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m11s

Two parked follow-ups from the session-aware host work:

#3 — KWin/Mutter virtual output not set primary. The auto-detected desktop path
*is* "stream this desktop", but the per-session virtual output wasn't promoted to
primary, so KDE/GNOME panels + windows stayed on an unstreamed real output and the
streamed screen showed only wallpaper. apply_session_env now defaults
PUNKTFUNK_KWIN_VIRTUAL_PRIMARY / PUNKTFUNK_MUTTER_VIRTUAL_PRIMARY on for the
auto path (explicit config still wins), so the streamed output becomes the sole
desktop.

#2 — input flaky after a mid-stream Gaming->Desktop switch. The xdg portal
(D-Bus-activated) and the systemd --user env still pointed at the old session, so
the host's RemoteDesktop portal opened against a half-stale env: it accepted
events but they didn't reach the compositor until a reconnect. New
vdisplay::settle_desktop_portal() pushes the live session env into the
systemd/D-Bus activation environment and (for KWin) restarts the portal so it
re-reads it, mirroring a fresh desktop login (and the existing wlroots portal
restart). Called from the mid-stream switch rebuild slot before the injector
reopens. GNOME uses Mutter's direct EIS, so it only gets the env push.

Compiles, clippy/fmt clean, 78 host tests pass. Live validation on the Bazzite
box next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 06:49:53 +00:00
parent 2448a33698
commit 336357643c
2 changed files with 71 additions and 0 deletions
+9
View File
@@ -2014,6 +2014,15 @@ fn virtual_stream(
env: sw.env, env: sw.env,
}); });
crate::vdisplay::apply_input_env(sw.compositor); crate::vdisplay::apply_input_env(sw.compositor);
// Switching INTO a desktop mid-stream: the xdg portal / systemd-user env may still
// point at the old session, so input would silently not land until a reconnect.
// Settle it (env push + KWin portal restart) before the injector reopens against it.
if matches!(
sw.compositor,
crate::vdisplay::Compositor::Kwin | crate::vdisplay::Compositor::Mutter
) {
crate::vdisplay::settle_desktop_portal(sw.compositor);
}
// Build the new backend's pipeline BEFORE dropping the old one (retry absorbs the // Build the new backend's pipeline BEFORE dropping the old one (retry absorbs the
// brief compositor-coexistence race during a switch); on failure keep the old. // brief compositor-coexistence race during a switch); on failure keep the old.
let rebuilt = let rebuilt =
+62
View File
@@ -367,10 +367,72 @@ pub fn apply_session_env(active: &ActiveSession) {
if active.kind == ActiveKind::DesktopGnome { if active.kind == ActiveKind::DesktopGnome {
std::env::set_var("PUNKTFUNK_FORCE_SHM", "1"); std::env::set_var("PUNKTFUNK_FORCE_SHM", "1");
} }
// Stream the desktop as the SOLE output: promote the per-session virtual output to PRIMARY so
// the panels + windows land on the streamed surface, not an unstreamed real output (the
// auto-detected desktop path *is* "stream this desktop"). Default-on for the auto path; an
// explicit `PUNKTFUNK_{KWIN,MUTTER}_VIRTUAL_PRIMARY` still wins.
match active.kind {
ActiveKind::DesktopKde if std::env::var_os("PUNKTFUNK_KWIN_VIRTUAL_PRIMARY").is_none() => {
std::env::set_var("PUNKTFUNK_KWIN_VIRTUAL_PRIMARY", "1");
}
ActiveKind::DesktopGnome
if std::env::var_os("PUNKTFUNK_MUTTER_VIRTUAL_PRIMARY").is_none() =>
{
std::env::set_var("PUNKTFUNK_MUTTER_VIRTUAL_PRIMARY", "1");
}
_ => {}
}
} }
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
pub fn apply_session_env(_active: &ActiveSession) {} pub fn apply_session_env(_active: &ActiveSession) {}
/// On a **mid-stream** switch to a desktop, the xdg-desktop-portal (D-Bus-activated) and the systemd
/// `--user` environment can still point at the OLD session, so the host's RemoteDesktop portal opens
/// against a half-stale env — it accepts events but they don't reach the compositor until a
/// reconnect. Push the live session env into the systemd/D-Bus activation environment and (for KWin,
/// whose input rides the xdg RemoteDesktop portal) restart the portal so it re-reads it — the same
/// settling a fresh desktop login does. Best-effort; mirrors the wlroots portal restart. GNOME uses
/// Mutter's *direct* EIS (no xdg portal), so it only needs the env push.
#[cfg(target_os = "linux")]
pub fn settle_desktop_portal(chosen: Compositor) {
const VARS: &[&str] = &[
"WAYLAND_DISPLAY",
"XDG_CURRENT_DESKTOP",
"DBUS_SESSION_BUS_ADDRESS",
"XDG_RUNTIME_DIR",
];
// Push our (correct) env into the systemd --user manager + the D-Bus activation environment so a
// re-activated portal/backend inherits the live session.
let _ = std::process::Command::new("systemctl")
.args(["--user", "import-environment"])
.args(VARS)
.status();
let _ = std::process::Command::new("dbus-update-activation-environment")
.arg("--systemd")
.args(VARS)
.status();
// KWin input goes through the xdg RemoteDesktop portal; the frontend routes RemoteDesktop to a
// backend by its OWN startup XDG_CURRENT_DESKTOP, so restart it (+ the KDE backend) to re-read
// the now-live session, then let it settle before the injector reopens against it.
if chosen == Compositor::Kwin {
let _ = std::process::Command::new("systemctl")
.args([
"--user",
"try-restart",
"xdg-desktop-portal-kde.service",
"xdg-desktop-portal.service",
])
.status();
std::thread::sleep(std::time::Duration::from_millis(600));
}
tracing::info!(
compositor = chosen.id(),
"settled desktop portal env for the switched-to session"
);
}
#[cfg(not(target_os = "linux"))]
pub fn settle_desktop_portal(_chosen: Compositor) {}
/// Route input to match the chosen video backend (they must not diverge), via the highest-priority /// Route input to match the chosen video backend (they must not diverge), via the highest-priority
/// `PUNKTFUNK_INPUT_BACKEND` knob the injector honors. For gamescope, the **default is a managed /// `PUNKTFUNK_INPUT_BACKEND` knob the injector honors. For gamescope, the **default is a managed
/// session at the client's mode** (tears the TV's autologin down on connect; restored on a debounced /// session at the client's mode** (tears the TV's autologin down on connect; restored on a debounced