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

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:
2026-07-11 01:06:35 +02:00
parent 979e38523b
commit 35d97ae6ac
34 changed files with 1945 additions and 544 deletions
@@ -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
+12 -12
View File
@@ -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())
}
}