ci / web (push) Successful in 51s
ci / docs-site (push) Successful in 1m5s
apple / swift (push) Successful in 1m20s
windows-drivers / probe-and-proto (push) Successful in 1m2s
decky / build-publish (push) Successful in 28s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 21s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 16s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
windows-drivers / driver-build (push) Successful in 2m23s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m5s
ci / bench (push) Successful in 6m3s
apple / screenshots (push) Successful in 6m41s
deb / build-publish (push) Successful in 12m20s
arch / build-publish (push) Successful in 12m47s
deb / build-publish-host (push) Successful in 13m7s
android / android (push) Successful in 16m14s
windows-host / package (push) Successful in 15m53s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m23s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m8s
ci / rust (push) Successful in 26m6s
docker / deploy-docs (push) Canceled after 25s
Field report (iPad via GameStream, 2026-07-23): cursor missing from the stream. The session's fresh target 257 reported cursor_excluded=false (DECLARED_TARGETS is keyed per target and only 259 had declared), so the host skipped forced-composite — but DWM's pointer exclusion after an IddCx hardware-cursor declare reaches EVERY later monitor of the adapter, not just the declaring target (proven on-glass: declare on 259 under WUDFHost 2192, then 257's frames streamed pointer-free with CURSOR_SHOWING set and SM_MOUSEPRESENT true). GameStream/Moonlight clients cannot draw a forwarded cursor, so those sessions were silently cursor-less. ADD replies now report any-declare-this-WUDFHost-life (the state's real scope — it dies with the adapter reset, same as before); per-target ids stay only as the dbglog audit trail. Channel-less sessions on a declared adapter then hit the existing forced-composite gate (GDI poller + blend) and carry the pointer in-video. Host code unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1490 lines
79 KiB
Rust
1490 lines
79 KiB
Rust
//! Shared binary contract between the punktfunk host and the `pf-vdisplay` IddCx driver.
|
||
//!
|
||
//! Two planes:
|
||
//! * [`control`] — the low-frequency `DeviceIoControl` plane (add/remove a virtual monitor, pin the
|
||
//! render adapter, keepalive, info, clear-all, deliver the frame channel). Owned, clean, versioned —
|
||
//! NOT the SudoVDA ABI.
|
||
//! * [`frame`] — the IDD-push frame transport: the host creates a ring of **unnamed** shared
|
||
//! keyed-mutex textures (+ a header + a frame-ready event), duplicates their handles into the
|
||
//! driver's WUDFHost process and delivers the handle VALUES over
|
||
//! [`control::IOCTL_SET_FRAME_CHANNEL`]; the driver publishes composited frames into them. There is
|
||
//! deliberately no object-name scheme: an unnamed object cannot be enumerated, opened by name, or
|
||
//! pre-created ("squatted") — only the two endpoint processes ever hold a handle to any frame object
|
||
//! (the sealed channel, `design/idd-push-security.md`). This crate owns the [`frame::SharedHeader`]
|
||
//! layout, the [`frame::FrameToken`] packing, the channel-delivery struct, and the driver-status
|
||
//! codes.
|
||
//!
|
||
//! Both planes were previously hand-duplicated, byte-for-byte, across `idd_push.rs`/`frame_transport.rs`
|
||
//! and `vdisplay/sudovda.rs`/`control.rs` with only "must match" comments guarding them. Defining them
|
||
//! once here — with bytemuck `Pod` derives and `const` size asserts — makes any drift a compile error.
|
||
//!
|
||
//! The GUID and LUID are carried as plain integers; the host converts to `windows::core::GUID` /
|
||
//! `windows::Win32::Foundation::LUID` and the driver to its own bindgen types via the same constants.
|
||
|
||
#![cfg_attr(not(test), no_std)]
|
||
|
||
extern crate alloc;
|
||
|
||
/// Freshly-minted pf-vdisplay device-interface GUID — `{70667664-7044-5350-a1b2-c3d4e5f60001}`.
|
||
/// Deliberately NOT SudoVDA's `{e5bcc234-…}`: we own the driver, so a private interface GUID signals
|
||
/// it and removes any accidental coexistence with a real SudoVDA install. Construct on each side via
|
||
/// `GUID::from_u128(PF_VDISPLAY_INTERFACE_GUID_U128)`.
|
||
pub const PF_VDISPLAY_INTERFACE_GUID_U128: u128 = 0x7066_7664_7044_5350_a1b2_c3d4_e5f6_0001;
|
||
|
||
/// The interface GUID split into Windows `GUID` fields — `(Data1, Data2, Data3, Data4)` — so the driver
|
||
/// (and host) can build a `windows`/`wdk_sys` `GUID` without re-deriving the byte layout. Standard GUID
|
||
/// layout from the u128: `Data1` = high 32 bits, `Data2`/`Data3` = next two 16-bit groups, `Data4` =
|
||
/// the low 64 bits big-endian. (This crate is `no_std` + provider-agnostic, so it returns the fields
|
||
/// rather than depend on a `GUID` type.)
|
||
#[must_use]
|
||
pub const fn interface_guid_fields() -> (u32, u16, u16, [u8; 8]) {
|
||
let g = PF_VDISPLAY_INTERFACE_GUID_U128;
|
||
(
|
||
(g >> 96) as u32,
|
||
(g >> 80) as u16,
|
||
(g >> 64) as u16,
|
||
(g as u64).to_be_bytes(),
|
||
)
|
||
}
|
||
|
||
/// Bumped on any incompatible change to either plane. Exchanged via [`control::IOCTL_GET_INFO`]; host
|
||
/// and driver assert a match at startup so a mismatched pair fails loudly instead of corrupting.
|
||
/// v2: the sealed frame channel — the frame objects are unnamed and delivered by handle duplication
|
||
/// ([`control::IOCTL_SET_FRAME_CHANNEL`]), and [`control::AddReply`] grew `wudf_pid` (the duplication
|
||
/// target). A v1 driver has no channel-delivery IOCTL and expects named objects, so the pairing is
|
||
/// incompatible by design.
|
||
/// v3: ring↔monitor binding hardening for parallel displays
|
||
/// (`design/windows-parallel-virtual-displays.md` §3): [`frame::SharedHeader`] names its monitor
|
||
/// (`target_id`, the former `_pad` — same size, same offsets) and the driver's publisher refuses to
|
||
/// attach a ring naming a different monitor ([`frame::DRV_STATUS_BIND_FAIL`], the gamepad channel's
|
||
/// `pad_index` validation applied to frames). A v2 host never stamps the field, so a v3 driver
|
||
/// against a v2 host would refuse every attach — lockstep by the handshake, as ever.
|
||
/// v4: ADDITIVE — [`control::IOCTL_UPDATE_MODES`] (the in-place mid-stream resize,
|
||
/// `design/first-frame-and-resize-latency.md` P2): the driver refreshes a LIVE monitor's advertised
|
||
/// target-mode list (`IddCxMonitorUpdateModes2`) so the OS can mode-set to an arbitrary new mode
|
||
/// without a REMOVE→ADD monitor hotplug. Nothing existing changed, so the host accepts a v3 driver
|
||
/// too ([`MIN_DRIVER_PROTOCOL_VERSION`]) and simply falls back to the re-arrival resize against it;
|
||
/// a v4 driver serving an older (v3-asserting) host fails that host's strict handshake — ship
|
||
/// driver+host together, as ever.
|
||
/// v5: ADDITIVE — the IddCx HARDWARE CURSOR channel (remote-desktop-sweep M2c):
|
||
/// [`control::AddRequest::hw_cursor`] (the former tail `_reserved` — same size, same offsets)
|
||
/// asks the driver to declare a hardware cursor for this monitor (DWM then EXCLUDES the pointer
|
||
/// from the desktop frame and delivers shape/position out-of-band), and
|
||
/// [`control::IOCTL_SET_CURSOR_CHANNEL`] delivers the host-created [`cursor::CursorShm`] section
|
||
/// the driver's cursor thread seqlock-publishes into. Nothing existing changed; the host gates
|
||
/// the feature on the handshake-reported version (`>= 5`) and keeps composited-cursor behavior
|
||
/// against older drivers.
|
||
/// v6: ADDITIVE — [`control::IOCTL_SET_CURSOR_FORWARD`] (the mid-stream cursor-render flip,
|
||
/// remote-desktop-sweep §8): the client's mouse-model flip (un)declares the hardware cursor on a
|
||
/// LIVE monitor, so the capture mouse model gets DWM's composited pointer back (full fidelity)
|
||
/// and the desktop model gets exclusion + forwarding. Nothing existing changed; against a v5
|
||
/// driver the unknown IOCTL fails and the host logs + keeps the declared-at-ADD behavior.
|
||
/// v6 tail ext (no bump, the `AddRequest` luminance-tail discipline):
|
||
/// [`control::AddReply::cursor_excluded`] — the driver reports whether its ADAPTER already
|
||
/// carries a hardware-cursor declare from an earlier session. A declare is IRREVOCABLE
|
||
/// (remote-desktop-sweep §8.6, proven on-glass) and its exclusion reaches EVERY later monitor of
|
||
/// the adapter, not just the declaring target (on-glass 2026-07-23: a declare on one target left
|
||
/// a different client's fresh target cursor-less): DWM never composites the software cursor back
|
||
/// into any of the adapter's frames until the adapter resets. The host uses the flag to
|
||
/// self-composite the pointer (GDI poller + blend) in sessions that never negotiate the
|
||
/// cursor channel — without it those sessions are silently cursor-less. Both skews degrade
|
||
/// cleanly: an old driver writes only the 20-byte reply prefix (host reads `0` = unknown/clean),
|
||
/// an old host retrieves a 20-byte buffer (driver writes just the prefix).
|
||
pub const PROTOCOL_VERSION: u32 = 6;
|
||
|
||
/// The OLDEST driver protocol this host still drives (v4 is additive over v3 — see the v4 note on
|
||
/// [`PROTOCOL_VERSION`]): a v3 driver lacks only `IOCTL_UPDATE_MODES`, which the host gates on the
|
||
/// handshake-reported version and covers with the re-arrival fallback.
|
||
pub const MIN_DRIVER_PROTOCOL_VERSION: u32 = 3;
|
||
|
||
/// `CTL_CODE(FILE_DEVICE_UNKNOWN = 0x22, func, METHOD_BUFFERED = 0, FILE_ANY_ACCESS = 0)`.
|
||
pub const fn ctl_code(func: u32) -> u32 {
|
||
(0x22u32 << 16) | (func << 2)
|
||
}
|
||
|
||
/// The control (`DeviceIoControl`) plane: add/remove a virtual monitor + adapter pin + keepalive +
|
||
/// frame-channel delivery.
|
||
pub mod control {
|
||
use super::ctl_code;
|
||
use super::frame::RING_LEN;
|
||
use bytemuck::{Pod, Zeroable};
|
||
|
||
// Contiguous op space at 0x900 — distinct from SudoVDA's gappy 0x800/0x888/0x8FF numbering.
|
||
/// Add a virtual monitor at a mode → [`AddReply`]. Input [`AddRequest`].
|
||
pub const IOCTL_ADD: u32 = ctl_code(0x900);
|
||
/// Remove a virtual monitor by session id. Input [`RemoveRequest`].
|
||
pub const IOCTL_REMOVE: u32 = ctl_code(0x901);
|
||
/// Pin the IddCx render adapter (hybrid-GPU IDD-push). Input [`SetRenderAdapterRequest`].
|
||
pub const IOCTL_SET_RENDER_ADAPTER: u32 = ctl_code(0x902);
|
||
/// Keepalive (resets the driver watchdog). No payload.
|
||
pub const IOCTL_PING: u32 = ctl_code(0x903);
|
||
/// Version + watchdog handshake → [`InfoReply`]. No input.
|
||
pub const IOCTL_GET_INFO: u32 = ctl_code(0x904);
|
||
/// Tear down every virtual monitor (host-startup orphan reap). No payload. First-class op — NOT the
|
||
/// SudoVDA "send-and-hope-it's-ignored" hack.
|
||
pub const IOCTL_CLEAR_ALL: u32 = ctl_code(0x905);
|
||
/// Deliver a monitor's IDD-push frame channel: the handle VALUES of the unnamed shared objects the
|
||
/// host duplicated into the driver's WUDFHost process. Input [`SetFrameChannelRequest`]. Sent once
|
||
/// after the ring is created and again on every mid-session ring recreate (HDR-mode flip).
|
||
pub const IOCTL_SET_FRAME_CHANNEL: u32 = ctl_code(0x906);
|
||
/// Refresh a LIVE monitor's advertised target-mode list to a new preferred mode (+ the built-in
|
||
/// fallbacks) via `IddCxMonitorUpdateModes2` — the in-place mid-stream resize (v4,
|
||
/// `design/first-frame-and-resize-latency.md` P2). Input [`UpdateModesRequest`]. The host then
|
||
/// CCD-forces the new mode active on the SAME monitor: no REMOVE→ADD hotplug, the monitor's OS
|
||
/// identity (saved per-monitor DPI) and the driver's swap-chain/stash machinery survive. A v3
|
||
/// driver fails this unknown IOCTL → the host falls back to the re-arrival resize.
|
||
pub const IOCTL_UPDATE_MODES: u32 = ctl_code(0x907);
|
||
/// Deliver a monitor's hardware-cursor channel (v5): the handle VALUE of the unnamed
|
||
/// [`cursor::CursorShm`](crate::cursor) file mapping the host duplicated into the WUDFHost
|
||
/// (same delivery model as [`IOCTL_SET_FRAME_CHANNEL`], no event — the host polls the
|
||
/// seqlock at its encode-tick pace). Sent once after ADD, only for a monitor whose
|
||
/// [`AddRequest::hw_cursor`] was set. The driver maps it, calls
|
||
/// `IddCxMonitorSetupHardwareCursor`, and starts its cursor-query thread. Input
|
||
/// [`SetCursorChannelRequest`].
|
||
pub const IOCTL_SET_CURSOR_CHANNEL: u32 = ctl_code(0x908);
|
||
/// Flip a LIVE monitor's hardware-cursor declaration (v6, the mid-stream cursor-render
|
||
/// flip): `enable = 1` re-declares (`IddCxMonitorSetupHardwareCursor` — DWM excludes the
|
||
/// pointer, the query/shape machinery resumes), `enable = 0` un-declares (the driver stops
|
||
/// re-declaring on mode commits and asks the OS to revert to the software cursor — DWM
|
||
/// composites the pointer into the frame, the pre-channel behavior the capture mouse
|
||
/// model wants). Only meaningful for a monitor whose cursor channel was delivered. Input
|
||
/// [`SetCursorForwardRequest`].
|
||
pub const IOCTL_SET_CURSOR_FORWARD: u32 = ctl_code(0x909);
|
||
|
||
/// `IOCTL_ADD` input. A monotonic `session_id` keys the monitor (the host's refcount manager owns
|
||
/// collision safety — no more SudoVDA's 16-byte GUID + pid-mangling). The driver advertises this
|
||
/// mode as preferred; the host still CCD-forces the active mode (the OS activates IDDs at a default).
|
||
///
|
||
/// **Size compatibility**: the client-HDR luminance tail (the three fields after
|
||
/// `preferred_monitor_id`) was appended without a protocol bump because BOTH directions degrade
|
||
/// cleanly: an un-upgraded driver reads the [`ADD_REQUEST_LEGACY_SIZE`]-byte prefix of a new
|
||
/// host's request (its `read_input` accepts a larger buffer) and keeps its built-in EDID
|
||
/// luminance; an upgraded driver accepts a legacy-size request and zero-fills the tail (`0` =
|
||
/// unknown → the built-in defaults). Any FURTHER field must follow the same discipline.
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
||
pub struct AddRequest {
|
||
pub session_id: u64,
|
||
pub width: u32,
|
||
pub height: u32,
|
||
pub refresh_hz: u32,
|
||
/// Host-preferred per-client monitor id (`1..=15`) — the EDID serial / IddCx `ConnectorIndex` /
|
||
/// `ContainerId` the driver names this monitor by. A given client (keyed by its cert fingerprint)
|
||
/// gets a STABLE id across reconnects, so the OS device path + EDID stay identical and Windows
|
||
/// reapplies that client's saved per-monitor config (DPI scaling). `0` = AUTO: the driver
|
||
/// allocates the lowest-free id (the original slot-based behavior — used for anonymous/TOFU and
|
||
/// GameStream sessions). Byte-compatible with the old `_reserved` (offset 20): an un-upgraded
|
||
/// driver ignores it (→ auto), which the host detects via [`AddReply::resolved_monitor_id`].
|
||
pub preferred_monitor_id: u32,
|
||
/// The CLIENT display's peak luminance in nits — written into this monitor's EDID CTA-861.3
|
||
/// HDR static-metadata block (Desired Content Max Luminance), so host apps and the OS
|
||
/// tone-map to the panel the stream actually lands on instead of the driver's built-in
|
||
/// ~1000-nit placeholder. `0` = unknown → the driver keeps its built-in default block.
|
||
pub max_luminance_nits: u32,
|
||
/// The client display's max frame-average luminance in nits (→ Desired Content Max
|
||
/// Frame-average Luminance). `0` = unknown/not indicated.
|
||
pub max_frame_avg_nits: u32,
|
||
/// The client display's min luminance in MILLI-nits (0.001 cd/m² — the CTA min-luminance
|
||
/// range lives well below 1 nit) → Desired Content Min Luminance. `0` = unknown.
|
||
pub min_luminance_millinits: u32,
|
||
/// Non-zero = declare an IddCx HARDWARE CURSOR for this monitor (v5, remote-desktop-sweep
|
||
/// M2c): DWM stops compositing the pointer into the frame and the driver publishes
|
||
/// shape/position into the [`cursor::CursorShm`](crate::cursor) section delivered by
|
||
/// [`IOCTL_SET_CURSOR_CHANNEL`]. Byte-compatible with the old tail `_reserved` (offset 36):
|
||
/// an un-upgraded driver ignores it (cursor stays composited — the host already gates on
|
||
/// the handshake version, this is defense in depth), an un-upgraded host sends `0` (off).
|
||
pub hw_cursor: u32,
|
||
}
|
||
|
||
/// [`AddRequest`]'s size before the client-HDR luminance tail — the prefix an un-upgraded
|
||
/// driver reads and the whole request an un-upgraded host sends (see the struct docs).
|
||
pub const ADD_REQUEST_LEGACY_SIZE: usize = 24;
|
||
|
||
/// `IOCTL_ADD` reply: the OS target id + the adapter LUID the IDD landed on (split low/high to
|
||
/// match `windows` `LUID { LowPart: u32, HighPart: i32 }`).
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
||
pub struct AddReply {
|
||
pub adapter_luid_low: u32,
|
||
pub adapter_luid_high: i32,
|
||
pub target_id: u32,
|
||
/// The monitor id the driver ACTUALLY used — echoes [`AddRequest::preferred_monitor_id`] when the
|
||
/// preference was honored, or the auto-allocated id otherwise. Byte-compatible with the old
|
||
/// `_reserved` (offset 12): an un-upgraded driver leaves it `0`, so the host can tell its
|
||
/// preference was ignored (stale driver) and log it instead of silently losing per-client config.
|
||
pub resolved_monitor_id: u32,
|
||
/// The driver's own process id (the WUDFHost hosting `pf_vdisplay`) — the target the host
|
||
/// duplicates the unnamed frame-object handles INTO (`OpenProcess(PROCESS_DUP_HANDLE)` +
|
||
/// `DuplicateHandle`, then [`IOCTL_SET_FRAME_CHANNEL`]). Reported per-ADD, not per-open, so a
|
||
/// WUDFHost restart between sessions can never leave the host duplicating into a dead process.
|
||
pub wudf_pid: u32,
|
||
/// Non-zero = the ADAPTER already carries an IRREVOCABLE hardware-cursor declare from an
|
||
/// earlier session (remote-desktop-sweep §8.6; reach is adapter-wide, not per-target —
|
||
/// on-glass 2026-07-23): DWM excludes the pointer from every frame on every monitor until
|
||
/// the adapter resets, and a session without the cursor channel must
|
||
/// self-composite (GDI poller + blend) or stream a cursor-less desktop. Appended after
|
||
/// [`ADD_REPLY_LEGACY_SIZE`] under the same dual-size discipline as the `AddRequest`
|
||
/// luminance tail: an un-upgraded driver writes only the legacy prefix (the host's
|
||
/// zero-initialized buffer then reads `0` = unknown/clean), an un-upgraded host retrieves a
|
||
/// legacy-size buffer (the driver writes just the prefix).
|
||
pub cursor_excluded: u32,
|
||
}
|
||
|
||
/// [`AddReply`]'s size before the `cursor_excluded` tail — the prefix an un-upgraded driver
|
||
/// writes and an un-upgraded host retrieves (see the field docs).
|
||
pub const ADD_REPLY_LEGACY_SIZE: usize = 20;
|
||
|
||
/// `IOCTL_REMOVE` input.
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
||
pub struct RemoveRequest {
|
||
pub session_id: u64,
|
||
}
|
||
|
||
/// `IOCTL_UPDATE_MODES` input (v4): the live monitor (by its ADD `session_id`) and the new
|
||
/// preferred mode its target-mode list should lead with. The driver replaces the stored list
|
||
/// (new mode first, then its built-in fallbacks — the same shape ADD produces) and pushes it to
|
||
/// the OS via `IddCxMonitorUpdateModes2`; success means the OS accepted the new list, after
|
||
/// which the host force-sets the mode via CCD/GDI as usual.
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
||
pub struct UpdateModesRequest {
|
||
pub session_id: u64,
|
||
pub width: u32,
|
||
pub height: u32,
|
||
pub refresh_hz: u32,
|
||
/// Pads the `u64`-aligned struct to a multiple of 8 (Pod forbids implicit tail padding).
|
||
pub _reserved: u32,
|
||
}
|
||
|
||
/// `IOCTL_SET_RENDER_ADAPTER` input (the GPU the IddCx swap-chain should render on).
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
||
pub struct SetRenderAdapterRequest {
|
||
pub luid_low: u32,
|
||
pub luid_high: i32,
|
||
}
|
||
|
||
/// `IOCTL_GET_INFO` reply: the protocol version (asserted against [`super::PROTOCOL_VERSION`]) and
|
||
/// the watchdog timeout the host must ping within.
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
||
pub struct InfoReply {
|
||
pub protocol_version: u32,
|
||
pub watchdog_timeout_s: u32,
|
||
}
|
||
|
||
/// `IOCTL_SET_FRAME_CHANNEL` input — the sealed frame channel's bootstrap. Every handle field is a
|
||
/// handle VALUE already duplicated into the driver's WUDFHost process by the host. Ownership is
|
||
/// **adopt-on-success-only** (`design/idd-push-security.md` invariant 5): the driver owns (and
|
||
/// eventually closes) the handles IFF it completes the IOCTL successfully — a replaced or
|
||
/// later-unconsumed delivery is then the driver's to close. On ANY error completion (malformed
|
||
/// request, unknown `target_id`) the driver must NOT close them: the HOST reaps its remote
|
||
/// duplicates (`DUPLICATE_CLOSE_SOURCE`). Exactly one side closes each value; a driver that closed
|
||
/// on error would double-close possibly-reused handle values against the host's reap.
|
||
///
|
||
/// Handle values are only meaningful inside the target process's handle table, so this struct is
|
||
/// harmless to any third party: reading it leaks nothing openable, and spoofing it (were the control
|
||
/// device reachable — it is ACL'd to SYSTEM + admins) could at worst feed the driver values that
|
||
/// don't resolve, a DoS of the attacker's own session. The frame objects themselves are unnamed and
|
||
/// therefore unreachable by any process that isn't one of the two endpoints.
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
||
pub struct SetFrameChannelRequest {
|
||
/// The OS target id from [`AddReply`] — which monitor this channel belongs to.
|
||
pub target_id: u32,
|
||
/// The ring generation these textures belong to (must match the shared header's generation at
|
||
/// attach time; a stale delivery is dropped by the driver — a fresh one follows every recreate).
|
||
pub generation: u32,
|
||
/// How many leading entries of `texture_handles` are valid (`1..=`[`RING_LEN`]).
|
||
pub ring_len: u32,
|
||
pub _pad: u32,
|
||
/// The shared-header file-mapping handle (the driver maps it and writes status/publish tokens).
|
||
pub header_handle: u64,
|
||
/// The frame-ready auto-reset event handle (the driver signals it after each publish).
|
||
pub event_handle: u64,
|
||
/// The ring textures' shared NT handles (opened via `ID3D11Device1::OpenSharedResource1`).
|
||
pub texture_handles: [u64; RING_LEN_USIZE],
|
||
}
|
||
|
||
/// [`RING_LEN`] as a usize for the `texture_handles` array length (the wire struct sizes the array
|
||
/// at the compile-time maximum; `ring_len` says how many entries are live).
|
||
pub const RING_LEN_USIZE: usize = RING_LEN as usize;
|
||
|
||
/// `IOCTL_SET_CURSOR_CHANNEL` input (v5): the hardware-cursor section for one monitor.
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
||
pub struct SetCursorChannelRequest {
|
||
/// The OS target id from [`AddReply`] — which monitor this channel belongs to.
|
||
pub target_id: u32,
|
||
pub _pad: u32,
|
||
/// The [`cursor::CursorShm`](crate::cursor) file-mapping handle VALUE, already duplicated
|
||
/// into the driver's WUDFHost process ([`AddReply::wudf_pid`]).
|
||
pub header_handle: u64,
|
||
}
|
||
|
||
/// `IOCTL_SET_CURSOR_FORWARD` input (v6): the mid-stream cursor-render flip for one monitor.
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
||
pub struct SetCursorForwardRequest {
|
||
/// The OS target id from [`AddReply`] — which monitor to flip.
|
||
pub target_id: u32,
|
||
/// `1` = declare the hardware cursor (exclude + forward), `0` = un-declare (DWM
|
||
/// composites — the capture mouse model).
|
||
pub enable: u32,
|
||
}
|
||
|
||
// Layout is load-bearing across the process boundary — pin it. (bytemuck's Pod derive already
|
||
// rejects any internal padding; these assert the externally-visible sizes too.) The `offset_of!`
|
||
// asserts additionally catch a SAME-SIZE field reorder, which the size+Pod checks alone miss.
|
||
const _: () = {
|
||
use core::mem::{offset_of, size_of};
|
||
|
||
assert!(size_of::<AddRequest>() == 40);
|
||
assert!(offset_of!(AddRequest, session_id) == 0);
|
||
assert!(offset_of!(AddRequest, width) == 8);
|
||
assert!(offset_of!(AddRequest, height) == 12);
|
||
assert!(offset_of!(AddRequest, refresh_hz) == 16);
|
||
assert!(offset_of!(AddRequest, preferred_monitor_id) == 20);
|
||
// The client-HDR luminance tail starts exactly at the legacy boundary (prefix-compat).
|
||
assert!(offset_of!(AddRequest, max_luminance_nits) == ADD_REQUEST_LEGACY_SIZE);
|
||
assert!(offset_of!(AddRequest, max_frame_avg_nits) == 28);
|
||
assert!(offset_of!(AddRequest, min_luminance_millinits) == 32);
|
||
// v5: the former tail `_reserved` — same offset, same total size (rename-only).
|
||
assert!(offset_of!(AddRequest, hw_cursor) == 36);
|
||
assert!(size_of::<AddRequest>() == 40);
|
||
|
||
assert!(size_of::<AddReply>() == 24);
|
||
assert!(offset_of!(AddReply, adapter_luid_low) == 0);
|
||
assert!(offset_of!(AddReply, adapter_luid_high) == 4);
|
||
assert!(offset_of!(AddReply, target_id) == 8);
|
||
assert!(offset_of!(AddReply, resolved_monitor_id) == 12);
|
||
assert!(offset_of!(AddReply, wudf_pid) == 16);
|
||
// The cursor-excluded tail starts exactly at the legacy boundary (prefix-compat).
|
||
assert!(offset_of!(AddReply, cursor_excluded) == ADD_REPLY_LEGACY_SIZE);
|
||
|
||
assert!(size_of::<SetFrameChannelRequest>() == 32 + 8 * RING_LEN_USIZE);
|
||
assert!(offset_of!(SetFrameChannelRequest, target_id) == 0);
|
||
assert!(offset_of!(SetFrameChannelRequest, generation) == 4);
|
||
assert!(offset_of!(SetFrameChannelRequest, ring_len) == 8);
|
||
assert!(offset_of!(SetFrameChannelRequest, header_handle) == 16);
|
||
assert!(offset_of!(SetFrameChannelRequest, event_handle) == 24);
|
||
assert!(offset_of!(SetFrameChannelRequest, texture_handles) == 32);
|
||
|
||
assert!(size_of::<RemoveRequest>() == 8);
|
||
assert!(offset_of!(RemoveRequest, session_id) == 0);
|
||
|
||
assert!(size_of::<SetCursorChannelRequest>() == 16);
|
||
assert!(offset_of!(SetCursorChannelRequest, target_id) == 0);
|
||
assert!(offset_of!(SetCursorChannelRequest, header_handle) == 8);
|
||
assert!(size_of::<SetCursorForwardRequest>() == 8);
|
||
assert!(offset_of!(SetCursorForwardRequest, target_id) == 0);
|
||
assert!(offset_of!(SetCursorForwardRequest, enable) == 4);
|
||
|
||
assert!(size_of::<UpdateModesRequest>() == 24);
|
||
assert!(offset_of!(UpdateModesRequest, session_id) == 0);
|
||
assert!(offset_of!(UpdateModesRequest, width) == 8);
|
||
assert!(offset_of!(UpdateModesRequest, height) == 12);
|
||
assert!(offset_of!(UpdateModesRequest, refresh_hz) == 16);
|
||
|
||
assert!(size_of::<SetRenderAdapterRequest>() == 8);
|
||
assert!(offset_of!(SetRenderAdapterRequest, luid_low) == 0);
|
||
assert!(offset_of!(SetRenderAdapterRequest, luid_high) == 4);
|
||
|
||
assert!(size_of::<InfoReply>() == 8);
|
||
assert!(offset_of!(InfoReply, protocol_version) == 0);
|
||
assert!(offset_of!(InfoReply, watchdog_timeout_s) == 4);
|
||
};
|
||
}
|
||
|
||
/// CTA-861.3 "Desired Content Luminance" coding for the pf-vdisplay EDID's HDR Static Metadata
|
||
/// Data Block — the three bytes that tell Windows (and through it every host app) what luminance
|
||
/// volume the virtual display's panel has. The HOST fills [`control::AddRequest`]'s luminance
|
||
/// fields from the CLIENT's real display volume and the DRIVER codes them here, so games tone-map
|
||
/// to the panel the stream actually lands on.
|
||
///
|
||
/// Lives in this shared crate (not the driver) deliberately: the driver only builds under the WDK
|
||
/// on Windows, but this byte-level coding is exactly the fiddly part that wants unit tests on
|
||
/// every dev machine BEFORE a driver build/sign/deploy cycle. `no_std` + integer-only (fixed
|
||
/// point), so it drops into the driver unchanged.
|
||
pub mod edid {
|
||
/// `2^(k/32)` for `k = 0..32` in Q16 fixed point (`round(2^(k/32) * 65536)`) — the fractional
|
||
/// step table for the CTA-861.3 luminance exponent.
|
||
const POW2_Q16: [u32; 32] = [
|
||
65536, 66971, 68438, 69936, 71468, 73032, 74632, 76266, 77936, 79642, 81386, 83169, 84990,
|
||
86851, 88752, 90696, 92682, 94711, 96785, 98905, 101070, 103283, 105545, 107856, 110218,
|
||
112631, 115098, 117618, 120194, 122825, 125515, 128263,
|
||
];
|
||
|
||
/// Decode a CTA-861.3 max / frame-average luminance code to MILLI-nits:
|
||
/// `L = 50 * 2^(CV/32)` cd/m², so `L_millinits = 50_000 * 2^(CV/32)`.
|
||
/// (`CV = 255` ≈ 12_525 nits — comfortably inside u64 at Q16.)
|
||
pub const fn cta_max_millinits(code: u8) -> u64 {
|
||
let whole = code as u32 / 32;
|
||
let frac = code as u32 % 32;
|
||
((50_000u64 << whole) * POW2_Q16[frac as usize] as u64) >> 16
|
||
}
|
||
|
||
/// Code a display's peak (or frame-average) luminance in nits as a CTA-861.3 luminance value:
|
||
/// the LARGEST code whose decoded luminance does not exceed the panel's — never advertise a
|
||
/// volume brighter than the glass, so a host app's tone map can't clip on the client. Clamped
|
||
/// to `1..=255`: `0` is "no data" on the wire, and callers gate on `nits > 0` themselves (a
|
||
/// sub-51-nit request — no real HDR panel — still codes as 1).
|
||
pub fn cta_max_luminance_code(nits: u32) -> u8 {
|
||
let target = nits as u64 * 1000;
|
||
let mut code = 1u8;
|
||
while code < 255 && cta_max_millinits(code + 1) <= target {
|
||
code += 1;
|
||
}
|
||
code
|
||
}
|
||
|
||
/// Floor integer square root (Newton's method — `u64::isqrt` needs Rust 1.84, above this
|
||
/// crate's 1.82 MSRV). Converges in ≤ 6 iterations from the power-of-two seed.
|
||
fn isqrt_u64(x: u64) -> u64 {
|
||
if x == 0 {
|
||
return 0;
|
||
}
|
||
// Seed strictly above sqrt(x): 2^(ceil(bits/2)).
|
||
let mut r = 1u64 << (64 - x.leading_zeros()).div_ceil(2);
|
||
loop {
|
||
let next = (r + x / r) / 2;
|
||
if next >= r {
|
||
return r;
|
||
}
|
||
r = next;
|
||
}
|
||
}
|
||
|
||
/// Code a display's min luminance (MILLI-nits) as the CTA-861.3 min-luminance value, which is
|
||
/// relative to the block's coded max: `L_min = L_max * (CV/255)^2 / 100`, so
|
||
/// `CV = 255 * sqrt(100 * L_min / L_max)` — rounded to nearest. `max_code` is the byte
|
||
/// produced by [`cta_max_luminance_code`]; a result of `0` (a true-black panel, or
|
||
/// `millinits = 0` = unknown) is valid on the wire.
|
||
pub fn cta_min_luminance_code(millinits: u32, max_code: u8) -> u8 {
|
||
let max_millinits = cta_max_millinits(max_code);
|
||
if millinits == 0 || max_millinits == 0 {
|
||
return 0;
|
||
}
|
||
// CV = sqrt(100 * 255^2 * L_min / L_max); round to nearest by comparing the two flanking
|
||
// squares (the integer sqrt floors).
|
||
let x = (100u64 * 255 * 255).saturating_mul(millinits as u64) / max_millinits;
|
||
let floor = isqrt_u64(x);
|
||
let cv = if (floor + 1) * (floor + 1) - x <= x - floor * floor {
|
||
floor + 1
|
||
} else {
|
||
floor
|
||
};
|
||
cv.min(255) as u8
|
||
}
|
||
}
|
||
|
||
/// The IDD-push frame transport: the host-created shared ring header, the publish token, and the
|
||
/// driver-status codes. The texture ring itself is host-created **unnamed** D3D11 keyed-mutex textures;
|
||
/// the driver reaches them (and the header + event) only through handles the host duplicated into its
|
||
/// process and delivered via [`crate::control::IOCTL_SET_FRAME_CHANNEL`] — the sealed channel. Only the
|
||
/// *layout/contract* lives here.
|
||
pub mod frame {
|
||
use bytemuck::{Pod, Zeroable};
|
||
|
||
/// Header magic (`"PFVD"` LE). The host stamps it LAST (after the ring textures exist) so the driver
|
||
/// only attaches to a fully-published ring.
|
||
pub const MAGIC: u32 = 0x4456_4650;
|
||
/// Frame-plane version (independent bump of the header layout).
|
||
pub const VERSION: u32 = 1;
|
||
/// Ring slots. Headroom so the driver's 0 ms-timeout publish always finds a free slot while the host
|
||
/// holds one across the convert/copy + the pipelined encode. MUST be identical on both sides — it is,
|
||
/// because both read this one constant.
|
||
pub const RING_LEN: u32 = 6;
|
||
|
||
/// `driver_status` values the driver writes into the host header (the host logs them on a timeout).
|
||
pub const DRV_STATUS_NONE: u32 = 0;
|
||
/// Driver attached to the ring and is publishing.
|
||
pub const DRV_STATUS_OPENED: u32 = 1;
|
||
/// Driver could not open the host's textures — render-adapter mismatch (it renders on a different GPU
|
||
/// than where the host created the ring). `driver_status_detail` carries the HRESULT.
|
||
pub const DRV_STATUS_TEX_FAIL: u32 = 2;
|
||
/// Driver has no `ID3D11Device1` to open shared resources.
|
||
pub const DRV_STATUS_NO_DEVICE1: u32 = 3;
|
||
/// Driver refused the attach because the mapped ring names a DIFFERENT monitor
|
||
/// ([`SharedHeader::target_id`] != the monitor the delivery landed on) — a host stash cross-wire
|
||
/// or stale-delivery race that, with parallel displays, would carry one client's frames into
|
||
/// another client's stream. Fail-closed binding validation (v3, invariant #10 of
|
||
/// `design/idd-push-security.md`); `driver_status_detail` carries the target id the ring claims.
|
||
pub const DRV_STATUS_BIND_FAIL: u32 = 4;
|
||
|
||
/// While `driver_status` is [`DRV_STATUS_OPENED`], `driver_status_detail` carries a LIVE
|
||
/// diagnostic word maintained by the driver's publisher (best-effort plain writes — the same
|
||
/// visibility contract as `driver_status` itself). Layout:
|
||
///
|
||
/// - bit 31 (this constant): stamped at attach by a detail-capable driver, so the host can
|
||
/// tell "pre-detail driver, no information" (field = 0) from "zero frames offered".
|
||
/// - bits 30..16: surfaces the swap-chain worker OFFERED to the ring (15-bit, saturating) —
|
||
/// every DWM compose that reached `publish()`, whatever its outcome.
|
||
/// - bits 15..0: publishes DROPPED for a descriptor mismatch (16-bit, saturating) — the
|
||
/// surface's size/format didn't match the ring's.
|
||
///
|
||
/// The host's wait-for-attach reads this on its first-frame timeout to NAME the failure
|
||
/// instead of guessing (the lid-closed field report was undiagnosable without it):
|
||
/// `offered == 0` → DWM never composed the display (powered-off / undamaged desktop, compose
|
||
/// kicks blocked); `offered > 0` with `seq` still 0 → every compose was dropped mismatched
|
||
/// (the host sized the ring from a stale or foreign-session GDI mode).
|
||
pub const OPENED_DETAIL_LIVE: u32 = 0x8000_0000;
|
||
|
||
/// Pack the live OPENED diagnostic word (see [`OPENED_DETAIL_LIVE`]); both counters saturate.
|
||
#[must_use]
|
||
pub const fn pack_opened_detail(offered: u32, mismatched: u32) -> u32 {
|
||
let o = if offered > 0x7FFF { 0x7FFF } else { offered };
|
||
let m = if mismatched > 0xFFFF {
|
||
0xFFFF
|
||
} else {
|
||
mismatched
|
||
};
|
||
OPENED_DETAIL_LIVE | (o << 16) | m
|
||
}
|
||
|
||
/// Unpack a live OPENED diagnostic word → `(offered, mismatched)`; `None` when the driver
|
||
/// never stamped [`OPENED_DETAIL_LIVE`] (a pre-detail driver — the field carries nothing).
|
||
#[must_use]
|
||
pub const fn unpack_opened_detail(detail: u32) -> Option<(u32, u32)> {
|
||
if detail & OPENED_DETAIL_LIVE == 0 {
|
||
return None;
|
||
}
|
||
Some(((detail >> 16) & 0x7FFF, detail & 0xFFFF))
|
||
}
|
||
|
||
/// The shared metadata header (host-created, mapped by both sides). Atomic fields (`magic`, `latest`,
|
||
/// `generation`) are accessed via each side's own atomic view over the mapping; this is the layout.
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable, Debug)]
|
||
pub struct SharedHeader {
|
||
pub magic: u32,
|
||
pub version: u32,
|
||
/// Bumped by the host on a ring recreate (HDR-mode flip → new texture format + a fresh
|
||
/// [`control::IOCTL_SET_FRAME_CHANNEL`](crate::control::IOCTL_SET_FRAME_CHANNEL) delivery). The
|
||
/// driver re-attaches when it changes; a publish carries it so the host rejects a stale-ring
|
||
/// publish.
|
||
pub generation: u32,
|
||
pub ring_len: u32,
|
||
pub width: u32,
|
||
pub height: u32,
|
||
pub dxgi_format: u32,
|
||
/// The OS target id of the monitor this ring belongs to (v3 — the former `_pad`, same
|
||
/// offset). Host-stamped at ring creation, BEFORE the magic (the magic-last publish ordering
|
||
/// guarantees the driver never reads it half-initialized) and never changed afterwards (a
|
||
/// mid-session recreate reuses the mapping, so the binding is stable for the ring's life).
|
||
/// The driver's publisher attaches only when it equals the monitor's own target id
|
||
/// ([`check_attach`]) — a mis-delivered ring fails closed ([`DRV_STATUS_BIND_FAIL`]) instead
|
||
/// of carrying another display's frames (invariant #10, `design/idd-push-security.md`).
|
||
pub target_id: u32,
|
||
/// Driver-written after each copy; host loads `Acquire`. See [`FrameToken`].
|
||
pub latest: u64,
|
||
pub qpc_pts: u64,
|
||
/// Driver-written: the adapter the swap-chain actually renders on (mismatch detection).
|
||
pub driver_render_luid_low: u32,
|
||
pub driver_render_luid_high: i32,
|
||
/// Driver-written status (visibility channel — UMDF hides OutputDebugString + the restricted
|
||
/// token blocks file writes, so this header is how the driver reports state).
|
||
pub driver_status: u32,
|
||
pub driver_status_detail: u32,
|
||
}
|
||
|
||
/// Why the driver's publisher must NOT attach a delivered channel to its monitor's ring — the
|
||
/// two reject outcomes of [`check_attach`], each with different driver behavior.
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub enum AttachReject {
|
||
/// The header isn't (or is no longer) the ring this delivery described: magic missing, or
|
||
/// the host recreated the ring again before the attach (a fresh delivery is on its way).
|
||
/// Benign — drop the delivery silently; no status is written.
|
||
Stale,
|
||
/// The ring names a DIFFERENT monitor (`SharedHeader::target_id` mismatch) — a host
|
||
/// stash/delivery cross-wire that, with parallel displays, would publish this monitor's
|
||
/// frames into another client's stream. Fail closed: refuse the attach and write
|
||
/// [`DRV_STATUS_BIND_FAIL`] so the host's wait-for-attach fails the open loudly.
|
||
BindMismatch,
|
||
}
|
||
|
||
/// The publisher's attach precondition (v3): given the mapped header's `magic`, `generation`
|
||
/// and `target_id` plus the delivery's generation and the monitor's own target id, decide
|
||
/// whether the attach may proceed. Staleness is checked FIRST — a superseded delivery's binding
|
||
/// is meaningless (the fresh delivery re-validates it), so it never false-alarms as a bind
|
||
/// failure. Pure and shared-crate-owned so the reject paths are unit-tested on every dev
|
||
/// machine (the driver workspace builds `panic = "abort"` and cannot host a test harness).
|
||
pub fn check_attach(
|
||
magic: u32,
|
||
header_generation: u32,
|
||
header_target_id: u32,
|
||
delivery_generation: u32,
|
||
monitor_target_id: u32,
|
||
) -> Result<(), AttachReject> {
|
||
if magic != MAGIC || header_generation != delivery_generation {
|
||
return Err(AttachReject::Stale);
|
||
}
|
||
if header_target_id != monitor_target_id {
|
||
return Err(AttachReject::BindMismatch);
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// The `SharedHeader.latest` publish token: `(generation << 40) | (seq << 8) | slot`.
|
||
/// `generation` is 24-bit, `seq` 32-bit, `slot` 8-bit. The generation tag lets the host REJECT a
|
||
/// publish from a stale ring (an old-generation publisher racing a mid-session recreate) so it never
|
||
/// consumes an unwritten new-ring slot — eliminating the toggle-time garbage frame.
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct FrameToken {
|
||
pub generation: u32,
|
||
pub seq: u32,
|
||
pub slot: u8,
|
||
}
|
||
|
||
impl FrameToken {
|
||
/// Low 24 bits of `generation` are significant (see the field docs).
|
||
pub const GENERATION_MASK: u32 = 0x00FF_FFFF;
|
||
|
||
pub const fn pack(self) -> u64 {
|
||
(((self.generation & Self::GENERATION_MASK) as u64) << 40)
|
||
| (((self.seq as u64) & 0xFFFF_FFFF) << 8)
|
||
| (self.slot as u64)
|
||
}
|
||
|
||
pub const fn unpack(v: u64) -> Self {
|
||
Self {
|
||
generation: ((v >> 40) as u32) & Self::GENERATION_MASK,
|
||
seq: ((v >> 8) & 0xFFFF_FFFF) as u32,
|
||
slot: (v & 0xFF) as u8,
|
||
}
|
||
}
|
||
}
|
||
|
||
// Size + per-field offsets are load-bearing: both sides access these via raw atomic views over the
|
||
// mapping, so a same-size field reorder would silently corrupt. Pin every offset. `target_id`
|
||
// (v3, the former `_pad`) after `dxgi_format` is what 8-aligns the `u64 latest` at offset 32 —
|
||
// assert that too.
|
||
const _: () = {
|
||
use core::mem::{offset_of, size_of};
|
||
|
||
assert!(size_of::<SharedHeader>() == 64);
|
||
assert!(offset_of!(SharedHeader, magic) == 0);
|
||
assert!(offset_of!(SharedHeader, version) == 4);
|
||
assert!(offset_of!(SharedHeader, generation) == 8);
|
||
assert!(offset_of!(SharedHeader, ring_len) == 12);
|
||
assert!(offset_of!(SharedHeader, width) == 16);
|
||
assert!(offset_of!(SharedHeader, height) == 20);
|
||
assert!(offset_of!(SharedHeader, dxgi_format) == 24);
|
||
assert!(offset_of!(SharedHeader, target_id) == 28);
|
||
assert!(offset_of!(SharedHeader, latest) == 32);
|
||
assert!(offset_of!(SharedHeader, qpc_pts) == 40);
|
||
assert!(offset_of!(SharedHeader, driver_render_luid_low) == 48);
|
||
assert!(offset_of!(SharedHeader, driver_render_luid_high) == 52);
|
||
assert!(offset_of!(SharedHeader, driver_status) == 56);
|
||
assert!(offset_of!(SharedHeader, driver_status_detail) == 60);
|
||
};
|
||
}
|
||
|
||
/// Gamepad shared-memory layouts (host ↔ the UMDF gamepad drivers `pf_xusb` / `pf_dualsense`).
|
||
///
|
||
/// These were hand-duplicated as `OFF_*`/`SHM_*` constants in `inject/{gamepad,dualsense}_windows.rs`
|
||
/// and (as bare literals — `*view.add(140)`) in the standalone `xusb-driver`/`dualsense-driver`
|
||
/// workspaces, guarded only by "must match" comments — the top ABI-drift hazard the audit flagged
|
||
/// (`design/windows-host-rewrite.md` §2.7). Owning them here with `Pod` derives + `offset_of!`
|
||
/// asserts makes a one-sided edit a compile error.
|
||
///
|
||
/// Since v2 the channel is **sealed** (`design/gamepad-channel-sealing.md`, mirroring the frame
|
||
/// channel): the host creates the DATA section ([`XusbShm`]/[`PadShm`]) UNNAMED (SYSTEM-only DACL)
|
||
/// and duplicates its handle into the driver's WUDFHost; only the tiny [`PadBootstrap`] mailbox
|
||
/// stays named (it carries nothing exploitable). Layout only; the sections are host-created.
|
||
pub mod gamepad {
|
||
use alloc::string::String;
|
||
use bytemuck::{Pod, Zeroable};
|
||
|
||
/// XUSB section magic — the exact u32 the shipped host + `pf_xusb` driver compare (loosely "PFXU").
|
||
pub const XUSB_MAGIC: u32 = 0x5558_4650;
|
||
/// Pad section magic — the exact u32 the shipped host + `pf_dualsense` driver compare (loosely
|
||
/// "PFDS"). (Note: the two magics happen to use opposite byte-order mnemonics in the legacy code;
|
||
/// only the u32 value is the contract.)
|
||
pub const PAD_MAGIC: u32 = 0x5046_4453;
|
||
|
||
/// `device_type` selector the `pf_dualsense` driver reads to pick its HID identity. The section is
|
||
/// zeroed, so `0` = DualSense is the default; one driver serves every identity.
|
||
pub const DEVTYPE_DUALSENSE: u8 = 0;
|
||
/// `device_type` = DualShock 4 (`VID_054C&PID_09CC` HID identity).
|
||
pub const DEVTYPE_DUALSHOCK4: u8 = 1;
|
||
/// `device_type` = DualSense Edge (`VID_054C&PID_0DF2` HID identity — the DualSense report
|
||
/// codec plus the four native back/Fn button bits).
|
||
pub const DEVTYPE_DUALSENSE_EDGE: u8 = 2;
|
||
/// `device_type` = Steam Deck controller (`VID_28DE&PID_1205` HID identity, the captured
|
||
/// controller-interface descriptor + the Steam `0x83`/`0xAE` feature contract). Promoted by
|
||
/// Steam Input on Windows when the devnode's synthesized USB hardware ids carry `&MI_02`
|
||
/// (the wired controller interface — the N4-spike finding).
|
||
pub const DEVTYPE_STEAMDECK: u8 = 3;
|
||
|
||
/// The value a gamepad driver writes into its section's `driver_proto` field once it attaches —
|
||
/// the host's positive "driver is alive on this section" signal (health check + version audit).
|
||
/// The section starts zeroed, so `0` always means "no driver has attached (yet)"; a pre-health
|
||
/// driver never writes the field and reads as not-attached, which the host log line calls out
|
||
/// (the remedy is the same: reinstall the drivers). Bump on a gamepad-layout change.
|
||
///
|
||
/// v2: the **sealed pad channel** (`design/gamepad-channel-sealing.md`) — the DATA section
|
||
/// ([`XusbShm`]/[`PadShm`]) is UNNAMED and reaches the driver only as a handle the host duplicated
|
||
/// into its WUDFHost, bootstrapped through the named [`PadBootstrap`] mailbox; the DATA section
|
||
/// gained `pad_index` (carved from reserved space) so the driver rejects a cross-pad delivery.
|
||
/// A v1 driver opens `Global\pf…-shm-<i>` (which no longer exists) and a v1 host never creates
|
||
/// the mailbox a v2 driver polls, so a mixed pairing fails closed either way.
|
||
pub const GAMEPAD_PROTO_VERSION: u32 = 2;
|
||
|
||
/// Bootstrap-mailbox magic (`"PFBT"` LE) — the host stamps it LAST (after `host_proto`), so a
|
||
/// driver only trusts a fully-initialized mailbox.
|
||
pub const BOOT_MAGIC: u32 = 0x5442_4650;
|
||
|
||
/// `Global\pfxusb-boot-<index>` — the virtual Xbox 360 pad's bootstrap mailbox ([`PadBootstrap`]).
|
||
pub fn xusb_boot_name(index: u8) -> String {
|
||
alloc::format!("Global\\pfxusb-boot-{index}")
|
||
}
|
||
/// `Global\pfds-boot-<index>` — the DualSense / DualShock 4 pad's bootstrap mailbox
|
||
/// ([`PadBootstrap`]).
|
||
pub fn pad_boot_name(index: u8) -> String {
|
||
alloc::format!("Global\\pfds-boot-{index}")
|
||
}
|
||
|
||
/// The per-pad bootstrap mailbox (32 B, named `Global\pf…-boot-<index>`, SY+LS DACL) — the ONLY
|
||
/// named object left on the gamepad channel. It exists because the pad drivers are UMDF HID
|
||
/// minidrivers with no control device (hidclass owns the stack), so there is no IOCTL to hand the
|
||
/// driver a duplicated handle or learn its WUDFHost pid; this mailbox is the late-bound handshake:
|
||
///
|
||
/// 1. host creates it (zeroed), stamps `host_proto` then `magic` (in that order);
|
||
/// 2. driver opens it by name (pad index from `pszDeviceLocation`), writes `driver_proto`, and —
|
||
/// iff `host_proto` matches its own version — publishes `driver_pid`;
|
||
/// 3. host polls `driver_pid`, verifies the pid is a genuine WUDFHost, duplicates the unnamed DATA
|
||
/// section into it, then writes `data_handle` + `handle_pid` and bumps `handle_seq` LAST;
|
||
/// 4. driver sees a fresh `handle_seq` addressed to its own pid, maps `data_handle`, and validates
|
||
/// the mapped section's magic + `pad_index` before use.
|
||
///
|
||
/// Deliberately safe to leave named + LS-openable: it carries only pids (not sensitive) and a
|
||
/// handle VALUE (meaningless outside the target WUDFHost's handle table). A sibling LocalService
|
||
/// that tampers with it can at worst mis-route a delivery — a gamepad DoS, never a read or an
|
||
/// injection (it cannot place a valid section handle in the WUDFHost, and the driver's
|
||
/// magic+`pad_index` validation rejects any handle that doesn't resolve to this pad's section).
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
||
pub struct PadBootstrap {
|
||
/// [`BOOT_MAGIC`], host-stamped last at creation.
|
||
pub magic: u32,
|
||
/// The host's [`GAMEPAD_PROTO_VERSION`]. A driver whose own version differs must NOT publish
|
||
/// its pid (fail closed) — it still writes `driver_proto` so the host can log the mismatch.
|
||
pub host_proto: u32,
|
||
/// The driver's WUDFHost process id (driver-written; `0` = no driver yet). The duplication
|
||
/// target the host verifies (`verify_is_wudfhost`) before duplicating the DATA section into it.
|
||
pub driver_pid: u32,
|
||
/// The driver's [`GAMEPAD_PROTO_VERSION`] (driver-written; diagnostics only).
|
||
pub driver_proto: u32,
|
||
/// The DATA-section handle VALUE the host duplicated into `handle_pid`'s handle table
|
||
/// (host-written; valid only inside that process).
|
||
pub data_handle: u64,
|
||
/// The pid `data_handle` was duplicated for — a driver whose pid differs ignores the delivery.
|
||
pub handle_pid: u32,
|
||
/// Bumped by the host (host-global monotonic, never 0) AFTER `data_handle`/`handle_pid` are in
|
||
/// place — the driver's new-delivery trigger.
|
||
pub handle_seq: u32,
|
||
}
|
||
|
||
/// Virtual Xbox 360 (XInput) shared section (64 B). The host writes the XInput state (a bumped
|
||
/// `packet` number + buttons/triggers/sticks in XInput conventions); the driver answers
|
||
/// `XInputGetState`. The driver writes force-feedback (`XInputSetState`) into `rumble_*`, bumping
|
||
/// `rumble_seq`, which the host relays to the client.
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable, Debug)]
|
||
pub struct XusbShm {
|
||
pub magic: u32,
|
||
/// XInput `dwPacketNumber` — bumped by the host on every state change.
|
||
pub packet: u32,
|
||
pub buttons: u16,
|
||
pub left_trigger: u8,
|
||
pub right_trigger: u8,
|
||
pub thumb_lx: i16,
|
||
pub thumb_ly: i16,
|
||
pub thumb_rx: i16,
|
||
pub thumb_ry: i16,
|
||
pub _reserved0: u32,
|
||
/// Bumped by the driver on a new force-feedback packet.
|
||
pub rumble_seq: u32,
|
||
pub rumble_large: u8,
|
||
pub rumble_small: u8,
|
||
pub _pad0: [u8; 2],
|
||
/// Written by the driver when it binds (device add) and on every serviced IOCTL:
|
||
/// [`GAMEPAD_PROTO_VERSION`]. `0` = no driver attached — the host health check keys off it.
|
||
pub driver_proto: u32,
|
||
/// Bumped by the driver on every serviced XInput IOCTL — proves the game-visible path (it
|
||
/// only advances while something polls the slot, so a static value is not an error).
|
||
pub driver_heartbeat: u32,
|
||
/// The pad index this section serves (host-stamped before the magic). The driver validates it
|
||
/// against its own `pszDeviceLocation` index when it maps the delivered handle, so a mis-routed
|
||
/// (or bootstrap-tampered) cross-pad delivery is rejected instead of silently cross-wiring two
|
||
/// pads. Carved from v1 reserved space (v2).
|
||
pub pad_index: u32,
|
||
pub _reserved1: [u8; 20],
|
||
}
|
||
|
||
/// The legacy (pre-ring) [`PadShm`] size. Old binaries on either side were built against a
|
||
/// 256-byte layout; every field they know sits below this offset, and the ring extension keeps
|
||
/// bytes `0..256` byte-identical. Pagefile-backed sections are page-granular, so a view of
|
||
/// either generation's size maps against either generation's section — but a driver must
|
||
/// still be able to fall back to mapping this size if the full-size map is ever refused
|
||
/// (see `pf_umdf_util::ChannelConfig::min_data_size`).
|
||
pub const PAD_SHM_LEGACY_SIZE: usize = 256;
|
||
|
||
/// Output-report ring depth. 8 slots at the host's ~4 ms poll tolerates a sustained 2 kHz
|
||
/// writer — double any real HID output rate.
|
||
pub const OUT_RING_LEN: u32 = 8;
|
||
pub const OUT_RING_LEN_USIZE: usize = OUT_RING_LEN as usize;
|
||
|
||
/// One slot of the lossless output-report ring: the report bytes as the game wrote them
|
||
/// (report id first), with the exact length — unlike the legacy latest-report slot, whose
|
||
/// fixed 64-byte copy can carry a stale tail from a previous longer report.
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable, Debug)]
|
||
pub struct OutSlot {
|
||
/// Valid bytes in `data` (0..=64). `0` = never written.
|
||
pub len: u32,
|
||
pub data: [u8; 64],
|
||
}
|
||
|
||
/// Virtual DualSense / DualShock 4 shared section (1024 B; bytes `0..256` are the v2 legacy
|
||
/// layout verbatim — [`PAD_SHM_LEGACY_SIZE`]). The host writes the `0x01`-style HID input
|
||
/// report into `input`; the driver feeds it to game `READ_REPORT`s and publishes a game's
|
||
/// `0x02` output (rumble / lightbar / player-LEDs / adaptive triggers) twice: into the legacy
|
||
/// latest-report `output` slot (bumping `out_seq` — every host generation reads this), and,
|
||
/// when the host stamped `out_ring_ver`, into the lossless `out_ring` (bumping `ring_head`).
|
||
/// The ring exists because the single slot COALESCES: a rumble-stop report overwritten by an
|
||
/// LED/trigger report inside one host poll window was gone forever — the confirmed stuck-rumble
|
||
/// path (`design/rumble-root-fix.md` §A). `device_type` selects the HID identity
|
||
/// ([`DEVTYPE_DUALSENSE`] / [`DEVTYPE_DUALSHOCK4`]).
|
||
///
|
||
/// Version posture: this is a TAIL extension negotiated by zeroed-reserved capability fields,
|
||
/// deliberately NOT a [`GAMEPAD_PROTO_VERSION`] bump — the bootstrap fails CLOSED on a version
|
||
/// mismatch (no pad at all), which is the wrong failure mode for a feedback-quality fix. An
|
||
/// old driver never reads the new fields; an old host never stamps `out_ring_ver`, so a new
|
||
/// driver stays on the legacy slot against it.
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable, Debug)]
|
||
pub struct PadShm {
|
||
pub magic: u32,
|
||
pub _reserved0: u32,
|
||
/// Input report region (host-written; the codec's report is <= 64 B — see
|
||
/// `inject::dualsense_proto::DS_INPUT_REPORT_LEN`). The region spans `magic`+pad .. `out_seq`.
|
||
pub input: [u8; 64],
|
||
/// Bumped by the driver when it publishes a new `output` report.
|
||
pub out_seq: u32,
|
||
/// Output report region (driver-written): rumble / lightbar / player-LEDs / adaptive triggers.
|
||
pub output: [u8; 64],
|
||
/// HID identity selector — see [`DEVTYPE_DUALSENSE`] / [`DEVTYPE_DUALSHOCK4`].
|
||
pub device_type: u8,
|
||
pub _pad0: [u8; 3],
|
||
/// Written by the driver's timer while it has the section mapped: [`GAMEPAD_PROTO_VERSION`].
|
||
/// `0` = no driver attached — the host health check keys off it.
|
||
pub driver_proto: u32,
|
||
/// Bumped by the driver's ~125 Hz timer each tick — a true liveness heartbeat (unlike the
|
||
/// XUSB one, this advances whenever the driver is loaded, game or not).
|
||
pub driver_heartbeat: u32,
|
||
/// The pad index this section serves (host-stamped before the magic) — see
|
||
/// [`XusbShm::pad_index`]. Carved from v1 reserved space (v2).
|
||
pub pad_index: u32,
|
||
/// Host-stamped `1` at section creation ⇔ "this section carries the `out_ring` region and
|
||
/// the host drains it". The section starts zeroed and an old host never writes it, so `0`
|
||
/// tells a new driver to stay legacy-only. Carved from v2 reserved space (v2.1).
|
||
pub out_ring_ver: u32,
|
||
/// Driver-bumped (AFTER writing `out_ring[ring_head % OUT_RING_LEN]`) count of reports
|
||
/// ever published to the ring — the host's drain cursor compares against its own tail and
|
||
/// detects overflow by `head - tail > OUT_RING_LEN`. Same publish-then-bump store order as
|
||
/// `out_seq` (the host's Acquire load orders the reads). Carved from v2 reserved space
|
||
/// (v2.1).
|
||
pub ring_head: u32,
|
||
pub _reserved1: [u8; 92],
|
||
/// The lossless output-report ring (v2.1) — see the struct docs and [`OutSlot`].
|
||
pub out_ring: [OutSlot; OUT_RING_LEN_USIZE],
|
||
pub _reserved2: [u8; 224],
|
||
}
|
||
|
||
// Offsets are the wire contract the shipped drivers already read by hand — pin every one. A failing
|
||
// assert here means the struct no longer matches the historical `OFF_*` layout (host) / `view.add(N)`
|
||
// literal (driver) and must be fixed before either side switches to the type.
|
||
const _: () = {
|
||
use core::mem::{offset_of, size_of};
|
||
|
||
assert!(size_of::<XusbShm>() == 64);
|
||
assert!(offset_of!(XusbShm, magic) == 0);
|
||
assert!(offset_of!(XusbShm, packet) == 4);
|
||
assert!(offset_of!(XusbShm, buttons) == 8);
|
||
assert!(offset_of!(XusbShm, left_trigger) == 10);
|
||
assert!(offset_of!(XusbShm, right_trigger) == 11);
|
||
assert!(offset_of!(XusbShm, thumb_lx) == 12);
|
||
assert!(offset_of!(XusbShm, thumb_ly) == 14);
|
||
assert!(offset_of!(XusbShm, thumb_rx) == 16);
|
||
assert!(offset_of!(XusbShm, thumb_ry) == 18);
|
||
assert!(offset_of!(XusbShm, rumble_seq) == 24);
|
||
assert!(offset_of!(XusbShm, rumble_large) == 28);
|
||
assert!(offset_of!(XusbShm, rumble_small) == 29);
|
||
assert!(offset_of!(XusbShm, driver_proto) == 32);
|
||
assert!(offset_of!(XusbShm, driver_heartbeat) == 36);
|
||
assert!(offset_of!(XusbShm, pad_index) == 40);
|
||
|
||
assert!(size_of::<PadShm>() == 1024);
|
||
assert!(offset_of!(PadShm, magic) == 0);
|
||
assert!(offset_of!(PadShm, input) == 8);
|
||
assert!(offset_of!(PadShm, out_seq) == 72);
|
||
assert!(offset_of!(PadShm, output) == 76);
|
||
assert!(offset_of!(PadShm, device_type) == 140);
|
||
assert!(offset_of!(PadShm, driver_proto) == 144);
|
||
assert!(offset_of!(PadShm, driver_heartbeat) == 148);
|
||
assert!(offset_of!(PadShm, pad_index) == 152);
|
||
// v2.1 ring extension — everything below PAD_SHM_LEGACY_SIZE is the v2 layout verbatim.
|
||
assert!(offset_of!(PadShm, out_ring_ver) == 156);
|
||
assert!(offset_of!(PadShm, ring_head) == 160);
|
||
assert!(offset_of!(PadShm, out_ring) == PAD_SHM_LEGACY_SIZE);
|
||
assert!(size_of::<OutSlot>() == 68);
|
||
|
||
assert!(size_of::<PadBootstrap>() == 32);
|
||
assert!(offset_of!(PadBootstrap, magic) == 0);
|
||
assert!(offset_of!(PadBootstrap, host_proto) == 4);
|
||
assert!(offset_of!(PadBootstrap, driver_pid) == 8);
|
||
assert!(offset_of!(PadBootstrap, driver_proto) == 12);
|
||
assert!(offset_of!(PadBootstrap, data_handle) == 16);
|
||
assert!(offset_of!(PadBootstrap, handle_pid) == 24);
|
||
assert!(offset_of!(PadBootstrap, handle_seq) == 28);
|
||
};
|
||
}
|
||
|
||
/// Virtual-pointer shared-memory layout (host ↔ the UMDF HID-mouse minidriver `pf_mouse`).
|
||
///
|
||
/// Why a virtual mouse exists at all: with no pointing device present (a headless Windows host —
|
||
/// no dongle attached), win32k reports the cursor as absent (`SM_MOUSEPRESENT` = 0) and DWM never
|
||
/// composites a cursor into the pf-vdisplay frame, so a streamed desktop has an invisible pointer
|
||
/// even though `SendInput` moves it. A resident HID mouse devnode makes Windows always consider a
|
||
/// pointer present — the Sunshine/Parsec-class fix. Injection stays `SendInput`; the report path
|
||
/// below exists for validation (`vmouse-spike`) and as the future higher-fidelity route.
|
||
///
|
||
/// The channel is the **sealed pad channel** verbatim (`design/gamepad-channel-sealing.md`): the
|
||
/// same [`gamepad::PadBootstrap`] mailbox handshake (and therefore the same
|
||
/// [`gamepad::GAMEPAD_PROTO_VERSION`] lockstep), a mouse-specific mailbox name
|
||
/// ([`mouse_boot_name`]) and DATA magic, and `pad_index` validation (a single resident mouse =
|
||
/// index 0). Reusing the handshake means `pf-umdf-util`'s audited `ChannelClient`/`PadChannel`
|
||
/// serve the mouse unchanged.
|
||
pub mod mouse {
|
||
use alloc::string::String;
|
||
use bytemuck::{Pod, Zeroable};
|
||
|
||
/// Mouse DATA-section magic ("PFMO" LE) — distinct from the pad magics so a cross-wired
|
||
/// delivery fails validation.
|
||
pub const MOUSE_MAGIC: u32 = 0x4F4D_4650;
|
||
|
||
/// `Global\pfmouse-boot-<index>` — the virtual mouse's bootstrap mailbox
|
||
/// ([`crate::gamepad::PadBootstrap`]).
|
||
pub fn mouse_boot_name(index: u8) -> String {
|
||
alloc::format!("Global\\pfmouse-boot-{index}")
|
||
}
|
||
|
||
/// HID identity both sides report/expect ("PF" / "MO" — an obviously-virtual identity; no
|
||
/// software matches on it, unlike the pads' cloned Sony/Valve ids).
|
||
pub const MOUSE_VID: u16 = 0x5046;
|
||
pub const MOUSE_PID: u16 = 0x4D4F;
|
||
pub const MOUSE_VER: u16 = 0x0100;
|
||
|
||
/// The one input report (id `0x01`): `[id, buttons(5 bits), x_lo, x_hi, y_lo, y_hi, wheel,
|
||
/// pan]` — absolute X/Y over `0..=`[`MOUSE_ABS_MAX`], relative wheel/pan.
|
||
pub const MOUSE_REPORT_ID: u8 = 0x01;
|
||
pub const MOUSE_REPORT_LEN: usize = 8;
|
||
/// Logical maximum of the absolute X/Y axes (15-bit, the HID-descriptor convention).
|
||
pub const MOUSE_ABS_MAX: u16 = 0x7FFF;
|
||
|
||
/// Build the 8-byte input report. Pure so the byte layout is unit-tested on every dev machine
|
||
/// (the driver workspace is `panic = "abort"` and hosts no test harness); the driver only
|
||
/// ferries these bytes, it never builds them.
|
||
#[must_use]
|
||
pub fn input_report(buttons: u8, x: u16, y: u16, wheel: i8, pan: i8) -> [u8; MOUSE_REPORT_LEN] {
|
||
let x = x.min(MOUSE_ABS_MAX);
|
||
let y = y.min(MOUSE_ABS_MAX);
|
||
[
|
||
MOUSE_REPORT_ID,
|
||
buttons & 0x1F,
|
||
(x & 0xFF) as u8,
|
||
(x >> 8) as u8,
|
||
(y & 0xFF) as u8,
|
||
(y >> 8) as u8,
|
||
wheel as u8,
|
||
pan as u8,
|
||
]
|
||
}
|
||
|
||
/// Virtual-mouse shared section (64 B). The host writes an input report then bumps `in_seq`
|
||
/// (Release); the driver's timer Acquire-loads `in_seq` and completes a pended `READ_REPORT`
|
||
/// with the fresh report — event-driven like a real mouse, so an idle section generates NO
|
||
/// HID traffic (a constant report stream would read as user activity to the OS).
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable, Debug)]
|
||
pub struct MouseShm {
|
||
pub magic: u32,
|
||
/// Bumped by the host AFTER `report` is in place (Release) — the driver's new-input
|
||
/// trigger. `0` = nothing published yet.
|
||
pub in_seq: u32,
|
||
/// The latest HID input report (id [`MOUSE_REPORT_ID`], [`MOUSE_REPORT_LEN`] bytes).
|
||
pub report: [u8; MOUSE_REPORT_LEN],
|
||
/// Written by the driver's timer while attached: [`crate::gamepad::GAMEPAD_PROTO_VERSION`]
|
||
/// (the mouse channel rides the gamepad handshake). `0` = no driver attached — the host
|
||
/// health check keys off it.
|
||
pub driver_proto: u32,
|
||
/// Bumped by the driver's timer each tick — liveness (advances whether or not input flows).
|
||
pub driver_heartbeat: u32,
|
||
/// The device index this section serves (host-stamped before the magic; the driver
|
||
/// validates it against its devnode Location — same fail-closed check as the pads).
|
||
pub pad_index: u32,
|
||
pub _reserved: [u8; 36],
|
||
}
|
||
|
||
// Offsets are the cross-process wire contract — pin every one (same discipline as `gamepad`).
|
||
const _: () = {
|
||
use core::mem::{offset_of, size_of};
|
||
|
||
assert!(size_of::<MouseShm>() == 64);
|
||
assert!(offset_of!(MouseShm, magic) == 0);
|
||
assert!(offset_of!(MouseShm, in_seq) == 4);
|
||
assert!(offset_of!(MouseShm, report) == 8);
|
||
assert!(offset_of!(MouseShm, driver_proto) == 16);
|
||
assert!(offset_of!(MouseShm, driver_heartbeat) == 20);
|
||
assert!(offset_of!(MouseShm, pad_index) == 24);
|
||
};
|
||
}
|
||
|
||
/// The v5 hardware-cursor channel (remote-desktop-sweep M2c): one unnamed file mapping per
|
||
/// monitor, host-created, delivered by handle value ([`control::IOCTL_SET_CURSOR_CHANNEL`]).
|
||
/// The DRIVER's cursor thread (woken by its IddCx `hNewCursorDataAvailable` event) seqlock-writes
|
||
/// shape + position + visibility; the HOST reads at its encode-tick pace — no event crosses the
|
||
/// boundary. Writer: bump [`CursorShm::seq`] to ODD, write fields (+ shape bytes when the OS said
|
||
/// the shape changed), bump to EVEN. Reader: read seq (retry while odd), copy, re-read seq —
|
||
/// unchanged ⇒ consistent snapshot. Position-only updates never touch the shape bytes, so a
|
||
/// reader that skips unchanged `shape_id`s never copies torn pixels.
|
||
pub mod cursor {
|
||
use bytemuck::{Pod, Zeroable};
|
||
|
||
/// First field of [`CursorShm`] — `b"PFCU"` little-endian; anything else = not attached yet.
|
||
pub const CURSOR_MAGIC: u32 = u32::from_le_bytes(*b"PFCU");
|
||
|
||
/// Max cursor side (px) the driver declares to the OS (`IDDCX_CURSOR_CAPS::MaxX/MaxY`) and
|
||
/// the section's shape buffer is sized for. Windows XL accessibility cursors top out here;
|
||
/// the host's wire forwarder downscales to its own cap anyway.
|
||
pub const CURSOR_SHAPE_MAX: u32 = 256;
|
||
|
||
/// Shape-buffer bytes: 32-bpp at the declared max.
|
||
pub const CURSOR_SHAPE_BYTES: usize = (CURSOR_SHAPE_MAX * CURSOR_SHAPE_MAX * 4) as usize;
|
||
|
||
/// Byte offset of the shape pixels inside the section (one cache-line-ish header).
|
||
pub const CURSOR_SHAPE_OFFSET: usize = 64;
|
||
|
||
/// Total section size.
|
||
pub const CURSOR_SHM_SIZE: usize = CURSOR_SHAPE_OFFSET + CURSOR_SHAPE_BYTES;
|
||
|
||
/// `IDDCX_CURSOR_SHAPE_TYPE` values mirrored for the host (the driver writes the OS value
|
||
/// verbatim into [`CursorShm::cursor_type`]).
|
||
pub const CURSOR_TYPE_MASKED_COLOR: u32 = 1;
|
||
pub const CURSOR_TYPE_ALPHA: u32 = 2;
|
||
|
||
/// The section header (the shape pixels follow at [`CURSOR_SHAPE_OFFSET`]). `x`/`y` are the
|
||
/// shape's TOP-LEFT in desktop coordinates (the IddCx `IDARG_OUT_QUERY_HWCURSOR::X/Y`
|
||
/// convention — position − hotspot, can be negative); `shape_id` is the OS's per-set counter
|
||
/// (bumps on every shape set, the overlay serial); pixels are the OS's 32-bpp rows at
|
||
/// `pitch` bytes (BGRA for ALPHA; color+mask for MASKED_COLOR — the host converts).
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable, Debug, PartialEq, Eq)]
|
||
pub struct CursorShm {
|
||
pub magic: u32,
|
||
/// Seqlock: odd = writer mid-update.
|
||
pub seq: u32,
|
||
pub visible: u32,
|
||
pub cursor_type: u32,
|
||
pub x: i32,
|
||
pub y: i32,
|
||
pub shape_id: u32,
|
||
pub width: u32,
|
||
pub height: u32,
|
||
pub pitch: u32,
|
||
pub hot_x: u32,
|
||
pub hot_y: u32,
|
||
/// Reserved expansion room up to [`CURSOR_SHAPE_OFFSET`].
|
||
pub _reserved: [u32; 4],
|
||
}
|
||
|
||
// Layout is load-bearing across the process boundary — pin it.
|
||
const _: () = {
|
||
use core::mem::{offset_of, size_of};
|
||
assert!(size_of::<CursorShm>() == 64);
|
||
assert!(size_of::<CursorShm>() <= CURSOR_SHAPE_OFFSET);
|
||
assert!(offset_of!(CursorShm, magic) == 0);
|
||
assert!(offset_of!(CursorShm, seq) == 4);
|
||
assert!(offset_of!(CursorShm, visible) == 8);
|
||
assert!(offset_of!(CursorShm, cursor_type) == 12);
|
||
assert!(offset_of!(CursorShm, x) == 16);
|
||
assert!(offset_of!(CursorShm, y) == 20);
|
||
assert!(offset_of!(CursorShm, shape_id) == 24);
|
||
assert!(offset_of!(CursorShm, width) == 28);
|
||
assert!(offset_of!(CursorShm, height) == 32);
|
||
assert!(offset_of!(CursorShm, pitch) == 36);
|
||
assert!(offset_of!(CursorShm, hot_x) == 40);
|
||
assert!(offset_of!(CursorShm, hot_y) == 44);
|
||
};
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use bytemuck::Zeroable;
|
||
|
||
#[test]
|
||
fn frame_token_roundtrips() {
|
||
for (g, s, slot) in [
|
||
(1u32, 0u32, 0u8),
|
||
(5, 12_345, 3),
|
||
(frame::FrameToken::GENERATION_MASK, 0xFFFF_FFFF, 5),
|
||
(0, 1, 255),
|
||
] {
|
||
let t = frame::FrameToken {
|
||
generation: g,
|
||
seq: s,
|
||
slot,
|
||
};
|
||
assert_eq!(frame::FrameToken::unpack(t.pack()), t);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn frame_token_packing_matches_legacy_layout() {
|
||
// The legacy code packed (gen<<40)|(seq<<8)|slot by hand; lock the bit positions.
|
||
let t = frame::FrameToken {
|
||
generation: 7,
|
||
seq: 42,
|
||
slot: 3,
|
||
};
|
||
assert_eq!(t.pack(), (7u64 << 40) | (42u64 << 8) | 3u64);
|
||
}
|
||
|
||
#[test]
|
||
fn opened_detail_roundtrips_and_saturates() {
|
||
use frame::{pack_opened_detail, unpack_opened_detail, OPENED_DETAIL_LIVE};
|
||
// Zero counters still stamp LIVE — "attached, nothing offered yet" is information.
|
||
assert_eq!(pack_opened_detail(0, 0), OPENED_DETAIL_LIVE);
|
||
assert_eq!(unpack_opened_detail(pack_opened_detail(0, 0)), Some((0, 0)));
|
||
// Roundtrip within range.
|
||
assert_eq!(
|
||
unpack_opened_detail(pack_opened_detail(1234, 567)),
|
||
Some((1234, 567))
|
||
);
|
||
// Saturation at each counter's width (15-bit offered, 16-bit mismatched).
|
||
assert_eq!(
|
||
unpack_opened_detail(pack_opened_detail(u32::MAX, u32::MAX)),
|
||
Some((0x7FFF, 0xFFFF))
|
||
);
|
||
// A pre-detail driver's field (any value without the LIVE bit) carries no information.
|
||
assert_eq!(unpack_opened_detail(0), None);
|
||
assert_eq!(unpack_opened_detail(0x7FFF_FFFF), None);
|
||
}
|
||
|
||
#[test]
|
||
fn shared_header_is_pod_and_64_bytes() {
|
||
let mut h = frame::SharedHeader::zeroed();
|
||
h.magic = frame::MAGIC;
|
||
h.width = 5120;
|
||
h.height = 1440;
|
||
h.target_id = 262;
|
||
let bytes = bytemuck::bytes_of(&h);
|
||
assert_eq!(bytes.len(), 64);
|
||
let back: frame::SharedHeader = *bytemuck::from_bytes(bytes);
|
||
assert_eq!(back.magic, frame::MAGIC);
|
||
assert_eq!(back.width, 5120);
|
||
assert_eq!(back.height, 1440);
|
||
// v3: the monitor binding occupies the old `_pad` slot at offset 28 — byte-compatible (a v2
|
||
// host left it zero there).
|
||
assert_eq!(bytes[28..32], 262u32.to_le_bytes());
|
||
}
|
||
|
||
#[test]
|
||
fn attach_check_binds_ring_to_monitor() {
|
||
use frame::{check_attach, AttachReject, MAGIC};
|
||
// The good path: magic + matching generation + matching monitor binding.
|
||
assert_eq!(check_attach(MAGIC, 7, 262, 7, 262), Ok(()));
|
||
// Missing magic / superseded generation → Stale (silent drop, re-delivery coming) — and
|
||
// staleness WINS over a binding mismatch, since a superseded delivery's binding is
|
||
// meaningless (the fresh one re-validates).
|
||
assert_eq!(
|
||
check_attach(0, 7, 262, 7, 262),
|
||
Err(AttachReject::Stale),
|
||
"no magic"
|
||
);
|
||
assert_eq!(
|
||
check_attach(MAGIC, 8, 262, 7, 262),
|
||
Err(AttachReject::Stale),
|
||
"recreated ring"
|
||
);
|
||
assert_eq!(
|
||
check_attach(0, 8, 999, 7, 262),
|
||
Err(AttachReject::Stale),
|
||
"stale outranks bind"
|
||
);
|
||
// The v3 hardening: a fresh, magic-valid ring naming a DIFFERENT monitor fails closed.
|
||
assert_eq!(
|
||
check_attach(MAGIC, 7, 999, 7, 262),
|
||
Err(AttachReject::BindMismatch)
|
||
);
|
||
// A v2-host header (never stamped, target_id = 0) also fails closed against a v3 driver —
|
||
// the GET_INFO handshake rejects that pairing first, but the channel must not rely on it.
|
||
assert_eq!(
|
||
check_attach(MAGIC, 7, 0, 7, 262),
|
||
Err(AttachReject::BindMismatch)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn control_structs_roundtrip_through_bytes() {
|
||
let req = control::AddRequest {
|
||
session_id: 0xDEAD_BEEF_CAFE_F00D,
|
||
width: 3840,
|
||
height: 2160,
|
||
refresh_hz: 120,
|
||
preferred_monitor_id: 7,
|
||
max_luminance_nits: 800,
|
||
max_frame_avg_nits: 400,
|
||
min_luminance_millinits: 50, // 0.05 nits
|
||
hw_cursor: 1,
|
||
};
|
||
let bytes = bytemuck::bytes_of(&req);
|
||
assert_eq!(bytes.len(), 40);
|
||
assert_eq!(*bytemuck::from_bytes::<control::AddRequest>(bytes), req);
|
||
// preferred_monitor_id occupies the old `_reserved` slot at offset 20 — byte-compatible.
|
||
assert_eq!(bytes[20..24], 7u32.to_le_bytes());
|
||
// The client-HDR luminance tail rides after the legacy boundary; a zero-filled tail decodes
|
||
// as "unknown" (the un-upgraded-host form the driver's legacy read synthesizes).
|
||
assert_eq!(bytes[24..28], 800u32.to_le_bytes());
|
||
let mut legacy = [0u8; 40];
|
||
legacy[..control::ADD_REQUEST_LEGACY_SIZE]
|
||
.copy_from_slice(&bytes[..control::ADD_REQUEST_LEGACY_SIZE]);
|
||
let old = *bytemuck::from_bytes::<control::AddRequest>(&legacy);
|
||
assert_eq!(old.preferred_monitor_id, 7);
|
||
assert_eq!(
|
||
(
|
||
old.max_luminance_nits,
|
||
old.max_frame_avg_nits,
|
||
old.min_luminance_millinits
|
||
),
|
||
(0, 0, 0)
|
||
);
|
||
|
||
let reply = control::AddReply {
|
||
adapter_luid_low: 0x1234_5678,
|
||
adapter_luid_high: -2,
|
||
target_id: 262,
|
||
resolved_monitor_id: 7,
|
||
wudf_pid: 4242,
|
||
cursor_excluded: 1,
|
||
};
|
||
let rbytes = bytemuck::bytes_of(&reply);
|
||
assert_eq!(rbytes.len(), 24);
|
||
assert_eq!(*bytemuck::from_bytes::<control::AddReply>(rbytes), reply);
|
||
// resolved_monitor_id occupies the old `_reserved` slot at offset 12 — byte-compatible.
|
||
assert_eq!(rbytes[12..16], 7u32.to_le_bytes());
|
||
// The v2 duplication-target pid trails at offset 16.
|
||
assert_eq!(rbytes[16..20], 4242u32.to_le_bytes());
|
||
// The cursor-excluded tail rides after the legacy boundary; an un-upgraded driver writes
|
||
// only the prefix, so a zero-filled tail reads as "unknown/clean" (see the field docs).
|
||
assert_eq!(rbytes[20..24], 1u32.to_le_bytes());
|
||
assert_eq!(control::ADD_REPLY_LEGACY_SIZE, 20);
|
||
}
|
||
|
||
#[test]
|
||
fn frame_channel_request_roundtrips_through_bytes() {
|
||
let mut req = control::SetFrameChannelRequest {
|
||
target_id: 262,
|
||
generation: 3,
|
||
ring_len: frame::RING_LEN,
|
||
_pad: 0,
|
||
header_handle: 0x0000_0000_0000_1a2c,
|
||
event_handle: 0x0000_0000_0000_1b30,
|
||
texture_handles: [0; control::RING_LEN_USIZE],
|
||
};
|
||
for (k, t) in req.texture_handles.iter_mut().enumerate() {
|
||
*t = 0x2000 + k as u64 * 4;
|
||
}
|
||
let bytes = bytemuck::bytes_of(&req);
|
||
assert_eq!(bytes.len(), 32 + 8 * control::RING_LEN_USIZE);
|
||
assert_eq!(
|
||
*bytemuck::from_bytes::<control::SetFrameChannelRequest>(bytes),
|
||
req
|
||
);
|
||
// The handle values ride at 8-byte alignment from offset 16 (header, event, then the ring).
|
||
assert_eq!(bytes[16..24], 0x1a2cu64.to_le_bytes());
|
||
assert_eq!(bytes[24..32], 0x1b30u64.to_le_bytes());
|
||
assert_eq!(bytes[32..40], 0x2000u64.to_le_bytes());
|
||
}
|
||
|
||
#[test]
|
||
fn update_modes_request_roundtrips_and_versions_cohere() {
|
||
let req = control::UpdateModesRequest {
|
||
session_id: 42,
|
||
width: 2560,
|
||
height: 1409, // deliberately arbitrary — the in-place path serves window-drag modes
|
||
refresh_hz: 120,
|
||
_reserved: 0,
|
||
};
|
||
let bytes = bytemuck::bytes_of(&req);
|
||
assert_eq!(bytes.len(), 24);
|
||
assert_eq!(
|
||
*bytemuck::from_bytes::<control::UpdateModesRequest>(bytes),
|
||
req
|
||
);
|
||
assert_eq!(bytes[8..12], 2560u32.to_le_bytes());
|
||
// The compat window: v4–v6 are additive over v3, so the host floor stays at 3.
|
||
assert_eq!(PROTOCOL_VERSION, 6);
|
||
assert_eq!(MIN_DRIVER_PROTOCOL_VERSION, 3);
|
||
}
|
||
|
||
#[test]
|
||
fn cursor_shm_layout_is_pinned() {
|
||
use cursor::*;
|
||
// The header must leave the shape offset intact whatever grows inside `_reserved`.
|
||
assert_eq!(core::mem::size_of::<CursorShm>(), 64);
|
||
assert_eq!(CURSOR_SHM_SIZE, 64 + 256 * 256 * 4);
|
||
assert_eq!(CURSOR_MAGIC, u32::from_le_bytes(*b"PFCU"));
|
||
// Seqlock snapshot discipline survives a bytemuck roundtrip.
|
||
let hdr = CursorShm {
|
||
magic: CURSOR_MAGIC,
|
||
seq: 2,
|
||
visible: 1,
|
||
cursor_type: CURSOR_TYPE_ALPHA,
|
||
x: -3,
|
||
y: 7,
|
||
shape_id: 42,
|
||
width: 32,
|
||
height: 32,
|
||
pitch: 128,
|
||
hot_x: 4,
|
||
hot_y: 5,
|
||
_reserved: [0; 4],
|
||
};
|
||
let bytes = bytemuck::bytes_of(&hdr);
|
||
assert_eq!(*bytemuck::from_bytes::<CursorShm>(bytes), hdr);
|
||
assert_eq!(bytes[16..20], (-3i32).to_le_bytes());
|
||
}
|
||
|
||
#[test]
|
||
fn gamepad_names_and_magics_are_stable() {
|
||
assert_eq!(gamepad::xusb_boot_name(0), "Global\\pfxusb-boot-0");
|
||
assert_eq!(gamepad::pad_boot_name(2), "Global\\pfds-boot-2");
|
||
// Lock the exact u32 magics the shipped host/drivers use (inject/{gamepad,dualsense}_windows.rs).
|
||
assert_eq!(gamepad::XUSB_MAGIC, 0x5558_4650);
|
||
assert_eq!(gamepad::PAD_MAGIC, 0x5046_4453);
|
||
// "PFBT" little-endian.
|
||
assert_eq!(gamepad::BOOT_MAGIC.to_le_bytes(), *b"PFBT");
|
||
}
|
||
|
||
#[test]
|
||
fn pad_bootstrap_roundtrips_through_bytes() {
|
||
let b = gamepad::PadBootstrap {
|
||
magic: gamepad::BOOT_MAGIC,
|
||
host_proto: gamepad::GAMEPAD_PROTO_VERSION,
|
||
driver_pid: 1234,
|
||
driver_proto: gamepad::GAMEPAD_PROTO_VERSION,
|
||
data_handle: 0x0000_0000_0000_2a4c,
|
||
handle_pid: 1234,
|
||
handle_seq: 7,
|
||
};
|
||
let bytes = bytemuck::bytes_of(&b);
|
||
assert_eq!(bytes.len(), 32);
|
||
assert_eq!(*bytemuck::from_bytes::<gamepad::PadBootstrap>(bytes), b);
|
||
// The handle value rides 8-aligned at offset 16; the seq trails at 28 (written LAST by the host).
|
||
assert_eq!(bytes[16..24], 0x2a4cu64.to_le_bytes());
|
||
assert_eq!(bytes[28..32], 7u32.to_le_bytes());
|
||
}
|
||
|
||
#[test]
|
||
fn ctl_codes_are_contiguous_and_distinct() {
|
||
assert_eq!(control::IOCTL_ADD, ctl_code(0x900));
|
||
let all = [
|
||
control::IOCTL_ADD,
|
||
control::IOCTL_REMOVE,
|
||
control::IOCTL_SET_RENDER_ADAPTER,
|
||
control::IOCTL_PING,
|
||
control::IOCTL_GET_INFO,
|
||
control::IOCTL_CLEAR_ALL,
|
||
control::IOCTL_SET_FRAME_CHANNEL,
|
||
control::IOCTL_UPDATE_MODES,
|
||
];
|
||
for (i, a) in all.iter().enumerate() {
|
||
for b in &all[i + 1..] {
|
||
assert_ne!(a, b);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn cta_luminance_codes_hit_the_reference_points() {
|
||
// The driver's historical built-in EDID block: 0x8A ≈ 993 nits, 0x60 = 400 nits (exact),
|
||
// 0x12 for a ~0.05-nit floor. Our coder must land on the same bytes for those volumes.
|
||
assert_eq!(edid::cta_max_millinits(0x60), 400_000); // 50·2^3 exactly
|
||
assert_eq!(edid::cta_max_millinits(0x8A) / 1000, 993);
|
||
assert_eq!(edid::cta_max_luminance_code(400), 0x60);
|
||
// 0x8A decodes to 993.481 nits, so 994 is the smallest whole-nit input that reaches it
|
||
// under the never-advertise-brighter floor.
|
||
assert_eq!(edid::cta_max_luminance_code(994), 0x8A);
|
||
assert_eq!(edid::cta_min_luminance_code(50, 0x8A), 0x12); // 0.05 nits @ a 993-nit max
|
||
// Floor semantics: never advertise brighter than the panel. 1000 nits sits between
|
||
// code 138 (993) and 139 (~1015) → 138.
|
||
assert_eq!(edid::cta_max_luminance_code(1000), 138);
|
||
assert!(edid::cta_max_millinits(edid::cta_max_luminance_code(1000)) <= 1_000_000);
|
||
// Every real code decodes below or at its input (round-down), within one step (~2.2%).
|
||
// (Starts above code 1's 51.094 nits — beneath that the documented clamp-to-1 wins.)
|
||
for nits in [52u32, 80, 120, 250, 400, 604, 800, 1_499, 4_000, 10_000] {
|
||
let c = edid::cta_max_luminance_code(nits);
|
||
let dec = edid::cta_max_millinits(c);
|
||
assert!(dec <= nits as u64 * 1000, "{nits} → {c} decoded {dec}");
|
||
assert!(
|
||
dec * 1023 / 1000 >= nits as u64 * 1000,
|
||
"{nits} → {c} more than a step low"
|
||
);
|
||
}
|
||
// Clamps: 0/tiny stays a valid on-wire code (callers gate presence on nits > 0); the
|
||
// ceiling saturates at 255.
|
||
assert_eq!(edid::cta_max_luminance_code(0), 1);
|
||
assert_eq!(edid::cta_max_luminance_code(u32::MAX), 255);
|
||
// Min-luminance: 0 = unknown/true black stays 0; a floor brighter than the max clamps.
|
||
assert_eq!(edid::cta_min_luminance_code(0, 0x8A), 0);
|
||
assert_eq!(edid::cta_min_luminance_code(u32::MAX, 1), 255);
|
||
// Round-trip a typical HDR400 panel: max 400 nits / min 0.4 nits.
|
||
let max_c = edid::cta_max_luminance_code(400);
|
||
let min_c = edid::cta_min_luminance_code(400, max_c);
|
||
// decode: L_min = L_max·(cv/255)²/100 — must come back within ~10% of 0.4 nits.
|
||
let back =
|
||
edid::cta_max_millinits(max_c) * (min_c as u64 * min_c as u64) / (255 * 255) / 100;
|
||
assert!((360..=440).contains(&back), "min decoded {back} millinits");
|
||
}
|
||
|
||
#[test]
|
||
fn mouse_report_and_names_are_stable() {
|
||
assert_eq!(mouse::mouse_boot_name(0), "Global\\pfmouse-boot-0");
|
||
// "PFMO" little-endian, and never colliding with a pad magic (cross-wire validation).
|
||
assert_eq!(mouse::MOUSE_MAGIC.to_le_bytes(), *b"PFMO");
|
||
assert_ne!(mouse::MOUSE_MAGIC, gamepad::XUSB_MAGIC);
|
||
assert_ne!(mouse::MOUSE_MAGIC, gamepad::PAD_MAGIC);
|
||
// The 8-byte report layout the driver ferries and the host builds.
|
||
let r = mouse::input_report(0b0000_0101, 0x1234, 0x7FFF, -3, 7);
|
||
assert_eq!(r, [0x01, 0x05, 0x34, 0x12, 0xFF, 0x7F, 0xFD, 0x07]);
|
||
// Clamps: axes to the 15-bit logical max, buttons to the declared 5.
|
||
let r = mouse::input_report(0xFF, 0xFFFF, 0, 0, 0);
|
||
assert_eq!((r[1], r[2], r[3]), (0x1F, 0xFF, 0x7F));
|
||
// A zeroed section reads as "nothing published" (in_seq 0) — the driver's idle state.
|
||
let shm = mouse::MouseShm::zeroed();
|
||
assert_eq!(shm.in_seq, 0);
|
||
assert_eq!(bytemuck::bytes_of(&shm).len(), 64);
|
||
}
|
||
|
||
#[test]
|
||
fn guid_is_not_sudovda() {
|
||
const SUDOVDA: u128 = 0xE5BC_C234_1E0C_418A_A0D4_EF8B_7501_414D;
|
||
assert_ne!(PF_VDISPLAY_INTERFACE_GUID_U128, SUDOVDA);
|
||
}
|
||
}
|