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:
2026-07-17 12:14:08 +02:00
parent f6c6e4e594
commit 27a5d8daac
29 changed files with 291 additions and 122 deletions
Generated
+31
View File
@@ -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",
+1
View File
@@ -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",
+61
View File
@@ -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"]
@@ -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(),
@@ -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
} }
@@ -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)),
@@ -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 {
@@ -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());
@@ -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);
@@ -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);
@@ -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)
@@ -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 {
@@ -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,
@@ -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:#}");
} }
@@ -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
@@ -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 =
+3
View File
@@ -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"
+1 -1
View File
@@ -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,
) )
+26 -9
View File
@@ -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