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
@@ -127,7 +127,7 @@ unsafe fn set_render_adapter(request: WDFREQUEST) {
/// `request` is the framework `WDFREQUEST`.
unsafe fn add(request: WDFREQUEST) {
// SAFETY: `request` is the framework WDFREQUEST.
let Some(req) = (unsafe { read_input::<control::AddRequest>(request) }) else {
let Some(req) = (unsafe { read_add_request(request) }) else {
complete(request, STATUS_INVALID_PARAMETER);
return;
};
@@ -141,6 +141,11 @@ unsafe fn add(request: WDFREQUEST) {
req.height,
req.refresh_hz,
req.preferred_monitor_id,
crate::edid::ClientLuminance {
max_nits: req.max_luminance_nits,
max_frame_avg_nits: req.max_frame_avg_nits,
min_millinits: req.min_luminance_millinits,
},
) else {
complete(request, STATUS_NOT_FOUND);
return;
@@ -207,6 +212,41 @@ unsafe fn remove(request: WDFREQUEST) {
complete(request, STATUS_SUCCESS);
}
/// Read an [`control::AddRequest`], accepting BOTH wire sizes: the full struct, or an un-upgraded
/// host's [`ADD_REQUEST_LEGACY_SIZE`](control::ADD_REQUEST_LEGACY_SIZE)-byte prefix (no client-HDR
/// luminance tail), whose missing tail zero-fills to "unknown" — so a new driver keeps serving an
/// old host (see the `AddRequest` size-compatibility docs).
///
/// # Safety
/// `request` is the framework `WDFREQUEST`.
unsafe fn read_add_request(request: WDFREQUEST) -> Option<control::AddRequest> {
let mut buf: *mut core::ffi::c_void = core::ptr::null_mut();
let mut len: usize = 0;
// SAFETY: `request` valid; `buf`/`len` are out-params written by the framework.
let st = unsafe {
call_unsafe_wdf_function_binding!(
WdfRequestRetrieveInputBuffer,
request,
control::ADD_REQUEST_LEGACY_SIZE,
&mut buf,
&mut len
)
};
if !nt_success(st) || buf.is_null() || len < control::ADD_REQUEST_LEGACY_SIZE {
return None;
}
let take = len.min(core::mem::size_of::<control::AddRequest>());
// Pod contract (pf-driver-proto derives Zeroable): all-zero = every optional tail field "unknown".
let mut req = pod_init!(control::AddRequest);
// SAFETY: `buf` has >= `take` readable bytes (framework contract: `len` bytes are valid);
// `req` is a Pod struct at least `take` bytes long; the ranges don't overlap (stack vs the
// framework's request buffer).
unsafe {
core::ptr::copy_nonoverlapping(buf.cast::<u8>(), (&raw mut req).cast::<u8>(), take);
}
Some(req)
}
/// Read a `Copy`/`Pod` input struct from the request's input buffer (None if too small / unavailable).
///
/// # Safety
@@ -63,8 +63,10 @@ const COLORIMETRY_DB: [u8; 4] = [
];
/// HDR Static Metadata Data Block (CTA extended tag 0x06): EOTFs = Traditional SDR (ET_0) + SMPTE ST
/// 2084 / PQ (ET_2); Static Metadata Type 1 (SM_0). Plus the optional desired-content luminance hints
/// (~993 nit max, ~400 nit max-frame-average, ~0.05 nit min) so the block is complete.
/// 2084 / PQ (ET_2); Static Metadata Type 1 (SM_0). Plus the desired-content luminance hints
/// (~993 nit max, ~400 nit max-frame-average, ~0.05 nit min) the BUILT-IN defaults, used when the
/// host reported no client volume; [`Edid::generate_with`] overwrites bytes 4..7 with the CLIENT
/// display's coded volume otherwise, so host apps tone-map to the panel the stream lands on.
#[rustfmt::skip]
const HDR_STATIC_METADATA_DB: [u8; 7] = [
0xE6, // tag 0b111 (use-extended-tag) | length 6
@@ -76,12 +78,30 @@ const HDR_STATIC_METADATA_DB: [u8; 7] = [
0x12, // Desired Content Min Luminance (code 18 ≈ 0.05 nits)
];
/// The client display's luminance volume for the CTA HDR block (the [`AddRequest`]
/// (pf_driver_proto::control::AddRequest) luminance tail, same units). `max_nits == 0` = unknown
/// (an SDR client, or an un-upgraded host whose short ADD zero-fills the tail) → the built-in
/// defaults stay.
#[derive(Debug, Clone, Copy, Default)]
pub struct ClientLuminance {
/// Peak luminance, nits. `0` = unknown → keep the built-in default block.
pub max_nits: u32,
/// Max frame-average luminance, nits. `0` = unknown ("no data" on the wire).
pub max_frame_avg_nits: u32,
/// Min luminance, milli-nits. `0` = unknown/true black ("no data" on the wire).
pub min_millinits: u32,
}
#[derive(Debug, Clone, Copy)]
pub struct Edid;
impl Edid {
/// Build the full 256-byte EDID for monitor `serial`, with both block checksums recomputed.
pub fn generate_with(serial: u32) -> Vec<u8> {
/// `lum` is the CLIENT display's luminance volume — coded into the HDR static-metadata block's
/// desired-content bytes (CTA-861.3, via the shared+unit-tested `pf_driver_proto::edid`
/// coders) so the OS/apps tone-map to the client's real panel; all-zero keeps the built-in
/// ~993-nit defaults.
pub fn generate_with(serial: u32, lum: ClientLuminance) -> Vec<u8> {
let mut edid = [0u8; 256];
// Block 0: base.
edid[..128].copy_from_slice(&BASE);
@@ -89,7 +109,18 @@ impl Edid {
// Block 1: CTA-861.3 extension (header + colorimetry + HDR static metadata; rest stays 0).
edid[128..132].copy_from_slice(&CTA_HEADER);
edid[132..136].copy_from_slice(&COLORIMETRY_DB);
edid[136..143].copy_from_slice(&HDR_STATIC_METADATA_DB);
let mut hdr_db = HDR_STATIC_METADATA_DB;
if lum.max_nits > 0 {
let max_code = pf_driver_proto::edid::cta_max_luminance_code(lum.max_nits);
hdr_db[4] = max_code;
hdr_db[5] = if lum.max_frame_avg_nits > 0 {
pf_driver_proto::edid::cta_max_luminance_code(lum.max_frame_avg_nits)
} else {
0 // "no data" — valid per CTA-861.3
};
hdr_db[6] = pf_driver_proto::edid::cta_min_luminance_code(lum.min_millinits, max_code);
}
edid[136..143].copy_from_slice(&hdr_db);
// Each 128-byte block ends in a checksum byte that makes the block sum ≡ 0 (mod 256).
Self::fix_block_checksum(&mut edid, 0);
Self::fix_block_checksum(&mut edid, 128);
@@ -23,8 +23,8 @@ use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use pf_driver_proto::control::SetFrameChannelRequest;
use pf_driver_proto::frame::{
DRV_STATUS_NO_DEVICE1, DRV_STATUS_OPENED, DRV_STATUS_TEX_FAIL, FrameToken, MAGIC, RING_LEN,
SharedHeader,
AttachReject, DRV_STATUS_BIND_FAIL, DRV_STATUS_NO_DEVICE1, DRV_STATUS_OPENED,
DRV_STATUS_TEX_FAIL, FrameToken, RING_LEN, SharedHeader, check_attach,
};
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::Graphics::Direct3D11::{
@@ -157,9 +157,12 @@ impl FramePublisher {
/// re-delivers on the next recreate — there is nothing to poll, so failure is terminal for THIS
/// delivery (the host's `wait_for_attach` sees the status code and fails the session open). All
/// early-return paths clean up explicitly (raw-handle style, no RAII — matches the rest of this
/// driver).
/// driver). `target_id` is the OWNING monitor's OS target id: the mapped ring must name it
/// (proto v3 binding validation — see step 3), so a cross-delivered ring can never carry this
/// monitor's frames into another client's stream.
pub fn from_channel(
mut channel: FrameChannel,
target_id: u32,
render_luid_low: u32,
render_luid_high: i32,
device: &ID3D11Device,
@@ -203,35 +206,70 @@ impl FramePublisher {
(*header).driver_render_luid_high = render_luid_high;
}
// 3. The host stamps magic==MAGIC BEFORE delivering the channel, and this channel's generation
// must match the header's CURRENT generation a mismatch means the host recreated the ring
// again before we attached (a fresh delivery is on its way); drop this stale one.
// 3. The host stamps magic==MAGIC BEFORE delivering the channel, this channel's generation
// must match the header's CURRENT generation (a mismatch means the host recreated the ring
// again before we attached a fresh delivery is on its way; drop this stale one), and —
// proto v3, `design/idd-push-security.md` invariant #10 — the mapped ring must NAME THIS
// MONITOR: the host stamps `target_id` before the magic, so with parallel displays a
// host-side stash cross-wire fails CLOSED here instead of publishing this monitor's frames
// into another client's ring. The shared `check_attach` (unit-tested in pf-driver-proto)
// owns the precedence: staleness first, binding second.
// SAFETY: `header` is the mapped host header; `magic`/`generation` live within it and are read
// atomically (Acquire) to pair with the host's Release publishes.
let (magic, header_gen) = unsafe {
// atomically (Acquire) to pair with the host's Release publishes; `target_id` is a plain
// in-bounds u32 read, stamped before the magic the Acquire load ordered us behind.
let (magic, header_gen, header_target) = unsafe {
(
(*(core::ptr::addr_of!((*header).magic) as *const AtomicU32))
.load(Ordering::Acquire),
(*(core::ptr::addr_of!((*header).generation) as *const AtomicU32))
.load(Ordering::Acquire),
(*header).target_id,
)
};
if magic != MAGIC || header_gen != channel.generation {
dbglog!(
"[pf-vd] frame-push(driver): dropping channel delivery (magic ok: {}, channel gen {} vs header gen {header_gen})",
magic == MAGIC,
channel.generation
);
// SAFETY: `header`/`map` are the live mapped view + taken handle; unmapped + closed once on
// this path.
unsafe {
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
Value: header.cast(),
});
let _ = CloseHandle(map);
match check_attach(
magic,
header_gen,
header_target,
channel.generation,
target_id,
) {
Ok(()) => {}
Err(AttachReject::Stale) => {
dbglog!(
"[pf-vd] frame-push(driver): dropping channel delivery (channel gen {} vs header gen {header_gen}) — superseded",
channel.generation
);
// SAFETY: `header`/`map` are the live mapped view + taken handle; unmapped + closed once
// on this path.
unsafe {
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
Value: header.cast(),
});
let _ = CloseHandle(map);
}
// E_BOUNDS — stand-in for "stale delivery"; the caller only drops the attempt.
return Err(windows::core::HRESULT(0x8000_000Bu32 as i32).into());
}
Err(AttachReject::BindMismatch) => {
dbglog!(
"[pf-vd] frame-push(driver): REFUSING attach — ring names target {header_target}, this monitor is {target_id} (host stash cross-wire?)"
);
// Report the refusal through the header so the host's wait_for_attach fails the open
// LOUDLY (DRV_STATUS_BIND_FAIL) instead of timing out mute; the detail carries the
// target id the ring claims.
// SAFETY: `header`/`map` are the live mapped view + taken handle; the status writes are
// in-bounds scalar writes, then both are released exactly once on this path.
unsafe {
(*header).driver_status_detail = header_target;
(*header).driver_status = DRV_STATUS_BIND_FAIL;
let _ = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
Value: header.cast(),
});
let _ = CloseHandle(map);
}
// E_INVALIDARG — the delivery itself is wrong; the caller only drops the attempt.
return Err(windows::core::HRESULT(0x8007_0057u32 as i32).into());
}
// E_BOUNDS — stand-in for "stale delivery"; the caller only drops the attempt.
return Err(windows::core::HRESULT(0x8000_000Bu32 as i32).into());
}
// 4. The frame-ready event (duplicated with the host handle's full access, so SetEvent works).
@@ -352,15 +352,18 @@ pub fn take_swap_chain_processor(
}
/// `IOCTL_ADD`: create + arrive a virtual monitor at `width`x`height`@`refresh` for `session_id`, naming it
/// by `preferred_id` (the host's per-client stable id; `0` = auto-allocate). Returns the resolved
/// `(monitor_id, target_id, adapter_luid_low, adapter_luid_high)` for the
/// [`AddReply`](pf_driver_proto::control::AddReply), or `None` on failure (no adapter yet / IddCx error).
/// by `preferred_id` (the host's per-client stable id; `0` = auto-allocate) and advertising the
/// CLIENT display's luminance volume in its EDID's CTA HDR block (`client_lum`; all-zero = the
/// built-in defaults). Returns the resolved `(monitor_id, target_id, adapter_luid_low,
/// adapter_luid_high)` for the [`AddReply`](pf_driver_proto::control::AddReply), or `None` on
/// failure (no adapter yet / IddCx error).
pub fn create_monitor(
session_id: u64,
width: u32,
height: u32,
refresh: u32,
preferred_id: u32,
client_lum: crate::edid::ClientLuminance,
) -> Option<(u32, u32, u32, i32)> {
let adapter = crate::adapter::adapter()?;
// Single identity per session (E1): if the host re-ADDs a still-live `session_id` (it shouldn't), depart
@@ -409,7 +412,7 @@ pub fn create_monitor(
};
// EDID (serial = id) describes the monitor; the OS calls back into parse_monitor_description.
let mut edid = crate::edid::Edid::generate_with(id);
let mut edid = crate::edid::Edid::generate_with(id, client_lum);
let mut desc = pod_init!(iddcx::IDDCX_MONITOR_DESCRIPTION);
desc.Size = core::mem::size_of::<iddcx::IDDCX_MONITOR_DESCRIPTION>() as u32;
desc.Type = iddcx::IDDCX_MONITOR_DESCRIPTION_TYPE::IDDCX_MONITOR_DESCRIPTION_TYPE_EDID;
@@ -283,8 +283,12 @@ impl SwapChainProcessor {
if let Some(channel) = crate::monitor::take_frame_channel(target_id) {
// `if let Ok` (not a `match` with an empty `Err` arm) keeps clippy's `single_match`
// happy under `-D warnings`; attach on success, drop the delivery on Err.
// `target_id` binds the attach: the mapped ring must name THIS monitor
// (proto v3 validation inside from_channel — a cross-delivered ring is
// refused, never published into).
if let Ok(p) = FramePublisher::from_channel(
channel,
target_id,
render_luid_low,
render_luid_high,
&device.device,