feat(vdisplay): wlroots/Sway backend — swaymsg headless output + xdpw chooser

The fourth VirtualDisplay backend: `swaymsg create_output` adds a HEADLESS-N
output (name found by diffing get_outputs), `output <NAME> mode --custom
WxH@HzHz` sets the client's exact mode (and the refresh clock a fresh headless
output needs to produce frames at all), and the PipeWire node comes from the
ScreenCast portal. Headless output selection is non-interactive via
xdg-desktop-portal-wlr's chooser hook: a managed config (chooser_type=simple,
chooser_cmd cats /tmp/punktfunk-xdpw-output; portal try-restarted when the
config changes) plus a per-session `Monitor: <NAME>` written to that file.
Teardown is RAII: drop ends the portal thread (zbus connection drop ends the
cast) then `swaymsg output <NAME> unplug`. swaymsg commands go after `--` so
tokens like `--custom` reach sway instead of swaymsg's getopt.

Validated live on headless sway 1.11 (gles2-on-NVIDIA, xdpw 0.8.1), zero-copy
dmabuf→CUDA on both runs: 720p60 257 frames p50 0.77 ms, 1080p60 480/480
frames p50 1.18 ms, output unplugged with the session both times. The
checked-in xdpw.config sample now matches the managed config (the old
chooser_type=none/HEADLESS-1 form would pin capture to the wrong output).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 15:23:28 +00:00
parent 977c792b4b
commit 7381ba8218
3 changed files with 319 additions and 10 deletions
+5 -5
View File
@@ -6,8 +6,8 @@
//! this trait:
//!
//! * **KWin** — privileged `zkde_screencast_unstable_v1::stream_virtual_output` ([`kwin`]).
//! * **wlroots/Sway** — `swaymsg create_output` + `output mode --custom` (TODO).
//! * **Mutter/GNOME** — D-Bus `RemoteDesktop` + `ScreenCast.RecordVirtual` (TODO).
//! * **wlroots/Sway** — `swaymsg create_output` + `output mode --custom` ([`wlroots`]).
//! * **Mutter/GNOME** — D-Bus `RemoteDesktop` + `ScreenCast.RecordVirtual` ([`mutter`]).
//!
//! [`VirtualDisplay::create`] returns a [`VirtualOutput`]: the PipeWire node to capture plus an
//! owned keepalive whose `Drop` releases the output (RAII — no explicit `destroy`). Capture
@@ -101,9 +101,7 @@ pub fn open(compositor: Compositor) -> Result<Box<dyn VirtualDisplay>> {
Compositor::Kwin => Ok(Box::new(kwin::KwinDisplay::new()?)),
Compositor::Gamescope => Ok(Box::new(gamescope::GamescopeDisplay::new()?)),
Compositor::Mutter => Ok(Box::new(mutter::MutterDisplay::new()?)),
Compositor::Wlroots => {
anyhow::bail!("wlroots virtual-output backend not yet implemented")
}
Compositor::Wlroots => Ok(Box::new(wlroots::WlrootsDisplay::new()?)),
}
}
#[cfg(not(target_os = "linux"))]
@@ -126,3 +124,5 @@ mod gamescope;
mod kwin;
#[cfg(target_os = "linux")]
mod mutter;
#[cfg(target_os = "linux")]
mod wlroots;