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
+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
//! 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
//! 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).
#![deny(clippy::undocumented_unsafe_blocks)]
@@ -21,8 +23,36 @@ pub use punktfunk_core::Mode;
#[cfg(target_os = "linux")]
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
/// [`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.
#[path = "vdisplay/backend.rs"]
pub(crate) mod backend;
@@ -104,7 +134,7 @@ impl Compositor {
Compositor::Gamescope => P::Gamescope,
// 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
// ([`pick_compositor`](crate::native::pick_compositor) resolves the family).
// (`pick_compositor` (host `native`) resolves the family).
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
// devnode present, interface gone): one device cycle + re-probe before giving up.
anyhow::ensure!(
pf_vdisplay::ensure_available(),
driver::ensure_available(),
"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)"
);
Ok(Box::new(pf_vdisplay::PfVdisplayDisplay::new()?))
Ok(Box::new(driver::PfVdisplayDisplay::new()?))
}
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
{
@@ -280,7 +310,7 @@ pub fn probe(compositor: Compositor) -> Result<()> {
#[cfg(target_os = "windows")]
{
let _ = compositor;
pf_vdisplay::probe()
driver::probe()
}
#[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
// it), so no cfg gate. See `design/display-management.md`.
#[path = "vdisplay/policy.rs"]
pub(crate) mod policy;
pub mod policy;
// The pure per-display lifecycle state machine (refcount + linger + pin), platform-neutral and
// 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
// later), for the management API's /display/state + /display/release.
#[path = "vdisplay/registry.rs"]
pub(crate) mod registry;
pub mod registry;
// 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.
@@ -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
// 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")]
#[path = "vdisplay/linux/gamescope.rs"]
mod gamescope;
@@ -371,7 +401,7 @@ pub(crate) mod identity;
// Platform-neutral mode-conflict admission (Stage 4): the separate/join/steal/reject decision + the
// live-session registry, wired into the punktfunk/1 handshake.
#[path = "vdisplay/admission.rs"]
pub(crate) mod admission;
pub mod admission;
#[cfg(target_os = "linux")]
#[path = "vdisplay/linux/hyprland.rs"]
@@ -383,7 +413,13 @@ mod kwin;
#[cfg(target_os = "windows")]
#[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")]
#[path = "vdisplay/linux/mutter.rs"]
@@ -391,7 +427,7 @@ mod mutter;
#[cfg(target_os = "windows")]
#[path = "vdisplay/windows/pf_vdisplay.rs"]
pub(crate) mod pf_vdisplay;
pub mod driver;
#[cfg(target_os = "linux")]
#[path = "vdisplay/linux/wlroots.rs"]
@@ -16,7 +16,7 @@
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
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.
#[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}"
));
}
if !crate::encode::can_open_another_session() {
if !pf_encode::can_open_another_session() {
return Admission::Reject(
"host encoder budget exhausted: no NVENC session headroom for another display"
.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.
pub preferred_mode: Option<(u32, u32, u32)>,
/// Windows capture identity (DXGI adapter LUID + GDI output name) for the pf-vdisplay backend —
/// what [`crate::capture::capture_virtual_output`] needs to duplicate the right output.
/// what the host `capture::capture_virtual_output` needs to duplicate the right output.
#[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.
pub keepalive: Box<dyn Send>,
/// Who owns this display's lifecycle (`design/gamemode-and-dedicated-sessions.md` A1). The
/// registry pools/keep-alives only [`DisplayOwnership::Owned`] outputs; `External`/`SessionManaged`
/// pass through (the capturer holds the keepalive, teardown on drop). Defaults to `Owned`.
pub ownership: DisplayOwnership,
/// `Some(gen)` when [`registry::acquire`](crate::vdisplay::registry::acquire) handed this back as a
/// `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
/// 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
/// re-wedging on the same dead node. `None` on a fresh create / non-poolable output. Linux-only (the
/// keep-alive pool is Linux).
#[cfg(target_os = "linux")]
pub reused_gen: Option<u64>,
/// The registry pool generation of this display (fresh AND reused — unlike `reused_gen`), so a
/// mid-stream mode-switch rebuild can [`registry::retire`](crate::vdisplay::registry::retire) the
/// 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
/// policy (`design/midstream-resolution-resize.md` H4). `None` for non-poolable outputs.
/// 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
/// 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 {
true
}
@@ -167,10 +167,10 @@ pub(crate) fn global() -> &'static Mutex<DisplayIdentityMap> {
pub(crate) fn resolve_slot(
fp: Option<[u8; 32]>,
mode: (u32, u32),
default: crate::vdisplay::policy::Identity,
default: crate::policy::Identity,
) -> Option<u32> {
use crate::vdisplay::policy::Identity;
let id_policy = crate::vdisplay::policy::prefs()
use crate::policy::Identity;
let id_policy = crate::policy::prefs()
.configured_effective()
.map(|e| e.identity)
.unwrap_or(default);
@@ -201,9 +201,9 @@ const SCALE_FILE: &str = "display-scale.json";
pub(crate) fn scale_key(
fp: Option<[u8; 32]>,
mode: (u32, u32),
default: crate::vdisplay::policy::Identity,
default: crate::policy::Identity,
) -> String {
let id_policy = crate::vdisplay::policy::prefs()
let id_policy = crate::policy::prefs()
.configured_effective()
.map(|e| e.identity)
.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.
fn scale_key_for(
policy: crate::vdisplay::policy::Identity,
policy: crate::policy::Identity,
fp: Option<[u8; 32]>,
mode: (u32, u32),
) -> String {
use crate::vdisplay::policy::Identity;
use crate::policy::Identity;
match (policy, fp) {
(Identity::Shared, _) | (_, None) => "shared".to_string(),
(Identity::PerClient, Some(fp)) => identity_key(fp, mode, false),
@@ -343,7 +343,7 @@ mod tests {
#[test]
fn scale_key_follows_the_identity_policy() {
use crate::vdisplay::policy::Identity;
use crate::policy::Identity;
// Shared / anonymous → the fixed shared slot.
assert_eq!(
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)]
mod tests {
use super::*;
use crate::vdisplay::policy::Position;
use crate::policy::Position;
use std::collections::BTreeMap;
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
// (`PUNKTFUNK_GAMESCOPE_SESSION`) and attach (`PUNKTFUNK_GAMESCOPE_NODE`) report
// `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.
crate::vdisplay::with_env_lock(|| {
crate::with_env_lock(|| {
std::env::var_os("PUNKTFUNK_GAMESCOPE_SESSION").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
/// `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
/// defaulting to managed and then bailing on the missing session script.
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);
/// * unconfigured → the historical 5 s [`RESTORE_DEBOUNCE`] (bit-for-bit today's behavior).
fn restore_delay() -> Option<Duration> {
use crate::vdisplay::policy::{self, Linger};
use crate::policy::{self, Linger};
match policy::prefs()
.configured_effective()
.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
/// 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` —
/// 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
/// 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
/// ([`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 {
// 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
// `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
@@ -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
// `set_var` of the same key (security-review 2026-06-28 #7).
.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())
.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
// 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`.
let slot = crate::vdisplay::identity::resolve_slot(
let slot = crate::identity::resolve_slot(
self.client_fp,
(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
let name = match slot {
@@ -192,8 +192,8 @@ impl VirtualDisplay for KwinDisplay {
// `Exclusive` makes it the SOLE desktop (others disabled, restored on teardown) — so
// 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).
use crate::vdisplay::policy::Topology;
let disabled = match crate::vdisplay::effective_topology() {
use crate::policy::Topology;
let disabled = match crate::effective_topology() {
Topology::Exclusive => apply_virtual_primary(&name),
Topology::Primary => {
apply_virtual_primary_only(&name);
@@ -22,7 +22,7 @@
//! `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
//! 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
//! topology `ApplyMonitorsConfig`, and the user's mid-session changes are polled from
//! DisplayConfig and written back.
@@ -72,7 +72,7 @@ pub struct MutterDisplay {
/// The connecting client's cert fingerprint (set before [`create`](VirtualDisplay::create)) —
/// keys the per-client persisted **scale** (GNOME can't persist it itself: Mutter mints a fresh
/// EDID serial per `RecordVirtual` monitor, so `monitors.xml` never rematches; see
/// [`identity::ScaleMap`](crate::vdisplay::identity)).
/// [`identity::ScaleMap`](crate::identity)).
client_fp: Option<[u8; 32]>,
/// The identity slot the last `create` resolved — reported to the registry via
/// [`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
// instead: the session thread reapplies the remembered scale and records the user's
// in-session changes under `scale_key`.
self.last_slot = crate::vdisplay::identity::resolve_slot(
self.last_slot = crate::identity::resolve_slot(
self.client_fp,
(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,
(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()
.unwrap()
.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).
/// `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
/// [`identity::ScaleMap`](crate::vdisplay::identity)).
/// [`identity::ScaleMap`](crate::identity)).
// 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
// 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
// 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.
use crate::vdisplay::policy::Topology;
let topo = crate::vdisplay::effective_topology();
use crate::policy::Topology;
let topo = crate::effective_topology();
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
// 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;
};
if (cur - *known).abs() > 1e-3 {
crate::vdisplay::identity::scales()
crate::identity::scales()
.lock()
.unwrap()
.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
/// [`crate::library`] custom-entry id scheme).
/// the host `library` custom-entry id scheme).
fn new_preset_id(name: &str) -> String {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
@@ -101,9 +101,11 @@ pub fn acquire(
vd.create(mode)
};
if out.is_ok() {
crate::events::emit(crate::events::EventKind::DisplayCreated {
crate::emit_display_event(crate::DisplayEvent::Created {
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
@@ -166,7 +168,7 @@ pub fn release(slot: Option<u64>) -> usize {
0
};
if released > 0 {
crate::events::emit(crate::events::EventKind::DisplayReleased {
crate::emit_display_event(crate::DisplayEvent::Released {
count: released as u32,
});
}
@@ -223,9 +225,9 @@ mod linux {
use anyhow::Result;
use super::DisplayInfo;
use crate::vdisplay::lifecycle::{self, Release};
use crate::vdisplay::policy::{self, Layout, Linger};
use crate::vdisplay::{Mode, VirtualDisplay, VirtualOutput};
use crate::lifecycle::{self, Release};
use crate::policy::{self, Layout, Linger};
use crate::{Mode, VirtualDisplay, VirtualOutput};
/// 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
@@ -345,7 +347,7 @@ mod linux {
// 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
// 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;
while i < entries.len() {
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
// be reused for a session launching game B). A4 reuse key: the current session epoch.
let launch = vd.launch_command();
let cur_epoch = crate::vdisplay::session_epoch();
let cur_epoch = crate::session_epoch();
let r = reg();
// 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
// (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.
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!(
backend,
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
// (I/O-free) — the backend apply is below, outside the lock.
let position = {
use crate::vdisplay::layout::Member;
use crate::layout::Member;
let layout_policy = policy::prefs()
.configured_effective()
.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-
/// order + auto-row/manual arrangement is unit-tested independent of the pool/global.
fn position_for_new(
mut existing: Vec<(u64, crate::vdisplay::layout::Member)>,
new: crate::vdisplay::layout::Member,
mut existing: Vec<(u64, crate::layout::Member)>,
new: crate::layout::Member,
layout_policy: &Layout,
) -> crate::vdisplay::layout::Placement {
) -> crate::layout::Placement {
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();
members.push(new);
*crate::vdisplay::layout::arrange(&members, layout_policy)
*crate::layout::arrange(&members, layout_policy)
.last()
.expect("members is non-empty (just pushed `new`)")
}
@@ -796,7 +798,7 @@ mod linux {
layout_policy: &Layout,
topology: &str,
) -> 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
// desktop backend → group 1 (with each gamescope spawn its own group).
@@ -977,7 +979,7 @@ mod linux {
#[cfg(test)]
mod tests {
use super::*;
use crate::vdisplay::policy::{Layout, LayoutMode, Position};
use crate::policy::{Layout, LayoutMode, Position};
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
@@ -1146,7 +1148,7 @@ mod linux {
#[test]
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 {
identity_slot: slot,
width: w,
@@ -1163,7 +1165,7 @@ mod linux {
#[test]
fn position_for_new_honors_a_manual_pin() {
use crate::vdisplay::layout::{Member, Placement};
use crate::layout::{Member, Placement};
let mut positions = BTreeMap::new();
positions.insert("5".to_string(), Position { x: 100, y: 200 });
let layout = Layout {
@@ -6,7 +6,7 @@
//! 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
//! 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
//! 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 crate::win_display::{
use pf_win_display::win_display::{
count_other_active, force_extend_topology, isolate_displays_ccd, resolve_gdi_name,
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"]
mod instance;
use instance::claim_instance;
pub(crate) use instance::claim_instance_eagerly;
pub use instance::claim_instance_eagerly;
#[path = "manager/knobs.rs"]
mod knobs;
@@ -89,11 +89,11 @@ struct Monitor {
impl Monitor {
/// 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
.clone()
.map(|n| crate::capture::dxgi::WinCaptureTarget {
adapter_luid: crate::capture::dxgi::pack_luid(self.luid),
.map(|n| pf_frame::dxgi::WinCaptureTarget {
adapter_luid: pf_frame::dxgi::pack_luid(self.luid),
gdi_name: n,
target_id: self.target_id,
wudf_pid: self.wudf_pid,
@@ -197,7 +197,7 @@ struct DeviceSlot {
}
/// The host-lifetime virtual-display manager: the single owner of the monitor lifecycle.
pub(crate) struct VirtualDisplayManager {
pub struct VirtualDisplayManager {
driver: Box<dyn VdisplayDriver>,
/// 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
@@ -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
/// 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()
.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
/// 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.
pub(crate) fn control_device_handle() -> Option<HANDLE> {
pub fn control_device_handle() -> Option<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
// 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.
if let Some((own, console)) = crate::interactive::console_session_mismatch() {
if let Some((own, console)) = pf_win_display::console_session_mismatch() {
tracing::error!(
own_session = own,
console_session = console,
@@ -532,7 +532,7 @@ impl VirtualDisplayManager {
// (`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
// admission anyway. One live slot can never trip it (max_displays >= 1).
let max = crate::vdisplay::policy::prefs()
let max = crate::policy::prefs()
.get()
.effective()
.max_displays;
@@ -659,11 +659,11 @@ impl VirtualDisplayManager {
/// 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.
fn apply_group_layout(&self, inner: &mut MgrInner) {
use crate::vdisplay::layout::{arrange, Member};
use crate::layout::{arrange, Member};
if inner.slots.len() < 2 {
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
// 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`.
@@ -691,7 +691,7 @@ impl VirtualDisplayManager {
.collect();
// SAFETY: `apply_source_positions` only drives the CCD query/apply FFI with owned local
// 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) {
if let Some(
SlotState::Active { mon, .. }
@@ -758,7 +758,7 @@ impl VirtualDisplayManager {
}
// 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.
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 {
thread::sleep(Duration::from_millis(200));
// SAFETY: as the resolve loops above.
@@ -831,7 +831,7 @@ impl VirtualDisplayManager {
// 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
// PUNKTFUNK_NO_ISOLATE=1 / the console policy.
use crate::vdisplay::policy::Topology;
use crate::policy::Topology;
let first_member = inner.slots.is_empty();
match topology_action() {
Topology::Exclusive => {
@@ -846,7 +846,7 @@ impl VirtualDisplayManager {
// suspected source of the periodic sole-virtual-display stutter (the
// rationale + evidence live in `windows/ddc.rs`). First member only:
// 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);
}
// 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
// events no longer trigger the Windows reaction cascade — the suspected
// 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 {
inner.group.pnp_disabled =
crate::monitor_devnode::disable_for_deactivated(
pf_win_display::monitor_devnode::disable_for_deactivated(
saved,
added.target_id,
);
@@ -934,10 +934,10 @@ impl VirtualDisplayManager {
// inactive and get disabled); in Extend the active physical panels are untouched
// by construction. First member only — the sweep is group-scoped like the
// 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();
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) {
inner.group.pnp_disabled.push(id);
}
@@ -1078,7 +1078,7 @@ impl VirtualDisplayManager {
/// # Safety
/// Drives the CCD topology FFI; call under the `state` lock.
unsafe fn reisolate_after_swap(&self, inner: &mut MgrInner, new_target: u32) {
use crate::vdisplay::policy::Topology;
use crate::policy::Topology;
match topology_action() {
Topology::Exclusive => {
// 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.
let pnp_disabled = std::mem::take(&mut inner.group.pnp_disabled);
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));
}
// 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
/// 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).
pub(crate) fn begin_idd_setup(
pub fn begin_idd_setup(
&'static self,
slot: u32,
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
/// 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.
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(
client_fp,
mode,
crate::vdisplay::policy::Identity::PerClient,
crate::policy::Identity::PerClient,
)
.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:
/// 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.
pub(crate) struct ManagedInfo {
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
/// 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.
pub(crate) fn claim_instance_eagerly() {
pub fn claim_instance_eagerly() {
if let Err(e) = claim_instance() {
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),
//! 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
/// wins when configured (`keep_alive`); otherwise the legacy `PUNKTFUNK_MONITOR_LINGER_MS` env knob,
/// else the 10 s default.
pub(super) fn linger_ms() -> u64 {
use crate::vdisplay::policy::{prefs, Linger};
use crate::policy::{prefs, Linger};
if let Some(eff) = prefs().configured_effective() {
return match eff.keep_alive.linger() {
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
/// lingering. Unconfigured hosts are never forever (default is a short linger).
pub(super) fn keep_alive_forever() -> bool {
use crate::vdisplay::policy::{prefs, Linger};
use crate::policy::{prefs, Linger};
prefs()
.configured_effective()
.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
/// [`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
/// extended; `Primary` makes it primary while keeping the physical(s) active; `Exclusive` disables the
/// physical(s) so the IDD is the sole composited desktop.
pub(super) fn topology_action() -> crate::vdisplay::policy::Topology {
use crate::vdisplay::policy::Topology;
if crate::vdisplay::policy::prefs()
pub(super) fn topology_action() -> crate::policy::Topology {
use crate::policy::Topology;
if crate::policy::prefs()
.configured_effective()
.is_some()
{
return crate::vdisplay::effective_topology();
return crate::effective_topology();
}
if std::env::var("PUNKTFUNK_NO_ISOLATE").is_ok() {
Topology::Extend
@@ -217,7 +217,7 @@ unsafe fn set_render_adapter(h: HANDLE, luid: LUID) -> Result<()> {
///
/// # Safety
/// `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,
req: &control::SetFrameChannelRequest,
) -> Result<()> {
@@ -386,8 +386,18 @@ impl VdisplayDriver for PfVdisplayDriver {
// 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
// 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)")?;
// 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 =
bytemuck::pod_read_unaligned(&info_buf[..size_of::<control::InfoReply>()]);
if info.protocol_version != pf_driver_proto::PROTOCOL_VERSION {
@@ -520,12 +530,20 @@ impl VdisplayDriver for PfVdisplayDriver {
}
other => other,
};
add_res.with_context(|| {
let n = add_res.with_context(|| {
format!(
"pf-vdisplay ADD {}x{}@{}",
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
// alignment, and `from_bytes` PANICS on a mismatch. This copies into an aligned `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/
# usbip) moved with it.
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).
quinn = "0.11"
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
// `IOCTL_SET_FRAME_CHANNEL` is sound (`send_frame_channel`'s precondition).
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),
req,
)
+26 -9
View File
@@ -29,9 +29,6 @@ mod wol;
#[cfg(target_os = "windows")]
#[path = "windows/crash.rs"]
mod crash;
#[cfg(target_os = "windows")]
#[path = "windows/ddc.rs"]
mod ddc;
#[cfg(target_os = "linux")]
#[path = "linux/drm_sync.rs"]
mod drm_sync;
@@ -76,13 +73,16 @@ mod session_status;
mod spike;
mod stats_recorder;
mod stream_marker;
mod vdisplay;
// The Windows display-topology cluster (CCD/GDI mode-set + PnP monitor devnodes) lives in the
// `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.)
// `monitor_devnode::startup_recover()` (below) re-enables PnP monitor devnodes disabled by a prior
// run; it lives in the `pf-win-display` leaf crate (plan §W6).
#[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
// every existing `crate::zerocopy::*` path valid for the host's remaining callers (session_plan).
#[cfg(target_os = "linux")]
@@ -183,6 +183,23 @@ fn real_main() -> Result<()> {
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
// copied) BEFORE anything touches DXGI (the virtual-display
// render-adapter selection creates a DXGI factory during virtual-display setup, well before