feat(vdisplay): compositor-embedded pointer for sessions without the cursor channel (Phase B)
Since the cursor-channel work, every Linux virtual output was created in metadata pointer mode — making ALL sessions depend on host-side cursor compositing, including the ones that can never use the channel (Moonlight/ GameStream, legacy clients, capture-mode starts). Those sessions paid the blend bring-up per session and, whenever a visible cursor was composited, the loss of NVENC's stream-ordered submit — for a strictly worse cursor than the compositor's own. Mirror the Windows no-regression gate: the already-wired per-session set_hw_cursor(cursor_forward) now drives the Linux backends too. A cursor-channel session gets metadata (shapes forwarded, composite flip blends host-side — today's validated path, unchanged); every other session gets the pointer compositor-EMBEDDED at creation (KWin zkde pointer=2, Mutter cursor-mode=1, wlroots/hyprland portal CursorMode::Embedded — their pre-channel default; the portal pair also gains the Metadata arm for channel sessions, wired but untested on-glass). SessionPlan.cursor_blend narrows to cursor-forward sessions and rides into the direct-SDK NVENC open, which skips the Vulkan slot-blend bring-up entirely when off — embedded sessions ring on plain CUDA surfaces and pay zero cursor cost, per-session or per-frame. The keep-alive registry's reuse key grows the created pointer mode (new VirtualDisplay::hw_cursor getter): a kept embedded display has no cursor metadata for a channel session to forward, and a kept metadata display would leave a channel-less session with no pointer in its frames. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -142,13 +142,25 @@ pub trait VirtualDisplay: Send {
|
||||
/// the backend's default EDID. Default: no-op — only the Windows pf-vdisplay backend can mint
|
||||
/// per-monitor EDIDs today (the Linux compositors' virtual outputs take no EDID from us).
|
||||
fn set_client_hdr(&mut self, _hdr: Option<punktfunk_core::quic::HdrMeta>) {}
|
||||
/// Ask the backend for an out-of-band HARDWARE CURSOR on the created output (the M2c cursor
|
||||
/// channel): the compositor/OS stops compositing the pointer into captured frames and the
|
||||
/// capture layer surfaces shape/position separately. Carried on the backend instance; set
|
||||
/// once before [`create`](Self::create). Default: no-op — only the Windows pf-vdisplay
|
||||
/// backend (IddCx hardware cursor, driver proto v5) implements it; the Linux portal path
|
||||
/// gets the same split from `SPA_META_Cursor` without asking.
|
||||
/// Ask the backend for an OUT-OF-BAND cursor on the created output (the cursor channel):
|
||||
/// the compositor/OS stops compositing the pointer into captured frames and the capture
|
||||
/// layer surfaces shape/position separately. Carried on the backend instance; set once
|
||||
/// before [`create`](Self::create) (both session paths pass `cursor_forward`). Off = the
|
||||
/// compositor EMBEDS the pointer into frames — zero host-side cursor work, the pre-channel
|
||||
/// path — which is what every session without the negotiated cursor cap gets (Moonlight /
|
||||
/// GameStream / legacy clients / capture-mode starts), mirroring the Windows no-regression
|
||||
/// gate. Implementations: Windows pf-vdisplay (IddCx hardware cursor, driver proto v5);
|
||||
/// KWin (zkde `pointer` metadata vs embedded); Mutter (`cursor-mode` metadata vs embedded);
|
||||
/// wlroots/hyprland (portal `CursorMode`). Default: no-op (gamescope has no cursor either
|
||||
/// way — see the Phase C source).
|
||||
fn set_hw_cursor(&mut self, _on: bool) {}
|
||||
/// The out-of-band-cursor request currently set (see [`set_hw_cursor`](Self::set_hw_cursor)).
|
||||
/// The registry includes it in the keep-alive REUSE key: a kept embedded-pointer display can
|
||||
/// never serve a cursor-channel session (its stream has no cursor metadata to forward) nor
|
||||
/// vice versa (the pointer would be missing from frames).
|
||||
fn hw_cursor(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// The stable identity slot the backend resolved for the most recent [`create`](Self::create) —
|
||||
/// the per-client id the identity policy assigned (`Some`), or `None` for shared/anonymous. The
|
||||
/// registry reads it right after `create` to key the display's group **arrangement** (manual
|
||||
|
||||
@@ -92,11 +92,19 @@ fn next_output_name() -> String {
|
||||
|
||||
/// The Hyprland virtual-display driver. Stateless — each [`create`](VirtualDisplay::create) adds one
|
||||
/// named headless output and spins up a portal thread owning the cast on it.
|
||||
pub struct HyprlandDisplay;
|
||||
pub struct HyprlandDisplay {
|
||||
/// Out-of-band cursor request (`set_hw_cursor`, the negotiated cursor channel): portal
|
||||
/// `CursorMode::Metadata` — shapes/positions ride `SPA_META_Cursor` for the channel + the
|
||||
/// composite blend. Off (every non-channel session): `Embedded` — the compositor paints the
|
||||
/// pointer into frames, zero host-side cursor work (the pre-channel default this backend
|
||||
/// always had). ⚠️ Metadata is UNTESTED on-glass for this backend (Phase B wired it so the
|
||||
/// channel isn't silently dead here; KWin/Mutter are the validated legs).
|
||||
hw_cursor: bool,
|
||||
}
|
||||
|
||||
impl HyprlandDisplay {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(HyprlandDisplay)
|
||||
Ok(HyprlandDisplay { hw_cursor: false })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +149,14 @@ impl VirtualDisplay for HyprlandDisplay {
|
||||
"hyprland"
|
||||
}
|
||||
|
||||
fn set_hw_cursor(&mut self, on: bool) {
|
||||
self.hw_cursor = on;
|
||||
}
|
||||
|
||||
fn hw_cursor(&self) -> bool {
|
||||
self.hw_cursor
|
||||
}
|
||||
|
||||
fn create(&mut self, mode: Mode) -> Result<VirtualOutput> {
|
||||
// Log the permission-system caveat once per process (silent black frames otherwise).
|
||||
preflight_once();
|
||||
@@ -167,9 +183,10 @@ impl VirtualDisplay for HyprlandDisplay {
|
||||
let (setup_tx, setup_rx) = std::sync::mpsc::channel::<Result<(OwnedFd, u32), String>>();
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let stop_thread = stop.clone();
|
||||
let hw_cursor = self.hw_cursor;
|
||||
thread::Builder::new()
|
||||
.name("punktfunk-hypr-vout".into())
|
||||
.spawn(move || portal_thread(setup_tx, stop_thread))
|
||||
.spawn(move || portal_thread(setup_tx, stop_thread, hw_cursor))
|
||||
.context("spawn hyprland portal thread")?;
|
||||
|
||||
let (fd, node_id) = match setup_rx.recv_timeout(Duration::from_secs(20)) {
|
||||
@@ -491,7 +508,17 @@ fn ensure_xdph_config() -> Result<()> {
|
||||
/// stopped (the zbus connection is the cast's lifetime). xdph answers source selection via our
|
||||
/// custom picker, no dialog. (Kept separate from wlroots' copy so each wlr-family backend stays
|
||||
/// self-owned per D1; unify if they ever diverge no further.)
|
||||
fn portal_thread(setup_tx: Sender<Result<(OwnedFd, u32), String>>, stop: Arc<AtomicBool>) {
|
||||
fn portal_thread(
|
||||
setup_tx: Sender<Result<(OwnedFd, u32), String>>,
|
||||
stop: Arc<AtomicBool>,
|
||||
hw_cursor: bool,
|
||||
) {
|
||||
// Portal cursor mode per the session's channel negotiation (see the struct doc).
|
||||
let cursor_mode = if hw_cursor {
|
||||
CursorMode::Metadata
|
||||
} else {
|
||||
CursorMode::Embedded
|
||||
};
|
||||
use ashpd::desktop::screencast::{CursorMode, Screencast, SelectSourcesOptions, SourceType};
|
||||
use ashpd::desktop::PersistMode;
|
||||
use ashpd::enumflags2::BitFlags;
|
||||
@@ -524,7 +551,7 @@ fn portal_thread(setup_tx: Sender<Result<(OwnedFd, u32), String>>, stop: Arc<Ato
|
||||
.select_sources(
|
||||
&session,
|
||||
SelectSourcesOptions::default()
|
||||
.set_cursor_mode(CursorMode::Embedded)
|
||||
.set_cursor_mode(cursor_mode)
|
||||
// xdph offers MONITOR; the custom picker selects our output.
|
||||
.set_sources(BitFlags::from_flag(SourceType::Monitor))
|
||||
.set_multiple(false)
|
||||
|
||||
@@ -56,12 +56,14 @@ use zkde::zkde_screencast_stream_unstable_v1::{
|
||||
};
|
||||
use zkde::zkde_screencast_unstable_v1::ZkdeScreencastUnstableV1 as Screencast;
|
||||
|
||||
/// `pointer` attachment mode (the protocol enum): ship the cursor as `SPA_META_Cursor` metadata
|
||||
/// (the capturer always negotiates the meta). Embedded mode would leave the cursor channel with
|
||||
/// nothing to forward AND the encoder blend with nothing to composite — the same trap as the
|
||||
/// Mutter backend's cursor-mode (see mutter.rs `CURSOR_METADATA`); the blend draws the pointer
|
||||
/// for sessions where the client does not render it itself.
|
||||
/// `pointer` attachment modes (the protocol enum), chosen per session by `set_hw_cursor`
|
||||
/// (Phase B — the Windows no-regression gate mirrored): a CURSOR-CHANNEL session gets METADATA
|
||||
/// (`SPA_META_Cursor` on the stream — shapes forwarded to the client, the composite flip blends
|
||||
/// host-side; embedded would leave both with nothing, the round-1 mutter trap), every other
|
||||
/// session gets EMBEDDED — KWin composites the pointer into frames itself, zero host-side
|
||||
/// cursor work, the pre-channel path Moonlight/legacy clients always had.
|
||||
const POINTER_METADATA: u32 = 4;
|
||||
const POINTER_EMBEDDED: u32 = 2;
|
||||
|
||||
/// The name we give the created output; KWin exposes it to output-management as `Virtual-<name>`.
|
||||
const VOUT_NAME: &str = "punktfunk";
|
||||
@@ -92,6 +94,9 @@ pub struct KwinDisplay {
|
||||
/// physical is re-enabled only when the display GROUP's last member drops (§6.1), not this session's.
|
||||
/// A backstop [`Drop`] runs it if the registry never took it (so a physical is never left dark).
|
||||
pending_restore: Option<Box<dyn FnOnce() + Send>>,
|
||||
/// Out-of-band cursor request (`set_hw_cursor`, i.e. the session negotiated the cursor
|
||||
/// channel): METADATA pointer mode at creation; off = EMBEDDED (see the consts above).
|
||||
hw_cursor: bool,
|
||||
}
|
||||
|
||||
impl Drop for KwinDisplay {
|
||||
@@ -128,6 +133,14 @@ impl VirtualDisplay for KwinDisplay {
|
||||
self.pending_restore.take()
|
||||
}
|
||||
|
||||
fn set_hw_cursor(&mut self, on: bool) {
|
||||
self.hw_cursor = on;
|
||||
}
|
||||
|
||||
fn hw_cursor(&self) -> bool {
|
||||
self.hw_cursor
|
||||
}
|
||||
|
||||
fn apply_position(&mut self, x: i32, y: i32) {
|
||||
// `last_name` holds the RESOLVED kscreen address (numeric output id, or the
|
||||
// `Virtual-<name>` fallback) — never re-derive from the name: during a supersede two
|
||||
@@ -166,6 +179,11 @@ impl VirtualDisplay for KwinDisplay {
|
||||
};
|
||||
self.last_name = Some(name.clone()); // for apply_position (registry-driven §6.2 layout)
|
||||
let (width, height) = (mode.width, mode.height);
|
||||
let pointer_mode = if self.hw_cursor {
|
||||
POINTER_METADATA
|
||||
} else {
|
||||
POINTER_EMBEDDED
|
||||
};
|
||||
let spawn_vout = |w: u32, h: u32| -> Result<(u32, Arc<AtomicBool>)> {
|
||||
let (setup_tx, setup_rx) = std::sync::mpsc::channel::<Result<u32, String>>();
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
@@ -173,7 +191,9 @@ impl VirtualDisplay for KwinDisplay {
|
||||
let name_thread = name.clone();
|
||||
thread::Builder::new()
|
||||
.name("punktfunk-kwin-vout".into())
|
||||
.spawn(move || virtual_output_thread(w, h, name_thread, setup_tx, stop_thread))
|
||||
.spawn(move || {
|
||||
virtual_output_thread(w, h, name_thread, pointer_mode, setup_tx, stop_thread)
|
||||
})
|
||||
.context("spawn KWin virtual-output thread")?;
|
||||
match setup_rx.recv_timeout(Duration::from_secs(20)) {
|
||||
Ok(Ok(v)) => Ok((v, stop)),
|
||||
@@ -201,7 +221,14 @@ impl VirtualDisplay for KwinDisplay {
|
||||
let want_high = mode.refresh_hz > 60;
|
||||
let birth_h = if want_high { height + 16 } else { height };
|
||||
let (mut node_id, mut stop) = spawn_vout(width, birth_h)?;
|
||||
tracing::info!(node_id, width, height, birth_h, "KWin virtual output ready");
|
||||
tracing::info!(
|
||||
node_id,
|
||||
width,
|
||||
height,
|
||||
birth_h,
|
||||
embedded_pointer = !self.hw_cursor,
|
||||
"KWin virtual output ready"
|
||||
);
|
||||
// ⚠️ ADDRESS BY NUMERIC KSCREEN ID, NEVER BY NAME: a supersede (mode switch) creates the
|
||||
// replacement output — SAME per-slot name, deliberately, for KWin's per-name config
|
||||
// persistence — while the superseded sibling is still alive (create-before-drop). Every
|
||||
@@ -716,10 +743,11 @@ fn virtual_output_thread(
|
||||
width: u32,
|
||||
height: u32,
|
||||
name: String,
|
||||
pointer_mode: u32,
|
||||
setup_tx: Sender<Result<u32, String>>,
|
||||
stop: Arc<AtomicBool>,
|
||||
) {
|
||||
if let Err(e) = run(width, height, &name, &setup_tx, &stop) {
|
||||
if let Err(e) = run(width, height, &name, pointer_mode, &setup_tx, &stop) {
|
||||
// If we never delivered a node id, report the failure to the waiting opener.
|
||||
let _ = setup_tx.send(Err(format!("{e:#}")));
|
||||
}
|
||||
@@ -760,6 +788,7 @@ fn run(
|
||||
width: u32,
|
||||
height: u32,
|
||||
name: &str,
|
||||
pointer_mode: u32,
|
||||
setup_tx: &Sender<Result<u32, String>>,
|
||||
stop: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
@@ -780,13 +809,14 @@ fn run(
|
||||
)
|
||||
})?;
|
||||
|
||||
// Create the virtual output sized to the client, cursor riding as stream metadata.
|
||||
// Create the virtual output sized to the client; the pointer rides as stream metadata
|
||||
// (cursor-channel session) or KWin embeds it into frames (everyone else — see the consts).
|
||||
let stream = screencast.stream_virtual_output(
|
||||
name.to_string(),
|
||||
width as i32,
|
||||
height as i32,
|
||||
1.0, // scale (logical == physical)
|
||||
POINTER_METADATA,
|
||||
pointer_mode,
|
||||
&qh,
|
||||
(),
|
||||
);
|
||||
|
||||
@@ -53,6 +53,11 @@ const APPLY_TEMPORARY: u32 = 1;
|
||||
/// Embedded mode would leave BOTH paths blind: no metadata means nothing to forward AND nothing
|
||||
/// to blend, and Mutter's own embedded painting is what the pre-channel path relied on.
|
||||
const CURSOR_METADATA: u32 = 2;
|
||||
/// `cursor-mode` embedded (mutter enum 1): Mutter composites the pointer into frames itself —
|
||||
/// zero host-side cursor work, the pre-channel path. Chosen for every session WITHOUT the
|
||||
/// negotiated cursor channel (`set_hw_cursor` off — Phase B, the Windows no-regression gate
|
||||
/// mirrored); metadata stays the cursor-channel sessions' mode (shapes forwarded / host blend).
|
||||
const CURSOR_EMBEDDED: u32 = 1;
|
||||
|
||||
/// Serializes, process-wide, every Mutter operation that adds/removes a virtual monitor or applies
|
||||
/// a monitor configuration. Each of these makes Mutter rebuild its monitor topology, and
|
||||
@@ -74,6 +79,9 @@ pub struct MutterDisplay {
|
||||
/// sole-monitor config (which would disable the first session's virtual). Defaults true (a lone
|
||||
/// session establishes topology as before).
|
||||
first_in_group: bool,
|
||||
/// Out-of-band cursor request (`set_hw_cursor`): metadata cursor-mode at creation; off =
|
||||
/// embedded (see [`CURSOR_EMBEDDED`]).
|
||||
hw_cursor: bool,
|
||||
/// The connecting client's cert fingerprint (set before [`create`](VirtualDisplay::create)) —
|
||||
/// keys the per-client persisted **scale** (GNOME can't persist it itself: Mutter mints a fresh
|
||||
/// EDID serial per `RecordVirtual` monitor, so `monitors.xml` never rematches; see
|
||||
@@ -89,6 +97,7 @@ impl MutterDisplay {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(MutterDisplay {
|
||||
first_in_group: true,
|
||||
hw_cursor: false,
|
||||
client_fp: None,
|
||||
last_slot: None,
|
||||
})
|
||||
@@ -117,6 +126,14 @@ impl VirtualDisplay for MutterDisplay {
|
||||
self.client_fp = fingerprint;
|
||||
}
|
||||
|
||||
fn set_hw_cursor(&mut self, on: bool) {
|
||||
self.hw_cursor = on;
|
||||
}
|
||||
|
||||
fn hw_cursor(&self) -> bool {
|
||||
self.hw_cursor
|
||||
}
|
||||
|
||||
fn last_identity_slot(&self) -> Option<u32> {
|
||||
self.last_slot
|
||||
}
|
||||
@@ -146,6 +163,7 @@ impl VirtualDisplay for MutterDisplay {
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let stop_thread = stop.clone();
|
||||
let first_in_group = self.first_in_group;
|
||||
let hw_cursor = self.hw_cursor;
|
||||
thread::Builder::new()
|
||||
.name("punktfunk-mutter-vout".into())
|
||||
.spawn(move || {
|
||||
@@ -154,6 +172,7 @@ impl VirtualDisplay for MutterDisplay {
|
||||
stop_thread,
|
||||
mode,
|
||||
first_in_group,
|
||||
hw_cursor,
|
||||
scale_key,
|
||||
remembered_scale,
|
||||
)
|
||||
@@ -208,6 +227,7 @@ fn session_thread(
|
||||
stop: Arc<AtomicBool>,
|
||||
mode: Mode,
|
||||
first_in_group: bool,
|
||||
hw_cursor: bool,
|
||||
scale_key: String,
|
||||
remembered_scale: Option<f64>,
|
||||
) {
|
||||
@@ -267,7 +287,7 @@ fn session_thread(
|
||||
}
|
||||
};
|
||||
|
||||
let session = match connect(mode, remembered_scale).await {
|
||||
let session = match connect(mode, hw_cursor, remembered_scale).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
let _ = setup_tx.send(Err(format!("{e:#}")));
|
||||
@@ -368,7 +388,11 @@ struct MutterSession {
|
||||
/// desktop scale, passed as the virtual mode's `preferred-scale` so Mutter creates the monitor
|
||||
/// already scaled (Mutter ≥ 48; older Mutter ignores unknown mode keys) — this covers the
|
||||
/// `extend` topology, where we never issue our own ApplyMonitorsConfig.
|
||||
async fn connect(mode: Mode, preferred_scale: Option<f64>) -> Result<MutterSession> {
|
||||
async fn connect(
|
||||
mode: Mode,
|
||||
hw_cursor: bool,
|
||||
preferred_scale: Option<f64>,
|
||||
) -> Result<MutterSession> {
|
||||
let conn = zbus::Connection::session()
|
||||
.await
|
||||
.context("connect session D-Bus")?;
|
||||
@@ -429,7 +453,14 @@ async fn connect(mode: Mode, preferred_scale: Option<f64>) -> Result<MutterSessi
|
||||
// once gated behind PUNKTFUNK_MUTTER_VIRTUAL_REFRESH; the stop-screencast-before-any-monitor-
|
||||
// reconfig teardown below fixed the crash, so pinning the client's refresh is now the default.)
|
||||
let mut rec: HashMap<&str, Value> = HashMap::new();
|
||||
rec.insert("cursor-mode", Value::from(CURSOR_METADATA));
|
||||
rec.insert(
|
||||
"cursor-mode",
|
||||
Value::from(if hw_cursor {
|
||||
CURSOR_METADATA
|
||||
} else {
|
||||
CURSOR_EMBEDDED
|
||||
}),
|
||||
);
|
||||
if mode.refresh_hz > 60 || preferred_scale.is_some() {
|
||||
let mut vmode: HashMap<&str, Value> = HashMap::new();
|
||||
vmode.insert("size", Value::from((mode.width, mode.height)));
|
||||
|
||||
@@ -54,11 +54,19 @@ chooser_cmd=cat {} 2>/dev/null || echo 'Monitor: HEADLESS-1'\n",
|
||||
|
||||
/// The wlroots/Sway virtual-display driver. Stateless — each [`create`](VirtualDisplay::create)
|
||||
/// adds one headless output and spins up a portal thread owning the cast on it.
|
||||
pub struct WlrootsDisplay;
|
||||
pub struct WlrootsDisplay {
|
||||
/// Out-of-band cursor request (`set_hw_cursor`, the negotiated cursor channel): portal
|
||||
/// `CursorMode::Metadata` — shapes/positions ride `SPA_META_Cursor` for the channel + the
|
||||
/// composite blend. Off (every non-channel session): `Embedded` — the compositor paints the
|
||||
/// pointer into frames, zero host-side cursor work (the pre-channel default this backend
|
||||
/// always had). ⚠️ Metadata is UNTESTED on-glass for this backend (Phase B wired it so the
|
||||
/// channel isn't silently dead here; KWin/Mutter are the validated legs).
|
||||
hw_cursor: bool,
|
||||
}
|
||||
|
||||
impl WlrootsDisplay {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(WlrootsDisplay)
|
||||
Ok(WlrootsDisplay { hw_cursor: false })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +81,14 @@ impl VirtualDisplay for WlrootsDisplay {
|
||||
"wlroots"
|
||||
}
|
||||
|
||||
fn set_hw_cursor(&mut self, on: bool) {
|
||||
self.hw_cursor = on;
|
||||
}
|
||||
|
||||
fn hw_cursor(&self) -> bool {
|
||||
self.hw_cursor
|
||||
}
|
||||
|
||||
fn create(&mut self, mode: Mode) -> Result<VirtualOutput> {
|
||||
let before = output_names()
|
||||
.context("swaymsg get_outputs (is the host inside the sway session env — SWAYSOCK?)")?;
|
||||
@@ -104,9 +120,10 @@ impl VirtualDisplay for WlrootsDisplay {
|
||||
let (setup_tx, setup_rx) = std::sync::mpsc::channel::<Result<(OwnedFd, u32), String>>();
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let stop_thread = stop.clone();
|
||||
let hw_cursor = self.hw_cursor;
|
||||
thread::Builder::new()
|
||||
.name("punktfunk-wlr-vout".into())
|
||||
.spawn(move || portal_thread(setup_tx, stop_thread))
|
||||
.spawn(move || portal_thread(setup_tx, stop_thread, hw_cursor))
|
||||
.context("spawn wlroots portal thread")?;
|
||||
|
||||
let (fd, node_id) = match setup_rx.recv_timeout(Duration::from_secs(20)) {
|
||||
@@ -255,7 +272,17 @@ fn ensure_xdpw_config() -> Result<()> {
|
||||
/// The ScreenCast portal handshake (same shape as the capture module's portal thread, but it
|
||||
/// reports the fd + node id and parks until stopped — the zbus connection is the cast's
|
||||
/// lifetime). xdpw answers the source selection via the chooser, no dialog.
|
||||
fn portal_thread(setup_tx: Sender<Result<(OwnedFd, u32), String>>, stop: Arc<AtomicBool>) {
|
||||
fn portal_thread(
|
||||
setup_tx: Sender<Result<(OwnedFd, u32), String>>,
|
||||
stop: Arc<AtomicBool>,
|
||||
hw_cursor: bool,
|
||||
) {
|
||||
// Portal cursor mode per the session's channel negotiation (see the struct doc).
|
||||
let cursor_mode = if hw_cursor {
|
||||
CursorMode::Metadata
|
||||
} else {
|
||||
CursorMode::Embedded
|
||||
};
|
||||
use ashpd::desktop::screencast::{CursorMode, Screencast, SelectSourcesOptions, SourceType};
|
||||
use ashpd::desktop::PersistMode;
|
||||
use ashpd::enumflags2::BitFlags;
|
||||
@@ -288,7 +315,7 @@ fn portal_thread(setup_tx: Sender<Result<(OwnedFd, u32), String>>, stop: Arc<Ato
|
||||
.select_sources(
|
||||
&session,
|
||||
SelectSourcesOptions::default()
|
||||
.set_cursor_mode(CursorMode::Embedded)
|
||||
.set_cursor_mode(cursor_mode)
|
||||
// xdpw offers MONITOR only; the chooser picks our output.
|
||||
.set_sources(BitFlags::from_flag(SourceType::Monitor))
|
||||
.set_multiple(false)
|
||||
|
||||
@@ -268,6 +268,11 @@ mod linux {
|
||||
/// Generation stamp: a [`DisplayLease`] only releases if its gen still matches (a stale lease
|
||||
/// — its entry was reused + re-stamped — is a no-op).
|
||||
gen: u64,
|
||||
/// The out-of-band-cursor mode this display was CREATED with (Phase B): metadata-pointer
|
||||
/// (cursor-channel session) vs compositor-embedded. Reuse requires an exact match — a kept
|
||||
/// embedded display has no cursor metadata for a channel session to forward, and a kept
|
||||
/// metadata display would leave a channel-less session with no pointer in its frames.
|
||||
hw_cursor: bool,
|
||||
}
|
||||
|
||||
/// A per-group topology-restore action (see [`Entry::topology_restore`]).
|
||||
@@ -473,6 +478,7 @@ mod linux {
|
||||
) && e.backend == backend
|
||||
&& e.mode == mode
|
||||
&& e.launch == launch
|
||||
&& e.hw_cursor == vd.hw_cursor()
|
||||
&& epoch_matches(e.backend, e.epoch, cur_epoch)
|
||||
})
|
||||
.map(|e| (e.gen, e.node_id))
|
||||
@@ -608,6 +614,7 @@ mod linux {
|
||||
launch: launch.clone(),
|
||||
epoch: cur_epoch,
|
||||
gen,
|
||||
hw_cursor: vd.hw_cursor(),
|
||||
};
|
||||
|
||||
// Compute this new display's position in its group (design §6.2) BEFORE pushing, then push
|
||||
@@ -1029,6 +1036,7 @@ mod linux {
|
||||
launch: None,
|
||||
epoch: 0,
|
||||
gen,
|
||||
hw_cursor: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -775,6 +775,10 @@ impl VirtualDisplay for PfVdisplayDisplay {
|
||||
self.hw_cursor = on;
|
||||
}
|
||||
|
||||
fn hw_cursor(&self) -> bool {
|
||||
self.hw_cursor
|
||||
}
|
||||
|
||||
fn set_quit_flag(&mut self, quit: std::sync::Arc<std::sync::atomic::AtomicBool>) {
|
||||
self.quit = Some(quit);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user