feat(windows): parallel virtual displays — proto v3 ring binding, manager slot map, group topology (W0–W3)
windows-drivers / probe-and-proto (push) Successful in 41s
ci / web (push) Successful in 1m2s
ci / docs-site (push) Successful in 1m7s
apple / swift (push) Successful in 1m11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 15s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 38s
windows-drivers / driver-build (push) Successful in 1m46s
ci / bench (push) Successful in 6m35s
docker / deploy-docs (push) Successful in 20s
windows-host / package (push) Successful in 9m37s
deb / build-publish (push) Successful in 13m48s
arch / build-publish (push) Successful in 14m21s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m34s
android / android (push) Successful in 15m45s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m20s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m18s
ci / rust (push) Successful in 18m48s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m41s
release / apple (push) Successful in 19m30s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m49s
flatpak / build-publish (push) Failing after 2m9s
apple / screenshots (push) Successful in 19m3s
windows-drivers / probe-and-proto (push) Successful in 41s
ci / web (push) Successful in 1m2s
ci / docs-site (push) Successful in 1m7s
apple / swift (push) Successful in 1m11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 15s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 38s
windows-drivers / driver-build (push) Successful in 1m46s
ci / bench (push) Successful in 6m35s
docker / deploy-docs (push) Successful in 20s
windows-host / package (push) Successful in 9m37s
deb / build-publish (push) Successful in 13m48s
arch / build-publish (push) Successful in 14m21s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m34s
android / android (push) Successful in 15m45s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m20s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m18s
ci / rust (push) Successful in 18m48s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m41s
release / apple (push) Successful in 19m30s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m49s
flatpak / build-publish (push) Failing after 2m9s
apple / screenshots (push) Successful in 19m3s
design/windows-parallel-virtual-displays.md (display-management Stage 7 / §6.6): N simultaneously-live pf-vdisplay monitors, one sealed ring each, every idd-push-security invariant preserved per-ring. - proto v3: SharedHeader._pad → target_id — the ring NAMES its monitor, host-stamped before the magic; the driver publisher refuses a cross-bound ring via the shared, unit-tested frame::check_attach (new DRV_STATUS_BIND_FAIL — the gamepad pad_index validation applied to frames, invariant #10); the host's wait_for_attach surfaces the refusal loudly and self-checks its own stamp. - manager: the one-monitor MgrState becomes a slot map keyed by the client's identity slot (0 = anonymous/GameStream); per-slot reconnect + dead-WUDFHost preempts, slot-scoped begin_idd_setup (a different identity is an admission question, never a preempt), ONE device-level watchdog pinger, per-slot /display/state + /display/release. - group topology: isolate_displays_ccd takes the managed target SET (a sibling slot is never deactivated); SavedConfig + the DDC/PnP axes move to the group record (first-in captures, last-out restores); desktop layout via CCD source origins from the pure layout::arrange (auto-row default, manual pins win), re-applied on create + reconfigure. - admission: the Windows separate→reject override now sits behind the PUNKTFUNK_WIN_SEPARATE=1 validation hatch (the wedge it guarded is structurally gone — a second identity gets its own monitor + ring; default flips in W5 after soak); max_displays and NVENC session-unit budgets decline an unaffordable display AT admission; kick_dwm_compose is process-globally throttled and per-display — cursor jump + 35 ms dwell (a sub-tick jump composes nothing; DWM reads dirties from current state at the next vsync tick). On-glass on the RTX box: V1/V2/V4/V5/V6/V9 green — two paired clients on two monitors streaming ~60 fps each with zero mismatches and zero bind failures, churn-hammer clean (no 0x80070490), per-ring mode-change recreate leaves the sibling untouched, typed budget rejection, fault-injected cross-bind refused loudly with the sibling undisturbed. V7: WUDFHost-kill shared fate is clean; in-process device recovery is a known follow-up (the retired-never-closed control handles block the adapter cycle — reset-pf-vdisplay.ps1 recovers). DWM composes two IDD monitors concurrently at 60 fps — the plan's load-bearing unknown, answered yes. Also carries the client-HDR EDID forwarding that shared this working tree (Hello::display_hdr → AddRequest luminance tail → the monitor's CTA-861.3 HDR block, PUNKTFUNK_CLIENT_PEAK_NITS hatch) and the Deck client fixes (40 ms rumble keep-alive with 1-LSB jitter, HDR self-diagnosing presenter warn, flatpak HDR env). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,7 +30,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
use windows::core::{w, Interface, PCWSTR, PWSTR};
|
||||
use windows::Win32::Foundation::{
|
||||
DuplicateHandle, DUPLICATE_CLOSE_SOURCE, DUPLICATE_HANDLE_OPTIONS, DUPLICATE_SAME_ACCESS,
|
||||
HANDLE, INVALID_HANDLE_VALUE, LUID, WAIT_OBJECT_0,
|
||||
HANDLE, INVALID_HANDLE_VALUE, LUID, POINT, WAIT_OBJECT_0,
|
||||
};
|
||||
use windows::Win32::Graphics::Direct3D11::{
|
||||
ID3D11Device, ID3D11DeviceContext, ID3D11ShaderResourceView, ID3D11Texture2D,
|
||||
@@ -59,13 +59,14 @@ use windows::Win32::System::Threading::{
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::{
|
||||
SendInput, INPUT, INPUT_0, INPUT_MOUSE, MOUSEEVENTF_MOVE, MOUSEINPUT,
|
||||
};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{GetCursorPos, SetCursorPos};
|
||||
|
||||
// The frame-transport contract — `SharedHeader` layout, `MAGIC`/`VERSION`/`RING_LEN`, the
|
||||
// `DRV_STATUS_*` codes and the channel-delivery struct — lives in `pf_driver_proto`; both sides
|
||||
// `use` it, so a layout/code drift is a compile error (the proto has `const` size asserts).
|
||||
use frame::{
|
||||
SharedHeader, DRV_STATUS_NO_DEVICE1, DRV_STATUS_OPENED, DRV_STATUS_TEX_FAIL, MAGIC, RING_LEN,
|
||||
VERSION,
|
||||
SharedHeader, DRV_STATUS_BIND_FAIL, DRV_STATUS_NO_DEVICE1, DRV_STATUS_OPENED,
|
||||
DRV_STATUS_TEX_FAIL, MAGIC, RING_LEN, VERSION,
|
||||
};
|
||||
|
||||
/// `DXGI_SHARED_RESOURCE_READ | _WRITE` for `CreateSharedHandle`/`OpenSharedResourceByName`. Local (not
|
||||
@@ -192,16 +193,67 @@ impl Drop for KeyedMutexGuard<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Nudge DWM into composing the virtual display: two net-zero 1 px relative mouse moves via
|
||||
/// `SendInput`. DWM presents a display only when something DIRTIES it — an idle desktop never does,
|
||||
/// so a freshly-attached ring (session open, or a mid-session ring recreate) can sit at E_PENDING
|
||||
/// with no first frame even though everything is healthy. pf-vdisplay implements no hardware-cursor
|
||||
/// plane, so a cursor move is composited into the frame — a guaranteed real present onto the IDD
|
||||
/// swap-chain (empirically what `punktfunk-probe --input-test` always relied on). Net-zero: the
|
||||
/// pointer ends exactly where it started; the 1 px round trip is imperceptible, and each event still
|
||||
/// dirties the cursor layer. Best-effort — injection can be unavailable on the secure desktop, where
|
||||
/// a fresh compose just happened anyway.
|
||||
fn kick_dwm_compose() {
|
||||
/// Nudge DWM into composing THE TARGET virtual display. DWM presents a display only when something
|
||||
/// DIRTIES it — an idle desktop never does, so a freshly-attached ring (session open, or a
|
||||
/// mid-session ring recreate) can sit at E_PENDING with no first frame even though everything is
|
||||
/// healthy. pf-vdisplay implements no hardware-cursor plane, so a cursor move is composited into
|
||||
/// the frame — a guaranteed real present onto the IDD swap-chain (empirically what
|
||||
/// `punktfunk-probe --input-test` always relied on).
|
||||
///
|
||||
/// The cursor only dirties the display it is ON — proven on-glass in the Stage-W3 two-display
|
||||
/// validation: display B's session-open kicks wiggled the cursor on display A and B never composed
|
||||
/// a first frame. So the kick is per-TARGET: when the cursor already sits inside `target_id`'s
|
||||
/// desktop region (always true single-display), two net-zero 1 px relative moves (the historical
|
||||
/// behavior, pointer ends exactly where it started); when it sits on a SIBLING display, jump the
|
||||
/// cursor to the target's center and straight back (`SetCursorPos` ×2 — each absolute move dirties
|
||||
/// the cursor layer of the display it lands on, so the target composes at least one frame; the
|
||||
/// round trip is sub-millisecond and throttled). Best-effort — injection can be unavailable on the
|
||||
/// secure desktop, where a fresh compose just happened anyway.
|
||||
fn kick_dwm_compose(target_id: u32) {
|
||||
// Process-GLOBAL throttle (Stage W3): with N parallel capturers each nudging on its own
|
||||
// schedule, DWM needs only one dirty per composition window — and the nudge is synthetic INPUT
|
||||
// (global, user-visible pointer state), so it must not multiply with capturer count. 50 ms
|
||||
// covers every composition interval we ship (≥ 60 Hz) while staying far under the callers' own
|
||||
// 600–800 ms per-capturer schedules.
|
||||
static LAST_KICK: Mutex<Option<Instant>> = Mutex::new(None);
|
||||
{
|
||||
let mut last = LAST_KICK.lock().unwrap();
|
||||
let now = Instant::now();
|
||||
if last.is_some_and(|t| now.duration_since(t) < Duration::from_millis(50)) {
|
||||
return;
|
||||
}
|
||||
*last = Some(now);
|
||||
}
|
||||
// Where is the cursor, and where does the target display live in desktop space?
|
||||
let mut pos = POINT::default();
|
||||
// SAFETY: plain FFI; `pos` is a valid out-param for this synchronous call.
|
||||
let have_pos = unsafe { GetCursorPos(&mut pos) }.is_ok();
|
||||
// SAFETY: `source_desktop_rect` only runs the CCD QueryDisplayConfig FFI over owned local
|
||||
// buffers; the `Copy` target id crosses by value.
|
||||
let rect = unsafe { crate::win_display::source_desktop_rect(target_id) };
|
||||
if let (true, Some((x, y, w, h))) = (have_pos, rect) {
|
||||
let inside = pos.x >= x && pos.x < x + w.max(1) && pos.y >= y && pos.y < y + h.max(1);
|
||||
if !inside {
|
||||
// The cursor is on a sibling display — a wiggle there dirties the WRONG display. Jump
|
||||
// to the target's center, DWELL one composition interval, then restore. The dwell is
|
||||
// load-bearing (proven on-glass, Stage W3): DWM computes dirty state from the CURRENT
|
||||
// cursor position at the next vsync tick, so a sub-tick jump-and-return is invisible
|
||||
// and the target never composes — 35 ms covers a 30 Hz tick with margin. The cursor
|
||||
// visibly leaves the sibling display for those ~2 frames; kicks only fire during THIS
|
||||
// display's session-open / recovery windows (throttled), so the blip is rare and brief.
|
||||
// SAFETY: plain FFI; coordinates are plain ints, and the second call restores the
|
||||
// observed original position.
|
||||
unsafe {
|
||||
let _ = SetCursorPos(x + w / 2, y + h / 2);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(35));
|
||||
// SAFETY: as above.
|
||||
unsafe {
|
||||
let _ = SetCursorPos(pos.x, pos.y);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
let mk = |dx: i32| INPUT {
|
||||
r#type: INPUT_MOUSE,
|
||||
Anonymous: INPUT_0 {
|
||||
@@ -1015,6 +1067,13 @@ impl IddPushCapturer {
|
||||
// Ring format = the display's composition format (FP16 in HDR, BGRA in SDR). The driver
|
||||
// reads this into its `ring_format` and drops any surface that doesn't match.
|
||||
(*header).dxgi_format = ring_fmt.0 as u32;
|
||||
// The ring NAMES its monitor (proto v3, `design/idd-push-security.md` invariant #10) —
|
||||
// stamped before the magic (below), never changed for the ring's life (a mid-session
|
||||
// recreate reuses this mapping). The driver refuses to attach a ring naming a different
|
||||
// monitor, so a stash cross-wire fails closed instead of leaking frames cross-client
|
||||
// (fail-closed refusal VALIDATED on-glass 2026-07-10 via a fault-injected build: driver
|
||||
// DRV_STATUS_BIND_FAIL + loud host open failure + sibling stream undisturbed).
|
||||
(*header).target_id = target.target_id;
|
||||
|
||||
// Frame-ready event (auto-reset) — UNNAMED, like everything on this channel.
|
||||
let event = CreateEventW(Some(&sa), false, false, PCWSTR::null())
|
||||
@@ -1118,6 +1177,20 @@ impl IddPushCapturer {
|
||||
/// session open the OS activates the virtual display → DWM composites it → a frame arrives within ~1 s,
|
||||
/// so this does not false-fail a normal (even idle) open; no frame within the window = genuinely broken.
|
||||
fn wait_for_attach(&self) -> Result<()> {
|
||||
// Symmetric host-side binding sanity (proto v3 §3.2): OUR header must still name OUR
|
||||
// monitor. The stamp is ours and nothing legitimate rewrites it, so a mismatch means a
|
||||
// host-side bug (a stash/capturer cross-wire) — the exact class the driver-side check
|
||||
// catches from the other end; failing here names the culprit in the same release.
|
||||
// SAFETY: in-bounds, aligned u32 read of the live, owned shared-header mapping (same access
|
||||
// pattern as the `driver_status` read below); no reference into the shared region is formed.
|
||||
let stamped = unsafe { (*self.header).target_id };
|
||||
if stamped != self.target_id {
|
||||
bail!(
|
||||
"IDD-push: our ring header names target {stamped} but this capturer serves target \
|
||||
{} — host-side ring↔monitor cross-wire (bug); failing the open",
|
||||
self.target_id
|
||||
);
|
||||
}
|
||||
let deadline = Instant::now() + Duration::from_secs(4);
|
||||
// Compose-kick schedule: DWM only presents a display something DIRTIED, so on an idle
|
||||
// desktop a perfectly healthy attach sees no first frame (E_PENDING forever) and this gate
|
||||
@@ -1160,12 +1233,23 @@ impl IddPushCapturer {
|
||||
the driver has no ID3D11Device1 to open shared resources)"
|
||||
);
|
||||
}
|
||||
if st == DRV_STATUS_BIND_FAIL {
|
||||
// SAFETY: as above — an in-bounds, aligned `u32` read of a best-effort diagnostic field
|
||||
// through the owned, live header mapping; no reference into the shared region is formed.
|
||||
let claimed = unsafe { (*self.header).driver_status_detail };
|
||||
bail!(
|
||||
"IDD-push driver REFUSED the ring↔monitor binding (DRV_STATUS_BIND_FAIL: the \
|
||||
delivered ring names target {claimed}, the monitor is {}) — host \
|
||||
stash/delivery cross-wire (bug); failing the open loudly (proto v3 §3.2)",
|
||||
self.target_id
|
||||
);
|
||||
}
|
||||
// Attached AND a frame has been published — the publish token's seq advances past 0.
|
||||
if st == DRV_STATUS_OPENED && frame::FrameToken::unpack(self.latest()).seq != 0 {
|
||||
return Ok(());
|
||||
}
|
||||
if Instant::now() >= next_kick {
|
||||
kick_dwm_compose();
|
||||
kick_dwm_compose(self.target_id);
|
||||
next_kick = Instant::now() + Duration::from_millis(800);
|
||||
}
|
||||
if Instant::now() > deadline {
|
||||
@@ -1227,6 +1311,11 @@ impl IddPushCapturer {
|
||||
DRV_STATUS_NO_DEVICE1 => {
|
||||
tracing::error!("IDD push: driver has no ID3D11Device1 to open shared resources")
|
||||
}
|
||||
DRV_STATUS_BIND_FAIL => tracing::error!(
|
||||
ring_claims_target = detail,
|
||||
our_target = self.target_id,
|
||||
"IDD push: driver REFUSED the ring↔monitor binding (host stash cross-wire?)"
|
||||
),
|
||||
other => tracing::warn!(other, render_luid, "IDD push: driver reported an unknown status"),
|
||||
}
|
||||
}
|
||||
@@ -1463,7 +1552,7 @@ impl IddPushCapturer {
|
||||
&& self.last_kick.elapsed() > Duration::from_millis(800)
|
||||
{
|
||||
self.last_kick = Instant::now();
|
||||
kick_dwm_compose();
|
||||
kick_dwm_compose(self.target_id);
|
||||
}
|
||||
}
|
||||
// Driver-death watch (the SDR path has no other signal): a dead WUDFHost stops publishing,
|
||||
|
||||
@@ -1054,6 +1054,23 @@ pub fn windows_codec_support() -> CodecSupport {
|
||||
caps
|
||||
}
|
||||
|
||||
/// Stage-W3 encoder session-budget seam (`design/windows-parallel-virtual-displays.md` §4.5):
|
||||
/// whether one more encode session fits the hardware budget — consulted by the display admission
|
||||
/// before admitting a parallel display, so an unaffordable display is DECLINED instead of silently
|
||||
/// degrading a live sibling's encode. NVENC is the only backend with hard session caps today
|
||||
/// (GeForce consumer limit); AMF/QSV equivalents follow the same seam when they grow accounting.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn can_open_another_session() -> bool {
|
||||
#[cfg(feature = "nvenc")]
|
||||
{
|
||||
nvenc::can_open_another_session()
|
||||
}
|
||||
#[cfg(not(feature = "nvenc"))]
|
||||
{
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
// Goal-1 stage 6: GPU/CPU encoders confined to `encode/windows/` (NVENC, native AMF, AMF/QSV
|
||||
// ffmpeg, software) and `encode/linux/` (NVENC/CUDA + VAAPI); `#[path]` keeps the
|
||||
// `crate::encode::*` module names flat.
|
||||
|
||||
@@ -244,6 +244,45 @@ fn codec_guid(codec: Codec) -> nv::GUID {
|
||||
}
|
||||
}
|
||||
|
||||
/// Live NVENC hardware-session units held by THIS host process (a plain session = 1; a forced
|
||||
/// split-encode session occupies one session per engine = 2–3) — the Stage-W3 encoder budget
|
||||
/// (`design/windows-parallel-virtual-displays.md` §4.5). Kept in ONE place so admitting a parallel
|
||||
/// display consults the same accounting every open/teardown maintains; other processes' sessions
|
||||
/// aren't visible here, but our own consumption is the deterministic part we can enforce
|
||||
/// fail-closed at admission.
|
||||
static LIVE_SESSION_UNITS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
|
||||
|
||||
/// The NVENC concurrent-session cap to budget against: GeForce (consumer) drivers allow 8
|
||||
/// concurrent encode sessions since R550 (pro cards are effectively unlimited).
|
||||
/// `PUNKTFUNK_NVENC_MAX_SESSIONS` overrides for older drivers / known-different cards.
|
||||
fn session_cap() -> u32 {
|
||||
std::env::var("PUNKTFUNK_NVENC_MAX_SESSIONS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(8)
|
||||
}
|
||||
|
||||
/// Whether one more (plain, non-split) encode session fits the NVENC budget — consulted by
|
||||
/// admission before admitting a parallel display (`vdisplay::admission`). On a box that never
|
||||
/// opened NVENC (AMD/Intel/none) the count is 0 and this always passes — the budget seam is
|
||||
/// NVENC-only until the AMF/QSV equivalents grow their own accounting.
|
||||
pub(crate) fn can_open_another_session() -> bool {
|
||||
LIVE_SESSION_UNITS.load(std::sync::atomic::Ordering::Relaxed) < session_cap()
|
||||
}
|
||||
|
||||
/// Session-unit weight of a chosen split-encode mode (one hardware session per engine).
|
||||
fn split_mode_units(split_mode: u32) -> u32 {
|
||||
match split_mode {
|
||||
m if m == nv::NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_THREE_FORCED_MODE as u32 => 3,
|
||||
m if m == nv::NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_TWO_FORCED_MODE as u32
|
||||
|| m == nv::NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_AUTO_FORCED_MODE as u32 =>
|
||||
{
|
||||
2
|
||||
}
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the operator asked for the two-thread async retrieve (`PUNKTFUNK_NVENC_ASYNC` truthy).
|
||||
/// Combined with the GPU's `NV_ENC_CAPS_ASYNC_ENCODE_SUPPORT` in `init_session`. Opt-in until
|
||||
/// on-glass validated; note an async-rejecting config surfaces as a failed session open — unset
|
||||
@@ -407,6 +446,10 @@ pub struct NvencD3d11Encoder {
|
||||
/// device on a desktop switch (normal ↔ Winlogon secure); when a frame carries a new device we
|
||||
/// tear down and re-init NVENC against it.
|
||||
init_device: *mut c_void,
|
||||
/// The hardware-session units THIS encoder holds against [`LIVE_SESSION_UNITS`] (1 plain, 2–3
|
||||
/// under forced split-encode — a split session occupies one session per engine). `0` while no
|
||||
/// session is open; set by `init_session`, returned by `teardown`.
|
||||
session_units: u32,
|
||||
}
|
||||
|
||||
// SAFETY: the `!Send` fields are the raw NVENC session/device handles (`encoder`, `init_device`),
|
||||
@@ -469,6 +512,7 @@ impl NvencD3d11Encoder {
|
||||
custom_vbv: false,
|
||||
last_rfi_range: None,
|
||||
init_device: ptr::null_mut(),
|
||||
session_units: 0,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -515,6 +559,9 @@ impl NvencD3d11Encoder {
|
||||
let _ = (api().destroy_bitstream_buffer)(self.encoder, bs);
|
||||
}
|
||||
let _ = (api().destroy_encoder)(self.encoder);
|
||||
// Return this session's units to the budget (see LIVE_SESSION_UNITS).
|
||||
LIVE_SESSION_UNITS.fetch_sub(self.session_units, std::sync::atomic::Ordering::Relaxed);
|
||||
self.session_units = 0;
|
||||
self.regs.clear(); // drops the texture clones, releasing our refs
|
||||
self.bitstreams.clear();
|
||||
self.pending.clear();
|
||||
@@ -1004,6 +1051,11 @@ impl NvencD3d11Encoder {
|
||||
}
|
||||
};
|
||||
self.encoder = enc;
|
||||
// Session-budget accounting (Stage W3): record what this open holds so admission can
|
||||
// decline a parallel display the hardware can't afford. Weighted by the FINAL split
|
||||
// mode (a split session occupies one hardware session per engine).
|
||||
self.session_units = split_mode_units(split_mode);
|
||||
LIVE_SESSION_UNITS.fetch_add(self.session_units, std::sync::atomic::Ordering::Relaxed);
|
||||
if self.bitrate_bps < requested_bps {
|
||||
tracing::info!(
|
||||
requested_mbps = requested_bps / 1_000_000,
|
||||
@@ -1661,7 +1713,7 @@ mod tests {
|
||||
fn encode_pattern(chroma: ChromaFormat, path: &str) {
|
||||
const W: u32 = 1280;
|
||||
const H: u32 = 720;
|
||||
// SAFETY (test-only): straight-line D3D11/DXGI COM calls on one thread; every out-pointer
|
||||
// SAFETY: (test-only) straight-line D3D11/DXGI COM calls on one thread; every out-pointer
|
||||
// is checked before use; the texture/device outlive the encoder (dropped at scope end).
|
||||
unsafe {
|
||||
let factory: IDXGIFactory1 = CreateDXGIFactory1().expect("DXGI factory");
|
||||
|
||||
@@ -299,16 +299,19 @@ fn open_gs_virtual_source(
|
||||
// the native session's ring (newest-wins) — each plane could freeze the other. GameStream has
|
||||
// no cooperative stop-flag plumbing, so it registers a flag nobody reads: a LATER session that
|
||||
// preempts this one signals it, waits the 3 s release grace, then force-preempts the monitor —
|
||||
// this session then fails on capture and tears down cleanly (the intended handover).
|
||||
// this session then fails on capture and tears down cleanly (the intended handover). GameStream
|
||||
// is anonymous (no client cert), so it holds the ANONYMOUS slot (0) — GS stays single-display,
|
||||
// and only a later slot-0 session (another GS/anonymous connect) preempts it.
|
||||
#[cfg(target_os = "windows")]
|
||||
let _idd_setup_guard = matches!(
|
||||
crate::session_plan::CaptureBackend::resolve(),
|
||||
crate::session_plan::CaptureBackend::IddPush
|
||||
)
|
||||
.then(|| {
|
||||
crate::vdisplay::manager::vdm().begin_idd_setup(std::sync::Arc::new(
|
||||
std::sync::atomic::AtomicBool::new(false),
|
||||
))
|
||||
crate::vdisplay::manager::vdm().begin_idd_setup(
|
||||
0,
|
||||
std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
)
|
||||
});
|
||||
let vout = crate::vdisplay::registry::acquire(
|
||||
&mut vd,
|
||||
|
||||
@@ -51,6 +51,19 @@ pub fn hdr_meta_from_display(
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an [`HdrMeta`] display volume into the pf-vdisplay `AddRequest` luminance fields —
|
||||
/// `(max nits, max frame-average nits, min MILLI-nits)` — which the driver codes into the virtual
|
||||
/// monitor's EDID CTA-861.3 HDR block. Pure unit conversion: mastering luminance is 0.0001 cd/m²
|
||||
/// (so nits = /10 000, milli-nits = /10); MaxFALL is already nits and doubles as the display's
|
||||
/// frame-average ceiling.
|
||||
pub fn vdisplay_luminance_fields(m: &HdrMeta) -> (u32, u32, u32) {
|
||||
(
|
||||
m.max_display_mastering_luminance / 10_000,
|
||||
m.max_fall as u32,
|
||||
m.min_display_mastering_luminance / 10,
|
||||
)
|
||||
}
|
||||
|
||||
/// A generic HDR10 default (BT.2020 primaries, D65 white, 1000-nit mastering, MaxCLL 1000 /
|
||||
/// MaxFALL 400) — the baseline a host sends until it reads the source display's real mastering
|
||||
/// metadata, and the values clients used to hardcode.
|
||||
@@ -150,6 +163,25 @@ mod tests {
|
||||
assert_eq!(p, [0x03, 0xE8, 0x01, 0x90]); // 1000, 400 big-endian
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vdisplay_luminance_fields_convert_units() {
|
||||
// An 800-nit / 0.05-nit panel with a 400-nit frame-average ceiling: the AddRequest fields
|
||||
// come out as whole nits / nits / MILLI-nits.
|
||||
let m = hdr_meta_from_display(
|
||||
(0.680, 0.320),
|
||||
(0.265, 0.690),
|
||||
(0.150, 0.060),
|
||||
(0.3127, 0.3290),
|
||||
800.0,
|
||||
0.05,
|
||||
0,
|
||||
400,
|
||||
);
|
||||
assert_eq!(vdisplay_luminance_fields(&m), (800, 400, 50));
|
||||
// The all-zero (unknown) volume stays all-zero — the driver keeps its EDID defaults.
|
||||
assert_eq!(vdisplay_luminance_fields(&HdrMeta::default()), (0, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clamps_out_of_range() {
|
||||
let m = hdr_meta_from_display(
|
||||
|
||||
@@ -1370,9 +1370,15 @@ async fn serve_session(
|
||||
// GetDesc1) as soon as capture starts and re-sends it on keyframes; the client applies the
|
||||
// latest it receives. This baseline covers the synthetic source and the pre-capture gap.
|
||||
if welcome.color.is_hdr() {
|
||||
let meta = crate::hdr::generic_hdr10();
|
||||
// Prefer the CLIENT's own display volume (Hello::display_hdr): the virtual display's EDID
|
||||
// now advertises it, so host apps tone-map to exactly that volume — echoing it here keeps
|
||||
// the mastering metadata honest end-to-end. Generic HDR10 only for older clients.
|
||||
let meta = hello.display_hdr.unwrap_or_else(crate::hdr::generic_hdr10);
|
||||
let _ = conn.send_datagram(punktfunk_core::quic::encode_hdr_meta_datagram(&meta).into());
|
||||
tracing::info!("sent HDR10 static metadata (0xCE; generic baseline)");
|
||||
tracing::info!(
|
||||
client_volume = hello.display_hdr.is_some(),
|
||||
"sent HDR10 static metadata (0xCE baseline)"
|
||||
);
|
||||
}
|
||||
|
||||
// Test hook (synthetic source only): a scripted feedback burst on the host→client
|
||||
@@ -1445,6 +1451,10 @@ async fn serve_session(
|
||||
};
|
||||
let stop_stream = stop.clone();
|
||||
let quit_stream = quit.clone();
|
||||
// The client display's HDR volume (Hello): the virtual display's EDID advertises it (host apps
|
||||
// tone-map to the client's real panel) and the 0xCE mastering metadata echoes it. `None` =
|
||||
// older client / no HDR display → the built-in defaults everywhere.
|
||||
let client_hdr = hello.display_hdr;
|
||||
let fec_target_dp = fec_target.clone(); // data-plane handle to the adaptive-FEC target
|
||||
let conn_stream = conn.clone(); // for sending the source's real HDR metadata (0xCE) mid-stream
|
||||
// Per-AU host-timing emission (0xCF): only when the client advertised the cap bit. All
|
||||
@@ -1533,6 +1543,7 @@ async fn serve_session(
|
||||
stats: stats_dp,
|
||||
client_label,
|
||||
launch: launch_for_dp,
|
||||
client_hdr,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3178,6 +3189,11 @@ struct SessionContext {
|
||||
/// command already resolved against the host's own library — nested into gamescope's bare spawn
|
||||
/// via `set_launch_command`, or spawned into the live session once capture is up.
|
||||
launch: Option<String>,
|
||||
/// The client display's HDR colour volume (`Hello::display_hdr`; `None` = older client / SDR).
|
||||
/// Threaded into the vdisplay backend before `create` (→ the pf-vdisplay EDID's CTA HDR block,
|
||||
/// so host apps tone-map to the client's real panel) and preferred over the generic baseline
|
||||
/// for the 0xCE mastering metadata.
|
||||
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
||||
}
|
||||
|
||||
fn virtual_stream(ctx: SessionContext) -> Result<()> {
|
||||
@@ -3217,6 +3233,7 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> {
|
||||
stats,
|
||||
client_label,
|
||||
launch,
|
||||
client_hdr,
|
||||
} = ctx;
|
||||
tracing::info!(
|
||||
compositor = compositor.id(),
|
||||
@@ -3234,6 +3251,10 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> {
|
||||
// reapplies the client's saved per-monitor config (DPI scaling) on reconnect. No-op on Linux backends
|
||||
// and for anonymous/GameStream clients (no fingerprint → the driver auto-allocates).
|
||||
vd.set_client_identity(endpoint::peer_fingerprint(&conn));
|
||||
// The client display's HDR volume (Hello) → a freshly created virtual monitor's EDID CTA HDR
|
||||
// block (pf-vdisplay), so host apps + the OS tone-map to the client's real panel instead of the
|
||||
// driver's built-in ~1000-nit placeholder. No-op on Linux backends and for older/SDR clients.
|
||||
vd.set_client_hdr(client_hdr);
|
||||
// Deliberate-quit wiring (Windows pf-vdisplay; no-op elsewhere): every lease the backend mints —
|
||||
// the retry-hold below AND the capturer's — carries the session's quit flag, so a user "stop"
|
||||
// (⌘D → the QUIT close code) tears the virtual monitor down the moment the pipeline drops instead
|
||||
@@ -3253,9 +3274,17 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> {
|
||||
// register THIS session's stop. The returned guard holds the setup lock across the pipeline build;
|
||||
// dropping it lets the next reconnect begin (and preempt us). Held BEFORE the monitor is created
|
||||
// (build_pipeline → vd.create), so the preempt still precedes this session's monitor creation.
|
||||
// SLOT-scoped (Stage W1): the preempt targets only a prior session holding THIS client's slot —
|
||||
// a different identity's session is an admission question, never a preempt.
|
||||
#[cfg(target_os = "windows")]
|
||||
let _idd_setup_guard = (plan.capture == crate::session_plan::CaptureBackend::IddPush)
|
||||
.then(|| crate::vdisplay::manager::vdm().begin_idd_setup(stop.clone()));
|
||||
let _idd_setup_guard =
|
||||
(plan.capture == crate::session_plan::CaptureBackend::IddPush).then(|| {
|
||||
let slot = crate::vdisplay::manager::slot_id_for(
|
||||
endpoint::peer_fingerprint(&conn),
|
||||
(mode.width, mode.height),
|
||||
);
|
||||
crate::vdisplay::manager::vdm().begin_idd_setup(slot, stop.clone())
|
||||
});
|
||||
let (mut capturer, mut enc, mut frame, mut interval, mut cur_node_id) =
|
||||
build_pipeline_with_retry(&mut vd, mode, bitrate_kbps, bit_depth, plan, &quit, &stop)?;
|
||||
// Setup done — release the IDD-push setup lock so the next reconnect can begin (and preempt us).
|
||||
@@ -3791,11 +3820,15 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> {
|
||||
diag_repeat = 0;
|
||||
diag_at = std::time::Instant::now();
|
||||
}
|
||||
// The source's static HDR mastering metadata (Windows GetDesc1; None on Linux/SDR) is the
|
||||
// single source of truth: hand it to the encoder (in-band SEI on keyframes) and, when it
|
||||
// changes, to the client (0xCE). Re-sent on each keyframe below so a dropped best-effort
|
||||
// datagram converges within a GOP.
|
||||
let hdr_meta = capturer.hdr_meta();
|
||||
// The source's static HDR mastering metadata is the single source of truth: hand it to the
|
||||
// encoder (in-band SEI on keyframes) and, when it changes, to the client (0xCE). Re-sent on
|
||||
// each keyframe below so a dropped best-effort datagram converges within a GOP. PRESENCE is
|
||||
// the capturer's call (Some iff the virtual display is in HDR mode); the VALUE prefers the
|
||||
// client's own display volume when it sent one — the virtual display's EDID advertises
|
||||
// exactly that volume, so host apps already tone-mapped the content into it and the honest
|
||||
// mastering description IS the client's panel. (The IDD capturer only knows the generic
|
||||
// baseline; if the driver ever forwards per-content IDDCX_HDR10_METADATA, prefer that here.)
|
||||
let hdr_meta = capturer.hdr_meta().map(|m| client_hdr.unwrap_or(m));
|
||||
enc.set_hdr_meta(hdr_meta);
|
||||
let mut resend_meta = hdr_meta != last_hdr_meta;
|
||||
if resend_meta {
|
||||
@@ -4840,6 +4873,7 @@ mod tests {
|
||||
2, // audio_channels (stereo)
|
||||
0, // video_codecs (0 → HEVC-only)
|
||||
0, // preferred_codec (auto)
|
||||
None, // display_hdr
|
||||
None, // launch
|
||||
None, // pin: TOFU — the operator's approval (not a PIN) authorizes this client
|
||||
Some((cert, key)),
|
||||
@@ -4906,6 +4940,7 @@ mod tests {
|
||||
2, // audio_channels (stereo)
|
||||
0, // video_codecs
|
||||
0, // preferred_codec
|
||||
None, // display_hdr
|
||||
None, // launch
|
||||
None,
|
||||
None,
|
||||
@@ -4934,6 +4969,7 @@ mod tests {
|
||||
2, // audio_channels (stereo)
|
||||
0, // video_codecs
|
||||
0, // preferred_codec
|
||||
None, // display_hdr
|
||||
None, // launch
|
||||
Some(host_fp),
|
||||
Some((cert.clone(), key.clone())),
|
||||
|
||||
@@ -132,6 +132,15 @@ pub trait VirtualDisplay: Send {
|
||||
/// leases live in the `VirtualDisplayManager`, which the registry's quit plumbing does not reach;
|
||||
/// Linux backends get the flag through `registry::acquire`).
|
||||
fn set_quit_flag(&mut self, _quit: std::sync::Arc<std::sync::atomic::AtomicBool>) {}
|
||||
/// Hand the backend the CLIENT display's HDR colour volume (`Hello::display_hdr` — primaries /
|
||||
/// white point / luminance range as reported by the client OS), so a freshly created virtual
|
||||
/// output can advertise the client's REAL panel in its EDID (pf-vdisplay codes the luminance
|
||||
/// into the CTA-861.3 HDR static-metadata block) — host apps and the OS then tone-map to the
|
||||
/// panel the stream actually lands on instead of a built-in placeholder volume. Carried on the
|
||||
/// backend instance; set once before [`create`](Self::create). `None` = unknown/SDR client →
|
||||
/// the backend's default EDID. Default: no-op — only the Windows pf-vdisplay backend can mint
|
||||
/// per-monitor EDIDs today (the Linux compositors' virtual outputs take no EDID from us).
|
||||
fn set_client_hdr(&mut self, _hdr: Option<punktfunk_core::quic::HdrMeta>) {}
|
||||
/// The stable identity slot the backend resolved for the most recent [`create`](Self::create) —
|
||||
/// the per-client id the identity policy assigned (`Some`), or `None` for shared/anonymous. The
|
||||
/// registry reads it right after `create` to key the display's group **arrangement** (manual
|
||||
|
||||
@@ -92,27 +92,57 @@ pub fn decide(
|
||||
|
||||
/// The effective `mode_conflict` policy for THIS host: the console value (default `Separate` when
|
||||
/// unconfigured), with the **Windows default applied**. On Windows `separate` — including the
|
||||
/// unconfigured default — resolves to **`reject`**: two concurrent Windows sessions would both drive the
|
||||
/// SAME pf-vdisplay monitor's single-capturer IDD-push channel ("newest-delivery-wins"), which freezes
|
||||
/// the live client and can wedge the driver (true multi-session capture is §6.6 / Stage 7). So a 2nd
|
||||
/// client gets a clean 503 and the live session is protected; `join`/`steal` stay as explicit opt-ins.
|
||||
/// Linux keeps `separate` (real multi-view). Shared by the native + GameStream admission paths.
|
||||
/// unconfigured default — still resolves to **`reject`** UNLESS the Stage-W3 validation hatch
|
||||
/// `PUNKTFUNK_WIN_SEPARATE=1` is set (`design/windows-parallel-virtual-displays.md` §4.3 — the
|
||||
/// default flips to real `separate` in W5, after the on-glass matrix is green).
|
||||
///
|
||||
/// The historical `reject` override guarded against a real wedge: two concurrent Windows sessions
|
||||
/// both drove the SAME pf-vdisplay monitor's single-capturer IDD-push channel
|
||||
/// ("newest-delivery-wins"), which froze the live client and could wedge the driver. With the
|
||||
/// manager's slot map (Stage W1) that wedge is structurally impossible — a second identity gets its
|
||||
/// OWN slot → own monitor → own sealed ring — so the override is now a validation-soak guard, not a
|
||||
/// correctness one. `join`/`steal` stay as explicit opt-ins. Linux keeps `separate` (real
|
||||
/// multi-view). Shared by the native + GameStream admission paths.
|
||||
pub fn effective_conflict() -> ModeConflict {
|
||||
let conflict = policy::prefs()
|
||||
.configured_effective()
|
||||
.map(|e| e.mode_conflict)
|
||||
.unwrap_or(ModeConflict::Separate);
|
||||
#[cfg(windows)]
|
||||
if matches!(conflict, ModeConflict::Separate) {
|
||||
if matches!(conflict, ModeConflict::Separate)
|
||||
&& !std::env::var("PUNKTFUNK_WIN_SEPARATE").is_ok_and(|v| v == "1")
|
||||
{
|
||||
return ModeConflict::Reject;
|
||||
}
|
||||
conflict
|
||||
}
|
||||
|
||||
/// Resolve the admission decision for a connecting native session: [`effective_conflict`] + [`decide`]
|
||||
/// against the live set.
|
||||
/// against the live set, then — when a SECOND display would actually be created (`Separate` with
|
||||
/// other clients live, Windows) — the Stage-W3 resource budgets: `max_displays` across the manager's
|
||||
/// live/kept slots, and the encoder's session headroom. Fail-closed at admission
|
||||
/// (`design/windows-parallel-virtual-displays.md` §2.5): a display we can't afford is DECLINED here,
|
||||
/// never admitted-then-degrading a live sibling.
|
||||
pub fn admit(req_identity: Option<[u8; 32]>) -> Admission {
|
||||
decide(effective_conflict(), req_identity, &table().lock().unwrap())
|
||||
let live = table().lock().unwrap();
|
||||
let decision = decide(effective_conflict(), req_identity, &live);
|
||||
#[cfg(windows)]
|
||||
if matches!(decision, Admission::Separate) && !live.is_empty() {
|
||||
let max = policy::prefs().get().effective().max_displays;
|
||||
let slots = super::manager::snapshot().len() as u32;
|
||||
if slots >= max {
|
||||
return Admission::Reject(format!(
|
||||
"host display budget exhausted: {slots} display(s) live/kept, max_displays = {max}"
|
||||
));
|
||||
}
|
||||
if !crate::encode::can_open_another_session() {
|
||||
return Admission::Reject(
|
||||
"host encoder budget exhausted: no NVENC session headroom for another display"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
decision
|
||||
}
|
||||
|
||||
/// Pure core of [`preempt_same_identity`]: the stop flags of live sessions owned by the SAME client
|
||||
|
||||
@@ -108,11 +108,13 @@ pub fn acquire(
|
||||
pub fn snapshot() -> Snapshot {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Windows is single-monitor at this stage (§6.6 multi-monitor is Stage 7): one group, index 0,
|
||||
// origin. Its per-client identity lives in the driver (EDID serial / ConnectorIndex), not
|
||||
// surfaced here yet.
|
||||
// Windows slots (Stage W1): one group — the shared desktop — with the manager's slot list in
|
||||
// acquire order (`display_index`), each at its group-layout position. `identity_slot` is the
|
||||
// slot key (`None` for the anonymous slot 0).
|
||||
let displays = super::manager::snapshot()
|
||||
.map(|i| DisplayInfo {
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(idx, i)| DisplayInfo {
|
||||
slot: i.gen,
|
||||
backend: i.backend.to_string(),
|
||||
mode: i.mode,
|
||||
@@ -121,12 +123,11 @@ pub fn snapshot() -> Snapshot {
|
||||
sessions: i.sessions,
|
||||
client: None,
|
||||
group: 1,
|
||||
display_index: 0,
|
||||
position: (0, 0),
|
||||
identity_slot: None,
|
||||
display_index: idx as u32,
|
||||
position: i.position,
|
||||
identity_slot: (i.slot_id != 0).then_some(i.slot_id),
|
||||
topology: topology_str(),
|
||||
})
|
||||
.into_iter()
|
||||
.collect();
|
||||
Snapshot { displays }
|
||||
}
|
||||
@@ -149,10 +150,9 @@ pub fn snapshot() -> Snapshot {
|
||||
pub fn release(slot: Option<u64>) -> usize {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Windows manages a single shared monitor at Stage 1, so `slot` is moot — release the one
|
||||
// lingering monitor if present. (Multi-monitor gives `slot` meaning later.)
|
||||
let _ = slot;
|
||||
usize::from(super::manager::force_release())
|
||||
// Windows slots (Stage W1): `slot` selects one kept monitor by its gen stamp
|
||||
// ([`DisplayInfo::slot`]); `None` releases every kept one.
|
||||
super::manager::force_release(slot)
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -432,14 +432,33 @@ impl VdisplayDriver for PfVdisplayDriver {
|
||||
mode: Mode,
|
||||
render_luid: Option<LUID>,
|
||||
preferred_monitor_id: u32,
|
||||
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
||||
) -> Result<AddedMonitor> {
|
||||
let session_id = next_session_id();
|
||||
// The client display's volume rides into the monitor's EDID CTA HDR block; all-zero =
|
||||
// unknown → the driver keeps its built-in defaults (also what an un-upgraded driver, which
|
||||
// reads only the legacy 24-byte prefix, does).
|
||||
let (max_luminance_nits, max_frame_avg_nits, min_luminance_millinits) = client_hdr
|
||||
.map(|m| crate::hdr::vdisplay_luminance_fields(&m))
|
||||
.unwrap_or((0, 0, 0));
|
||||
if max_luminance_nits > 0 {
|
||||
tracing::info!(
|
||||
max_luminance_nits,
|
||||
max_frame_avg_nits,
|
||||
min_luminance_millinits,
|
||||
"pf-vdisplay ADD: advertising the client display's HDR volume in the monitor EDID"
|
||||
);
|
||||
}
|
||||
let add = control::AddRequest {
|
||||
session_id,
|
||||
width: mode.width,
|
||||
height: mode.height,
|
||||
refresh_hz: mode.refresh_hz,
|
||||
preferred_monitor_id,
|
||||
max_luminance_nits,
|
||||
max_frame_avg_nits,
|
||||
min_luminance_millinits,
|
||||
_reserved: 0,
|
||||
};
|
||||
// SET_RENDER_ADAPTER (opt-in; pf-vdisplay IMPLEMENTS it). Non-fatal on failure: the driver reports
|
||||
// its real render LUID in the shared header, so the host binds correctly even if this is ignored.
|
||||
@@ -550,6 +569,7 @@ impl VdisplayDriver for PfVdisplayDriver {
|
||||
target_id: reply.target_id,
|
||||
luid,
|
||||
wudf_pid: reply.wudf_pid,
|
||||
resolved_monitor_id: reply.resolved_monitor_id,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -590,6 +610,11 @@ pub struct PfVdisplayDisplay {
|
||||
/// The connecting client's cert fingerprint (`None` = anonymous/GameStream → the manager's auto id).
|
||||
/// Set by [`set_client_identity`](VirtualDisplay::set_client_identity) before `create`.
|
||||
client_fp: Option<[u8; 32]>,
|
||||
/// The client display's HDR colour volume (`None` = unknown/SDR → the driver's built-in EDID
|
||||
/// defaults). Set by [`set_client_hdr`](VirtualDisplay::set_client_hdr) before `create`; a
|
||||
/// freshly created monitor's EDID advertises this volume so host apps tone-map to the client's
|
||||
/// real panel.
|
||||
client_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
||||
/// The session's deliberate-quit flag (`None` = no signal → the linger policy applies). Set by
|
||||
/// [`set_quit_flag`](VirtualDisplay::set_quit_flag) before `create`; rides into every lease this
|
||||
/// backend mints so a user "stop" tears the monitor down immediately instead of lingering.
|
||||
@@ -601,6 +626,7 @@ impl PfVdisplayDisplay {
|
||||
super::manager::init(Box::new(PfVdisplayDriver)).open_backend()?;
|
||||
Ok(Self {
|
||||
client_fp: None,
|
||||
client_hdr: None,
|
||||
quit: None,
|
||||
})
|
||||
}
|
||||
@@ -615,12 +641,16 @@ impl VirtualDisplay for PfVdisplayDisplay {
|
||||
self.client_fp = fingerprint;
|
||||
}
|
||||
|
||||
fn set_client_hdr(&mut self, hdr: Option<punktfunk_core::quic::HdrMeta>) {
|
||||
self.client_hdr = hdr;
|
||||
}
|
||||
|
||||
fn set_quit_flag(&mut self, quit: std::sync::Arc<std::sync::atomic::AtomicBool>) {
|
||||
self.quit = Some(quit);
|
||||
}
|
||||
|
||||
fn create(&mut self, mode: Mode) -> Result<VirtualOutput> {
|
||||
super::manager::vdm().acquire(mode, self.client_fp, self.quit.clone())
|
||||
super::manager::vdm().acquire(mode, self.client_fp, self.client_hdr, self.quit.clone())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -386,17 +386,18 @@ unsafe fn query_active_config() -> Option<SavedConfig> {
|
||||
Some((paths, modes))
|
||||
}
|
||||
|
||||
/// Count currently-ACTIVE display paths whose target id != `keep_target_id` — i.e. displays that would
|
||||
/// still be lit besides the virtual one. `None` on query failure. Used to VERIFY isolation actually
|
||||
/// took, and (in the `primary` topology) to detect a physical that is ALREADY active so we can skip a
|
||||
/// force-EXTEND that would reset its refresh.
|
||||
pub(crate) unsafe fn count_other_active(keep_target_id: u32) -> Option<u32> {
|
||||
/// Count currently-ACTIVE display paths whose target id is not in `keep_target_ids` — i.e. displays
|
||||
/// that would still be lit besides the managed virtual set. `None` on query failure. Used to VERIFY
|
||||
/// isolation actually took, and (in the `primary` topology) to detect a physical that is ALREADY
|
||||
/// active so we can skip a force-EXTEND that would reset its refresh.
|
||||
pub(crate) unsafe fn count_other_active(keep_target_ids: &[u32]) -> Option<u32> {
|
||||
let (paths, _) = query_active_config()?;
|
||||
Some(
|
||||
paths
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
p.targetInfo.id != keep_target_id && p.flags & DISPLAYCONFIG_PATH_ACTIVE != 0
|
||||
!keep_target_ids.contains(&p.targetInfo.id)
|
||||
&& p.flags & DISPLAYCONFIG_PATH_ACTIVE != 0
|
||||
})
|
||||
.count() as u32,
|
||||
)
|
||||
@@ -406,24 +407,28 @@ pub(crate) unsafe fn count_other_active(keep_target_id: u32) -> Option<u32> {
|
||||
/// ChangeDisplaySettings) MISSES displays on a hybrid box — an iGPU-attached physical monitor isn't
|
||||
/// flagged `ATTACHED_TO_DESKTOP` in the GDI enum, so it's never detached and the secure desktop /
|
||||
/// lock screen lands on IT while our virtual output freezes. `QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS)`
|
||||
/// sees every active path; we deactivate all of them EXCEPT the SudoVDA target's, leaving the virtual
|
||||
/// display as the sole desktop so ALL content (incl. Winlogon) renders to it. Apollo isolates the same
|
||||
/// way (CCD). Returns the original active config to restore on teardown.
|
||||
/// sees every active path; we deactivate all of them EXCEPT the managed virtual target **set**
|
||||
/// (`design/display-management.md` §6.1: "exclusive" means the managed set stays active — with
|
||||
/// parallel displays a sibling slot is never deactivated), leaving the virtual display(s) as the sole
|
||||
/// desktop so ALL content (incl. Winlogon) renders to them. Apollo isolates the same way (CCD).
|
||||
/// Re-issued with the grown/shrunk set on each slot add/remove while the group lives; the FIRST call's
|
||||
/// returned config is what teardown restores (the caller keeps it on the group record and discards
|
||||
/// later returns). Returns the original active config to restore on teardown.
|
||||
// pub(crate) so vdisplay::pf_vdisplay can reuse this backend-neutral CCD isolation helper
|
||||
// (it operates on a real OS target id — a pf-vdisplay monitor's target_id qualifies).
|
||||
pub(crate) unsafe fn isolate_displays_ccd(keep_target_id: u32) -> Option<SavedConfig> {
|
||||
// (it operates on real OS target ids — a pf-vdisplay monitor's target_id qualifies).
|
||||
pub(crate) unsafe fn isolate_displays_ccd(keep_target_ids: &[u32]) -> Option<SavedConfig> {
|
||||
// Snapshot the ORIGINAL active config ONCE for restore-on-teardown, before any changes.
|
||||
let saved = query_active_config()?;
|
||||
|
||||
// Deactivate every non-keep display, then VERIFY and RETRY. A field-reported bug had a physical
|
||||
// monitor STAY ACTIVE in exclusive mode, so we don't trust a single SetDisplayConfig: re-query the
|
||||
// live topology each attempt and re-apply until ONLY the keep target is active. Secure-desktop
|
||||
// live topology each attempt and re-apply until ONLY the keep set is active. Secure-desktop
|
||||
// correctness depends on this — the lock screen must not land on a stray panel while we stream.
|
||||
for attempt in 1..=4u32 {
|
||||
let (mut paths, modes) = query_active_config()?;
|
||||
let mut others = 0u32;
|
||||
for p in paths.iter_mut() {
|
||||
if p.targetInfo.id == keep_target_id {
|
||||
if keep_target_ids.contains(&p.targetInfo.id) {
|
||||
continue;
|
||||
}
|
||||
if p.flags & DISPLAYCONFIG_PATH_ACTIVE != 0 {
|
||||
@@ -446,19 +451,104 @@ pub(crate) unsafe fn isolate_displays_ccd(keep_target_id: u32) -> Option<SavedCo
|
||||
let rc = SetDisplayConfig(Some(paths.as_slice()), Some(modes.as_slice()), flags);
|
||||
|
||||
// VERIFY the OUTCOME (rc alone lies — a "successful" apply can leave a panel active): re-query
|
||||
// and confirm no non-keep display survived. Only then is the virtual truly the sole desktop.
|
||||
let survivors = count_other_active(keep_target_id).unwrap_or(0);
|
||||
// and confirm no non-keep display survived. Only then is the virtual set truly the sole desktop.
|
||||
let survivors = count_other_active(keep_target_ids).unwrap_or(0);
|
||||
if survivors == 0 {
|
||||
tracing::info!("display isolate (CCD): target {keep_target_id} is the SOLE active desktop (attempt {attempt}/4, deactivated {others}, rc={rc:#x})");
|
||||
tracing::info!("display isolate (CCD): target set {keep_target_ids:?} is the SOLE active desktop (attempt {attempt}/4, deactivated {others}, rc={rc:#x})");
|
||||
return Some(saved);
|
||||
}
|
||||
tracing::warn!("display isolate (CCD): {survivors} display(s) STILL active after attempt {attempt}/4 (deactivated {others}, rc={rc:#x}) — re-querying + retrying");
|
||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||
}
|
||||
tracing::error!("display isolate (CCD): FAILED to isolate target {keep_target_id} after 4 attempts — a non-virtual display stayed active (the field-reported exclusive-mode bug)");
|
||||
tracing::error!("display isolate (CCD): FAILED to isolate target set {keep_target_ids:?} after 4 attempts — a non-virtual display stayed active (the field-reported exclusive-mode bug)");
|
||||
Some(saved)
|
||||
}
|
||||
|
||||
/// The desktop-space rectangle `(x, y, w, h)` of `target_id`'s SOURCE — where this display's
|
||||
/// region lives in the desktop coordinate space. `None` while the target isn't an active path.
|
||||
/// Used by the IDD-push compose kick to dirty THE TARGET display: with parallel displays the
|
||||
/// cursor sits on ONE of them, and a cursor wiggle only dirties that one — a sibling display's
|
||||
/// kick must first know where to send the cursor (Stage W3 on-glass finding).
|
||||
pub(crate) unsafe fn source_desktop_rect(target_id: u32) -> Option<(i32, i32, i32, i32)> {
|
||||
let (paths, modes) = query_active_config()?;
|
||||
for p in &paths {
|
||||
if p.targetInfo.id != target_id || p.flags & DISPLAYCONFIG_PATH_ACTIVE == 0 {
|
||||
continue;
|
||||
}
|
||||
let idx = p.sourceInfo.Anonymous.modeInfoIdx as usize;
|
||||
let m = modes.get(idx)?;
|
||||
if m.infoType != DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE {
|
||||
return None;
|
||||
}
|
||||
let sm = m.Anonymous.sourceMode;
|
||||
return Some((
|
||||
sm.position.x,
|
||||
sm.position.y,
|
||||
sm.width as i32,
|
||||
sm.height as i32,
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Place each managed virtual target's SOURCE at the given desktop-space origin, as ONE atomic CCD
|
||||
/// `SetDisplayConfig` (design `display-management.md` §6.2 — the Windows arm of the pure
|
||||
/// `vdisplay/layout.rs` arrangement; positions come from `arrange`, this only commits them). Windows
|
||||
/// treats the source at `(0,0)` as primary, so auto-row's first member lands primary — the group's
|
||||
/// designated member. Paths not named stay where they are. Best-effort: a failure leaves the OS
|
||||
/// placement (mouse crossing may not match the layout table until the next apply).
|
||||
pub(crate) unsafe fn apply_source_positions(positions: &[(u32, i32, i32)]) {
|
||||
if positions.len() < 2 {
|
||||
return; // a single (or no) member sits at the origin — nothing to arrange
|
||||
}
|
||||
let Some((paths, mut modes)) = query_active_config() else {
|
||||
return;
|
||||
};
|
||||
// Dedup source-mode indices (a cloned group shares one) — same discipline as
|
||||
// `set_virtual_primary_ccd`.
|
||||
let mut done = std::collections::HashSet::new();
|
||||
let mut moved = 0u32;
|
||||
for p in paths.iter() {
|
||||
let Some(&(_, x, y)) = positions.iter().find(|(t, _, _)| *t == p.targetInfo.id) else {
|
||||
continue;
|
||||
};
|
||||
let idx = p.sourceInfo.Anonymous.modeInfoIdx as usize;
|
||||
if !done.insert(idx) {
|
||||
continue;
|
||||
}
|
||||
let Some(m) = modes.get_mut(idx) else {
|
||||
continue;
|
||||
};
|
||||
if m.infoType != DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE {
|
||||
continue;
|
||||
}
|
||||
m.Anonymous.sourceMode.position = POINTL { x, y };
|
||||
moved += 1;
|
||||
}
|
||||
if moved == 0 {
|
||||
return;
|
||||
}
|
||||
let rc = SetDisplayConfig(
|
||||
Some(paths.as_slice()),
|
||||
Some(modes.as_slice()),
|
||||
SDC_APPLY
|
||||
| SDC_USE_SUPPLIED_DISPLAY_CONFIG
|
||||
| SDC_ALLOW_CHANGES
|
||||
| SDC_FORCE_MODE_ENUMERATION,
|
||||
);
|
||||
if rc == 0 {
|
||||
tracing::info!(
|
||||
?positions,
|
||||
"display layout (CCD): group source origins applied"
|
||||
);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
?positions,
|
||||
"display layout (CCD): SetDisplayConfig rc={rc:#x}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// **Primary (topology=primary)** — make the virtual output the PRIMARY display while KEEPING every
|
||||
/// other display ACTIVE (unlike [`isolate_displays_ccd`], which deactivates them). Windows treats the
|
||||
/// display whose source sits at the desktop origin `(0,0)` as primary, so we move the virtual's source
|
||||
|
||||
Reference in New Issue
Block a user