refactor(host/W6.2): extract virtual-display orchestration into the pf-vdisplay crate
vdisplay.rs + vdisplay/* (the per-compositor Linux backends — KWin zkde-screencast,
wlroots swaymsg, Mutter RemoteDesktop, Hyprland — and the Windows IddCx/pf-vdisplay
driver backend, behind one VirtualDisplay trait; the mode-conflict admission
registry, the display policy/identity/custom-preset state, and the session-env /
gamescope routing) move into crates/pf-vdisplay (plan §W6). The DDC/CI panel-power
control (used only here) and the KWin zkde protocol XML move with it. This
completes the host-crate decomposition: capture, encode, inject, and vdisplay are
now four subsystem crates over the shared leaves, and punktfunk-host is the
orchestrator (serve/supervisor + native + gamestream + mgmt).
Coupling breaks (all down-only, cargo-tree acyclic):
- capture::dxgi identity -> pf_frame::dxgi; win_display/monitor_devnode/
console_session_mismatch -> pf-win-display leaf; can_open_another_session ->
pf-encode (the NVENC session-budget admission gate — acyclic peer edge).
- The registry's DisplayCreated/DisplayReleased emits into the host SSE event bus
invert to a leaf hook: pf-vdisplay emits a neutral DisplayEvent to a
host-registered DISPLAY_EVENT_SINK, so it never reaches the orchestrator's
events module.
- The IddCx driver module is renamed pf_vdisplay -> driver (its old name collided
with the crate name through the host's `mod vdisplay` shim glob).
The host keeps `mod vdisplay { pub use pf_vdisplay::* }` so every crate::vdisplay::*
path (serve/mgmt/native/the capture FrameChannelSender seam) is unchanged; the
heavy deps (wayland/ashpd/tokio + the zkde protocol) moved with the crate.
Co-authored: a fail-closed IOCTL-reply-length security fix (reject short/zeroed
pf-vdisplay driver replies before trusting protocol_version/target_id/wudf_pid/luid,
security-review 2026-07-17) rides this commit in the moved driver module.
Verified: Linux clippy -D warnings (pf-vdisplay + host nvenc,vulkan-encode,pyrowave
--all-targets) + pf-vdisplay 63/63 + host 167/167 tests; Windows clippy -D warnings
(pf-vdisplay --all-targets + host nvenc,amf-qsv --all-targets) Finished exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+31
@@ -2930,6 +2930,36 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pf-vdisplay"
|
||||||
|
version = "0.12.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"ashpd",
|
||||||
|
"bytemuck",
|
||||||
|
"futures-util",
|
||||||
|
"hex",
|
||||||
|
"libc",
|
||||||
|
"pf-driver-proto",
|
||||||
|
"pf-encode",
|
||||||
|
"pf-frame",
|
||||||
|
"pf-gpu",
|
||||||
|
"pf-host-config",
|
||||||
|
"pf-paths",
|
||||||
|
"pf-win-display",
|
||||||
|
"punktfunk-core",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"utoipa",
|
||||||
|
"wayland-backend",
|
||||||
|
"wayland-client",
|
||||||
|
"wayland-scanner",
|
||||||
|
"windows 0.62.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pf-win-display"
|
name = "pf-win-display"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
@@ -3259,6 +3289,7 @@ dependencies = [
|
|||||||
"pf-host-config",
|
"pf-host-config",
|
||||||
"pf-inject",
|
"pf-inject",
|
||||||
"pf-paths",
|
"pf-paths",
|
||||||
|
"pf-vdisplay",
|
||||||
"pf-win-display",
|
"pf-win-display",
|
||||||
"pf-zerocopy",
|
"pf-zerocopy",
|
||||||
"pipewire",
|
"pipewire",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ members = [
|
|||||||
"crates/pf-encode",
|
"crates/pf-encode",
|
||||||
"crates/pf-capture",
|
"crates/pf-capture",
|
||||||
"crates/pf-inject",
|
"crates/pf-inject",
|
||||||
|
"crates/pf-vdisplay",
|
||||||
"crates/pyrowave-sys",
|
"crates/pyrowave-sys",
|
||||||
"clients/probe",
|
"clients/probe",
|
||||||
"clients/linux",
|
"clients/linux",
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Virtual-display orchestration (plan §W6): the on-demand client-sized headless output — per-compositor
|
||||||
|
# Linux backends (KWin zkde-screencast, wlroots swaymsg, Mutter RemoteDesktop, Hyprland) and the
|
||||||
|
# Windows IddCx/pf-vdisplay driver backend — behind one VirtualDisplay trait, plus the mode-conflict
|
||||||
|
# admission registry and the DDC/CI panel control. Extracted into a subsystem crate; depends on the
|
||||||
|
# shared leaves (pf-frame's DXGI identity, pf-win-display's CCD helpers, pf-gpu, pf-paths) + pf-encode
|
||||||
|
# (the NVENC session-budget admission gate), never on capture/inject or the orchestrator (the display
|
||||||
|
# lifecycle events invert to a host-registered sink).
|
||||||
|
[package]
|
||||||
|
name = "pf-vdisplay"
|
||||||
|
version = "0.12.0"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version.workspace = true
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
description = "punktfunk host virtual-display orchestration: per-compositor Linux backends + the Windows IddCx driver backend behind one VirtualDisplay trait."
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
punktfunk-core = { path = "../punktfunk-core", features = ["quic"] }
|
||||||
|
pf-frame = { path = "../pf-frame" }
|
||||||
|
pf-gpu = { path = "../pf-gpu" }
|
||||||
|
pf-host-config = { path = "../pf-host-config" }
|
||||||
|
pf-paths = { path = "../pf-paths" }
|
||||||
|
pf-win-display = { path = "../pf-win-display" }
|
||||||
|
# The Windows admission gate consults NVENC's session budget (can_open_another_session).
|
||||||
|
pf-encode = { path = "../pf-encode" }
|
||||||
|
anyhow = "1"
|
||||||
|
tracing = "0.1"
|
||||||
|
# The platform-neutral policy/identity/custom-preset state is serde-serialized (persisted + the mgmt
|
||||||
|
# API), and the policy/preset types derive utoipa `ToSchema` for the OpenAPI document.
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
utoipa = { version = "5", features = ["axum_extras"] }
|
||||||
|
sha2 = "0.10"
|
||||||
|
hex = "0.4"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
libc = "0.2"
|
||||||
|
# The Mutter backend drives D-Bus RemoteDesktop + ScreenCast.RecordVirtual via ashpd on a tokio
|
||||||
|
# runtime; the gamescope restore worker + portal handshakes use tokio too.
|
||||||
|
ashpd = { version = "0.13", features = ["screencast", "remote_desktop"] }
|
||||||
|
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "net", "time"] }
|
||||||
|
futures-util = "0.3"
|
||||||
|
# KWin virtual-output creation via the privileged `zkde_screencast_unstable_v1` protocol (vendored in
|
||||||
|
# `protocols/`); the generated interface tables reference `wayland-backend`.
|
||||||
|
wayland-client = "0.31"
|
||||||
|
wayland-scanner = "0.31"
|
||||||
|
wayland-backend = "0.3"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
# The host<->driver wire contract for the pf-vdisplay IddCx backend (control IOCTLs + Pod structs).
|
||||||
|
pf-driver-proto = { path = "../pf-driver-proto" }
|
||||||
|
bytemuck = { version = "1.19", features = ["derive"] }
|
||||||
|
windows = { version = "0.62", features = [
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_Devices_DeviceAndDriverInstallation",
|
||||||
|
"Win32_Devices_Display",
|
||||||
|
"Win32_Graphics_Gdi",
|
||||||
|
"Win32_Storage_FileSystem",
|
||||||
|
"Win32_System_IO",
|
||||||
|
"Win32_System_Threading",
|
||||||
|
] }
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//! Virtual display orchestration (plan §6) — the project's differentiator.
|
//! Virtual display orchestration (plan §6 / §W6) — the project's differentiator.
|
||||||
//!
|
//!
|
||||||
//! A [`VirtualDisplay`] creates a *client-sized* output on demand, rendered natively and
|
//! A [`VirtualDisplay`] creates a *client-sized* output on demand, rendered natively and
|
||||||
//! headless (no scaling), to be captured and streamed, then torn down on disconnect. There is
|
//! headless (no scaling), to be captured and streamed, then torn down on disconnect. There is
|
||||||
@@ -11,8 +11,10 @@
|
|||||||
//!
|
//!
|
||||||
//! [`VirtualDisplay::create`] returns a [`VirtualOutput`]: the PipeWire node to capture plus an
|
//! [`VirtualDisplay::create`] returns a [`VirtualOutput`]: the PipeWire node to capture plus an
|
||||||
//! owned keepalive whose `Drop` releases the output (RAII — no explicit `destroy`). Capture
|
//! owned keepalive whose `Drop` releases the output (RAII — no explicit `destroy`). Capture
|
||||||
//! consumes the node via [`crate::capture::capture_virtual_output`].
|
//! consumes the node via the host `capture::capture_virtual_output`.
|
||||||
|
|
||||||
|
// Scaffold: some backend paths + Stage-3 identity are defined ahead of the target that uses them.
|
||||||
|
#![allow(dead_code)]
|
||||||
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
||||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
|
|
||||||
@@ -21,8 +23,36 @@ pub use punktfunk_core::Mode;
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use std::os::fd::OwnedFd;
|
use std::os::fd::OwnedFd;
|
||||||
|
|
||||||
|
/// A display-lifecycle event the registry emits when it creates or releases a managed virtual
|
||||||
|
/// display. The host wires [`DISPLAY_EVENT_SINK`] to translate these into its SSE event bus
|
||||||
|
/// (`crate::events` in the orchestrator), so this crate emits lifecycle signals without owning the
|
||||||
|
/// bus type — the one reach into the orchestrator's event module, inverted to a leaf hook (plan §W6).
|
||||||
|
pub enum DisplayEvent {
|
||||||
|
/// A virtual display was created on `backend` at `width`×`height`@`refresh_hz`.
|
||||||
|
Created {
|
||||||
|
backend: String,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
refresh_hz: u32,
|
||||||
|
},
|
||||||
|
/// `count` managed displays were released.
|
||||||
|
Released { count: u32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The host-registered sink that forwards [`DisplayEvent`]s to the SSE bus. Set once at startup; a
|
||||||
|
/// display event before it is set is silently dropped (no subscriber yet).
|
||||||
|
pub static DISPLAY_EVENT_SINK: std::sync::OnceLock<Box<dyn Fn(DisplayEvent) + Send + Sync>> =
|
||||||
|
std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
/// Emit a [`DisplayEvent`] to the host sink, if registered.
|
||||||
|
pub(crate) fn emit_display_event(ev: DisplayEvent) {
|
||||||
|
if let Some(sink) = DISPLAY_EVENT_SINK.get() {
|
||||||
|
sink(ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The virtual-display backend contract — [`DisplayOwnership`], [`VirtualOutput`], and the
|
/// The virtual-display backend contract — [`DisplayOwnership`], [`VirtualOutput`], and the
|
||||||
/// [`VirtualDisplay`] trait (plan §W3). Re-exported so `crate::vdisplay::VirtualDisplay` etc. stay
|
/// [`VirtualDisplay`] trait (plan §W3). Re-exported so `crate::VirtualDisplay` etc. stay
|
||||||
/// stable for the ~30 external call sites.
|
/// stable for the ~30 external call sites.
|
||||||
#[path = "vdisplay/backend.rs"]
|
#[path = "vdisplay/backend.rs"]
|
||||||
pub(crate) mod backend;
|
pub(crate) mod backend;
|
||||||
@@ -104,7 +134,7 @@ impl Compositor {
|
|||||||
Compositor::Gamescope => P::Gamescope,
|
Compositor::Gamescope => P::Gamescope,
|
||||||
// D2: no distinct wire byte for Hyprland — it shares the wlroots-family `Wlroots` pref.
|
// D2: no distinct wire byte for Hyprland — it shares the wlroots-family `Wlroots` pref.
|
||||||
// A client asking for `wlroots`/`hyprland` gets whichever of the two is the live session
|
// A client asking for `wlroots`/`hyprland` gets whichever of the two is the live session
|
||||||
// ([`pick_compositor`](crate::native::pick_compositor) resolves the family).
|
// (`pick_compositor` (host `native`) resolves the family).
|
||||||
Compositor::Hyprland => P::Wlroots,
|
Compositor::Hyprland => P::Wlroots,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,11 +274,11 @@ pub fn open(compositor: Compositor) -> Result<Box<dyn VirtualDisplay>> {
|
|||||||
// `ensure_available` self-heals the hostless-zombie state a WUDFHost crash leaves (adapter
|
// `ensure_available` self-heals the hostless-zombie state a WUDFHost crash leaves (adapter
|
||||||
// devnode present, interface gone): one device cycle + re-probe before giving up.
|
// devnode present, interface gone): one device cycle + re-probe before giving up.
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
pf_vdisplay::ensure_available(),
|
driver::ensure_available(),
|
||||||
"pf-vdisplay driver interface not found — the pf-vdisplay IddCx driver is not installed or \
|
"pf-vdisplay driver interface not found — the pf-vdisplay IddCx driver is not installed or \
|
||||||
not loaded (the host installer bundles it; reinstall or check the driver state)"
|
not loaded (the host installer bundles it; reinstall or check the driver state)"
|
||||||
);
|
);
|
||||||
Ok(Box::new(pf_vdisplay::PfVdisplayDisplay::new()?))
|
Ok(Box::new(driver::PfVdisplayDisplay::new()?))
|
||||||
}
|
}
|
||||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||||
{
|
{
|
||||||
@@ -280,7 +310,7 @@ pub fn probe(compositor: Compositor) -> Result<()> {
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
let _ = compositor;
|
let _ = compositor;
|
||||||
pf_vdisplay::probe()
|
driver::probe()
|
||||||
}
|
}
|
||||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||||
{
|
{
|
||||||
@@ -293,7 +323,7 @@ pub fn probe(compositor: Compositor) -> Result<()> {
|
|||||||
// layered above the per-compositor backends — platform-neutral (the mgmt API + both host paths read
|
// layered above the per-compositor backends — platform-neutral (the mgmt API + both host paths read
|
||||||
// it), so no cfg gate. See `design/display-management.md`.
|
// it), so no cfg gate. See `design/display-management.md`.
|
||||||
#[path = "vdisplay/policy.rs"]
|
#[path = "vdisplay/policy.rs"]
|
||||||
pub(crate) mod policy;
|
pub mod policy;
|
||||||
|
|
||||||
// The pure per-display lifecycle state machine (refcount + linger + pin), platform-neutral and
|
// The pure per-display lifecycle state machine (refcount + linger + pin), platform-neutral and
|
||||||
// property-tested; the registry executes the side effects its transitions dictate.
|
// property-tested; the registry executes the side effects its transitions dictate.
|
||||||
@@ -303,7 +333,7 @@ pub(crate) mod lifecycle;
|
|||||||
// The neutral snapshot/release facade over the per-OS lifecycle owners (Windows manager; Linux pool
|
// The neutral snapshot/release facade over the per-OS lifecycle owners (Windows manager; Linux pool
|
||||||
// later), for the management API's /display/state + /display/release.
|
// later), for the management API's /display/state + /display/release.
|
||||||
#[path = "vdisplay/registry.rs"]
|
#[path = "vdisplay/registry.rs"]
|
||||||
pub(crate) mod registry;
|
pub mod registry;
|
||||||
|
|
||||||
// The pure display-arrangement engine (auto-row / manual → per-member positions), platform-neutral
|
// The pure display-arrangement engine (auto-row / manual → per-member positions), platform-neutral
|
||||||
// and unit-tested; the registry (state readout) and the KWin position apply consume it.
|
// and unit-tested; the registry (state readout) and the KWin position apply consume it.
|
||||||
@@ -356,7 +386,7 @@ pub fn effective_topology() -> policy::Topology {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Goal-1 stage 6: per-compositor Linux backends under `vdisplay/linux/`, the Windows IddCx/SudoVDA
|
// Goal-1 stage 6: per-compositor Linux backends under `vdisplay/linux/`, the Windows IddCx/SudoVDA
|
||||||
// backends under `vdisplay/windows/`; `#[path]` keeps the `crate::vdisplay::*` module names flat.
|
// backends under `vdisplay/windows/`; `#[path]` keeps the `crate::*` module names flat.
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
#[path = "vdisplay/linux/gamescope.rs"]
|
#[path = "vdisplay/linux/gamescope.rs"]
|
||||||
mod gamescope;
|
mod gamescope;
|
||||||
@@ -371,7 +401,7 @@ pub(crate) mod identity;
|
|||||||
// Platform-neutral mode-conflict admission (Stage 4): the separate/join/steal/reject decision + the
|
// Platform-neutral mode-conflict admission (Stage 4): the separate/join/steal/reject decision + the
|
||||||
// live-session registry, wired into the punktfunk/1 handshake.
|
// live-session registry, wired into the punktfunk/1 handshake.
|
||||||
#[path = "vdisplay/admission.rs"]
|
#[path = "vdisplay/admission.rs"]
|
||||||
pub(crate) mod admission;
|
pub mod admission;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
#[path = "vdisplay/linux/hyprland.rs"]
|
#[path = "vdisplay/linux/hyprland.rs"]
|
||||||
@@ -383,7 +413,13 @@ mod kwin;
|
|||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
#[path = "vdisplay/windows/manager.rs"]
|
#[path = "vdisplay/windows/manager.rs"]
|
||||||
pub(crate) mod manager;
|
pub mod manager;
|
||||||
|
|
||||||
|
// DDC/CI panel power control (physical monitors), used only by the Windows manager to blank/wake the
|
||||||
|
// box's real panels around a virtual-display session — moved in with the subsystem (plan §W6).
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
#[path = "vdisplay/ddc.rs"]
|
||||||
|
mod ddc;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
#[path = "vdisplay/linux/mutter.rs"]
|
#[path = "vdisplay/linux/mutter.rs"]
|
||||||
@@ -391,7 +427,7 @@ mod mutter;
|
|||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
#[path = "vdisplay/windows/pf_vdisplay.rs"]
|
#[path = "vdisplay/windows/pf_vdisplay.rs"]
|
||||||
pub(crate) mod pf_vdisplay;
|
pub mod driver;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
#[path = "vdisplay/linux/wlroots.rs"]
|
#[path = "vdisplay/linux/wlroots.rs"]
|
||||||
+2
-2
@@ -16,7 +16,7 @@
|
|||||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||||
use std::sync::{Arc, Mutex, OnceLock};
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
|
|
||||||
use crate::vdisplay::policy::{self, ModeConflict};
|
use crate::policy::{self, ModeConflict};
|
||||||
|
|
||||||
/// A currently-live session, as admission sees it.
|
/// A currently-live session, as admission sees it.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -135,7 +135,7 @@ pub fn admit(req_identity: Option<[u8; 32]>) -> Admission {
|
|||||||
"host display budget exhausted: {slots} display(s) live/kept, max_displays = {max}"
|
"host display budget exhausted: {slots} display(s) live/kept, max_displays = {max}"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if !crate::encode::can_open_another_session() {
|
if !pf_encode::can_open_another_session() {
|
||||||
return Admission::Reject(
|
return Admission::Reject(
|
||||||
"host encoder budget exhausted: no NVENC session headroom for another display"
|
"host encoder budget exhausted: no NVENC session headroom for another display"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
+6
-6
@@ -46,25 +46,25 @@ pub struct VirtualOutput {
|
|||||||
/// its virtual monitor FROM the negotiation**, so here it's what makes the client's mode real.
|
/// its virtual monitor FROM the negotiation**, so here it's what makes the client's mode real.
|
||||||
pub preferred_mode: Option<(u32, u32, u32)>,
|
pub preferred_mode: Option<(u32, u32, u32)>,
|
||||||
/// Windows capture identity (DXGI adapter LUID + GDI output name) for the pf-vdisplay backend —
|
/// 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.
|
/// what the host `capture::capture_virtual_output` needs to duplicate the right output.
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub win_capture: Option<crate::capture::dxgi::WinCaptureTarget>,
|
pub win_capture: Option<pf_frame::dxgi::WinCaptureTarget>,
|
||||||
/// Keeps the output — and whatever connection/thread backs it — alive; dropped on teardown.
|
/// Keeps the output — and whatever connection/thread backs it — alive; dropped on teardown.
|
||||||
pub keepalive: Box<dyn Send>,
|
pub keepalive: Box<dyn Send>,
|
||||||
/// Who owns this display's lifecycle (`design/gamemode-and-dedicated-sessions.md` A1). The
|
/// Who owns this display's lifecycle (`design/gamemode-and-dedicated-sessions.md` A1). The
|
||||||
/// registry pools/keep-alives only [`DisplayOwnership::Owned`] outputs; `External`/`SessionManaged`
|
/// registry pools/keep-alives only [`DisplayOwnership::Owned`] outputs; `External`/`SessionManaged`
|
||||||
/// pass through (the capturer holds the keepalive, teardown on drop). Defaults to `Owned`.
|
/// pass through (the capturer holds the keepalive, teardown on drop). Defaults to `Owned`.
|
||||||
pub ownership: DisplayOwnership,
|
pub ownership: DisplayOwnership,
|
||||||
/// `Some(gen)` when [`registry::acquire`](crate::vdisplay::registry::acquire) handed this back as a
|
/// `Some(gen)` when [`registry::acquire`](crate::registry::acquire) handed this back as a
|
||||||
/// **reused** kept display (`design/gamemode-and-dedicated-sessions.md` A2), so the pipeline builder
|
/// **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
|
/// can [`registry::mark_failed(gen)`](crate::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
|
/// 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
|
/// re-wedging on the same dead node. `None` on a fresh create / non-poolable output. Linux-only (the
|
||||||
/// keep-alive pool is Linux).
|
/// keep-alive pool is Linux).
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub reused_gen: Option<u64>,
|
pub reused_gen: Option<u64>,
|
||||||
/// The registry pool generation of this display (fresh AND reused — unlike `reused_gen`), so a
|
/// 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
|
/// mid-stream mode-switch rebuild can [`registry::retire`](crate::registry::retire) the
|
||||||
/// display it supersedes instead of leaving it to accumulate under a linger/forever keep-alive
|
/// 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.
|
/// policy (`design/midstream-resolution-resize.md` H4). `None` for non-poolable outputs.
|
||||||
/// Linux-only (the keep-alive pool is Linux).
|
/// Linux-only (the keep-alive pool is Linux).
|
||||||
@@ -197,7 +197,7 @@ pub trait VirtualDisplay: Send {
|
|||||||
/// it (its nested session dies when the game exits, independently of any compositor); KWin/Mutter
|
/// 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.
|
/// nodes die only with their compositor, which the session-epoch invalidation (A4) already reaps.
|
||||||
///
|
///
|
||||||
/// [`mark_failed`]: crate::vdisplay::registry::mark_failed
|
/// [`mark_failed`]: crate::registry::mark_failed
|
||||||
fn kept_display_alive(&mut self, _node_id: u32) -> bool {
|
fn kept_display_alive(&mut self, _node_id: u32) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
+8
-8
@@ -167,10 +167,10 @@ pub(crate) fn global() -> &'static Mutex<DisplayIdentityMap> {
|
|||||||
pub(crate) fn resolve_slot(
|
pub(crate) fn resolve_slot(
|
||||||
fp: Option<[u8; 32]>,
|
fp: Option<[u8; 32]>,
|
||||||
mode: (u32, u32),
|
mode: (u32, u32),
|
||||||
default: crate::vdisplay::policy::Identity,
|
default: crate::policy::Identity,
|
||||||
) -> Option<u32> {
|
) -> Option<u32> {
|
||||||
use crate::vdisplay::policy::Identity;
|
use crate::policy::Identity;
|
||||||
let id_policy = crate::vdisplay::policy::prefs()
|
let id_policy = crate::policy::prefs()
|
||||||
.configured_effective()
|
.configured_effective()
|
||||||
.map(|e| e.identity)
|
.map(|e| e.identity)
|
||||||
.unwrap_or(default);
|
.unwrap_or(default);
|
||||||
@@ -201,9 +201,9 @@ const SCALE_FILE: &str = "display-scale.json";
|
|||||||
pub(crate) fn scale_key(
|
pub(crate) fn scale_key(
|
||||||
fp: Option<[u8; 32]>,
|
fp: Option<[u8; 32]>,
|
||||||
mode: (u32, u32),
|
mode: (u32, u32),
|
||||||
default: crate::vdisplay::policy::Identity,
|
default: crate::policy::Identity,
|
||||||
) -> String {
|
) -> String {
|
||||||
let id_policy = crate::vdisplay::policy::prefs()
|
let id_policy = crate::policy::prefs()
|
||||||
.configured_effective()
|
.configured_effective()
|
||||||
.map(|e| e.identity)
|
.map(|e| e.identity)
|
||||||
.unwrap_or(default);
|
.unwrap_or(default);
|
||||||
@@ -212,11 +212,11 @@ pub(crate) fn scale_key(
|
|||||||
|
|
||||||
/// Pure core of [`scale_key`] (policy already resolved) — unit-testable without the global store.
|
/// Pure core of [`scale_key`] (policy already resolved) — unit-testable without the global store.
|
||||||
fn scale_key_for(
|
fn scale_key_for(
|
||||||
policy: crate::vdisplay::policy::Identity,
|
policy: crate::policy::Identity,
|
||||||
fp: Option<[u8; 32]>,
|
fp: Option<[u8; 32]>,
|
||||||
mode: (u32, u32),
|
mode: (u32, u32),
|
||||||
) -> String {
|
) -> String {
|
||||||
use crate::vdisplay::policy::Identity;
|
use crate::policy::Identity;
|
||||||
match (policy, fp) {
|
match (policy, fp) {
|
||||||
(Identity::Shared, _) | (_, None) => "shared".to_string(),
|
(Identity::Shared, _) | (_, None) => "shared".to_string(),
|
||||||
(Identity::PerClient, Some(fp)) => identity_key(fp, mode, false),
|
(Identity::PerClient, Some(fp)) => identity_key(fp, mode, false),
|
||||||
@@ -343,7 +343,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scale_key_follows_the_identity_policy() {
|
fn scale_key_follows_the_identity_policy() {
|
||||||
use crate::vdisplay::policy::Identity;
|
use crate::policy::Identity;
|
||||||
// Shared / anonymous → the fixed shared slot.
|
// Shared / anonymous → the fixed shared slot.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
scale_key_for(Identity::Shared, Some(fp(1)), (1920, 1080)),
|
scale_key_for(Identity::Shared, Some(fp(1)), (1920, 1080)),
|
||||||
+1
-1
@@ -69,7 +69,7 @@ pub fn arrange(members: &[Member], layout: &Layout) -> Vec<Placement> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::vdisplay::policy::Position;
|
use crate::policy::Position;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
fn m(slot: Option<u32>, width: i32) -> Member {
|
fn m(slot: Option<u32>, width: i32) -> Member {
|
||||||
+8
-8
@@ -195,9 +195,9 @@ impl VirtualDisplay for GamescopeDisplay {
|
|||||||
// Only a bare SPAWN is registry-poolable (its `create` reports `Owned`); managed
|
// Only a bare SPAWN is registry-poolable (its `create` reports `Owned`); managed
|
||||||
// (`PUNKTFUNK_GAMESCOPE_SESSION`) and attach (`PUNKTFUNK_GAMESCOPE_NODE`) report
|
// (`PUNKTFUNK_GAMESCOPE_SESSION`) and attach (`PUNKTFUNK_GAMESCOPE_NODE`) report
|
||||||
// `SessionManaged`/`External`, so the registry must not reuse a kept spawn for them (same
|
// `SessionManaged`/`External`, so the registry must not reuse a kept spawn for them (same
|
||||||
// backend name). Mirrors [`crate::vdisplay::launch_is_nested`]; read under the env lock the
|
// backend name). Mirrors [`crate::launch_is_nested`]; read under the env lock the
|
||||||
// sub-mode ladder writes these keys under.
|
// sub-mode ladder writes these keys under.
|
||||||
crate::vdisplay::with_env_lock(|| {
|
crate::with_env_lock(|| {
|
||||||
std::env::var_os("PUNKTFUNK_GAMESCOPE_SESSION").is_none()
|
std::env::var_os("PUNKTFUNK_GAMESCOPE_SESSION").is_none()
|
||||||
&& std::env::var_os("PUNKTFUNK_GAMESCOPE_NODE").is_none()
|
&& std::env::var_os("PUNKTFUNK_GAMESCOPE_NODE").is_none()
|
||||||
})
|
})
|
||||||
@@ -395,7 +395,7 @@ fn steamos_session_present() -> bool {
|
|||||||
|
|
||||||
/// Does this box have the infrastructure the MANAGED gamescope mode drives — Bazzite's
|
/// Does this box have the infrastructure the MANAGED gamescope mode drives — Bazzite's
|
||||||
/// `gamescope-session-plus` or SteamOS's `gamescope-session`? The sub-mode ladder
|
/// `gamescope-session-plus` or SteamOS's `gamescope-session`? The sub-mode ladder
|
||||||
/// ([`crate::vdisplay::apply_input_env`]) only defaults to managed when this is true; a plain
|
/// ([`crate::apply_input_env`]) only defaults to managed when this is true; a plain
|
||||||
/// distro (neither present) falls through to the bare-spawn path instead of the old behaviour of
|
/// distro (neither present) falls through to the bare-spawn path instead of the old behaviour of
|
||||||
/// defaulting to managed and then bailing on the missing session script.
|
/// defaulting to managed and then bailing on the missing session script.
|
||||||
pub fn managed_session_available() -> bool {
|
pub fn managed_session_available() -> bool {
|
||||||
@@ -1027,7 +1027,7 @@ pub fn cancel_pending_restore() {
|
|||||||
/// manual return to gaming mode (the `gaming-rig` "the TV model" story, now truthful on gamescope);
|
/// manual return to gaming mode (the `gaming-rig` "the TV model" story, now truthful on gamescope);
|
||||||
/// * unconfigured → the historical 5 s [`RESTORE_DEBOUNCE`] (bit-for-bit today's behavior).
|
/// * unconfigured → the historical 5 s [`RESTORE_DEBOUNCE`] (bit-for-bit today's behavior).
|
||||||
fn restore_delay() -> Option<Duration> {
|
fn restore_delay() -> Option<Duration> {
|
||||||
use crate::vdisplay::policy::{self, Linger};
|
use crate::policy::{self, Linger};
|
||||||
match policy::prefs()
|
match policy::prefs()
|
||||||
.configured_effective()
|
.configured_effective()
|
||||||
.map(|e| e.keep_alive.linger())
|
.map(|e| e.keep_alive.linger())
|
||||||
@@ -1328,19 +1328,19 @@ fn stop_session(unit_name: &str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// File where the wrapper below writes gamescope's `LIBEI_SOCKET` (its EIS server socket), read by
|
/// File where the wrapper below writes gamescope's `LIBEI_SOCKET` (its EIS server socket), read by
|
||||||
/// the libei injector to drive input into the nested app. See [`crate::inject`].
|
/// the libei injector to drive input into the nested app. See the `pf-inject` crate.
|
||||||
///
|
///
|
||||||
/// Placed under `$XDG_RUNTIME_DIR` (a per-user, 0700 directory) — NOT a world-writable `/tmp` —
|
/// Placed under `$XDG_RUNTIME_DIR` (a per-user, 0700 directory) — NOT a world-writable `/tmp` —
|
||||||
/// so a second unprivileged local user can neither read the relayed socket path nor pre-plant the
|
/// so a second unprivileged local user can neither read the relayed socket path nor pre-plant the
|
||||||
/// file to redirect the host's injector to a rogue EIS server (which would let them keylog or deny
|
/// file to redirect the host's injector to a rogue EIS server (which would let them keylog or deny
|
||||||
/// the remote session's keyboard/mouse input; security-review 2026-06-28 #6). Falls back to `/tmp`
|
/// the remote session's keyboard/mouse input; security-review 2026-06-28 #6). Falls back to `/tmp`
|
||||||
/// only if `XDG_RUNTIME_DIR` is unset (gamescope itself requires it, so this is rare); the reader
|
/// only if `XDG_RUNTIME_DIR` is unset (gamescope itself requires it, so this is rare); the reader
|
||||||
/// ([`crate::inject`]) additionally rejects a symlinked relay file as defense-in-depth.
|
/// (the `pf-inject` crate) additionally rejects a symlinked relay file as defense-in-depth.
|
||||||
pub fn ei_socket_file() -> std::path::PathBuf {
|
pub fn ei_socket_file() -> std::path::PathBuf {
|
||||||
// The path itself is the shared `pf_paths::gamescope_ei_socket_file` contract (also read by the
|
// The path itself is the shared `pf_paths::gamescope_ei_socket_file` contract (also read by the
|
||||||
// libei injector). Compute it under the session env lock so a concurrent session handshake's
|
// libei injector). Compute it under the session env lock so a concurrent session handshake's
|
||||||
// `apply_session_env` XDG_RUNTIME_DIR retarget can't race this producer-side read.
|
// `apply_session_env` XDG_RUNTIME_DIR retarget can't race this producer-side read.
|
||||||
crate::vdisplay::with_env_lock(pf_paths::gamescope_ei_socket_file)
|
crate::with_env_lock(pf_paths::gamescope_ei_socket_file)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shape a resolved launch command for a bare-spawn gamescope session. A Steam URI launch
|
/// Shape a resolved launch command for a bare-spawn gamescope session. A Steam URI launch
|
||||||
@@ -1398,7 +1398,7 @@ fn spawn(w: u32, h: u32, hz: u32, cmd: Option<&str>, log: &std::path::Path) -> R
|
|||||||
// Read the env fallback under the shared env lock so it can't race a concurrent session's
|
// Read the env fallback under the shared env lock so it can't race a concurrent session's
|
||||||
// `set_var` of the same key (security-review 2026-06-28 #7).
|
// `set_var` of the same key (security-review 2026-06-28 #7).
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
crate::vdisplay::with_env_lock(|| std::env::var("PUNKTFUNK_GAMESCOPE_APP").ok())
|
crate::with_env_lock(|| std::env::var("PUNKTFUNK_GAMESCOPE_APP").ok())
|
||||||
})
|
})
|
||||||
.filter(|s| !s.trim().is_empty())
|
.filter(|s| !s.trim().is_empty())
|
||||||
.unwrap_or_else(|| "sleep infinity".to_string());
|
.unwrap_or_else(|| "sleep infinity".to_string());
|
||||||
+4
-4
@@ -147,10 +147,10 @@ impl VirtualDisplay for KwinDisplay {
|
|||||||
// persists by name). Shared / anonymous → the base `punktfunk` (today's single name). Linux
|
// persists by name). Shared / anonymous → the base `punktfunk` (today's single name). Linux
|
||||||
// defaults to Shared when unconfigured, so this is a no-op change until a policy opts in — AND
|
// defaults to Shared when unconfigured, so this is a no-op change until a policy opts in — AND
|
||||||
// it fixes the latent clash where two concurrent sessions both used `Virtual-punktfunk`.
|
// it fixes the latent clash where two concurrent sessions both used `Virtual-punktfunk`.
|
||||||
let slot = crate::vdisplay::identity::resolve_slot(
|
let slot = crate::identity::resolve_slot(
|
||||||
self.client_fp,
|
self.client_fp,
|
||||||
(mode.width, mode.height),
|
(mode.width, mode.height),
|
||||||
crate::vdisplay::policy::Identity::Shared,
|
crate::policy::Identity::Shared,
|
||||||
);
|
);
|
||||||
self.last_slot = slot; // reported to the registry for the group arrangement + state slot
|
self.last_slot = slot; // reported to the registry for the group arrangement + state slot
|
||||||
let name = match slot {
|
let name = match slot {
|
||||||
@@ -192,8 +192,8 @@ impl VirtualDisplay for KwinDisplay {
|
|||||||
// `Exclusive` makes it the SOLE desktop (others disabled, restored on teardown) — so
|
// `Exclusive` makes it the SOLE desktop (others disabled, restored on teardown) — so
|
||||||
// plasmashell + windows land on the streamed surface, not the headless `kwin --virtual`
|
// 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).
|
// bootstrap output. Read from the policy (replacing the PUNKTFUNK_KWIN_VIRTUAL_PRIMARY boolean).
|
||||||
use crate::vdisplay::policy::Topology;
|
use crate::policy::Topology;
|
||||||
let disabled = match crate::vdisplay::effective_topology() {
|
let disabled = match crate::effective_topology() {
|
||||||
Topology::Exclusive => apply_virtual_primary(&name),
|
Topology::Exclusive => apply_virtual_primary(&name),
|
||||||
Topology::Primary => {
|
Topology::Primary => {
|
||||||
apply_virtual_primary_only(&name);
|
apply_virtual_primary_only(&name);
|
||||||
+11
-11
@@ -22,7 +22,7 @@
|
|||||||
//! `monitors.xml` keyed by connector+vendor+product+**serial**, but Mutter mints a fresh serial
|
//! `monitors.xml` keyed by connector+vendor+product+**serial**, but Mutter mints a fresh serial
|
||||||
//! (`0x%.6x`, a per-shell counter) for every `RecordVirtual` monitor and the API offers no way to
|
//! (`0x%.6x`, a per-shell counter) for every `RecordVirtual` monitor and the API offers no way to
|
||||||
//! pass a stable identity — so GNOME's own persistence can never rematch our virtual output. The
|
//! pass a stable identity — so GNOME's own persistence can never rematch our virtual output. The
|
||||||
//! host persists the scale instead ([`identity::ScaleMap`](crate::vdisplay::identity), keyed per
|
//! host persists the scale instead ([`identity::ScaleMap`](crate::identity), keyed per
|
||||||
//! client / per the policy): reapplied at connect via the mode's `preferred-scale` plus the
|
//! client / per the policy): reapplied at connect via the mode's `preferred-scale` plus the
|
||||||
//! topology `ApplyMonitorsConfig`, and the user's mid-session changes are polled from
|
//! topology `ApplyMonitorsConfig`, and the user's mid-session changes are polled from
|
||||||
//! DisplayConfig and written back.
|
//! DisplayConfig and written back.
|
||||||
@@ -72,7 +72,7 @@ pub struct MutterDisplay {
|
|||||||
/// The connecting client's cert fingerprint (set before [`create`](VirtualDisplay::create)) —
|
/// 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
|
/// 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
|
/// EDID serial per `RecordVirtual` monitor, so `monitors.xml` never rematches; see
|
||||||
/// [`identity::ScaleMap`](crate::vdisplay::identity)).
|
/// [`identity::ScaleMap`](crate::identity)).
|
||||||
client_fp: Option<[u8; 32]>,
|
client_fp: Option<[u8; 32]>,
|
||||||
/// The identity slot the last `create` resolved — reported to the registry via
|
/// The identity slot the last `create` resolved — reported to the registry via
|
||||||
/// [`last_identity_slot`](VirtualDisplay::last_identity_slot) to key the group arrangement +
|
/// [`last_identity_slot`](VirtualDisplay::last_identity_slot) to key the group arrangement +
|
||||||
@@ -123,17 +123,17 @@ impl VirtualDisplay for MutterDisplay {
|
|||||||
// owns the identity), so the per-client scaling that policy promises is host-persisted
|
// owns the identity), so the per-client scaling that policy promises is host-persisted
|
||||||
// instead: the session thread reapplies the remembered scale and records the user's
|
// instead: the session thread reapplies the remembered scale and records the user's
|
||||||
// in-session changes under `scale_key`.
|
// in-session changes under `scale_key`.
|
||||||
self.last_slot = crate::vdisplay::identity::resolve_slot(
|
self.last_slot = crate::identity::resolve_slot(
|
||||||
self.client_fp,
|
self.client_fp,
|
||||||
(mode.width, mode.height),
|
(mode.width, mode.height),
|
||||||
crate::vdisplay::policy::Identity::Shared,
|
crate::policy::Identity::Shared,
|
||||||
);
|
);
|
||||||
let scale_key = crate::vdisplay::identity::scale_key(
|
let scale_key = crate::identity::scale_key(
|
||||||
self.client_fp,
|
self.client_fp,
|
||||||
(mode.width, mode.height),
|
(mode.width, mode.height),
|
||||||
crate::vdisplay::policy::Identity::Shared,
|
crate::policy::Identity::Shared,
|
||||||
);
|
);
|
||||||
let remembered_scale = crate::vdisplay::identity::scales()
|
let remembered_scale = crate::identity::scales()
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get(&scale_key);
|
.get(&scale_key);
|
||||||
@@ -195,7 +195,7 @@ impl Drop for StopGuard {
|
|||||||
/// non-first sibling extends into the group's already-exclusive desktop instead of re-clobbering it).
|
/// non-first sibling extends into the group's already-exclusive desktop instead of re-clobbering it).
|
||||||
/// `scale_key`/`remembered_scale` carry the per-client persisted scale: reapplied at connect,
|
/// `scale_key`/`remembered_scale` carry the per-client persisted scale: reapplied at connect,
|
||||||
/// and the user's in-session changes are recorded back under the key (GNOME itself can't — see
|
/// and the user's in-session changes are recorded back under the key (GNOME itself can't — see
|
||||||
/// [`identity::ScaleMap`](crate::vdisplay::identity)).
|
/// [`identity::ScaleMap`](crate::identity)).
|
||||||
// TOPOLOGY_LOCK is deliberately held across the awaits of the setup/teardown sequences: each
|
// TOPOLOGY_LOCK is deliberately held across the awaits of the setup/teardown sequences: each
|
||||||
// session owns this dedicated OS thread and its own single-future runtime, so the guard never
|
// session owns this dedicated OS thread and its own single-future runtime, so the guard never
|
||||||
// blocks a shared executor — it blocks exactly the sibling session threads, which is the point
|
// blocks a shared executor — it blocks exactly the sibling session threads, which is the point
|
||||||
@@ -230,8 +230,8 @@ fn session_thread(
|
|||||||
// value. `Extend` leaves the virtual output an extension (no config change); `Primary` makes
|
// value. `Extend` leaves the virtual output an extension (no config change); `Primary` makes
|
||||||
// it the primary monitor but keeps the physicals as secondaries; `Exclusive` makes it the
|
// it the primary monitor but keeps the physicals as secondaries; `Exclusive` makes it the
|
||||||
// SOLE output (physicals disabled). `Auto` never reaches here — it's resolved upstream.
|
// SOLE output (physicals disabled). `Auto` never reaches here — it's resolved upstream.
|
||||||
use crate::vdisplay::policy::Topology;
|
use crate::policy::Topology;
|
||||||
let topo = crate::vdisplay::effective_topology();
|
let topo = crate::effective_topology();
|
||||||
let topo_policy = matches!(topo, Topology::Primary | Topology::Exclusive);
|
let topo_policy = matches!(topo, Topology::Primary | Topology::Exclusive);
|
||||||
// Group-aware (§6.1): only the FIRST display of the group establishes the topology. A later
|
// Group-aware (§6.1): only the FIRST display of the group establishes the topology. A later
|
||||||
// sibling extends into the already-exclusive desktop — re-applying the sole-monitor config would
|
// sibling extends into the already-exclusive desktop — re-applying the sole-monitor config would
|
||||||
@@ -789,7 +789,7 @@ async fn persist_scale_change(dc: &zbus::Proxy<'_>, vconn: &str, scale_key: &str
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if (cur - *known).abs() > 1e-3 {
|
if (cur - *known).abs() > 1e-3 {
|
||||||
crate::vdisplay::identity::scales()
|
crate::identity::scales()
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set(scale_key, cur);
|
.set(scale_key, cur);
|
||||||
+1
-1
@@ -575,7 +575,7 @@ fn save_custom_presets(presets: &[CustomPreset]) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 12 hex chars from the name + wall-clock nanos — collision-free in practice, no uuid dep (the
|
/// 12 hex chars from the name + wall-clock nanos — collision-free in practice, no uuid dep (the
|
||||||
/// [`crate::library`] custom-entry id scheme).
|
/// the host `library` custom-entry id scheme).
|
||||||
fn new_preset_id(name: &str) -> String {
|
fn new_preset_id(name: &str) -> String {
|
||||||
let nanos = SystemTime::now()
|
let nanos = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
+21
-19
@@ -101,9 +101,11 @@ pub fn acquire(
|
|||||||
vd.create(mode)
|
vd.create(mode)
|
||||||
};
|
};
|
||||||
if out.is_ok() {
|
if out.is_ok() {
|
||||||
crate::events::emit(crate::events::EventKind::DisplayCreated {
|
crate::emit_display_event(crate::DisplayEvent::Created {
|
||||||
backend: backend.to_string(),
|
backend: backend.to_string(),
|
||||||
mode: crate::events::mode_str(mode.width, mode.height, mode.refresh_hz),
|
width: mode.width,
|
||||||
|
height: mode.height,
|
||||||
|
refresh_hz: mode.refresh_hz,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
@@ -166,7 +168,7 @@ pub fn release(slot: Option<u64>) -> usize {
|
|||||||
0
|
0
|
||||||
};
|
};
|
||||||
if released > 0 {
|
if released > 0 {
|
||||||
crate::events::emit(crate::events::EventKind::DisplayReleased {
|
crate::emit_display_event(crate::DisplayEvent::Released {
|
||||||
count: released as u32,
|
count: released as u32,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -223,9 +225,9 @@ mod linux {
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use super::DisplayInfo;
|
use super::DisplayInfo;
|
||||||
use crate::vdisplay::lifecycle::{self, Release};
|
use crate::lifecycle::{self, Release};
|
||||||
use crate::vdisplay::policy::{self, Layout, Linger};
|
use crate::policy::{self, Layout, Linger};
|
||||||
use crate::vdisplay::{Mode, VirtualDisplay, VirtualOutput};
|
use crate::{Mode, VirtualDisplay, VirtualOutput};
|
||||||
|
|
||||||
/// One pooled display: the lifecycle state + the backend's REAL keepalive (kept alive here so the
|
/// One pooled display: the lifecycle state + the backend's REAL keepalive (kept alive here so the
|
||||||
/// compositor output — and thus its PipeWire `node_id` — survives past the session), plus the
|
/// compositor output — and thus its PipeWire `node_id` — survives past the session), plus the
|
||||||
@@ -345,7 +347,7 @@ mod linux {
|
|||||||
// now means nothing. gamescope spawns are exempt (`epoch_matches` — independent nested sessions).
|
// now means nothing. gamescope spawns are exempt (`epoch_matches` — independent nested sessions).
|
||||||
// An Active entry is left to its own session's capture-loss rebuild (which, under the bumped
|
// An Active entry is left to its own session's capture-loss rebuild (which, under the bumped
|
||||||
// epoch, won't reuse it); `invalidate_backend` clears a whole desktop backend on a known switch.
|
// epoch, won't reuse it); `invalidate_backend` clears a whole desktop backend on a known switch.
|
||||||
let cur_epoch = crate::vdisplay::session_epoch();
|
let cur_epoch = crate::session_epoch();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < entries.len() {
|
while i < entries.len() {
|
||||||
let dead_epoch = !epoch_matches(entries[i].backend, entries[i].epoch, cur_epoch)
|
let dead_epoch = !epoch_matches(entries[i].backend, entries[i].epoch, cur_epoch)
|
||||||
@@ -425,7 +427,7 @@ mod linux {
|
|||||||
// A2 reuse key: the launch command this acquire carries (a kept spawn running game A must never
|
// A2 reuse key: the launch command this acquire carries (a kept spawn running game A must never
|
||||||
// be reused for a session launching game B). A4 reuse key: the current session epoch.
|
// be reused for a session launching game B). A4 reuse key: the current session epoch.
|
||||||
let launch = vd.launch_command();
|
let launch = vd.launch_command();
|
||||||
let cur_epoch = crate::vdisplay::session_epoch();
|
let cur_epoch = crate::session_epoch();
|
||||||
let r = reg();
|
let r = reg();
|
||||||
|
|
||||||
// Reap expired first (run any group restores + drop outside the lock).
|
// Reap expired first (run any group restores + drop outside the lock).
|
||||||
@@ -552,7 +554,7 @@ mod linux {
|
|||||||
// owns their lifecycle (its own restore machinery), so the registry must not keep them
|
// owns their lifecycle (its own restore machinery), so the registry must not keep them
|
||||||
// (the stale-node reuse wedge). Their unit keepalive tears nothing down on drop.
|
// (the stale-node reuse wedge). Their unit keepalive tears nothing down on drop.
|
||||||
// * `remote_fd = Some` — wlroots' sandboxed xdpw portal fd can't be re-opened per attach.
|
// * `remote_fd = Some` — wlroots' sandboxed xdpw portal fd can't be re-opened per attach.
|
||||||
if real.ownership != crate::vdisplay::DisplayOwnership::Owned || real.remote_fd.is_some() {
|
if real.ownership != crate::DisplayOwnership::Owned || real.remote_fd.is_some() {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
backend,
|
backend,
|
||||||
ownership = ?real.ownership,
|
ownership = ?real.ownership,
|
||||||
@@ -589,7 +591,7 @@ mod linux {
|
|||||||
// (rightmost under auto-row). `position_for_new` is pure; the lock is held only across it
|
// (rightmost under auto-row). `position_for_new` is pure; the lock is held only across it
|
||||||
// (I/O-free) — the backend apply is below, outside the lock.
|
// (I/O-free) — the backend apply is below, outside the lock.
|
||||||
let position = {
|
let position = {
|
||||||
use crate::vdisplay::layout::Member;
|
use crate::layout::Member;
|
||||||
let layout_policy = policy::prefs()
|
let layout_policy = policy::prefs()
|
||||||
.configured_effective()
|
.configured_effective()
|
||||||
.map(|e| e.layout)
|
.map(|e| e.layout)
|
||||||
@@ -761,15 +763,15 @@ mod linux {
|
|||||||
/// the pure [`layout`] engine, taking the new member's placement. Pure — so the append-in-acquire-
|
/// the pure [`layout`] engine, taking the new member's placement. Pure — so the append-in-acquire-
|
||||||
/// order + auto-row/manual arrangement is unit-tested independent of the pool/global.
|
/// order + auto-row/manual arrangement is unit-tested independent of the pool/global.
|
||||||
fn position_for_new(
|
fn position_for_new(
|
||||||
mut existing: Vec<(u64, crate::vdisplay::layout::Member)>,
|
mut existing: Vec<(u64, crate::layout::Member)>,
|
||||||
new: crate::vdisplay::layout::Member,
|
new: crate::layout::Member,
|
||||||
layout_policy: &Layout,
|
layout_policy: &Layout,
|
||||||
) -> crate::vdisplay::layout::Placement {
|
) -> crate::layout::Placement {
|
||||||
existing.sort_by_key(|(g, _)| *g);
|
existing.sort_by_key(|(g, _)| *g);
|
||||||
let mut members: Vec<crate::vdisplay::layout::Member> =
|
let mut members: Vec<crate::layout::Member> =
|
||||||
existing.into_iter().map(|(_, m)| m).collect();
|
existing.into_iter().map(|(_, m)| m).collect();
|
||||||
members.push(new);
|
members.push(new);
|
||||||
*crate::vdisplay::layout::arrange(&members, layout_policy)
|
*crate::layout::arrange(&members, layout_policy)
|
||||||
.last()
|
.last()
|
||||||
.expect("members is non-empty (just pushed `new`)")
|
.expect("members is non-empty (just pushed `new`)")
|
||||||
}
|
}
|
||||||
@@ -796,7 +798,7 @@ mod linux {
|
|||||||
layout_policy: &Layout,
|
layout_policy: &Layout,
|
||||||
topology: &str,
|
topology: &str,
|
||||||
) -> Vec<DisplayInfo> {
|
) -> Vec<DisplayInfo> {
|
||||||
use crate::vdisplay::layout::{self, Member};
|
use crate::layout::{self, Member};
|
||||||
|
|
||||||
// Small stable group ids by sorted group key — deterministic; in practice a host runs one live
|
// Small stable group ids by sorted group key — deterministic; in practice a host runs one live
|
||||||
// desktop backend → group 1 (with each gamescope spawn its own group).
|
// desktop backend → group 1 (with each gamescope spawn its own group).
|
||||||
@@ -977,7 +979,7 @@ mod linux {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::vdisplay::policy::{Layout, LayoutMode, Position};
|
use crate::policy::{Layout, LayoutMode, Position};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -1146,7 +1148,7 @@ mod linux {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn position_for_new_appends_right_in_acquire_order() {
|
fn position_for_new_appends_right_in_acquire_order() {
|
||||||
use crate::vdisplay::layout::{Member, Placement};
|
use crate::layout::{Member, Placement};
|
||||||
let m = |slot, w| Member {
|
let m = |slot, w| Member {
|
||||||
identity_slot: slot,
|
identity_slot: slot,
|
||||||
width: w,
|
width: w,
|
||||||
@@ -1163,7 +1165,7 @@ mod linux {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn position_for_new_honors_a_manual_pin() {
|
fn position_for_new_honors_a_manual_pin() {
|
||||||
use crate::vdisplay::layout::{Member, Placement};
|
use crate::layout::{Member, Placement};
|
||||||
let mut positions = BTreeMap::new();
|
let mut positions = BTreeMap::new();
|
||||||
positions.insert("5".to_string(), Position { x: 100, y: 200 });
|
positions.insert("5".to_string(), Position { x: 100, y: 200 });
|
||||||
let layout = Layout {
|
let layout = Layout {
|
||||||
+27
-27
@@ -6,7 +6,7 @@
|
|||||||
//! a **typed** [`OwnedHandle`] control device (no more raw `isize` smuggled across the pinger/linger
|
//! a **typed** [`OwnedHandle`] control device (no more raw `isize` smuggled across the pinger/linger
|
||||||
//! threads). The backend differences — the IOCTL protocol and the per-monitor REMOVE key — are the only
|
//! threads). The backend differences — the IOCTL protocol and the per-monitor REMOVE key — are the only
|
||||||
//! thing behind the [`VdisplayDriver`] seam; the state machine, the render-adapter pin decision, the
|
//! thing behind the [`VdisplayDriver`] seam; the state machine, the render-adapter pin decision, the
|
||||||
//! GDI/CCD glue (`crate::win_display`), and the generation-stamped [`MonitorLease`] are backend-neutral.
|
//! GDI/CCD glue (`pf_win_display::win_display`), and the generation-stamped [`MonitorLease`] are backend-neutral.
|
||||||
//!
|
//!
|
||||||
//! It's a process-wide singleton ([`vdm`]) initialised once with the chosen backend's driver — the
|
//! It's a process-wide singleton ([`vdm`]) initialised once with the chosen backend's driver — the
|
||||||
//! host runs exactly one virtual-display backend per process. The session holds a [`MonitorLease`];
|
//! host runs exactly one virtual-display backend per process. The session holds a [`MonitorLease`];
|
||||||
@@ -33,7 +33,7 @@ use windows::Win32::System::Threading::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{DisplayOwnership, Mode, VirtualOutput};
|
use super::{DisplayOwnership, Mode, VirtualOutput};
|
||||||
use crate::win_display::{
|
use pf_win_display::win_display::{
|
||||||
count_other_active, force_extend_topology, isolate_displays_ccd, resolve_gdi_name,
|
count_other_active, force_extend_topology, isolate_displays_ccd, resolve_gdi_name,
|
||||||
restore_displays_ccd, set_active_mode, set_virtual_primary_ccd, SavedConfig,
|
restore_displays_ccd, set_active_mode, set_virtual_primary_ccd, SavedConfig,
|
||||||
};
|
};
|
||||||
@@ -45,7 +45,7 @@ pub(crate) use driver::{AddedMonitor, MonitorKey, VdisplayDriver};
|
|||||||
#[path = "manager/instance.rs"]
|
#[path = "manager/instance.rs"]
|
||||||
mod instance;
|
mod instance;
|
||||||
use instance::claim_instance;
|
use instance::claim_instance;
|
||||||
pub(crate) use instance::claim_instance_eagerly;
|
pub use instance::claim_instance_eagerly;
|
||||||
|
|
||||||
#[path = "manager/knobs.rs"]
|
#[path = "manager/knobs.rs"]
|
||||||
mod knobs;
|
mod knobs;
|
||||||
@@ -89,11 +89,11 @@ struct Monitor {
|
|||||||
|
|
||||||
impl Monitor {
|
impl Monitor {
|
||||||
/// The capture target handed to a session (`None` until the GDI name resolves on a WDDM GPU).
|
/// The capture target handed to a session (`None` until the GDI name resolves on a WDDM GPU).
|
||||||
fn target(&self) -> Option<crate::capture::dxgi::WinCaptureTarget> {
|
fn target(&self) -> Option<pf_frame::dxgi::WinCaptureTarget> {
|
||||||
self.gdi_name
|
self.gdi_name
|
||||||
.clone()
|
.clone()
|
||||||
.map(|n| crate::capture::dxgi::WinCaptureTarget {
|
.map(|n| pf_frame::dxgi::WinCaptureTarget {
|
||||||
adapter_luid: crate::capture::dxgi::pack_luid(self.luid),
|
adapter_luid: pf_frame::dxgi::pack_luid(self.luid),
|
||||||
gdi_name: n,
|
gdi_name: n,
|
||||||
target_id: self.target_id,
|
target_id: self.target_id,
|
||||||
wudf_pid: self.wudf_pid,
|
wudf_pid: self.wudf_pid,
|
||||||
@@ -197,7 +197,7 @@ struct DeviceSlot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The host-lifetime virtual-display manager: the single owner of the monitor lifecycle.
|
/// The host-lifetime virtual-display manager: the single owner of the monitor lifecycle.
|
||||||
pub(crate) struct VirtualDisplayManager {
|
pub struct VirtualDisplayManager {
|
||||||
driver: Box<dyn VdisplayDriver>,
|
driver: Box<dyn VdisplayDriver>,
|
||||||
/// Control device, opened on first acquire and REOPENED after a gone-classified failure retired
|
/// Control device, opened on first acquire and REOPENED after a gone-classified failure retired
|
||||||
/// it (see [`DeviceSlot`]). Typed + `Send+Sync`, so the pinger/linger threads share it via the
|
/// it (see [`DeviceSlot`]). Typed + `Send+Sync`, so the pinger/linger threads share it via the
|
||||||
@@ -244,7 +244,7 @@ pub(crate) fn init(driver: Box<dyn VdisplayDriver>) -> &'static VirtualDisplayMa
|
|||||||
|
|
||||||
/// The process-wide manager. Panics if reached before a backend called [`init`] — by construction a
|
/// The process-wide manager. Panics if reached before a backend called [`init`] — by construction a
|
||||||
/// session is only ever created after `vdisplay::open` constructed the backend (which calls `init`).
|
/// session is only ever created after `vdisplay::open` constructed the backend (which calls `init`).
|
||||||
pub(crate) fn vdm() -> &'static VirtualDisplayManager {
|
pub fn vdm() -> &'static VirtualDisplayManager {
|
||||||
VDM.get()
|
VDM.get()
|
||||||
.expect("VirtualDisplayManager used before a backend initialised it")
|
.expect("VirtualDisplayManager used before a backend initialised it")
|
||||||
}
|
}
|
||||||
@@ -254,7 +254,7 @@ pub(crate) fn vdm() -> &'static VirtualDisplayManager {
|
|||||||
/// for the process lifetime — a dead one is RETIRED (kept alive, see [`DeviceSlot`]), so a stale copy
|
/// for the process lifetime — a dead one is RETIRED (kept alive, see [`DeviceSlot`]), so a stale copy
|
||||||
/// can only fail IOCTLs, never dangle. `None` before the first backend open — impossible for a
|
/// can only fail IOCTLs, never dangle. `None` before the first backend open — impossible for a
|
||||||
/// capturer, which only exists on a monitor the manager created.
|
/// capturer, which only exists on a monitor the manager created.
|
||||||
pub(crate) fn control_device_handle() -> Option<HANDLE> {
|
pub fn control_device_handle() -> Option<HANDLE> {
|
||||||
VDM.get().and_then(VirtualDisplayManager::device_handle)
|
VDM.get().and_then(VirtualDisplayManager::device_handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +384,7 @@ impl VirtualDisplayManager {
|
|||||||
// session then dies far downstream as "no frame published within 4s" (the lid-closed field
|
// session then dies far downstream as "no frame published within 4s" (the lid-closed field
|
||||||
// report). Name the real problem up front, once per acquire. Non-fatal: the OS-side
|
// report). Name the real problem up front, once per acquire. Non-fatal: the OS-side
|
||||||
// persistence-DB activation can still succeed, so the attempt proceeds.
|
// persistence-DB activation can still succeed, so the attempt proceeds.
|
||||||
if let Some((own, console)) = crate::interactive::console_session_mismatch() {
|
if let Some((own, console)) = pf_win_display::console_session_mismatch() {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
own_session = own,
|
own_session = own,
|
||||||
console_session = console,
|
console_session = console,
|
||||||
@@ -532,7 +532,7 @@ impl VirtualDisplayManager {
|
|||||||
// (`max_displays` across Active+Lingering+Pinned slots; the identity-slot ceiling of 15 is
|
// (`max_displays` across Active+Lingering+Pinned slots; the identity-slot ceiling of 15 is
|
||||||
// the hard limit behind it) — this is the fail-closed backstop for a session that got past
|
// the hard limit behind it) — this is the fail-closed backstop for a session that got past
|
||||||
// admission anyway. One live slot can never trip it (max_displays >= 1).
|
// admission anyway. One live slot can never trip it (max_displays >= 1).
|
||||||
let max = crate::vdisplay::policy::prefs()
|
let max = crate::policy::prefs()
|
||||||
.get()
|
.get()
|
||||||
.effective()
|
.effective()
|
||||||
.max_displays;
|
.max_displays;
|
||||||
@@ -659,11 +659,11 @@ impl VirtualDisplayManager {
|
|||||||
/// single member (it sits at the origin), so the single-display path issues no positioning at
|
/// single member (it sits at the origin), so the single-display path issues no positioning at
|
||||||
/// all. Records each monitor's applied position for the `/display/state` readout.
|
/// all. Records each monitor's applied position for the `/display/state` readout.
|
||||||
fn apply_group_layout(&self, inner: &mut MgrInner) {
|
fn apply_group_layout(&self, inner: &mut MgrInner) {
|
||||||
use crate::vdisplay::layout::{arrange, Member};
|
use crate::layout::{arrange, Member};
|
||||||
if inner.slots.len() < 2 {
|
if inner.slots.len() < 2 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let layout_policy = crate::vdisplay::policy::prefs().get().effective().layout;
|
let layout_policy = crate::policy::prefs().get().effective().layout;
|
||||||
// Members in acquire (gen) order — the auto-row order; identity slot 0 = anonymous (no
|
// Members in acquire (gen) order — the auto-row order; identity slot 0 = anonymous (no
|
||||||
// manual pin can address it, so it always auto-rows). `(slot, gen, target_id, width)`
|
// manual pin can address it, so it always auto-rows). `(slot, gen, target_id, width)`
|
||||||
// copied out so the arrangement below can write back through `get_mut`.
|
// copied out so the arrangement below can write back through `get_mut`.
|
||||||
@@ -691,7 +691,7 @@ impl VirtualDisplayManager {
|
|||||||
.collect();
|
.collect();
|
||||||
// SAFETY: `apply_source_positions` only drives the CCD query/apply FFI with owned local
|
// SAFETY: `apply_source_positions` only drives the CCD query/apply FFI with owned local
|
||||||
// buffers, under the `state` lock — the sole topology mutator.
|
// buffers, under the `state` lock — the sole topology mutator.
|
||||||
unsafe { crate::win_display::apply_source_positions(&positions) };
|
unsafe { pf_win_display::win_display::apply_source_positions(&positions) };
|
||||||
for (&(slot, ..), p) in ordered.iter().zip(&placements) {
|
for (&(slot, ..), p) in ordered.iter().zip(&placements) {
|
||||||
if let Some(
|
if let Some(
|
||||||
SlotState::Active { mon, .. }
|
SlotState::Active { mon, .. }
|
||||||
@@ -758,7 +758,7 @@ impl VirtualDisplayManager {
|
|||||||
}
|
}
|
||||||
// SAFETY: `activate_target_path` runs the CCD query/apply FFI with owned local buffers; the
|
// SAFETY: `activate_target_path` runs the CCD query/apply FFI with owned local buffers; the
|
||||||
// `Copy` target id is passed by value, under the `state` lock — the sole topology mutator.
|
// `Copy` target id is passed by value, under the `state` lock — the sole topology mutator.
|
||||||
if unsafe { crate::win_display::activate_target_path(target_id) } {
|
if unsafe { pf_win_display::win_display::activate_target_path(target_id) } {
|
||||||
for _ in 0..15 {
|
for _ in 0..15 {
|
||||||
thread::sleep(Duration::from_millis(200));
|
thread::sleep(Duration::from_millis(200));
|
||||||
// SAFETY: as the resolve loops above.
|
// SAFETY: as the resolve loops above.
|
||||||
@@ -831,7 +831,7 @@ impl VirtualDisplayManager {
|
|||||||
// group layout. `Extend` leaves it a plain extension. Both isolate + primary go
|
// group layout. `Extend` leaves it a plain extension. Both isolate + primary go
|
||||||
// through the atomic CCD path (no MODE_CHANGE storm). Opt out (extend) with
|
// through the atomic CCD path (no MODE_CHANGE storm). Opt out (extend) with
|
||||||
// PUNKTFUNK_NO_ISOLATE=1 / the console policy.
|
// PUNKTFUNK_NO_ISOLATE=1 / the console policy.
|
||||||
use crate::vdisplay::policy::Topology;
|
use crate::policy::Topology;
|
||||||
let first_member = inner.slots.is_empty();
|
let first_member = inner.slots.is_empty();
|
||||||
match topology_action() {
|
match topology_action() {
|
||||||
Topology::Exclusive => {
|
Topology::Exclusive => {
|
||||||
@@ -846,7 +846,7 @@ impl VirtualDisplayManager {
|
|||||||
// suspected source of the periodic sole-virtual-display stutter (the
|
// suspected source of the periodic sole-virtual-display stutter (the
|
||||||
// rationale + evidence live in `windows/ddc.rs`). First member only:
|
// rationale + evidence live in `windows/ddc.rs`). First member only:
|
||||||
// the physicals are already dark for a sibling.
|
// the physicals are already dark for a sibling.
|
||||||
if crate::vdisplay::policy::prefs().ddc_power_off() {
|
if crate::policy::prefs().ddc_power_off() {
|
||||||
inner.group.ddc_panels_off = crate::ddc::panel_off_except(n);
|
inner.group.ddc_panels_off = crate::ddc::panel_off_except(n);
|
||||||
}
|
}
|
||||||
// SAFETY: `isolate_displays_ccd` is `unsafe` for its CCD topology FFI; it
|
// SAFETY: `isolate_displays_ccd` is `unsafe` for its CCD topology FFI; it
|
||||||
@@ -859,10 +859,10 @@ impl VirtualDisplayManager {
|
|||||||
// across hot-plug re-arrival) so a standby monitor/TV's periodic wake
|
// across hot-plug re-arrival) so a standby monitor/TV's periodic wake
|
||||||
// events no longer trigger the Windows reaction cascade — the suspected
|
// events no longer trigger the Windows reaction cascade — the suspected
|
||||||
// hiccup mechanism (rationale + crash journal in `windows/monitor_devnode.rs`).
|
// hiccup mechanism (rationale + crash journal in `windows/monitor_devnode.rs`).
|
||||||
if crate::vdisplay::policy::prefs().pnp_disable_monitors() {
|
if crate::policy::prefs().pnp_disable_monitors() {
|
||||||
if let Some(saved) = &inner.group.ccd_saved {
|
if let Some(saved) = &inner.group.ccd_saved {
|
||||||
inner.group.pnp_disabled =
|
inner.group.pnp_disabled =
|
||||||
crate::monitor_devnode::disable_for_deactivated(
|
pf_win_display::monitor_devnode::disable_for_deactivated(
|
||||||
saved,
|
saved,
|
||||||
added.target_id,
|
added.target_id,
|
||||||
);
|
);
|
||||||
@@ -934,10 +934,10 @@ impl VirtualDisplayManager {
|
|||||||
// inactive and get disabled); in Extend the active physical panels are untouched
|
// inactive and get disabled); in Extend the active physical panels are untouched
|
||||||
// by construction. First member only — the sweep is group-scoped like the
|
// by construction. First member only — the sweep is group-scoped like the
|
||||||
// isolate; later members join an already-swept desktop.
|
// isolate; later members join an already-swept desktop.
|
||||||
if first_member && crate::vdisplay::policy::prefs().pnp_disable_monitors() {
|
if first_member && crate::policy::prefs().pnp_disable_monitors() {
|
||||||
let mut keep = inner.target_ids();
|
let mut keep = inner.target_ids();
|
||||||
keep.push(added.target_id);
|
keep.push(added.target_id);
|
||||||
for id in crate::monitor_devnode::disable_connected_inactive(&keep) {
|
for id in pf_win_display::monitor_devnode::disable_connected_inactive(&keep) {
|
||||||
if !inner.group.pnp_disabled.contains(&id) {
|
if !inner.group.pnp_disabled.contains(&id) {
|
||||||
inner.group.pnp_disabled.push(id);
|
inner.group.pnp_disabled.push(id);
|
||||||
}
|
}
|
||||||
@@ -1078,7 +1078,7 @@ impl VirtualDisplayManager {
|
|||||||
/// # Safety
|
/// # Safety
|
||||||
/// Drives the CCD topology FFI; call under the `state` lock.
|
/// Drives the CCD topology FFI; call under the `state` lock.
|
||||||
unsafe fn reisolate_after_swap(&self, inner: &mut MgrInner, new_target: u32) {
|
unsafe fn reisolate_after_swap(&self, inner: &mut MgrInner, new_target: u32) {
|
||||||
use crate::vdisplay::policy::Topology;
|
use crate::policy::Topology;
|
||||||
match topology_action() {
|
match topology_action() {
|
||||||
Topology::Exclusive => {
|
Topology::Exclusive => {
|
||||||
// Grown-set semantics: isolate to the surviving siblings + the new target. The returned
|
// Grown-set semantics: isolate to the surviving siblings + the new target. The returned
|
||||||
@@ -1145,7 +1145,7 @@ impl VirtualDisplayManager {
|
|||||||
// Extend/Primary sessions too, where no isolate snapshot exists to restore.
|
// Extend/Primary sessions too, where no isolate snapshot exists to restore.
|
||||||
let pnp_disabled = std::mem::take(&mut inner.group.pnp_disabled);
|
let pnp_disabled = std::mem::take(&mut inner.group.pnp_disabled);
|
||||||
if !pnp_disabled.is_empty() {
|
if !pnp_disabled.is_empty() {
|
||||||
crate::monitor_devnode::enable_instances(&pnp_disabled);
|
pf_win_display::monitor_devnode::enable_instances(&pnp_disabled);
|
||||||
thread::sleep(Duration::from_millis(300));
|
thread::sleep(Duration::from_millis(300));
|
||||||
}
|
}
|
||||||
// Re-attach detached display(s) BEFORE the REMOVE so the box is never left with zero
|
// Re-attach detached display(s) BEFORE the REMOVE so the box is never left with zero
|
||||||
@@ -1306,7 +1306,7 @@ impl VirtualDisplayManager {
|
|||||||
/// monitor is created. Slot-scoped since Stage W1: a DIFFERENT identity's session is an admission
|
/// monitor is created. Slot-scoped since Stage W1: a DIFFERENT identity's session is an admission
|
||||||
/// question, never a preempt. Returns the setup guard; the caller holds it across the pipeline
|
/// question, never a preempt. Returns the setup guard; the caller holds it across the pipeline
|
||||||
/// build, then drops it so the next reconnect can begin (and preempt this one).
|
/// build, then drops it so the next reconnect can begin (and preempt this one).
|
||||||
pub(crate) fn begin_idd_setup(
|
pub fn begin_idd_setup(
|
||||||
&'static self,
|
&'static self,
|
||||||
slot: u32,
|
slot: u32,
|
||||||
stop: Arc<AtomicBool>,
|
stop: Arc<AtomicBool>,
|
||||||
@@ -1449,11 +1449,11 @@ impl Drop for MonitorLease {
|
|||||||
/// anonymous slot at a time — exactly the pre-slot-map semantics, since an anonymous re-acquire has
|
/// anonymous slot at a time — exactly the pre-slot-map semantics, since an anonymous re-acquire has
|
||||||
/// no identity to find any other slot by). Shared by `acquire` and the session setup's
|
/// no identity to find any other slot by). Shared by `acquire` and the session setup's
|
||||||
/// [`VirtualDisplayManager::begin_idd_setup`], so both address the same slot.
|
/// [`VirtualDisplayManager::begin_idd_setup`], so both address the same slot.
|
||||||
pub(crate) fn slot_id_for(client_fp: Option<[u8; 32]>, mode: (u32, u32)) -> u32 {
|
pub fn slot_id_for(client_fp: Option<[u8; 32]>, mode: (u32, u32)) -> u32 {
|
||||||
super::identity::resolve_slot(
|
super::identity::resolve_slot(
|
||||||
client_fp,
|
client_fp,
|
||||||
mode,
|
mode,
|
||||||
crate::vdisplay::policy::Identity::PerClient,
|
crate::policy::Identity::PerClient,
|
||||||
)
|
)
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
@@ -1500,7 +1500,7 @@ fn warn_if_pick_moved(mon: &Monitor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A read-only view of one managed slot for the mgmt `/display/state` endpoint (Goal:
|
/// A read-only view of one managed slot for the mgmt `/display/state` endpoint (Goal:
|
||||||
/// display-management registry facade). Backend-neutral; the [`crate::vdisplay::registry`] facade
|
/// display-management registry facade). Backend-neutral; the [`crate::registry`] facade
|
||||||
/// maps it into the wire shape.
|
/// maps it into the wire shape.
|
||||||
pub(crate) struct ManagedInfo {
|
pub(crate) struct ManagedInfo {
|
||||||
pub backend: &'static str,
|
pub backend: &'static str,
|
||||||
+1
-1
@@ -23,7 +23,7 @@ pub(super) fn claim_instance() -> Result<()> {
|
|||||||
/// Eager startup claim for the serve/service path (Windows): reserves this process as THE
|
/// Eager startup claim for the serve/service path (Windows): reserves this process as THE
|
||||||
/// pf-vdisplay manager before any client connects. Failure is a loud warning, not fatal — sessions
|
/// pf-vdisplay manager before any client connects. Failure is a loud warning, not fatal — sessions
|
||||||
/// then fail with the same clear in-use error until the other instance exits.
|
/// then fail with the same clear in-use error until the other instance exits.
|
||||||
pub(crate) fn claim_instance_eagerly() {
|
pub fn claim_instance_eagerly() {
|
||||||
if let Err(e) = claim_instance() {
|
if let Err(e) = claim_instance() {
|
||||||
tracing::warn!("pf-vdisplay single-instance claim failed at startup: {e:#}");
|
tracing::warn!("pf-vdisplay single-instance claim failed at startup: {e:#}");
|
||||||
}
|
}
|
||||||
+8
-8
@@ -1,12 +1,12 @@
|
|||||||
//! Runtime display-management knobs read from the console policy (with legacy env-var fallbacks),
|
//! Runtime display-management knobs read from the console policy (with legacy env-var fallbacks),
|
||||||
//! carved out of the manager (plan §W3): the linger window, the keep-alive-forever pin, and the
|
//! carved out of the manager (plan §W3): the linger window, the keep-alive-forever pin, and the
|
||||||
//! per-monitor topology action. Pure readers of [`crate::vdisplay::policy`] + env — no manager state.
|
//! per-monitor topology action. Pure readers of [`crate::policy`] + env — no manager state.
|
||||||
|
|
||||||
/// Linger window before a session-less monitor is torn down. The console display-management policy
|
/// Linger window before a session-less monitor is torn down. The console display-management policy
|
||||||
/// wins when configured (`keep_alive`); otherwise the legacy `PUNKTFUNK_MONITOR_LINGER_MS` env knob,
|
/// wins when configured (`keep_alive`); otherwise the legacy `PUNKTFUNK_MONITOR_LINGER_MS` env knob,
|
||||||
/// else the 10 s default.
|
/// else the 10 s default.
|
||||||
pub(super) fn linger_ms() -> u64 {
|
pub(super) fn linger_ms() -> u64 {
|
||||||
use crate::vdisplay::policy::{prefs, Linger};
|
use crate::policy::{prefs, Linger};
|
||||||
if let Some(eff) = prefs().configured_effective() {
|
if let Some(eff) = prefs().configured_effective() {
|
||||||
return match eff.keep_alive.linger() {
|
return match eff.keep_alive.linger() {
|
||||||
Linger::Immediate => 0,
|
Linger::Immediate => 0,
|
||||||
@@ -27,7 +27,7 @@ pub(super) fn linger_ms() -> u64 {
|
|||||||
/// gaming-rig preset. `release` uses this to keep the last-released monitor indefinitely instead of
|
/// gaming-rig preset. `release` uses this to keep the last-released monitor indefinitely instead of
|
||||||
/// lingering. Unconfigured hosts are never forever (default is a short linger).
|
/// lingering. Unconfigured hosts are never forever (default is a short linger).
|
||||||
pub(super) fn keep_alive_forever() -> bool {
|
pub(super) fn keep_alive_forever() -> bool {
|
||||||
use crate::vdisplay::policy::{prefs, Linger};
|
use crate::policy::{prefs, Linger};
|
||||||
prefs()
|
prefs()
|
||||||
.configured_effective()
|
.configured_effective()
|
||||||
.map(|eff| matches!(eff.keep_alive.linger(), Linger::Forever))
|
.map(|eff| matches!(eff.keep_alive.linger(), Linger::Forever))
|
||||||
@@ -35,17 +35,17 @@ pub(super) fn keep_alive_forever() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The effective display topology for a freshly-created monitor (never `Auto`): the console policy's
|
/// The effective display topology for a freshly-created monitor (never `Auto`): the console policy's
|
||||||
/// [`effective_topology`](crate::vdisplay::effective_topology) when configured, else the legacy
|
/// [`effective_topology`](crate::effective_topology) when configured, else the legacy
|
||||||
/// `PUNKTFUNK_NO_ISOLATE` env knob (`Extend`) / `Exclusive` (today's default). `Extend` leaves the IDD
|
/// `PUNKTFUNK_NO_ISOLATE` env knob (`Extend`) / `Exclusive` (today's default). `Extend` leaves the IDD
|
||||||
/// extended; `Primary` makes it primary while keeping the physical(s) active; `Exclusive` disables the
|
/// extended; `Primary` makes it primary while keeping the physical(s) active; `Exclusive` disables the
|
||||||
/// physical(s) so the IDD is the sole composited desktop.
|
/// physical(s) so the IDD is the sole composited desktop.
|
||||||
pub(super) fn topology_action() -> crate::vdisplay::policy::Topology {
|
pub(super) fn topology_action() -> crate::policy::Topology {
|
||||||
use crate::vdisplay::policy::Topology;
|
use crate::policy::Topology;
|
||||||
if crate::vdisplay::policy::prefs()
|
if crate::policy::prefs()
|
||||||
.configured_effective()
|
.configured_effective()
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
return crate::vdisplay::effective_topology();
|
return crate::effective_topology();
|
||||||
}
|
}
|
||||||
if std::env::var("PUNKTFUNK_NO_ISOLATE").is_ok() {
|
if std::env::var("PUNKTFUNK_NO_ISOLATE").is_ok() {
|
||||||
Topology::Extend
|
Topology::Extend
|
||||||
+21
-3
@@ -217,7 +217,7 @@ unsafe fn set_render_adapter(h: HANDLE, luid: LUID) -> Result<()> {
|
|||||||
///
|
///
|
||||||
/// # Safety
|
/// # Safety
|
||||||
/// `dev` must be a live pf-vdisplay control handle (see [`super::manager::control_device_handle`]).
|
/// `dev` must be a live pf-vdisplay control handle (see [`super::manager::control_device_handle`]).
|
||||||
pub(crate) unsafe fn send_frame_channel(
|
pub unsafe fn send_frame_channel(
|
||||||
dev: HANDLE,
|
dev: HANDLE,
|
||||||
req: &control::SetFrameChannelRequest,
|
req: &control::SetFrameChannelRequest,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@@ -386,8 +386,18 @@ impl VdisplayDriver for PfVdisplayDriver {
|
|||||||
// takes no input (`&[]`) and writes into `info_buf`, a stack `[u8; size_of::<InfoReply>()]`
|
// takes no input (`&[]`) and writes into `info_buf`, a stack `[u8; size_of::<InfoReply>()]`
|
||||||
// whose length is passed as the output size — so `DeviceIoControl` can't write OOB — and which
|
// whose length is passed as the output size — so `DeviceIoControl` can't write OOB — and which
|
||||||
// outlives this synchronous call.
|
// outlives this synchronous call.
|
||||||
unsafe { ioctl(raw, control::IOCTL_GET_INFO, &[], &mut info_buf) }
|
let n = unsafe { ioctl(raw, control::IOCTL_GET_INFO, &[], &mut info_buf) }
|
||||||
.context("pf-vdisplay IOCTL_GET_INFO (version handshake)")?;
|
.context("pf-vdisplay IOCTL_GET_INFO (version handshake)")?;
|
||||||
|
// Fail closed on a short driver reply instead of decoding trusted-looking zeros — the decoded
|
||||||
|
// `protocol_version` (and below, the ADD reply's pid/luid/target) gate host behavior, so a
|
||||||
|
// buggy/compromised driver under-writing the buffer must not be silently trusted
|
||||||
|
// (security-review 2026-07-17).
|
||||||
|
if (n as usize) < size_of::<control::InfoReply>() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"pf-vdisplay IOCTL_GET_INFO returned {n} bytes, expected {}",
|
||||||
|
size_of::<control::InfoReply>()
|
||||||
|
);
|
||||||
|
}
|
||||||
let info: control::InfoReply =
|
let info: control::InfoReply =
|
||||||
bytemuck::pod_read_unaligned(&info_buf[..size_of::<control::InfoReply>()]);
|
bytemuck::pod_read_unaligned(&info_buf[..size_of::<control::InfoReply>()]);
|
||||||
if info.protocol_version != pf_driver_proto::PROTOCOL_VERSION {
|
if info.protocol_version != pf_driver_proto::PROTOCOL_VERSION {
|
||||||
@@ -520,12 +530,20 @@ impl VdisplayDriver for PfVdisplayDriver {
|
|||||||
}
|
}
|
||||||
other => other,
|
other => other,
|
||||||
};
|
};
|
||||||
add_res.with_context(|| {
|
let n = add_res.with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"pf-vdisplay ADD {}x{}@{}",
|
"pf-vdisplay ADD {}x{}@{}",
|
||||||
mode.width, mode.height, mode.refresh_hz
|
mode.width, mode.height, mode.refresh_hz
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
// Fail closed on a short reply — `target_id`/`wudf_pid`/`luid` below feed OpenProcess + the
|
||||||
|
// WUDFHost verification, so don't decode a partially-written (zeroed) reply as authoritative.
|
||||||
|
if (n as usize) < size_of::<control::AddReply>() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"pf-vdisplay ADD returned {n} bytes, expected {}",
|
||||||
|
size_of::<control::AddReply>()
|
||||||
|
);
|
||||||
|
}
|
||||||
// `pod_read_unaligned` (NOT `from_bytes`): `out` is a stack `[u8; N]` with no guaranteed 4-byte
|
// `pod_read_unaligned` (NOT `from_bytes`): `out` is a stack `[u8; N]` with no guaranteed 4-byte
|
||||||
// alignment, and `from_bytes` PANICS on a mismatch. This copies into an aligned `AddReply`.
|
// alignment, and `from_bytes` PANICS on a mismatch. This copies into an aligned `AddReply`.
|
||||||
let reply: control::AddReply =
|
let reply: control::AddReply =
|
||||||
@@ -38,6 +38,9 @@ pf-capture = { path = "../pf-capture" }
|
|||||||
# subsystem crate (plan §W6). Consumes core::input; the heavy input deps (wayland/reis/xkbcommon/
|
# subsystem crate (plan §W6). Consumes core::input; the heavy input deps (wayland/reis/xkbcommon/
|
||||||
# usbip) moved with it.
|
# usbip) moved with it.
|
||||||
pf-inject = { path = "../pf-inject" }
|
pf-inject = { path = "../pf-inject" }
|
||||||
|
# Virtual-display orchestration (per-compositor Linux backends + the Windows IddCx driver backend),
|
||||||
|
# extracted to a subsystem crate (plan §W6). The DDC panel control + KWin zkde protocol moved with it.
|
||||||
|
pf-vdisplay = { path = "../pf-vdisplay" }
|
||||||
# M3 native control plane (the `punktfunk/1` QUIC handshake; data plane stays native-thread UDP).
|
# M3 native control plane (the `punktfunk/1` QUIC handshake; data plane stays native-thread UDP).
|
||||||
quinn = "0.11"
|
quinn = "0.11"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ pub fn capture_virtual_output(
|
|||||||
// closed for the process lifetime, so reconstructing the `HANDLE` and issuing the
|
// closed for the process lifetime, so reconstructing the `HANDLE` and issuing the
|
||||||
// `IOCTL_SET_FRAME_CHANNEL` is sound (`send_frame_channel`'s precondition).
|
// `IOCTL_SET_FRAME_CHANNEL` is sound (`send_frame_channel`'s precondition).
|
||||||
unsafe {
|
unsafe {
|
||||||
crate::vdisplay::pf_vdisplay::send_frame_channel(
|
crate::vdisplay::driver::send_frame_channel(
|
||||||
windows::Win32::Foundation::HANDLE(control_raw as *mut core::ffi::c_void),
|
windows::Win32::Foundation::HANDLE(control_raw as *mut core::ffi::c_void),
|
||||||
req,
|
req,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ mod wol;
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
#[path = "windows/crash.rs"]
|
#[path = "windows/crash.rs"]
|
||||||
mod crash;
|
mod crash;
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
#[path = "windows/ddc.rs"]
|
|
||||||
mod ddc;
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
#[path = "linux/drm_sync.rs"]
|
#[path = "linux/drm_sync.rs"]
|
||||||
mod drm_sync;
|
mod drm_sync;
|
||||||
@@ -76,13 +73,16 @@ mod session_status;
|
|||||||
mod spike;
|
mod spike;
|
||||||
mod stats_recorder;
|
mod stats_recorder;
|
||||||
mod stream_marker;
|
mod stream_marker;
|
||||||
mod vdisplay;
|
// `monitor_devnode::startup_recover()` (below) re-enables PnP monitor devnodes disabled by a prior
|
||||||
// The Windows display-topology cluster (CCD/GDI mode-set + PnP monitor devnodes) lives in the
|
// run; it lives in the `pf-win-display` leaf crate (plan §W6).
|
||||||
// `pf-win-display` leaf crate (plan §W6); import the modules at the crate root so the host's
|
|
||||||
// `crate::{win_display,monitor_devnode}::*` paths stay valid. (`display_events` moved with the
|
|
||||||
// IDD-push capturer into pf-capture, which names it directly.)
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
use pf_win_display::{monitor_devnode, win_display};
|
use pf_win_display::monitor_devnode;
|
||||||
|
// Virtual-display orchestration lives in the `pf-vdisplay` subsystem crate (plan §W6); this shim
|
||||||
|
// keeps every existing `crate::vdisplay::*` path valid (serve/mgmt/native/capture consume the trait,
|
||||||
|
// registry, and manager through it). The DDC panel control + the KWin zkde protocol moved with it.
|
||||||
|
mod vdisplay {
|
||||||
|
pub(crate) use pf_vdisplay::*;
|
||||||
|
}
|
||||||
// The zero-copy GPU plumbing lives in the `pf-zerocopy` leaf crate (plan §W6); this shim keeps
|
// The zero-copy GPU plumbing lives in the `pf-zerocopy` leaf crate (plan §W6); this shim keeps
|
||||||
// every existing `crate::zerocopy::*` path valid for the host's remaining callers (session_plan).
|
// every existing `crate::zerocopy::*` path valid for the host's remaining callers (session_plan).
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@@ -183,6 +183,23 @@ fn real_main() -> Result<()> {
|
|||||||
punktfunk_core::ABI_VERSION
|
punktfunk_core::ABI_VERSION
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Wire pf-vdisplay's display-lifecycle events into the SSE event bus (the subsystem crate emits a
|
||||||
|
// neutral DisplayEvent; the orchestrator owns the bus type — plan §W6). Set once, ignore re-set.
|
||||||
|
let _ = pf_vdisplay::DISPLAY_EVENT_SINK.set(Box::new(|ev| match ev {
|
||||||
|
pf_vdisplay::DisplayEvent::Created {
|
||||||
|
backend,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
refresh_hz,
|
||||||
|
} => events::emit(events::EventKind::DisplayCreated {
|
||||||
|
backend,
|
||||||
|
mode: events::mode_str(width, height, refresh_hz),
|
||||||
|
}),
|
||||||
|
pf_vdisplay::DisplayEvent::Released { count } => {
|
||||||
|
events::emit(events::EventKind::DisplayReleased { count })
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
// Install the win32u GPU-preference hook (same technique as Apollo, reimplemented — no GPL source
|
// Install the win32u GPU-preference hook (same technique as Apollo, reimplemented — no GPL source
|
||||||
// copied) BEFORE anything touches DXGI (the virtual-display
|
// copied) BEFORE anything touches DXGI (the virtual-display
|
||||||
// render-adapter selection creates a DXGI factory during virtual-display setup, well before
|
// render-adapter selection creates a DXGI factory during virtual-display setup, well before
|
||||||
|
|||||||
Reference in New Issue
Block a user