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:
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user