refactor(host/W3): extract vdisplay backend contract into vdisplay/backend.rs
apple / swift (push) Successful in 1m20s
windows-host / package (push) Successful in 8m41s
android / android (push) Failing after 1m11s
apple / screenshots (push) Successful in 4m50s
ci / web (push) Successful in 1m3s
ci / docs-site (push) Successful in 1m47s
decky / build-publish (push) Successful in 21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
ci / bench (push) Successful in 5m55s
arch / build-publish (push) Successful in 12m28s
ci / rust (push) Successful in 17m58s
deb / build-publish (push) Successful in 12m7s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m38s
docker / deploy-docs (push) Successful in 23s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m39s
apple / swift (push) Successful in 1m20s
windows-host / package (push) Successful in 8m41s
android / android (push) Failing after 1m11s
apple / screenshots (push) Successful in 4m50s
ci / web (push) Successful in 1m3s
ci / docs-site (push) Successful in 1m47s
decky / build-publish (push) Successful in 21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
ci / bench (push) Successful in 5m55s
arch / build-publish (push) Successful in 12m28s
ci / rust (push) Successful in 17m58s
deb / build-publish (push) Successful in 12m7s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m38s
docker / deploy-docs (push) Successful in 23s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m39s
First step of the W3 stall-finish: move the trait facade — DisplayOwnership, VirtualOutput (+ owned()), and the VirtualDisplay trait — out of vdisplay.rs into vdisplay/backend.rs, re-exported so `crate::vdisplay::VirtualDisplay` etc. stay stable for the ~30 external call sites. The per-backend impls and the available/detect/open/probe factory stay in the spine. vdisplay.rs 1369→1173. Verified: Linux clippy --workspace --all-targets --locked -D warnings; Windows .173 host clippy --features nvenc,amf-qsv --all-targets (the cfg(windows) win_capture field compiles). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,203 +21,12 @@ pub use punktfunk_core::Mode;
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::os::fd::OwnedFd;
|
||||
|
||||
/// Who owns a [`VirtualOutput`]'s lifecycle — the honest declaration that lets the registry
|
||||
/// (`design/gamemode-and-dedicated-sessions.md` Part A1) pool **only what it owns** instead of
|
||||
/// keeping outputs whose real lifecycle lives elsewhere (the gamescope managed/attach paths, which
|
||||
/// are governed by the gamescope module's own session machinery). Extends the CLAUDE.md invariant
|
||||
/// "the registry owns display lifecycle" with its converse: what the registry does not own, it must
|
||||
/// not pretend to keep.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum DisplayOwnership {
|
||||
/// The registry owns the lifecycle: it may pool, linger, pin, and tear this display down (KWin,
|
||||
/// Mutter, wlroots, gamescope **bare spawn**, and the Windows manager-delegated monitor). The
|
||||
/// default — a backend that says nothing is registry-owned.
|
||||
#[default]
|
||||
Owned,
|
||||
/// Someone else's display, merely mirrored: no keep-alive, no topology, no reuse (gamescope
|
||||
/// **attach** to a foreign session). Codifies the design-doc §7 "attach = unmanaged pass-through"
|
||||
/// row.
|
||||
External,
|
||||
/// A box-level session the gamescope module manages (the managed `gamescope-session-plus` /
|
||||
/// SteamOS takeover). Passed through by the registry (its restore lifecycle is the gamescope
|
||||
/// module's until Part A3 hands the registry a real keepalive + restore duty).
|
||||
SessionManaged,
|
||||
}
|
||||
|
||||
/// A created virtual output: a PipeWire source to capture, plus an owned keepalive whose drop
|
||||
/// tears the output down (releases the compositor-side resource).
|
||||
///
|
||||
/// Allowed dead on non-Linux: the backends that construct it are all `cfg(target_os = "linux")`.
|
||||
#[allow(dead_code)]
|
||||
pub struct VirtualOutput {
|
||||
/// PipeWire node id of the output's screencast stream.
|
||||
pub node_id: u32,
|
||||
/// Portal/remote PipeWire fd when the node lives on a sandboxed remote (e.g. Mutter's
|
||||
/// RemoteDesktop+ScreenCast). `None` means the node is on the user's default PipeWire daemon
|
||||
/// (KWin `zkde_screencast`), captured by connecting to that daemon directly.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub remote_fd: Option<OwnedFd>,
|
||||
/// `(width, height, refresh_hz)` to prefer in the PipeWire format negotiation. KWin and
|
||||
/// gamescope outputs are created at the exact size, so this just confirms it; **Mutter sizes
|
||||
/// its virtual monitor FROM the negotiation**, so here it's what makes the client's mode real.
|
||||
pub preferred_mode: Option<(u32, u32, u32)>,
|
||||
/// Windows capture identity (DXGI adapter LUID + GDI output name) for the pf-vdisplay backend —
|
||||
/// what [`crate::capture::capture_virtual_output`] needs to duplicate the right output.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub win_capture: Option<crate::capture::dxgi::WinCaptureTarget>,
|
||||
/// Keeps the output — and whatever connection/thread backs it — alive; dropped on teardown.
|
||||
pub keepalive: Box<dyn Send>,
|
||||
/// Who owns this display's lifecycle (`design/gamemode-and-dedicated-sessions.md` A1). The
|
||||
/// registry pools/keep-alives only [`DisplayOwnership::Owned`] outputs; `External`/`SessionManaged`
|
||||
/// pass through (the capturer holds the keepalive, teardown on drop). Defaults to `Owned`.
|
||||
pub ownership: DisplayOwnership,
|
||||
/// `Some(gen)` when [`registry::acquire`](crate::vdisplay::registry::acquire) handed this back as a
|
||||
/// **reused** kept display (`design/gamemode-and-dedicated-sessions.md` A2), so the pipeline builder
|
||||
/// can [`registry::mark_failed(gen)`](crate::vdisplay::registry::mark_failed) if the first frame
|
||||
/// fails on it — tearing the corpse down so the retry loop's next acquire creates fresh instead of
|
||||
/// re-wedging on the same dead node. `None` on a fresh create / non-poolable output. Linux-only (the
|
||||
/// keep-alive pool is Linux).
|
||||
#[cfg(target_os = "linux")]
|
||||
pub reused_gen: Option<u64>,
|
||||
/// The registry pool generation of this display (fresh AND reused — unlike `reused_gen`), so a
|
||||
/// mid-stream mode-switch rebuild can [`registry::retire`](crate::vdisplay::registry::retire) the
|
||||
/// display it supersedes instead of leaving it to accumulate under a linger/forever keep-alive
|
||||
/// policy (`design/midstream-resolution-resize.md` H4). `None` for non-poolable outputs.
|
||||
/// Linux-only (the keep-alive pool is Linux).
|
||||
#[cfg(target_os = "linux")]
|
||||
pub pool_gen: Option<u64>,
|
||||
}
|
||||
|
||||
impl VirtualOutput {
|
||||
/// A registry-[owned](DisplayOwnership::Owned) output — the common case (KWin/Mutter/wlroots,
|
||||
/// gamescope bare-spawn, Windows). Fills `ownership: Owned`; the caller sets the platform fields.
|
||||
pub fn owned(
|
||||
node_id: u32,
|
||||
preferred_mode: Option<(u32, u32, u32)>,
|
||||
keepalive: Box<dyn Send>,
|
||||
) -> VirtualOutput {
|
||||
VirtualOutput {
|
||||
node_id,
|
||||
#[cfg(target_os = "linux")]
|
||||
remote_fd: None,
|
||||
preferred_mode,
|
||||
#[cfg(target_os = "windows")]
|
||||
win_capture: None,
|
||||
keepalive,
|
||||
ownership: DisplayOwnership::Owned,
|
||||
#[cfg(target_os = "linux")]
|
||||
reused_gen: None,
|
||||
#[cfg(target_os = "linux")]
|
||||
pool_gen: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pluggable virtual-output creation, per compositor.
|
||||
pub trait VirtualDisplay: Send {
|
||||
/// Human-readable backend name (e.g. `"kwin"`, `"wlroots"`, `"mutter"`).
|
||||
fn name(&self) -> &'static str;
|
||||
/// Create a virtual output of the given mode. Teardown is RAII: drop the returned
|
||||
/// [`VirtualOutput`]'s `keepalive`.
|
||||
fn create(&mut self, mode: Mode) -> Result<VirtualOutput>;
|
||||
/// Set the per-session command this display should launch into its nested output (the resolved
|
||||
/// app/game). Carried on the backend instance — NOT a process-global env var — so concurrent
|
||||
/// sessions can't stomp each other's launch target. Default: no-op (backends that attach to an
|
||||
/// existing session / don't spawn a nested command ignore it; only gamescope's spawn path uses it).
|
||||
fn set_launch_command(&mut self, _cmd: Option<String>) {}
|
||||
/// Set the connecting client's cert fingerprint so the backend can give that client a STABLE virtual
|
||||
/// monitor identity across reconnects and its saved per-monitor config (notably DPI scaling) is
|
||||
/// reapplied — via the OS (Windows EDID serial), the compositor (KWin per-slot output name), or
|
||||
/// host-side persistence (Mutter, whose virtual monitors can't carry a stable identity). Carried on
|
||||
/// the backend instance; set once before [`create`](Self::create). Default: no-op (wlroots/gamescope
|
||||
/// have no per-client identity). `None` = anonymous/unpaired/GameStream → the backend's auto
|
||||
/// (slot-based/shared) identity.
|
||||
fn set_client_identity(&mut self, _fingerprint: Option<[u8; 32]>) {}
|
||||
/// Hand the backend the session's deliberate-quit flag (set when the client closes with the QUIT
|
||||
/// application code — a user "stop", not a network drop) so the last lease's drop can tear the
|
||||
/// display down IMMEDIATELY, skipping the keep-alive linger — the Windows analogue of the Linux
|
||||
/// registry's `Linger::Immediate` path. Carried on the backend instance; set once before
|
||||
/// [`create`](Self::create). Default: no-op — only the Windows pf-vdisplay backend needs it (its
|
||||
/// leases live in the `VirtualDisplayManager`, which the registry's quit plumbing does not reach;
|
||||
/// Linux backends get the flag through `registry::acquire`).
|
||||
fn set_quit_flag(&mut self, _quit: std::sync::Arc<std::sync::atomic::AtomicBool>) {}
|
||||
/// Hand the backend the CLIENT display's HDR colour volume (`Hello::display_hdr` — primaries /
|
||||
/// white point / luminance range as reported by the client OS), so a freshly created virtual
|
||||
/// output can advertise the client's REAL panel in its EDID (pf-vdisplay codes the luminance
|
||||
/// into the CTA-861.3 HDR static-metadata block) — host apps and the OS then tone-map to the
|
||||
/// panel the stream actually lands on instead of a built-in placeholder volume. Carried on the
|
||||
/// backend instance; set once before [`create`](Self::create). `None` = unknown/SDR client →
|
||||
/// 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>) {}
|
||||
/// 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
|
||||
/// per-slot positions) and to label the mgmt `/display/state` slot. Default `None`: a backend
|
||||
/// with no per-client identity (wlroots/gamescope) always auto-rows. KWin (per-slot output
|
||||
/// naming) and Mutter (host-persisted per-client scale) report a real slot on Linux.
|
||||
fn last_identity_slot(&self) -> Option<u32> {
|
||||
None
|
||||
}
|
||||
/// Place the most-recently-[created](Self::create) output at `(x, y)` in the desktop coordinate
|
||||
/// space (design `display-management.md` §6.2 — layout). The registry, which owns the display
|
||||
/// **group**, computes the position from the whole group (auto-row or the console's manual
|
||||
/// arrangement) and calls this right after `create`. Default no-op: only backends that can position
|
||||
/// an output (KWin) implement it; the registry never calls it for the desktop origin `(0, 0)`, so a
|
||||
/// single-display / first-of-group session issues no positioning at all. Best-effort — a failure
|
||||
/// leaves the compositor's default placement.
|
||||
fn apply_position(&mut self, _x: i32, _y: i32) {}
|
||||
/// Take the topology **restore** action this [`create`](Self::create) prepared — the work that
|
||||
/// un-does an `exclusive`/`primary` topology change (e.g. re-enable the physical outputs KWin
|
||||
/// disabled). The registry lifts it into the display **group** so it runs **once, when the group's
|
||||
/// last display is torn down** (design §6.1 — per-group restore), not when this one session's
|
||||
/// display drops: a sibling `exclusive` session must not have the physical re-enabled under it.
|
||||
/// Called right after `create`; the backend must not also run it itself. Default `None` — a backend
|
||||
/// whose topology auto-reverts (Mutter `APPLY_TEMPORARY`) or that changes nothing has nothing to
|
||||
/// hand off.
|
||||
fn take_topology_restore(&mut self) -> Option<Box<dyn FnOnce() + Send>> {
|
||||
None
|
||||
}
|
||||
/// Tell the backend whether this create will be the **first** display in its group — i.e. no
|
||||
/// sibling of the same backend is already live (design §6.1). A backend that *establishes* the
|
||||
/// group's topology (Mutter's sole-monitor `exclusive` `ApplyMonitorsConfig`) applies it only when
|
||||
/// first; a later sibling **extends** into the already-exclusive desktop instead of re-clobbering it
|
||||
/// (a fresh sole-monitor config would disable the first session's virtual output). Set by the
|
||||
/// registry right before [`create`](Self::create). Default no-op: KWin recognises siblings at
|
||||
/// runtime by output name (first-slot-wins + a group-aware disable filter), and single-display
|
||||
/// backends never have a sibling.
|
||||
fn set_first_in_group(&mut self, _first: bool) {}
|
||||
/// Will a [`create`](Self::create) for the CURRENT request produce a registry-poolable
|
||||
/// ([`DisplayOwnership::Owned`], keep-alive-able) display? The registry consults this **before**
|
||||
/// its keep-alive reuse lookup, so it never hands a kept display of one flavor to a request of
|
||||
/// another — specifically a gamescope managed/attach acquire must not reuse a kept **bare-spawn**
|
||||
/// (they share the backend name `"gamescope"`). Default `true`; only gamescope overrides it,
|
||||
/// returning `false` when the env selects attach/managed (consistent with the `ownership` its
|
||||
/// `create` will report). See `design/gamemode-and-dedicated-sessions.md` A1.
|
||||
fn poolable_now(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// The resolved launch command carried on this backend instance (set via
|
||||
/// [`set_launch_command`](Self::set_launch_command)). The registry reads it to key keep-alive reuse
|
||||
/// on `(backend, mode, launch)` (`design/gamemode-and-dedicated-sessions.md` A2) — a kept display
|
||||
/// running game A must never be handed to a session that asked to launch game B. Default `None`
|
||||
/// (backends that never nest a command); only gamescope reports its `cmd`.
|
||||
fn launch_command(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
/// Is the kept display's `node_id` still live, checked **before** the registry REUSES it on a
|
||||
/// reconnect (`design/gamemode-and-dedicated-sessions.md` A2)? A `false` tells the registry to tear
|
||||
/// the dead entry down and create fresh instead of handing back a corpse (which would then fail
|
||||
/// capture and burn a retry). Default `true` (honest optimism — the [`mark_failed`] path is the
|
||||
/// backstop for a display that dies between this check and first frame). Only gamescope overrides
|
||||
/// it (its nested session dies when the game exits, independently of any compositor); KWin/Mutter
|
||||
/// nodes die only with their compositor, which the session-epoch invalidation (A4) already reaps.
|
||||
///
|
||||
/// [`mark_failed`]: crate::vdisplay::registry::mark_failed
|
||||
fn kept_display_alive(&mut self, _node_id: u32) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
/// The virtual-display backend contract — [`DisplayOwnership`], [`VirtualOutput`], and the
|
||||
/// [`VirtualDisplay`] trait (plan §W3). Re-exported so `crate::vdisplay::VirtualDisplay` etc. stay
|
||||
/// stable for the ~30 external call sites.
|
||||
#[path = "vdisplay/backend.rs"]
|
||||
pub(crate) mod backend;
|
||||
pub use backend::{DisplayOwnership, VirtualDisplay, VirtualOutput};
|
||||
|
||||
/// The **session epoch** — bumped whenever session detection observes a different compositor
|
||||
/// *instance*: an [`ActiveKind`] change, **or** a new compositor PID for the same kind (the
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
//! Virtual-display backend contract (plan §W3 — the trait facade carved out of [`super`]).
|
||||
//! [`DisplayOwnership`] declares who owns an output's lifecycle, [`VirtualOutput`] is the created
|
||||
//! output (PipeWire node + RAII keepalive), and [`VirtualDisplay`] is the per-compositor backend
|
||||
//! trait `super::open` returns boxed. The per-backend `impl`s and the factory stay in `super`.
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Who owns a [`VirtualOutput`]'s lifecycle — the honest declaration that lets the registry
|
||||
/// (`design/gamemode-and-dedicated-sessions.md` Part A1) pool **only what it owns** instead of
|
||||
/// keeping outputs whose real lifecycle lives elsewhere (the gamescope managed/attach paths, which
|
||||
/// are governed by the gamescope module's own session machinery). Extends the CLAUDE.md invariant
|
||||
/// "the registry owns display lifecycle" with its converse: what the registry does not own, it must
|
||||
/// not pretend to keep.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum DisplayOwnership {
|
||||
/// The registry owns the lifecycle: it may pool, linger, pin, and tear this display down (KWin,
|
||||
/// Mutter, wlroots, gamescope **bare spawn**, and the Windows manager-delegated monitor). The
|
||||
/// default — a backend that says nothing is registry-owned.
|
||||
#[default]
|
||||
Owned,
|
||||
/// Someone else's display, merely mirrored: no keep-alive, no topology, no reuse (gamescope
|
||||
/// **attach** to a foreign session). Codifies the design-doc §7 "attach = unmanaged pass-through"
|
||||
/// row.
|
||||
External,
|
||||
/// A box-level session the gamescope module manages (the managed `gamescope-session-plus` /
|
||||
/// SteamOS takeover). Passed through by the registry (its restore lifecycle is the gamescope
|
||||
/// module's until Part A3 hands the registry a real keepalive + restore duty).
|
||||
SessionManaged,
|
||||
}
|
||||
|
||||
/// A created virtual output: a PipeWire source to capture, plus an owned keepalive whose drop
|
||||
/// tears the output down (releases the compositor-side resource).
|
||||
///
|
||||
/// Allowed dead on non-Linux: the backends that construct it are all `cfg(target_os = "linux")`.
|
||||
#[allow(dead_code)]
|
||||
pub struct VirtualOutput {
|
||||
/// PipeWire node id of the output's screencast stream.
|
||||
pub node_id: u32,
|
||||
/// Portal/remote PipeWire fd when the node lives on a sandboxed remote (e.g. Mutter's
|
||||
/// RemoteDesktop+ScreenCast). `None` means the node is on the user's default PipeWire daemon
|
||||
/// (KWin `zkde_screencast`), captured by connecting to that daemon directly.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub remote_fd: Option<OwnedFd>,
|
||||
/// `(width, height, refresh_hz)` to prefer in the PipeWire format negotiation. KWin and
|
||||
/// gamescope outputs are created at the exact size, so this just confirms it; **Mutter sizes
|
||||
/// its virtual monitor FROM the negotiation**, so here it's what makes the client's mode real.
|
||||
pub preferred_mode: Option<(u32, u32, u32)>,
|
||||
/// Windows capture identity (DXGI adapter LUID + GDI output name) for the pf-vdisplay backend —
|
||||
/// what [`crate::capture::capture_virtual_output`] needs to duplicate the right output.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub win_capture: Option<crate::capture::dxgi::WinCaptureTarget>,
|
||||
/// Keeps the output — and whatever connection/thread backs it — alive; dropped on teardown.
|
||||
pub keepalive: Box<dyn Send>,
|
||||
/// Who owns this display's lifecycle (`design/gamemode-and-dedicated-sessions.md` A1). The
|
||||
/// registry pools/keep-alives only [`DisplayOwnership::Owned`] outputs; `External`/`SessionManaged`
|
||||
/// pass through (the capturer holds the keepalive, teardown on drop). Defaults to `Owned`.
|
||||
pub ownership: DisplayOwnership,
|
||||
/// `Some(gen)` when [`registry::acquire`](crate::vdisplay::registry::acquire) handed this back as a
|
||||
/// **reused** kept display (`design/gamemode-and-dedicated-sessions.md` A2), so the pipeline builder
|
||||
/// can [`registry::mark_failed(gen)`](crate::vdisplay::registry::mark_failed) if the first frame
|
||||
/// fails on it — tearing the corpse down so the retry loop's next acquire creates fresh instead of
|
||||
/// re-wedging on the same dead node. `None` on a fresh create / non-poolable output. Linux-only (the
|
||||
/// keep-alive pool is Linux).
|
||||
#[cfg(target_os = "linux")]
|
||||
pub reused_gen: Option<u64>,
|
||||
/// The registry pool generation of this display (fresh AND reused — unlike `reused_gen`), so a
|
||||
/// mid-stream mode-switch rebuild can [`registry::retire`](crate::vdisplay::registry::retire) the
|
||||
/// display it supersedes instead of leaving it to accumulate under a linger/forever keep-alive
|
||||
/// policy (`design/midstream-resolution-resize.md` H4). `None` for non-poolable outputs.
|
||||
/// Linux-only (the keep-alive pool is Linux).
|
||||
#[cfg(target_os = "linux")]
|
||||
pub pool_gen: Option<u64>,
|
||||
}
|
||||
|
||||
impl VirtualOutput {
|
||||
/// A registry-[owned](DisplayOwnership::Owned) output — the common case (KWin/Mutter/wlroots,
|
||||
/// gamescope bare-spawn, Windows). Fills `ownership: Owned`; the caller sets the platform fields.
|
||||
pub fn owned(
|
||||
node_id: u32,
|
||||
preferred_mode: Option<(u32, u32, u32)>,
|
||||
keepalive: Box<dyn Send>,
|
||||
) -> VirtualOutput {
|
||||
VirtualOutput {
|
||||
node_id,
|
||||
#[cfg(target_os = "linux")]
|
||||
remote_fd: None,
|
||||
preferred_mode,
|
||||
#[cfg(target_os = "windows")]
|
||||
win_capture: None,
|
||||
keepalive,
|
||||
ownership: DisplayOwnership::Owned,
|
||||
#[cfg(target_os = "linux")]
|
||||
reused_gen: None,
|
||||
#[cfg(target_os = "linux")]
|
||||
pool_gen: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pluggable virtual-output creation, per compositor.
|
||||
pub trait VirtualDisplay: Send {
|
||||
/// Human-readable backend name (e.g. `"kwin"`, `"wlroots"`, `"mutter"`).
|
||||
fn name(&self) -> &'static str;
|
||||
/// Create a virtual output of the given mode. Teardown is RAII: drop the returned
|
||||
/// [`VirtualOutput`]'s `keepalive`.
|
||||
fn create(&mut self, mode: Mode) -> Result<VirtualOutput>;
|
||||
/// Set the per-session command this display should launch into its nested output (the resolved
|
||||
/// app/game). Carried on the backend instance — NOT a process-global env var — so concurrent
|
||||
/// sessions can't stomp each other's launch target. Default: no-op (backends that attach to an
|
||||
/// existing session / don't spawn a nested command ignore it; only gamescope's spawn path uses it).
|
||||
fn set_launch_command(&mut self, _cmd: Option<String>) {}
|
||||
/// Set the connecting client's cert fingerprint so the backend can give that client a STABLE virtual
|
||||
/// monitor identity across reconnects and its saved per-monitor config (notably DPI scaling) is
|
||||
/// reapplied — via the OS (Windows EDID serial), the compositor (KWin per-slot output name), or
|
||||
/// host-side persistence (Mutter, whose virtual monitors can't carry a stable identity). Carried on
|
||||
/// the backend instance; set once before [`create`](Self::create). Default: no-op (wlroots/gamescope
|
||||
/// have no per-client identity). `None` = anonymous/unpaired/GameStream → the backend's auto
|
||||
/// (slot-based/shared) identity.
|
||||
fn set_client_identity(&mut self, _fingerprint: Option<[u8; 32]>) {}
|
||||
/// Hand the backend the session's deliberate-quit flag (set when the client closes with the QUIT
|
||||
/// application code — a user "stop", not a network drop) so the last lease's drop can tear the
|
||||
/// display down IMMEDIATELY, skipping the keep-alive linger — the Windows analogue of the Linux
|
||||
/// registry's `Linger::Immediate` path. Carried on the backend instance; set once before
|
||||
/// [`create`](Self::create). Default: no-op — only the Windows pf-vdisplay backend needs it (its
|
||||
/// leases live in the `VirtualDisplayManager`, which the registry's quit plumbing does not reach;
|
||||
/// Linux backends get the flag through `registry::acquire`).
|
||||
fn set_quit_flag(&mut self, _quit: std::sync::Arc<std::sync::atomic::AtomicBool>) {}
|
||||
/// Hand the backend the CLIENT display's HDR colour volume (`Hello::display_hdr` — primaries /
|
||||
/// white point / luminance range as reported by the client OS), so a freshly created virtual
|
||||
/// output can advertise the client's REAL panel in its EDID (pf-vdisplay codes the luminance
|
||||
/// into the CTA-861.3 HDR static-metadata block) — host apps and the OS then tone-map to the
|
||||
/// panel the stream actually lands on instead of a built-in placeholder volume. Carried on the
|
||||
/// backend instance; set once before [`create`](Self::create). `None` = unknown/SDR client →
|
||||
/// 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>) {}
|
||||
/// 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
|
||||
/// per-slot positions) and to label the mgmt `/display/state` slot. Default `None`: a backend
|
||||
/// with no per-client identity (wlroots/gamescope) always auto-rows. KWin (per-slot output
|
||||
/// naming) and Mutter (host-persisted per-client scale) report a real slot on Linux.
|
||||
fn last_identity_slot(&self) -> Option<u32> {
|
||||
None
|
||||
}
|
||||
/// Place the most-recently-[created](Self::create) output at `(x, y)` in the desktop coordinate
|
||||
/// space (design `display-management.md` §6.2 — layout). The registry, which owns the display
|
||||
/// **group**, computes the position from the whole group (auto-row or the console's manual
|
||||
/// arrangement) and calls this right after `create`. Default no-op: only backends that can position
|
||||
/// an output (KWin) implement it; the registry never calls it for the desktop origin `(0, 0)`, so a
|
||||
/// single-display / first-of-group session issues no positioning at all. Best-effort — a failure
|
||||
/// leaves the compositor's default placement.
|
||||
fn apply_position(&mut self, _x: i32, _y: i32) {}
|
||||
/// Take the topology **restore** action this [`create`](Self::create) prepared — the work that
|
||||
/// un-does an `exclusive`/`primary` topology change (e.g. re-enable the physical outputs KWin
|
||||
/// disabled). The registry lifts it into the display **group** so it runs **once, when the group's
|
||||
/// last display is torn down** (design §6.1 — per-group restore), not when this one session's
|
||||
/// display drops: a sibling `exclusive` session must not have the physical re-enabled under it.
|
||||
/// Called right after `create`; the backend must not also run it itself. Default `None` — a backend
|
||||
/// whose topology auto-reverts (Mutter `APPLY_TEMPORARY`) or that changes nothing has nothing to
|
||||
/// hand off.
|
||||
fn take_topology_restore(&mut self) -> Option<Box<dyn FnOnce() + Send>> {
|
||||
None
|
||||
}
|
||||
/// Tell the backend whether this create will be the **first** display in its group — i.e. no
|
||||
/// sibling of the same backend is already live (design §6.1). A backend that *establishes* the
|
||||
/// group's topology (Mutter's sole-monitor `exclusive` `ApplyMonitorsConfig`) applies it only when
|
||||
/// first; a later sibling **extends** into the already-exclusive desktop instead of re-clobbering it
|
||||
/// (a fresh sole-monitor config would disable the first session's virtual output). Set by the
|
||||
/// registry right before [`create`](Self::create). Default no-op: KWin recognises siblings at
|
||||
/// runtime by output name (first-slot-wins + a group-aware disable filter), and single-display
|
||||
/// backends never have a sibling.
|
||||
fn set_first_in_group(&mut self, _first: bool) {}
|
||||
/// Will a [`create`](Self::create) for the CURRENT request produce a registry-poolable
|
||||
/// ([`DisplayOwnership::Owned`], keep-alive-able) display? The registry consults this **before**
|
||||
/// its keep-alive reuse lookup, so it never hands a kept display of one flavor to a request of
|
||||
/// another — specifically a gamescope managed/attach acquire must not reuse a kept **bare-spawn**
|
||||
/// (they share the backend name `"gamescope"`). Default `true`; only gamescope overrides it,
|
||||
/// returning `false` when the env selects attach/managed (consistent with the `ownership` its
|
||||
/// `create` will report). See `design/gamemode-and-dedicated-sessions.md` A1.
|
||||
fn poolable_now(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// The resolved launch command carried on this backend instance (set via
|
||||
/// [`set_launch_command`](Self::set_launch_command)). The registry reads it to key keep-alive reuse
|
||||
/// on `(backend, mode, launch)` (`design/gamemode-and-dedicated-sessions.md` A2) — a kept display
|
||||
/// running game A must never be handed to a session that asked to launch game B. Default `None`
|
||||
/// (backends that never nest a command); only gamescope reports its `cmd`.
|
||||
fn launch_command(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
/// Is the kept display's `node_id` still live, checked **before** the registry REUSES it on a
|
||||
/// reconnect (`design/gamemode-and-dedicated-sessions.md` A2)? A `false` tells the registry to tear
|
||||
/// the dead entry down and create fresh instead of handing back a corpse (which would then fail
|
||||
/// capture and burn a retry). Default `true` (honest optimism — the [`mark_failed`] path is the
|
||||
/// backstop for a display that dies between this check and first frame). Only gamescope overrides
|
||||
/// it (its nested session dies when the game exits, independently of any compositor); KWin/Mutter
|
||||
/// nodes die only with their compositor, which the session-epoch invalidation (A4) already reaps.
|
||||
///
|
||||
/// [`mark_failed`]: crate::vdisplay::registry::mark_failed
|
||||
fn kept_display_alive(&mut self, _node_id: u32) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user