diff --git a/crates/pf-vdisplay-proto/src/lib.rs b/crates/pf-vdisplay-proto/src/lib.rs index 3653c08..b7c98b9 100644 --- a/crates/pf-vdisplay-proto/src/lib.rs +++ b/crates/pf-vdisplay-proto/src/lib.rs @@ -276,7 +276,7 @@ pub mod frame { /// 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 -/// (`docs/windows-host-rewrite-audit.md` §6.1). Owning them here with `Pod` derives + `offset_of!` +/// (`docs/windows-host-rewrite.md` §2.7). Owning them here with `Pod` derives + `offset_of!` /// asserts makes a one-sided edit a compile error. /// /// The host creates the section (privileged, permissive DACL so the restricted WUDFHost token can diff --git a/crates/punktfunk-host/src/capture/windows/idd_push.rs b/crates/punktfunk-host/src/capture/windows/idd_push.rs index e7625b7..51ecef0 100644 --- a/crates/punktfunk-host/src/capture/windows/idd_push.rs +++ b/crates/punktfunk-host/src/capture/windows/idd_push.rs @@ -435,7 +435,7 @@ impl IddPushCapturer { /// Block (bounded) until the driver has ATTACHED to the host ring (`DRV_STATUS_OPENED`) **and published /// a first frame**, else fail so the caller can fall back to DDA (audit §5.1 + - /// `docs/windows-host-rewrite-game-capture-bug.md` P3/Stage 1). + /// `docs/windows-host-rewrite.md` §2.5 — the GB1 game-capture fix). /// /// Requiring the first frame — not just the attach — catches the *reconnect-into-a-broken-state* case: /// a fullscreen game can leave the virtual display in a format/size that the driver's `publish()` guard diff --git a/crates/punktfunk-host/src/config.rs b/crates/punktfunk-host/src/config.rs index 2705955..b571a97 100644 --- a/crates/punktfunk-host/src/config.rs +++ b/crates/punktfunk-host/src/config.rs @@ -4,7 +4,7 @@ //! environment before the host starts, and **for the knobs captured here the environment is constant for the //! process lifetime**, so a lazily-parsed global is equivalent to "parsed once at startup". //! -//! **Goal-1 stages 1–2** (`docs/windows-host-goal1-plan.md`): stage 1 stood this up; stage 2 migrated the +//! **Goal-1 stages 1–2** (`docs/windows-host-rewrite.md` §2.2): stage 1 stood this up; stage 2 migrated the //! genuinely-constant operator/dispatch knobs onto it (the dispatch-disagreement bug class: `idd_push`, //! `capture_backend`, `encoder_pref`, `render_adapter`, `no_wgc`, the vdisplay backend select — plus the //! plan-named `secure_dda`/`idd_depth`/`zerocopy`/`ten_bit` and the multi-site `perf`/`compositor`/ diff --git a/crates/punktfunk-host/src/session_plan.rs b/crates/punktfunk-host/src/session_plan.rs index 1e83aa8..ecb9d3d 100644 --- a/crates/punktfunk-host/src/session_plan.rs +++ b/crates/punktfunk-host/src/session_plan.rs @@ -1,7 +1,7 @@ //! `SessionPlan` — the per-session capture / topology / encoder decision, resolved **once** from //! [`HostConfig`](crate::config) (+ the handshake-negotiated bit depth) into a typed, logged value. //! -//! **Goal-1 stage 3** (`docs/windows-host-goal1-plan.md`): before this, the Windows session decision was +//! **Goal-1 stage 3** (`docs/windows-host-rewrite.md` §2.2): before this, the Windows session decision was //! re-derived at three call sites — the capture backend inside `capture::capture_virtual_output`, the //! process topology in `punktfunk1::should_use_helper`, and the encode backend in //! `encode::windows_resolved_backend` — each reading [`config`](crate::config) independently, with no diff --git a/docs/windows-host-goal1-plan.md b/docs/windows-host-goal1-plan.md deleted file mode 100644 index 6cfedcd..0000000 --- a/docs/windows-host-goal1-plan.md +++ /dev/null @@ -1,204 +0,0 @@ -# Goal-1 (clean, layered host architecture) — staged execution plan - -The design is in [`windows-host-rewrite.md`](windows-host-rewrite.md) §2.2–2.4. This file is the **ordered, -independently-shippable execution plan**, because the host is **live-validated** (GameStream + punktfunk/1, -NVENC + IDD-push on-glass) and Goal-1 rewires its session/config/dispatch flow — so every stage must -**preserve behavior**, compile + box-verify on its own, and be committed before the next starts. The plan's -own §14 makes the §1 preservation checklist a mandatory per-module assert contract; honour it. - -> **Status (2026-06-25):** all six staged stages, §2.5 (the ownership-model rewrite), **and** all three -> Stage-5 seam-trait tightenings (incl. `EncoderCaps`, `0ccd0fe`) are **DONE** — each is code + -> box-`cargo check --features nvenc` + (where it touches the deployed path) on-glass validated, except the -> Windows-only `EncoderCaps` NVENC override which is Linux-clippy-clean + CI-gated. Work lives on branch -> **`windows-host-goal1`** (off `main`, **not merged**). The Goal-1 host refactor is **functionally -> complete**; what's left is non-blocking — see [Remaining (next session)](#remaining-next-session). - -## Why staged (not one big rewrite) - -`main` is at parity and shipping. A monolithic rewrite would put the validated host in a broken -intermediate state for a long window and make a regression impossible to bisect. Each stage below is a -behaviour-preserving transform with its own verification, so a regression is caught at the stage that -introduced it. - -## Stages (ordered; each = goal · files · risk · verify) - -**Stage 1 — `HostConfig` foundation. ✅ DONE (this commit).** -`config.rs`: typed `HostConfig` parsed ONCE from env (`idd_push`/`encoder_pref`/`no_helper`/`force_helper`). -Migrated the two highest-churn dispatch reads onto it (`encode::windows_resolved_backend`, -`punktfunk1::should_use_helper`). Risk: low (env constant at runtime → identical behaviour). Verify: box -`cargo check --features nvenc`. - -**Stage 2 — finish `HostConfig` + resolve-once. ✅ DONE (this commit).** -Migrated **31** genuinely-constant operator/dispatch sites onto `HostConfig`: `idd_push` ×7 (the -capture/topology disagreement knob), `no_wgc`, `capture_backend`, `render_adapter`, `encoder_pref` (Linux), -the Windows vdisplay-backend select, plus the plan-named `secure_dda`/`idd_depth`/`zerocopy`/`ten_bit` and the -multi-site `perf` ×4 / `compositor` ×5 / `video_source` ×3 / `gamepad`. Each `HostConfig` field's parser is -**byte-identical** to the read it replaced, so `old == new` by construction (the §1 "flipped bool" guard). - -**Scope correction (the plan's "~64 sites / Linux XDG+compositor / grep→0" was unsafe as written):** two -classes of `env::var` read are deliberately **kept live** and documented in `config.rs`: -- **Runtime-mutated session vars.** On Linux, `vdisplay::apply_session_env` *rewrites the process env on - every connect* (the Bazzite Gaming↔Desktop follow): `WAYLAND_DISPLAY`, `XDG_CURRENT_DESKTOP`, - `XDG_RUNTIME_DIR`, `DBUS_SESSION_BUS_ADDRESS`, and the derived `PUNKTFUNK_INPUT_BACKEND`, - `PUNKTFUNK_GAMESCOPE_SESSION/NODE`, `PUNKTFUNK_KWIN/MUTTER_VIRTUAL_PRIMARY`, `PUNKTFUNK_FORCE_SHM`. - Parse-once would freeze them at startup → silent session-following regression. They are NOT constant. -- **Single-use local tuning** (no resolve-once benefit, call-site-local default/clamp, and `FEC_PCT` even has - *two different* semantics): `FEC_PCT`, `VIDEO_DROP`, `VBV_FRAMES`, `SPLIT_ENCODE`, `PACE_BURST_KB`, the - `capture/dxgi.rs` timing knobs, the `*_LIVE`/test gates, plus path/dynamic reads (config-dir, `PATH` - search, env-forward-to-child). `PUNKTFUNK_ZEROCOPY` is split on purpose: Windows presence-semantics moved - to the field; Linux keeps its own truthy parser. - -Risk: medium (semantics-preservation). Verify: Linux `cargo check`/`clippy`/`fmt` green (the Windows-only -edits are 1:1 substitutions, compile-verified on the box as part of Stage 3's build). - -**Stage 3 — `SessionPlan` (the single biggest clarity lever, plan §2.4). ✅ DONE (box-build + on-glass validated).** -New `src/session_plan.rs`: a `Copy` `SessionPlan { capture, topology, encoder, bit_depth, hdr }` resolved -**once** from `HostConfig` (+ the negotiated `bit_depth`) in `virtual_stream`, logged, and threaded through -`build_pipeline_with_retry`/`build_pipeline`. The three dispatch points now read it: -- **capture** — `capture::capture_virtual_output` takes a `CaptureBackend` IN (was re-deriving from - `config().idd_push`/`capture_backend`/`no_wgc`); `CaptureBackend::resolve()` is the one resolver (also - used by the GameStream + spike call sites). -- **topology** — `virtual_stream` reads `plan.topology` (`should_use_helper` deleted; its logic is - `session_plan::resolve_topology`, verbatim). The IDD-preempt guard reads `plan.capture` too. -- **encoder** — recorded as `EncoderBackend` from `encode::windows_resolved_backend` (config-backed + - GPU-vendor cached since stage 2, already a single source). Threading `encoder`/`input_format` into the - encoder + capturer opens (which removes the `dxgi.rs` back-reference) is **stage 5**. - -Every decision is provably equivalent to the pre-stage-3 scattered reads (same `config()` + cached probes), -so it is behavior-preserving. Risk: medium-high (rewires the deployed decision). Verify: -- **Box build ✅** — `cargo check -p punktfunk-host --features nvenc` (the deployed config: NVENC SDK + - `cudarc` + `encode/nvenc.rs`) is **clean, zero warnings**, on the RTX box (`192.168.1.173`), in an - isolated worktree. This also covers stage 2's Windows-only edits (their first real Windows compile). -- **On-glass ✅** — deployed my Stage-3 host into the SCM service (Session-1 launch, the real IDD-push - environment) on the RTX box and drove a `punktfunk-probe` loopback session. The host logged - `resolved session plan { capture: IddPush, topology: SingleProcess, encoder: Nvenc, bit_depth: 8, - hdr: false }` — the **correct** resolution for the deployed config (IDD_PUSH + VDISPLAY=pf + nvenc) — - and routed correctly (IDD-push capturer → shared ring → IDD→DDA fallback). This box has a pre-existing - **hybrid-GPU IDD render-adapter mismatch** (driver renders on the iGPU `af4825`, host ring on the 4090 - `294d29`) that yielded no published frame in this loopback scenario; an **A/B against the shipping - binary reproduced the identical `frames=0`**, proving the no-frame is environmental, **not** a Stage-3 - regression. Stage 3 is behavior-equivalent to the shipping host. Box restored to its deployed state. - -**Stage 4 — `SessionContext` (the arg-bundling). ✅ DONE (box-build validated). `SessionFactory`/`Session::drop` deferred to §2.5 — see below.** -Bundled the 13-positional-argument `#[allow(too_many_arguments)]` session entry (`virtual_stream` **and** -`virtual_stream_relay`) into one owned `SessionContext` struct, moved into the stream thread. The receivers -move in (`virtual_stream` is their only consumer), retiring the `&Receiver` borrow plumbing. **Behavior- -identical by construction**: each function destructures the context into the same local names at the top, so -the ~400-line loop bodies are byte-for-byte unchanged. Removed both `#[allow(too_many_arguments)]` attrs. - -**Scoped deliberately.** The plan's `SessionFactory.build()` owning a `vdm.lease(mode) → open_capturer → -open_encoder → spawn` RAII chain with `Session::drop` as the *only* teardown is **coupled to §2.5's -ownership-model rewrite** — it needs a host-side `VirtualDisplayManager`/`MonitorLease` that does not exist -yet (the lifecycle still lives in the `CURRENT_MON_GEN`/`IDD_SETUP_LOCK` globals + the per-compositor -`vdisplay` backends). The current teardown is **already drop-based** (the capturer owns the keepalive whose -`Drop` releases the monitor — "restore displays before REMOVE" lives there; only `send_thread.join()` is -explicit), and it is the validated shipping path. Wrapping the deployed reconfig/switch/rebuild loop in a -`Session::drop` for a behavior-preserving change would add real regression risk for marginal gain. So the -`SessionFactory`/`Session::drop`/`vdm.lease` work is folded into §2.5 (its natural home); this stage delivers -the concrete, safe arg-bundling. Risk: low (behavior-identical). Verify: Linux + box build (the relay -destructure is the only Windows-only piece); the teardown on-glass gate moves to the §2.5 work. - -**Stage 5 — seam-trait tightenings (plan §2.3). ✅ Tightening 1 + 3 DONE; 2 folded into §2.5.** -The three §2.3 tightenings have different coupling, so they split: -- **(1) `OutputFormat` into the capturer ✅** — the headline (the explicit Stage-3 deferral; §5's - "highest-severity coupling"). New `capture::OutputFormat { gpu, hdr }`, resolved once per session and - passed **into** `capture_virtual_output` (`SessionPlan::output_format()` for the native path — - `gpu = encoder.is_gpu()`, no second probe; `OutputFormat::resolve()` for the GameStream/spike paths). - `dxgi::DuplCapturer::open` takes `gpu` in and **its `windows_resolved_backend()` recompute is deleted** — - capture no longer re-derives the encode backend. Behavior-preserving (the `gpu` passed in equals the value - the capturer used to compute). Linux + box-build clean. -- **(2) HDR/release → `VirtualLease`** — **moved to §2.5.** `await_released` as a lease method needs the - monitor-generation carried *on the lease* (today it's the `CURRENT_MON_GEN` global + the - `sudovda::wait_for_monitor_released` free fn), and the keepalive becoming `Box` is the - ownership-model change. It belongs with the `VirtualDisplayManager`/`MonitorLease` work, not bolted on here. -- **(3) `EncoderCaps` ✅ (`0ccd0fe`)** — `Encoder::caps() -> EncoderCaps { supports_rfi, supports_hdr_metadata }`, - a default-`false` query (so every SDR/libavcodec backend — Linux NVENC, VAAPI, AMF/QSV, software — is - unchanged); only the Windows direct-NVENC path overrides it, reporting the real `rfi_supported` (probed - once at open) + `hdr`. Consumer: the GameStream encode loop hoists `supports_rfi` once and gates the - loss-recovery path on it — `!(supports_rfi && invalidate_ref_frames(..))` forces a keyframe directly on - non-RFI encoders instead of an always-`false` call every loss event (behavior-preserving, intent - explicit). Linux `clippy -D warnings` clean; the NVENC override is Windows-only → CI/on-glass gate. - -Risk: medium (Tightening 1 is behavior-preserving + Windows-only → box-compile is the gate; on-glass parity is -the same env-limited story as Stage 3). - -**Stage 6 — `windows/` + `linux/` tree confinement (cfg-sprawl, plan §2.2). ✅ DONE (Linux + box-build validated).** -Moved **36 platform-specific files** into per-module `windows/` and `linux/` folders (and the shared HID -codecs into `inject/proto/`): `capture/{windows,linux}/`, `encode/{windows,linux}/`, -`inject/{windows,linux,proto}/`, `audio/{windows,linux}/`, `vdisplay/{windows,linux}/`, and the top-level -`src/windows/` (service, wgc_helper, win_adapter, win_display) + `src/linux/` (dmabuf_fence, drm_sync, -zerocopy/). - -**Done with `#[path]`, not a module rename** — every file moves into its folder while the `crate::*::*` module -names stay **flat**, so all caller paths and every internal `super::`/`crate::` reference are **unchanged** -(only the parent `mod` decls gained `#[path = "…"]`). This is the codebase's existing pattern (inject's -`gamepad_windows`) and makes the move byte-identical in behaviour with **zero reference churn** — far lower -risk than collapsing to a single `crate::capture::windows::` namespace (that deeper rename is an optional -follow-on; this delivers the folder confinement the stage is about). Done LAST, after the semantic stages. -Verify: Linux `cargo check`/`clippy`/`fmt` clean; all 36 `#[path]` targets exist; no internal -`#[path]`/`include!`/file-child-`mod` in any moved file; **box `cargo check --features nvenc` clean**. - -**§2.5 — ownership-model rewrite: `VirtualDisplayManager` + `MonitorLease`. ✅ DONE (3 steps; code + box + on-glass reconnect-leak validated).** -The natural home for the deferrals above (Stage 4's `SessionFactory`/`Session::drop`/`vdm.lease`; Stage 5 -tightening 2's HDR/release → `VirtualLease`). A 5-agent map first established two facts that shaped the work: -**`CURRENT_MON_GEN` was WRITE-ONLY** (its only reader, `idd_push::my_gen`, was set-but-never-read — the -"per-frame monitor-gen bail" the docs describe was never wired; per-frame staleness is the *separate* ring -`FrameToken.generation`), so the design's "carry the monitor gen through `WinCaptureTarget`" was unnecessary; -and the two Windows backends (`sudovda` + `pf_vdisplay`) **duplicated the Idle/Active/Lingering refcount -state machine verbatim** (differing only in IOCTL proto + REMOVE key). User-approved shape: **one OnceLock -singleton `VirtualDisplayManager`**, not a threaded `Arc`. - -- **Step 1 (`1520201`)** — delete the dead/write-only code: `CURRENT_MON_GEN`/`my_gen`, `IDD_PERSIST`/ - `open_or_reuse`/`IddReuseHandle` (~150 lines). -- **Step 2 (`d9b8b88`)** — new `vdisplay/windows/manager.rs`: the two duplicated `MGR: Mutex` globals - collapse into one OnceLock `VirtualDisplayManager` { `Box`, `Arc` device - (typed — kills the raw-`isize` cross-thread smuggle **and** fixes a latent control-handle leak), - `Mutex`, `AtomicU64` gen }. `sudovda`/`pf_vdisplay` shrink to thin `VdisplayDriver` impls - (`open`/`add_monitor`/`remove_monitor`/`ping`) + thin `VirtualDisplay` wrappers; the IOCTL surface is the - only backend-specific code left. `MonitorKey = Guid(GUID) | Session(u64)`. `MON_GEN` + - `wait_for_monitor_released` move onto the manager; `MonitorLease::drop → vdm().release(gen)` preserves the - stale-lease no-op verbatim. -- **Step 3 (`fe61597`)** — the last two globals (`IDD_SETUP_LOCK`/`IDD_SESSION_STOP`) move onto the manager - behind `vdm().begin_idd_setup(stop)`; `punktfunk1` no longer reaches into vdisplay internals for the preempt. - -Net: `CURRENT_MON_GEN` / `MON_GEN` / two `MGR` / `IDD_PERSIST` / `IDD_SETUP_LOCK` / `IDD_SESSION_STOP` — -**all gone**, replaced by one encapsulated, typed manager. Behavior-preserving (the state machine is the -canonical `sudovda` copy routed through the driver seam). - -**On-glass reconnect-leak test ✅ (`683c81b`)** — it earned its keep: the box *compile* was clean, but the -first deploy **panicked** (`VirtualDisplayManager used before a backend initialised it`) because -`begin_idd_setup` called `vdm()` **before** `vdisplay::open` constructs the backend that runs -`manager::init()` (the old globals needed no init, so the ordering only broke once it became a manager -method). Fixed by opening the backend first — it does no monitor work, so the preempt-before-monitor-creation -semantics are preserved. After the fix: **0 panics**, the new `manager` module owns the lifecycle -(`vdisplay::manager: virtual-display monitor removed`), create == removed (net 0, **bounded**), **0 leaked -active monitors** across many reconnects; an A/B vs the shipping binary confirmed §2.5 is behaviour-equivalent. -Verified live on the **IDD-push zero-copy path** (`new_fps ~200` @5120×1440@240, **0 DDA fallbacks**). - -## Remaining (next session) - -The layered architecture is **complete**: all 6 staged stages, §2.5, **and** the three Stage-5 seam-trait -tightenings have landed. What's left is non-blocking: - -1. **Optional `crate::*::windows::` namespace collapse** — Stage 6 confined the platform files into - `windows/`/`linux/` folders via `#[path]` (flat module names, zero reference churn); the deeper rename to - real `crate::capture::windows::` paths is optional cleanup, not required. **Decision: skip** — pure churn - for no behavior/clarity gain, and it would touch every `super::`/`crate::` path in the moved files. -2. **Merge `windows-host-goal1` → `main`** — the branch is off `main` and **not merged**; local `main` is - also ~20 commits ahead of `origin/main` with unpushed audit/Stage work. Outward-facing (a 30+-commit push - to `origin`) → **confirm with the user before landing**; the [Work-on-main] habit otherwise applies. -3. **(driver — NOT the host refactor) pf-vdisplay slot reclaim** — surfaced on-glass: sustained ADD/REMOVE - churn wedges the driver (`ADD → 0x80070490 ERROR_NOT_FOUND`) because it doesn't reclaim IddCx monitor - slots on REMOVE (ghost monitor nodes accumulate, `target_id`s climb). Recovery today is - `packaging/windows/reset-pf-vdisplay.ps1`; the real fix lives in the driver WIP. **This belongs to the - greenfield driver rewrite, not the Goal-1 host refactor** — tracked in - [`windows-host-rewrite.md`](windows-host-rewrite.md) (alongside the fullscreen-game capture bug). - Dev-iteration helpers `reset-pf-vdisplay.ps1` + `redeploy-pf-vdisplay.ps1` are committed under - `packaging/windows/` (validated live). - -## Guardrails (mandatory, plan §14) - -- Each stage is its own commit; box-verify before moving on. -- Stages 3–5 touch the deployed path → **on-glass re-test** (NVENC + IDD-push, a mode switch, a - connect/disconnect cycle) before the next stage. -- Preserve every `PUNKTFUNK_*` var's exact semantics; when in doubt, assert old==new at the call site. diff --git a/docs/windows-host-rewrite-audit.md b/docs/windows-host-rewrite-audit.md deleted file mode 100644 index 9c19992..0000000 --- a/docs/windows-host-rewrite-audit.md +++ /dev/null @@ -1,288 +0,0 @@ -# Windows Host Rewrite — Audit - -Status: **audit** (2026-06-25). Reviews the state of the Windows host rewrite against its plan -([`docs/windows-host-rewrite.md`](windows-host-rewrite.md)). Read-only assessment — no code changed. -Scope: the new IddCx driver workspace (`packaging/windows/drivers/`), the owned ABI crate -(`crates/pf-vdisplay-proto`), the host-side IDD-push path (`capture/idd_push.rs`, -`vdisplay/pf_vdisplay.rs`), and the deployment/packaging seam. Evidence is cited as `file:line`. - -> **Remediation in progress (2026-06-25).** The findings below were the state at audit time; several are -> already being worked through. Resolved since: the **cutover (§3)** — STEP 8 gave the new driver its own -> `.inx` and re-vendored the installer to the new wdk-sys build (`pf_vdisplay.dll` 613 KB → 251 KB), so the -> new driver is now the shipped one; and the **proto ABI hardening (§6.1/§6.2)** — offset asserts + the -> owned gamepad SHM layouts have landed. **Live progress + the hand-off task list are tracked in -> [`docs/windows-host-rewrite-remediation.md`](windows-host-rewrite-remediation.md).** - ---- - -## 0. Bottom line - -The framing "the Windows host has been rewritten with IDD-push as the main path" **overstates what is -on disk.** What actually landed is the **driver rewrite** (plan M0 + M1, STEPs 0–7): a clean, new, -all-Rust IddCx driver (`packaging/windows/drivers/pf-vdisplay`, ~2,000 LOC) on the unified -`windows-drivers-rs` stack, speaking an owned ABI crate (`pf-vdisplay-proto`), validated on-glass through -HDR. That is the hardest, highest-risk part of the plan (the `/INTEGRITYCHECK` answer, the `iddcx` binding -on `wdk-sys`, on-glass IDD-push + HDR) and it is genuinely well executed. - -Three facts the framing hides: - -1. **The new path is not the shipped path — it is not shipped at all.** The installer still vendors and - installs the **old** `vdisplay-driver/` (wdf-umdf) build - (`packaging/windows/pf-vdisplay/pf_vdisplay.dll`, dated 2026-06-24). The new driver has **no INF - in-tree**, is not vendored, and therefore cannot be packaged. IDD-push capture is gated behind - `PUNKTFUNK_IDD_PUSH`, which is **not set** in `scripts/windows/host.env.example`, so the default - capture path is **WGC→DDA** and the default display backend falls back to **SudoVDA** whenever the new - driver interface isn't enumerable. The new path runs only on a hand-built bench box with the env var - set. -2. **The host-side rewrite — Goal 1 — has not started.** No `src/windows/` tree, no `config.rs`/ - `HostConfig`, no `SessionFactory`/`SessionPlan`, no `session/`. The old god-files are intact. SudoVDA - was not removed (135 refs; `sudovda.rs` is a *hard dependency* of the new path). Unsafe went **up**, - not down. -3. **The new driver itself diverges from its own spec in load-bearing ways** — the watchdog is dead code, - `SET_RENDER_ADAPTER` is a stub, the §2.5 ownership-model refactor wasn't done, and world-writable - logging was re-introduced. - -So the riskiest **proof** is done (real progress). The **rewrite** (clean architecture, cutover, -hardening) is still ahead. - ---- - -## 1. Goal / milestone scorecard - -| Goal / milestone | Status | Evidence | -|---|---|---| -| **M0** proto ABI + driver toolchain + `/INTEGRITYCHECK` + iddcx binding | ✅ Done | `pf-vdisplay-proto`, vendored `windows-drivers-rs`, `clear-force-integrity.ps1` | -| **M1** new IddCx driver, first light + HDR | ✅ Done (on-glass) | STEPs 0–7; `swap_chain_processor.rs`, `frame_transport.rs`, `callbacks.rs` | -| **Goal 1** clean, layered host architecture | ❌ Not started | no `src/windows/`, `config.rs`, `session/`, `SessionFactory`/`SessionPlan` | -| **Goal 2** drop every trace of SudoVDA | ❌ Not done | 135 `sudovda` refs; `sudovda.rs` (1,193 LOC) is a hard dep of `pf_vdisplay.rs` + `idd_push.rs` | -| **Goal 3** minimize unsafe + P0 lints | ❌ Regressed | host unsafe ~476 (↑); driver ~160 vs ~60 target; **no** P0 lints anywhere; `OwnedHandle` in **0** host files | -| **§2.5** delete driver global statics / DeviceContext-owned state / `EvtCleanupCallback` | ❌ Not done | `MONITOR_MODES`/`NEXT_ID`/`ADAPTER`/`DEVICE_POOL` still process-globals; `DeviceContext{_device}` empty; no monitor cleanup callback | -| **M4** unify gamepad drivers onto new stack | ❌ Not started | workspace members = `wdk-probe/wdk-iddcx/pf-vdisplay` only; gamepad drivers still standalone wdf-umdf | -| **M6** cutover + delete old monoliths | ❌ Not reached | old driver trees + `dxgi/wgc/wgc_relay/sudovda/punktfunk1` all present (partly by-design as "reference until parity") | - ---- - -## 2. What landed well (preserve, do not regress) - -- **The §1 driver "jewels" survived the port.** The two real swap-chain leak fixes are verbatim with - their rationale: borrow `IDXGIDevice` once across `SetDevice` retries - (`swap_chain_processor.rs:174`), and check `terminate` at the loop top during a frame burst (`:238`). - `DEVICE_POOL` keyed by render LUID (the NVIDIA UMD-thread/VRAM leak fix) is intact - (`direct_3d_device.rs:115`). Monitor lock discipline (drop the worker **outside** `MONITOR_MODES`) is - correct (`monitor.rs:343-390`). -- **The frame transport is clean and correct** — the standout module. `FramePublisher` uses - `pf_vdisplay_proto::frame` for header/token/names (no hand-rolled offsets), straight-line - acquire→copy→release with no `?` between lock/unlock (`frame_transport.rs:266-275`), format guard - before `CopyResource`, stale-ring generation detection, correct drop order. -- **The proto control plane is properly owned**: fresh GUID (not SudoVDA's `e5bcc234`), centralized - `FrameToken::pack/unpack` used by both sides, and a **real version handshake the host actually - asserts** and bails on mismatch (`pf_vdisplay.rs:455-466`). Typed IOCTL dispatch collapsed the - per-call unsafe (`control.rs`). -- **Per-block `// SAFETY:` discipline** is already present throughout the new driver — most of the value - of `clippy::undocumented_unsafe_blocks` without the lint being on yet. - ---- - -## 3. Deployment gap (the headline) - -The new path is built and validated but not reachable by an installed product. - -- **Installer ships the old driver.** `packaging/windows/stage-pf-vdisplay.ps1:7-8` vendors the signed - output of `packaging/windows/vdisplay-driver/` (the wdf-umdf tree); `punktfunk-host.iss` installs that - via `install-pf-vdisplay.ps1`. The vendored binary is `packaging/windows/pf-vdisplay/pf_vdisplay.dll` - (613,760 bytes — the old build). -- **New driver is not packageable.** `find packaging/windows/drivers -name '*.inf'` → none. The new - workspace is built + FORCE_INTEGRITY-cleared in CI (`windows-drivers.yml`) as a **compile/link gate - only**; nothing signs or vendors its output. -- **GUID split keeps them apart.** The old driver exposes the old SudoVDA interface GUID; the host's - `sudovda.rs` backend opens it. The new driver exposes the fresh `70667664-…` GUID; only - `pf_vdisplay.rs` opens it. With the old driver installed, `pf_vdisplay::is_available()` → false → the - host silently uses the SudoVDA backend. -- **IDD-push is off by default.** `scripts/windows/host.env.example` sets only - `PUNKTFUNK_ENCODER=auto`, `PUNKTFUNK_VIDEO_SOURCE=virtual`, `PUNKTFUNK_SECURE_DDA=1`, `RUST_LOG=info`. - `PUNKTFUNK_IDD_PUSH` is checked via `var_os(...).is_some()` (`capture.rs:348`, `punktfunk1.rs:2223+`, - `pf_vdisplay.rs:57`) but never set in deployment. - -Net: a freshly installed Windows host runs **old driver + SudoVDA backend + WGC/DDA capture** — the -pre-rewrite path. The rewrite is a manually-validated parallel track, not a delivered feature. - ---- - -## 4. Driver code audit — stability / correctness - -### 4.1 P0 — the watchdog is dead code; host-crash leaks an orphan monitor - -`WATCHDOG_PINGS` is incremented on `IOCTL_PING` (`control.rs:35`) but **nothing reads it** — the only -`thread::spawn` in the driver is the swap-chain worker (`swap_chain_processor.rs:104`). The comments are -misleading: "STEP 4's watchdog thread samples it" (`control.rs:17`) and "the watchdog reaps all monitors" -(`control.rs:14`) describe a thread that does not exist; `adapter_init_finished` -(`callbacks.rs:30-37`) does not start one despite its doc claiming so. - -Consequence: if `serve` dies or the service is stopped with `TerminateProcess` (skipping `Drop` → no -`IOCTL_REMOVE`), the virtual monitor + its worker thread + pooled D3D device persist in WUDFHost until the -**next** host start issues `IOCTL_CLEAR_ALL`. If the host is not restarted, the orphan monitor stays -plugged into the desktop topology indefinitely. - -The plan called for host-gone detection by **`EvtCleanupCallback` RAII**, a **polling watchdog**, or -**`EvtFileClose`** (§3.4) — none is implemented. Fix: implement the watchdog thread, or (preferred) wire -`EvtFileClose` so "host holds the control handle open" = liveness; and remove the false comments. - -### 4.2 P1 — `SET_RENDER_ADAPTER` is a stub → hybrid-GPU is a hard failure - -`control.rs:47` returns `STATUS_NOT_IMPLEMENTED`, contradicting plan §3.2 (which made it unconditional). -The driver renders the virtual monitor on whatever adapter the OS picks (`callbacks.rs:275`, -`pooled_device(luid)`) and reports that LUID to the host. On a hybrid **iGPU+dGPU** box, if the OS picks -the iGPU, the host's ring textures (created on the NVENC dGPU) fail `OpenSharedResourceByName` → -`DRV_STATUS_TEX_FAIL` (`frame_transport.rs:195-208`) → the host's 20 s hard bail (§5.1). This is a silent -hard failure on common Optimus/hybrid configs. The single-dGPU RTX bench box never reproduced it. - -### 4.3 P1 — the §2.5 ownership refactor wasn't done - -State is still process-global: `MONITOR_MODES`/`NEXT_ID` (`monitor.rs:63,65`), `ADAPTER` -(`adapter.rs:41`), `DEVICE_POOL` (`direct_3d_device.rs:115`); `DeviceContext` is an empty `{ _device }` -(`entry.rs:20`). No `EvtCleanupCallback` on the monitor object (`monitor.rs:292-296` sets only Size + -scope). Monitor identity is still 3-keyed (`id`/`object`/`session_id`), not the collapsed single -`Monitor`. - -This is why the plan's central payoff — *stable monitor reuse → drop the preempt dance → unblock -`max_concurrent>1` on Windows* — was not achieved. The host still does fresh-monitor-per-session with the -`IDD_SETUP_LOCK` preempt + `wait_for_monitor_released` dance (`punktfunk1.rs:2216-2237`), so Windows -IDD-push is effectively single-client even though `DEFAULT_MAX_CONCURRENT = 4`. - -### 4.4 P2 — world-writable logging re-introduced - -Plan §6 said delete the `C:\Users\Public\*.log` driver logging; the new driver re-added it -(`pf-vdisplay/src/log.rs:18` → `C:\Users\Public\pfvd-driver.log`). Info-leak / DoS surface; should move to -ETW or be gated off release builds. - -### 4.5 P2 — no control-plane input validation - -`create_monitor` receives `width/height/refresh` from the IOCTL with no bounds check (`control.rs:62-63` -→ `monitor.rs:243`). The host is a trusted LocalSystem process so the trust boundary holds, but a buggy -host could request an absurd mode. `read_input` uses `T: Copy`, not `bytemuck::Pod` (`control.rs:96`); -Pod would be a stronger guarantee. - ---- - -## 5. Host code audit - -### 5.1 P1 — when IDD-push is engaged there is no fallback - -The plan kept WGC/DDA as a safety net; the code commits hard. `capture.rs:345` consumes the keepalive and -returns the IDD-push capturer with "no fall-through"; attach failure surfaces as a **20 s deadline -`bail!`** (`idd_push.rs:820-846`) that tears the session down black rather than degrading to DDA. Combined -with §4.2, hybrid-GPU = a guaranteed 20 s black-then-drop. - -### 5.2 P1 — SudoVDA is a hard dependency of the "new" path - -`pf_vdisplay.rs` and `idd_push.rs` import `isolate_displays_ccd`/`resolve_render_adapter_luid`/ -`set_advanced_color`/`CURRENT_MON_GEN` directly from `super::sudovda` (`pf_vdisplay.rs:43-46`, -`idd_push.rs:351-356,809`). `punktfunk1.rs:2231` calls `crate::vdisplay::sudovda::wait_for_monitor_released` -even when pf-vdisplay is the live backend — benign **today** only because pf-vdisplay preempts inline and -the SudoVDA `MGR` is empty (`pf_vdisplay.rs:645-647`), but it is a fragile cross-static landmine. Plan §9 -(move CCD/adapter helpers into neutral `windows/display_ccd.rs` + `adapter.rs`) is the right fix and is -unstarted. - -### 5.3 P2 — texture-ownership contract is convention, not types - -The §4 in-place-encode hazard is *mitigated* by a host-owned 3-slot `OUT_RING` + -`pipeline_depth().clamp(1, OUT_RING)` (`idd_push.rs:60,867-872`) — sound for the live synchronous loop — -but nothing type-enforces it. `nvenc.rs:7-10` still carries the "safe because the loop is synchronous" -comment, and `repeat_last()` (`idd_push.rs:755-766`) can re-hand an out-ring slot that may still be -encoding under depth>1. Narrow, but it is the residual corruption edge the plan wanted closed type-level. - -### 5.4 P2 — HDR toggle recreates the whole ring mid-session - -`recreate_ring` (`idd_push.rs:582-617`) drops + recreates all 6 keyed-mutex textures on an HDR mode flip, -polled on a 250 ms throttle (`idd_push.rs:622-626`) → up to a 250 ms format-mismatch freeze window where -the driver drops every frame (`frame_transport.rs:256-260`). Works, but heavy and visibly janky. - ---- - -## 6. ABI / proto - -### 6.1 P1 — gamepad SHM was not migrated into proto (the one real drift hazard) - -Plan §3.1 wanted `XusbShm` (64 B) and `PadShm` (256 B incl. `device_type`) in `pf-vdisplay-proto`. They -are hand-duplicated across four sides on two build graphs, with `device_type` as a bare literal `140`: -host `inject/dualsense_windows.rs:45-52` (`OFF_DEVTYPE=140`) vs driver `dualsense-driver/src/lib.rs:753` -(`*view.add(140)`); XUSB host `inject/gamepad_windows.rs:36-47` vs driver `xusb-driver/src/lib.rs`. A -one-sided edit compiles clean on both and silently mis-routes. The `pf-vdisplay` frame/control contract -got compile-error-on-drift; the gamepad contract did not. (The gamepad drivers being standalone cargo -workspaces is the structural blocker — folding them into the unified workspace, M4, fixes both.) - -### 6.2 P2 — proto advertises offset asserts but only has size asserts - -`SharedHeader` (14 mixed-width fields + a `_pad`) is guarded by `size_of == 64` + bytemuck-Pod -(`pf-vdisplay-proto/src/lib.rs:232`), which catches most regressions but not a same-size field reorder. -Add `offset_of!` asserts for `magic/latest/generation/dxgi_format/driver_status` and the `AddReply` LUID -split. - ---- - -## 7. Performance opportunities - -- **Hybrid-GPU cross-adapter copy** (once §4.2 `SET_RENDER_ADAPTER` works): pinning the driver render to - the NVENC GPU removes a cross-adapter staging path entirely — correctness *and* latency. -- **HDR ring recreate** (§5.4) is the heaviest per-session-event op; if the display HDR state is known at - `open()` from the negotiated mode, size the ring right the first time and skip the recreate + 250 ms - window in the common case. -- **Keyed-mutex acquire timeout is 8 ms** on the host consume side (`idd_push.rs:725`) — at 240 Hz - (4.2 ms/frame) one stall already drops ≥2 frames. Reasonable as a safety bound; worth measuring under - load against a tighter value plus an explicit drop counter. -- The encode|send split, microburst pacing, and `pipeline_depth=2` convert/copy-vs-NVENC overlap are - preserved — no regression on the hot path. - ---- - -## 8. Hygiene (Goal 3) - -- **No P0 lints anywhere.** Neither the host crate nor the new driver crates carry - `deny(unsafe_op_in_unsafe_fn)` / `warn(clippy::undocumented_unsafe_blocks)` / - `warn(clippy::multiple_unsafe_ops_per_block)`. The plan claimed the driver workspace "already has it"; - it does not (`pf-vdisplay/src/lib.rs:11` is only `allow(...)`). A few-line, high-leverage first step - before any further unsafe work. -- **`OwnedHandle`/`from_raw_handle` used in zero host files** — the plan's "single biggest cheap win." - `pf_vdisplay.rs` holds a raw `isize` device handle in the pinger thread; `idd_push.rs` holds raw - event/map handles. Obvious first conversions. -- **Unsafe counts moved the wrong way.** Host ~476 (target ~35); new driver ~160 (target for all three - drivers ~60), and the old gamepad drivers are untouched on top of that. - ---- - -## 9. Recommended priority order - -**P0 — correctness/stability, before relying on the path** -1. Make host-gone detection real: implement the watchdog thread **or** `EvtFileClose`, and delete the - false "watchdog" comments. Verify service stop is cooperative (named stop event → `Drop` → - `IOCTL_REMOVE`), not `TerminateProcess`. (§4.1) -2. Implement `SET_RENDER_ADAPTER` (pin driver render to the NVENC adapter) **and** add a real capture - fallback (IDD-push attach failure → DDA) instead of the 20 s black bail. (§4.2, §5.1) - -**P1 — ship-ability + the actual rewrite** -3. Cutover plan: give the new driver an in-tree INF, vendor *its* signed output, flip - `stage-pf-vdisplay.ps1`, and make IDD-push the code default (WGC/DDA fallback) or set - `PUNKTFUNK_IDD_PUSH=1` in `host.env`. Until then the rewrite does not reach users. (§3) -4. Migrate the gamepad SHM into `pf-vdisplay-proto` (kills the `140`-literal drift hazard). (§6.1) -5. Add the P0 lints; convert raw handles to `OwnedHandle`. (§8) - -**P2 — the host-side architecture (Goal 1, the bulk of "rewrite the host")** -6. §2.5 driver ownership refactor (DeviceContext state + `EvtCleanupCallback` + single monitor identity) - — the prerequisite to `max_concurrent>1` on Windows. (§4.3) -7. §9 SudoVDA decoupling (split CCD/adapter helpers into neutral modules), then the §2.2/§2.4 host tree - (`config.rs`/`SessionFactory`) — the clean architecture that was Goal 1. (§5.2) -8. Offset asserts in proto; remove world-writable driver logging; M4 gamepad-driver unification; then M6 - deletion of the old monoliths. (§6.2, §4.4) - ---- - -## Appendix — methodology - -Full read of the new driver (`packaging/windows/drivers/pf-vdisplay/src/*.rs`, `wdk-iddcx/src/lib.rs`) -and `pf-vdisplay-proto`; targeted read of the host IDD-push path (`capture/idd_push.rs`, -`vdisplay/pf_vdisplay.rs`, `capture.rs`, `vdisplay.rs`, `encode.rs`, `encode/nvenc.rs`); structural -grep/diff of plan §2.2/§6/§8/§9/§10 against the on-disk tree; packaging/CI inspection -(`punktfunk-host.iss`, `stage-pf-vdisplay.ps1`, `windows-drivers.yml`, `scripts/windows/host.env.example`). -Unsafe counts are raw `grep -c unsafe` over the relevant subtrees (occurrences, not blocks). Not validated -on hardware — this audit reads code and packaging only; on-glass behavior is per the commit log and -[`docs/windows-host-rewrite.md`](windows-host-rewrite.md) §13–14. diff --git a/docs/windows-host-rewrite-game-capture-bug.md b/docs/windows-host-rewrite-game-capture-bug.md deleted file mode 100644 index 2133042..0000000 --- a/docs/windows-host-rewrite-game-capture-bug.md +++ /dev/null @@ -1,260 +0,0 @@ -# pf-vdisplay: fullscreen game breaks video (IDD-push capture) — issue analysis - -> **Status: FIXED ✅ (2026-06-25).** Resolved by the **resolution-listening recovery** — see -> [Resolution](#resolution-fixed-2026-06-25) below. The investigation that follows is kept as the record -> of how it was diagnosed. Companion to [`windows-host-rewrite.md`](./windows-host-rewrite.md). - -## Resolution (fixed 2026-06-25) - -The fix landed as the **recover-or-drop** design (host-only, **no protocol bump**), *not* the -composing-capturer mid-session failover originally sketched in -[Recommended fix](#recommended-fix-staged): - -- **`c87bfe0` — IDD-push *recovers* from a game mode-set (the "resolution-listening" work).** The ring now - **tracks the display's actual mode**. At open it is sized to the display's real resolution (new - `win_display::active_resolution`, CCD/GDI). Mid-session the 250 ms poll — previously HDR-toggle-only — - now also **follows the active resolution**; on *any* descriptor change (size **or** HDR) it recreates the - ring at the new mode (`recreate_ring` generalized to a new size), the driver re-attaches via the existing - `is_stale()` path, and frames resume at the game's mode. **No freeze, no reconnect.** If a change is - genuinely unrecoverable (e.g. an exclusive flip the host can't follow) a `recovering_since` clock fires - after 3 s and `try_consume` drops the session cleanly so the client reconnects, instead of freezing - forever. A pure idle desktop (no mode change) never triggers it. -- **`f98ab07` — open-time first-frame failover to DDA (GB1 pt 1).** `wait_for_attach` now requires the - driver to publish a *first frame* (not just `DRV_STATUS_OPENED`); a display the driver attaches to but - whose frames its `publish()` guard rejects now fails `open()` within ~4 s → `capture.rs` falls back to - DDA → the game is captured + visible after a reconnect. A normal/idle open (frame within ~1 s) is never - false-failed, and DDA is itself a working path, so even a false positive degrades gracefully. -- **`789ad49` — driver `publish()` width/height guard + a process-lifetime flushed log appender** (GB3 - groundwork): drops a surface whose descriptor no longer matches the host ring (`CopyResource` needs - matching dims too, else garbage) and logs the actual descriptor once per mismatch episode, so the - swap-chain WORKER-thread lines land (closing the bug-doc **S3** observability gap). Needs a driver - rebuild + re-vendor to deploy (separate from the host-only GB1 fix). - -**Why this instead of the composing capturer (original Stage 1):** the host reads the display's real -resolution straight from Windows (CCD/GDI), so it doesn't need the driver to report it over a new -`SharedHeader` field — the original **Stage 2's protocol bump is unnecessary**. In-place recovery keeps the -fast IDD-push (zero-copy) path live *through* a game mode-set instead of permanently demoting to DDA; -open-time DDA failover (`f98ab07`) covers the "display already in a broken mode at connect" case. - -**Deferred (non-blocking):** Stage 3 (trim `default_modes`) — deprioritized (recovery handles mode-sets and -trimming risks the live display-activation path); Stage S driver resilience (S1/S2) — gated on the -`789ad49` logging once a fresh repro is captured. Owner-confirmed the resolution-listening recovery fixes -the user-visible bug (2026-06-25). - -## Context - -The all-Rust `pf-vdisplay` IddCx virtual-display driver (STEP 0–8 of the Windows host rewrite, -now on `main`, on-glass-validated for plain desktop + HDR streaming) breaks when a **fullscreen -game** runs on the stream. - -**Reproduction (RTX 4090 box `192.168.1.158`):** launch *Doom the Dark Ages* while streaming → the -desktop image **flashes** (a display mode-set fired), the game is **never visible**, and **disconnect -+ reconnect yields a black screen with working audio**. (The box was rebooted afterward, so live -logs from the incident are gone.) - -**Runtime config in play** (`C:\ProgramData\punktfunk\host.env`): -- `PUNKTFUNK_IDD_PUSH=1` → capture comes from the driver's **shared-memory frame ring**, not DDA/WGC. -- `PUNKTFUNK_10BIT=1` (+ `PUNKTFUNK_HDR_SHADER_P010=1`) → **HDR active**; the ring is FP16. -- `PUNKTFUNK_MONITOR_LINGER_MS=0` → every (re)connect builds a **fresh** monitor + ring. -- `PUNKTFUNK_VDISPLAY=pf`, `PUNKTFUNK_ENCODER=nvenc`, `PUNKTFUNK_SECURE_DDA=1`. - -The driver log (`C:\Users\Public\pfvd-driver.log`) at inspection showed **8 fresh -`IddCxMonitorCreate`/`Arrival` pairs (ids 1–8), all `0x0`, and ZERO swap-chain-processor lines** — -so monitor creation is healthy and the break is entirely **downstream of monitor creation** -(swap-chain drain / frame publish / host consume), exactly where a game-induced mode change lands. - -## Root cause (one sentence) - -The IDD-push ring is created **once** at session start with a **fixed format and fixed size** -derived from session-start state, there is **no channel for the driver to report the actual -acquired-surface descriptor** back to the host, and there is **no mid-session fallback** — so when -a game forces a format and/or resolution change on the virtual display, the driver silently drops -every frame, the host never learns it needs to adapt, and the stream goes black and then hard-crashes. - -## How the symptom maps to the code - -1. Game launches → forces a **mode set** on the virtual display (the "desktop flash"). This changes - the OS-composed surface's **DXGI format and/or width/height**, and triggers a swap-chain - unassign→reassign in the driver. -2. The driver's `publish()` copies the acquired surface into the host ring **only if formats match - exactly** (`desc.Format` u32 compare) — and `CopyResource` *also* silently requires identical - dimensions, which is never checked. → **every frame dropped.** -3. The host's only ring-recreate trigger is polling Windows' **HDR-enabled toggle**. A game-driven - format/size change it can't observe → **host never recreates the ring** → driver re-attaches to - the same mismatched ring → keeps dropping. -4. Once `PUNKTFUNK_IDD_PUSH=1`, the ring is the **sole** capture source (no DDA/WGC fallback). - `next_frame()` repeats the last good frame, then **`bail!`s after a 20 s deadline → the stream - dies.** -5. **Reconnect stays black** because the game is still holding the display in the changed state; the - fresh ring is rebuilt at the **session-negotiated** format/size again and re-mismatches. Audio is - a fully independent plane, so it survives — matching "black + audio." - ---- - -## Identified issues - -### Primary - -**P1 — IDD-push ring format is fixed at session start; host can't observe a game-driven format change.** -- Host picks the ring format once: FP16 (`DXGI_FORMAT_R16G16B16A16_FLOAT`) if - `advanced_color_enabled(target_id)` else `DXGI_FORMAT_B8G8R8A8_UNORM`. - `crates/punktfunk-host/src/capture/idd_push.rs:340-361` -- Driver drops any frame whose `desc.Format` ≠ the ring format, silently. - `packaging/windows/drivers/pf-vdisplay/src/frame_transport.rs:281-286` -- Host recreates the ring **only** on a Windows HDR-toggle poll (250 ms), never on a format change - it can't see. `idd_push.rs:619-640` (`poll_display_hdr` → `recreate_ring` at `:582-617`). -- Driver re-attaches on a host generation bump (`is_stale`), but nothing bumps it for this case. - `frame_transport.rs:259-270`. -- **No `SharedHeader` field carries the driver's actual acquired-surface format** — the driver only - writes `driver_status`, `driver_status_detail`, `driver_render_luid_low/high` back. - -**P2 — IDD-push ring size is fixed at session start; a resolution change is never detected.** -- `header.width/height` written once at `idd_push.rs:396-397`; ring slots sized once and never - resized; consumed frames always report the session size (`idd_push.rs:744-745`). -- `publish()` guards **format only, not width/height** (`frame_transport.rs:284`). `CopyResource` - requires identical dimensions, so a resolution change → silent no-op/garbage, no error logged. -- Driver never reports the acquired surface's real width/height to the host. - -**P3 — No mid-session capture fallback; a 20 s hard crash instead of degrade.** -- `PUNKTFUNK_IDD_PUSH=1` returns the IDD-push capturer early with the keepalive moved into it — **no - fall-through**. `crates/punktfunk-host/src/capture.rs:348-356`. -- `next_frame()` waits on the frame-ready event (16 ms), repeats the last frame, and **`bail!`s - after a 20 s deadline** → the encode loop tears the session down. - `idd_push.rs:819-847`. -- The WGC→DDA fallback that exists (`capture.rs:389-404`) is **open-time only** and on the - **non**-IDD-push path; it does not help here. -- The `VirtualOutput` already carries a `WinCaptureTarget { adapter_luid, gdi_name, target_id }` - (`vdisplay/pf_vdisplay.rs` `Monitor::target()`), so a DDA/WGC capturer **can** be opened on the - same virtual output — the wiring just doesn't exist for IDD-push. - -### Secondary (verify during the fix; not the proven primary cause) - -**S1 — Driver `run_core` exits permanently on a swap-chain error, with no clear re-arm.** -- On a `ReleaseAndAcquireBuffer2` error (e.g. `DXGI_ERROR_ACCESS_LOST` when a game grabs the - display), `run_core` `break`s and returns; the worker exits and deletes the swap-chain object. - `packaging/windows/drivers/pf-vdisplay/src/swap_chain_processor.rs:359-362` (+ delete at `:141-143`). -- A mode change drives unassign→assign which **does** respawn a fresh processor - (`callbacks.rs:309-318`, `:249-305`), so a clean mode change recovers. **Open question:** whether - the OS reliably re-assigns after a bare `ACCESS_LOST` exit (no unassign), or whether the monitor - stalls with a dead-but-installed processor. Confirm against the IddCx contract / upstream - `virtual-display-rs`. The standard IddCx model expects the OS to re-assign, but this needs proof. - -**S2 — `IddCxSwapChainSetDevice` give-up leaves a dead-but-installed processor.** -- `assign_swap_chain` returns `STATUS_SUCCESS` and installs the processor **before** the worker's - `SetDevice` retries run; if all 60 retries (≈3 s) fail during a mode flap, the worker returns and - the processor is dead, but the OS believes the swap chain is assigned → potential permanent stall. - `swap_chain_processor.rs:191-226`, `callbacks.rs:279-293`. - -**S3 — Driver worker-thread diagnostics are not landing (impairs root-causing).** -- `dbglog!` → `log.rs` opens/append/closes the file per call with **no explicit flush**, and the - observed log had only control-plane (IOCTL-thread) lines, no swap-chain-processor lines. - `packaging/windows/drivers/pf-vdisplay/src/log.rs:9-22`. -- Whatever the exact reason (write race / token / interleave), the practical effect is the - swap-chain processor's behavior during the break is **invisible**, which is why the cause can't be - pinned from logs alone today. **Fix this first** so the next repro is conclusive. - ---- - -## Verified facts that de-risk the fix - -- **The encoder already adapts to a mid-session size/format change.** `encode/nvenc.rs:580-618`: - `submit` detects `size_changed`/`hdr_changed`/device change per frame, tears down, and re-inits - adopting the new frame's geometry + pixel format. So a capturer that changes resolution/format - mid-session is handled downstream — **no encoder API change is needed** for either fix direction. -- **The stream loop relays per-frame geometry.** `CapturedFrame` carries `width`/`height`/`format` - (`capture.rs:50-57`); the loop reads `pipeline_depth()` live and forwards whatever `try_latest()` - returns. -- **WGC and DDA emit the same pixel formats the IDD-push path emits** (`Bgra` / `Rgb10a2`), so a - failover capturer feeds the encoder compatible frames. -- **A failover capturer fits the existing `Capturer` trait** (`next_frame` + `try_latest`, - `capture.rs:120-155`) — a composing capturer that owns the ring capturer + a lazily-opened - WGC/DDA capturer and switches between them is a clean drop-in. - ---- - -## Recommended fix (staged) - -> **Superseded — see [Resolution](#resolution-fixed-2026-06-25).** This was the original plan; the bug -> was fixed by the simpler **recover-or-drop** approach (host follows the OS resolution + open-time DDA -> failover), so Stage 1's composing capturer and Stage 2's protocol bump were not needed. Kept for context. - -Defense-in-depth. Stages 0–1 are **host-only** (no driver rebuild, no protocol bump) and are the -fast, robust, user-visible fix. Stages 2–3 harden the fast path and need the driver re-vendor loop. - -- **Stage 0 — Diagnostics first (land before anything else).** - - `log.rs`: flush after each write (or keep a process-lifetime appender) and confirm worker-thread - writes land. (S3) - - Driver: in `publish()`, log/record the acquired surface's **actual format + width + height** - even on the drop path, so a repro shows exactly what changed. - - Host: replace the silent 20 s wait with a `tracing::warn!` at ~2 s of no fresh frame, including - `driver_status`/`driver_status_detail` and the host's expected ring format/size. - - Goal: the next Doom-launch repro definitively classifies the cause (format mismatch vs size - mismatch vs `run_core` exit vs no-reassign). - -- **Stage 1 — Mid-session fallback IDD-push → WGC/DDA (robust to ALL failure modes).** (P3) - - Add a composing `Capturer` that owns the IDD-push capturer and, when it yields no fresh frame - for a **short** window (~1.5 s, not 20 s), opens a DDA/WGC capturer on the same - `WinCaptureTarget` and serves from it for the rest of the session (optionally probing the ring - for recovery). Encoder follows the new format/size automatically (verified above). - - This alone guarantees the session never goes permanently black again and makes Doom playable via - WGC/DDA when the ring path is defeated — independent of the *why*. - - Touch points: `capture.rs:334-356` (wire the composing capturer behind `PUNKTFUNK_IDD_PUSH`), - `idd_push.rs` (expose a "stalled?" signal + shorten the deadline), reuse `dxgi.rs`/`wgc.rs`. - -- **Stage 2 — Adaptive ring (makes the fast IDD-push path itself survive a game mode change).** (P1, P2) - - Driver writes the **actual acquired-surface format + width + height** into new `SharedHeader` - fields, in `publish()`, **even when about to drop the frame**. - - Host watches those fields and, on any change vs the ring's current format/size, **recreates the - ring at the new descriptor + bumps `generation`** (generalize `recreate_ring`/`poll_display_hdr` - from "HDR toggled" to "descriptor changed"). Driver re-attaches via existing `is_stale()`. - - Driver `publish()` gains a **width/height guard** alongside the format guard. - - **Implications:** bump `pf_vdisplay_proto::PROTOCOL_VERSION` (host does a HARD version check in - `pf_vdisplay.rs::mgr_ensure_device`), update the `const` size/offset asserts in - `crates/pf-vdisplay-proto/src/frame.rs`, and deploy host + driver **in lockstep** (rebuild + - re-sign + re-vendor `packaging/windows/pf-vdisplay/{dll,inf,cat}` on the RTX box, WUDFHost - reload). - -- **Stage 3 — Prevention (frequency reducer, not a standalone fix).** (reduces P1/P2 triggers) - - Trim `monitor.rs::default_modes()` so the IDD advertises essentially only the negotiated mode, so - a game can't pick a different fullscreen resolution. Verify it doesn't break mid-stream - `Reconfigure`. Optionally re-assert the active mode after a detected mode change. - -- **Stage S — Driver resilience (address S1/S2 once Stage 0 reveals if they fire).** - - If logs show a permanent stall after `ACCESS_LOST`/SetDevice-give-up, add a re-arm path (e.g. - delete the swap chain so the OS re-assigns, or signal `assign_swap_chain` to retry) and avoid - installing a processor that has already failed `SetDevice`. - -## Validation plan (RTX box `ssh "Enrico Bühler@192.168.1.158"`) - -1. Deploy the Stage-0 host (+ driver if rebuilt); `punktfunk-host service stop/start`. -2. Connect a client, confirm normal stream. `type C:\Users\Public\pfvd-driver.log` to baseline. -3. Launch *Doom the Dark Ages* (or any fullscreen/HDR game). Capture: driver log + host service log - (find where the in-session `serve` logs land; `RUST_LOG=info`). -4. Read which mechanism fired (format/size/exit/no-reassign) from the Stage-0 diagnostics. -5. **Success:** game is visible, the stream survives the mode-set flash, no 20 s crash, reconnect - restores video. With Stage 1: the failover to WGC/DDA is logged and frames keep flowing. With - Stage 2: the ring recreates at the new descriptor and the fast path resumes. - -## File map - -| Area | Path | -|---|---| -| Host ring consumer | `crates/punktfunk-host/src/capture/idd_push.rs` | -| Capture selection / trait | `crates/punktfunk-host/src/capture.rs` | -| NVENC re-init (no change needed) | `crates/punktfunk-host/src/encode/nvenc.rs:564-618` | -| DDA / WGC capturers (failover targets) | `crates/punktfunk-host/src/capture/{dxgi,wgc}.rs` | -| Host monitor lifecycle / capture target | `crates/punktfunk-host/src/vdisplay/pf_vdisplay.rs` | -| Shared contract (Stage 2 fields + version) | `crates/pf-vdisplay-proto/src/{lib,frame}.rs` | -| Driver frame publisher (guards + reporting) | `packaging/windows/drivers/pf-vdisplay/src/frame_transport.rs` | -| Driver swap-chain lifecycle (S1/S2) | `packaging/windows/drivers/pf-vdisplay/src/swap_chain_processor.rs`, `callbacks.rs` | -| Driver logging (S3) | `packaging/windows/drivers/pf-vdisplay/src/log.rs` | -| Advertised modes (Stage 3) | `packaging/windows/drivers/pf-vdisplay/src/monitor.rs` (`default_modes`) | -| Vendored signed driver (Stage 2 re-vendor) | `packaging/windows/pf-vdisplay/{pf_vdisplay.dll,.inf,.cat}` | - -## Notes / caveats - -- Doc lag (unrelated to the fix, worth flagging): `stage-pf-vdisplay.ps1` / packaging comments still - reference the OLD `packaging/windows/vdisplay-driver/` tree; the active driver source is the NEW - `packaging/windows/drivers/pf-vdisplay/` tree (re-vendored in commit `a11b0dd`). -- The exact trigger (format vs resolution vs exclusive-flip vs processor-death) is **not yet proven - from logs** — Stage 0 exists to pin it. Stage 1 fixes the user-visible symptom regardless. diff --git a/docs/windows-host-rewrite-remediation.md b/docs/windows-host-rewrite-remediation.md deleted file mode 100644 index 2c94765..0000000 --- a/docs/windows-host-rewrite-remediation.md +++ /dev/null @@ -1,168 +0,0 @@ -# Windows Host Rewrite — Audit Remediation Tracker - -Status: **in progress** (2026-06-25). Living hand-off doc for working through the findings in -[`docs/windows-host-rewrite-audit.md`](windows-host-rewrite-audit.md) (the audit of the IDD-push rewrite -vs [`docs/windows-host-rewrite.md`](windows-host-rewrite.md)). Keep this updated as items land so the work -can be handed off without losing tasks. - -## TL;DR - -- **9 commits on `main`, NOT pushed** (`+9` ahead of `origin/main`, tip `e60cda3`). Each is compile-verified - on the RTX box (see [Verification](#verification)). -- **Done:** the entire audit **P0 + P1 + P2** payload, the driver `unsafe` lint, and **F1** (SudoVDA helper - decoupling) complete. -- **Remaining:** **D2** (OwnedHandle), **D1-host** (unsafe-lint sweep), **E1** (driver ownership refactor), - **G** (gamepad-driver unification + old-tree deletion + host `src/windows/` tree). -- **Two cross-cutting follow-ups:** (1) **on-glass behavioral validation** of the committed driver/host - fixes (the box is single-GPU + headless-ish, so hybrid-GPU / HDR-toggle / fallback paths weren't - exercised at runtime); (2) **push** to run the full CI matrix (the local checks skip the `amf-qsv` path). - -## Done — committed on `main` (unpushed) - -| Commit | Audit § | What | Compile-verified | -|---|---|---|---| -| `0badc17` | — | The audit doc itself | — | -| `95dcef3` | §6.1/6.2 | **A** proto: `offset_of!` asserts on `SharedHeader`/`AddReply`/control structs; owned `XusbShm`/`PadShm` gamepad layouts (+ `min_const_generics`) | local `cargo test` + MSVC (box) | -| `0a7ae5e` | §4.1/4.2/4.4/4.5 | **B** driver: real host-gone **watchdog** (was dead code), **`SET_RENDER_ADAPTER`** impl, world-writable-log gate, mode bounds + `display_info` u64-saturate | driver `cargo build` (box) | -| `e5c9ee8` | §4.2h/6.1 | **C2/C5** host: render-pin comment/activation (driver now honors it); gamepad SHM consumers derive from `pf_vdisplay_proto::gamepad` | host clippy (box) | -| `ed58365` | §5.1 | **C1** host: IDD-push **attach fallback to DDA** (open() hands keepalive back; bounded `wait_for_attach` on `DRV_STATUS_OPENED`) instead of the 20s black bail | host clippy (box) | -| `b0d2838` | §5.3/5.4 | **C3/C4** host: `repeat_last` rotates+copies into a fresh out-ring slot; HDR ring sized FP16 at open when advanced-color is enabled | host clippy (box) | -| `a755d6e` | §8 | **D1-driver** `#![deny(unsafe_op_in_unsafe_fn)]` on `pf-vdisplay` + `wdk-iddcx` | driver `cargo build` (box) | -| `d638a93` | §9 | **F1 pt1**: `resolve_render_adapter_luid` → neutral `crate::win_adapter` | host clippy (box) | -| `e60cda3` | §9 | **F1 rest**: 6 CCD/HDR helpers + `SavedConfig` → neutral `crate::win_display`; SudoVDA reach-in fully broken | host clippy (box) + Linux `cargo check` | - -## Remaining — to do - -Ordered by suggested sequence. **On-glass = cannot be *finished* without a real session on the RTX box, -driven by a human** (driver install + client connect). - -### D2 — `OwnedHandle` on the new path · audit §8 · compile-verifiable · moderate -- **Goal:** replace raw `HANDLE`/`isize` handles held across their lifetime with - `std::os::windows::io::OwnedHandle` (RAII close, fixes leak-on-error, deletes manual `CloseHandle`). -- **Targets:** `vdisplay/pf_vdisplay.rs` — the pinger thread's raw `isize` device handle (`pf_vdisplay.rs` - ~324-344); `capture/idd_push.rs` — `IddPushCapturer { map, event, dbg_map: HANDLE }` (manually closed in - `Drop`). The plan also lists events/jobs/tokens/sections in `windows/process.rs`/`service.rs` (broader). -- **Risk:** handle ownership (double-close / premature close). Compile catches type errors; lifecycle - needs care. Touches the live IDD-push path → ideally smoke-tested on glass after. -- **Verify:** host clippy on the box (the new path is `--features nvenc`). - -### D1-host — host-wide `unsafe` lint sweep · audit §8 · large/mechanical -- **Goal:** add `#![deny(unsafe_op_in_unsafe_fn)]` + `#![warn(clippy::undocumented_unsafe_blocks)]` - (+ optionally `multiple_unsafe_ops_per_block`) to the **host crate** (`crates/punktfunk-host/src/main.rs`), - and fix the fallout. -- **Scope:** large — hundreds of `unsafe` blocks across **both** Linux and Windows code need explicit - `unsafe {}` wrapping inside `unsafe fn`s and `// SAFETY:` comments. The driver already has the `deny` - (`a755d6e`); the host has none. -- **Verify:** Linux `cargo clippy -p punktfunk-host --all-targets -- -D warnings` (Linux/cross paths) **and** - host clippy on the box (Windows paths). Do it incrementally per-subsystem to keep the diff reviewable. - -### E1 — driver ownership refactor · audit §4.3 / plan §2.5 + §14 step 5 · **on-glass-gated** · large -- **Goal:** move the driver's process-global statics (`MONITOR_MODES`, `NEXT_ID`, `ADAPTER`, `DEVICE_POOL`) - into a WDF `DeviceContext`; **wire `EvtCleanupCallback` on the `IDDCX_MONITOR` object** so the - `SwapChainProcessor` + D3D drop via RAII; collapse the 3-key monitor identity (`id`/`object`/`session_id`) - to one. Unblocks `max_concurrent>1` on Windows + removes the host-side preempt dance. -- **Why on-glass:** the plan's critique is explicit — *instrument that `MonitorContext::Drop` actually - RAN*; if the cleanup callback does not fire on this UMDF/IddCx stack, **keep the current explicit - REMOVE/teardown path as the fallback**. Cannot be signed off compile-only. -- **Files:** `packaging/windows/drivers/pf-vdisplay/src/{entry,adapter,monitor,callbacks,swap_chain_processor}.rs`. -- **Verify:** driver `cargo build` (compile) on the box; then on-glass reconnect-storm + leak check - (`LIVE_DEVICES` counter in `direct_3d_device.rs`, the world-readable log when `PFVD_DEBUG_LOG` is set). - -### G — gamepad-driver unification (M4) + deletion (M6) + host tree · audit §6/§10 + plan §2.2 · **on-glass-gated** · largest -- **M4:** fold `pf_dualsense` + `pf_xusb` (today standalone `packaging/windows/{dualsense,xusb}-driver/` on - the old `wdf` stack) into the unified `packaging/windows/drivers/` workspace on `windows-drivers-rs`. This - also enables the **driver-side** gamepad-SHM→proto switch (host side already done in C5 — the driver still - hand-reads `view.add(140)`; point it at `pf_vdisplay_proto::gamepad::PadShm`/`XusbShm`). -- **M6:** delete the old `packaging/windows/vdisplay-driver/` tree + the old gamepad driver trees + the - bring-up scaffolding (`DebugBlock`/`spawn_observer`/`IDD_PERSIST`/`open_or_reuse` in `idd_push.rs`) — **only - after on-glass parity** of the new path. -- **Host architecture (Goal 1, plan §2.2/2.4):** the `src/windows/` subtree + `config.rs` (`HostConfig`) + - `SessionFactory`/`SessionPlan` — **not started**. The biggest clarity lever; large. - -### Cross-cutting follow-ups (not a single task) -- **On-glass validation of the committed fixes** — needs the RTX box + a client. Specifically: the - **watchdog** actually reaps on host-kill (B1); **`SET_RENDER_ADAPTER`** pins correctly on a *hybrid* box - (B2/C2 — the lab box is single-dGPU, so this path is unexercised); the **IDD-push→DDA fallback** triggers - + the happy path still attaches within 4s (C1); **HDR ring sizing** + **out-ring repeat** under real HDR / - static-desktop pipelining (C3/C4). -- **Push** to run the full CI matrix — the local host checks use `--features nvenc` only (no FFmpeg), so the - `amf-qsv` encode path is unexercised locally; CI (`windows-host.yml`) covers it. - -## Related workstream — fullscreen-game IDD-push capture bug (separate doc) - -A **separate, newly-found bug** (NOT an audit finding) in the same IDD-push subsystem, with its own staged -fix plan: [`docs/windows-host-rewrite-game-capture-bug.md`](windows-host-rewrite-game-capture-bug.md). -**Symptom:** launching a fullscreen game (Doom the Dark Ages) on an HDR IDD-push stream flashes the desktop, -the game never shows, and reconnect = black screen + working audio. **Root cause:** the IDD-push ring is -fixed format+size at session start; the driver silently drops every frame whose surface descriptor no longer -matches (a game forces a mode-set); the host has no channel to learn the descriptor changed; and there is no -mid-session fallback → 20 s `bail!`. - -**Intersections with this remediation — read before implementing:** -- **Stage 1 builds on our C1 (`ed58365`); do not duplicate it.** C1 added an IDD-push→DDA fallback, but - **open-time only** (driver never attaches). The game bug is **mid-session** (attached, then a game changes - format/size). The bug doc's Stage 1 (a composing capturer that fails over mid-session) is the - generalization — build it on C1's `open()`-returns-keepalive + bounded-attach infrastructure. -- **The bug doc was written against pre-remediation `main` (`a11b0dd`).** Its line numbers and its claim - "`capture.rs:348-356` … no fall-through" are **stale after our 9 commits** (C1 changed exactly that). - Rebase on current `main` first. -- **Stage 2 (new `SharedHeader` fields + `PROTOCOL_VERSION` bump)** must update the **`offset_of!`/size - asserts added in A (`95dcef3`)** — they catch drift at compile time (the intended safety net). Note: those - asserts live in the `frame` module of `crates/pf-vdisplay-proto/src/lib.rs` (the doc says `frame.rs`). -- **Stage 0 / S3 diagnostics rely on the driver log**, which **B3 (`0a7ae5e`) gated off in release builds** - (`debug_assertions || PFVD_DEBUG_LOG`). Enable it (`PFVD_DEBUG_LOG=1` or a debug build) for the repro. -- **S1/S2 (driver swap-chain resilience)** is adjacent to **E1** (same `swap_chain_processor.rs`/ - `callbacks.rs`); coordinate so they don't conflict. -- The bug doc's "doc-lag" note (`stage-pf-vdisplay.ps1` still names the old `vdisplay-driver/` tree) is part - of our **G / M6** packaging cleanup. - -**Stages (detail in the bug doc):** Stage 0 diagnostics (S3) → Stage 1 mid-session fallback (P3, host-only, -the user-visible fix) → Stage 2 adaptive ring (P1/P2; proto bump + driver re-vendor) → Stage 3 trim -advertised modes → Stage S driver resilience (S1/S2). Tracked as GB0–GB3 in the task list. - -**Progress (2026-06-25):** **GB1 landed host-side** — *recover-or-drop, no DDA* (per the owner's call): the -ring now tracks the display's ACTUAL mode (CCD `active_resolution`), recreating on a size/HDR change so a -game mode-set recovers in-place; if no frame resumes within 3 s it drops the session cleanly (client -reconnects). Commits `f98ab07` (first-frame failover) + `c87bfe0`. **Awaiting on-glass Doom validation.** -**GB3 groundwork landed** — driver `publish()` width/height guard + descriptor-on-drop logging + a flushed -process-lifetime log appender so the swap-chain worker's lines land (commit `789ad49`); **needs a driver -rebuild + re-vendor to deploy.** Stage 3 (trim modes) deprioritized; Stage S code-fix gated on these -diagnostics showing whether S1/S2 fire on-glass. - -## Verification - -The persistent validator is the **RTX box** `ssh "Enrico Bühler"@` (ENRICOS-DESKTOP, RTX 4090, -PS shell). **The IP FLOATS — DHCP + boots to Proxmox on reboot (new lease each time); recently `.173` / -`.158`, confirm the current IP first. EPHEMERAL — never reboot it, never depend on it surviving.** It has -WDK 26100 + LLVM 21.1.2 + the Rust toolchain. Build clone: `C:\Users\Public\pf-rewrite`. - -```sh -# 0. (local, cross-platform) the proto crate + the Linux host build -cargo test -p pf-vdisplay-proto -cargo check -p punktfunk-host # Linux paths; the win_* mods are #[cfg(windows)] - -# 1. reset the box clone to a clean base, then overlay your changed files -# ssh ... "cd C:\Users\Public\pf-rewrite; git fetch -q origin; git reset -q --hard origin/main; git clean -qfd; git checkout -q " -# scp "Enrico Bühler@:C:/Users/Public/pf-rewrite/" - -# 2. host clippy (warm target ~4s). NVENC import lib at C:\t\nvenc; no FFmpeg needed (amf-qsv off). -ssh ... "cd C:\Users\Public\pf-rewrite; $env:PUNKTFUNK_NVENC_LIB_DIR='C:\t\nvenc'; \ - cargo clippy -p punktfunk-host --features nvenc --target x86_64-pc-windows-msvc -- -D warnings" - -# 3. driver workspace build (fires deny(unsafe_op_in_unsafe_fn)); ~5s -ssh ... "cd C:\Users\Public\pf-rewrite\packaging\windows\drivers; \ - $env:Version_Number='10.0.26100.0'; $env:LIBCLANG_PATH='C:\Program Files\LLVM\bin'; cargo build" -``` - -Gotchas: the box username has a `ü` → quote it; PS shell, filter output with `Select-Object -Last N`. After -a `git reset --hard` on the box clone, re-`scp` your working files (reset discards them). Do **not** build in -`C:\Users\Public\punktfunk-native` (the deployed host). - -## New modules introduced by this work - -- `crates/pf-vdisplay-proto/src/lib.rs` → added `mod gamepad` (`XusbShm`/`PadShm`/magics/name helpers) + - `offset_of!` asserts. -- `crates/punktfunk-host/src/win_adapter.rs` → `resolve_render_adapter_luid` (plan's `windows/adapter.rs`). -- `crates/punktfunk-host/src/win_display.rs` → CCD/HDR display helpers (plan's `windows/display_ccd.rs`). -- Driver: `start_watchdog`/`reap_orphaned` (control.rs/monitor.rs), `set_render_adapter` (adapter.rs), - `file_log_enabled` gate (log.rs). diff --git a/docs/windows-host-rewrite.md b/docs/windows-host-rewrite.md index b158071..e94ee7f 100644 --- a/docs/windows-host-rewrite.md +++ b/docs/windows-host-rewrite.md @@ -1,843 +1,397 @@ -# Windows Host Rewrite — Design & Plan +# Windows Host — Architecture, Status & Roadmap -Status: **largely implemented** (updated 2026-06-25 — see [§15 Current status](#15-current-status-2026-06-25) -for the milestone-by-milestone state; §0–§14 below are the original design and remain the reference). This -plan takes the current, hard-won Windows host (pf-vdisplay all-Rust IddCx driver + IDD-push zero-copy -capture, live-validated 5120×1440@240 HDR on the RTX box) as a *knowledge base* and re-derives a clean, -stable, well-layered architecture from it. It drops all SudoVDA back-compat (we own both ends now) and -drives `unsafe` to a contained minimum. - -It supersedes the stale conclusion in `docs/windows-virtual-display-rust-port.md` ("IDD-push not -viable") — that verdict was written in the *same commit* (`e2c9bfd`) that shipped the working -922-line consumer + 424-line producer. **IDD-push works and is the architecture.** The breakthrough the -prose never recorded: once the CCD topology makes the virtual display the sole composited desktop in the -console session, DWM composites to it and the IddCx swap-chain *is* assigned -(`run_core: FIRST FRAME acquired — DWM IS compositing the virtual display!`). Per the owner, **IDD-push -also captures the secure desktop (Winlogon / UAC / lock)** — so it is the universal primary path, not -just the normal-desktop path. - -### Decisions resolved (2026-06-24) - -| # | Decision | Chosen | -|---|----------|--------| -| A. Execution | greenfield vs staged | **Greenfield rewrite** — rebuild the Windows host fresh against the clean architecture, salvaging the validated "jewels" (§1) verbatim. (Risk acknowledged: no CI for the Windows paths — mitigated by the §1 preservation checklist + on-glass gates, §10.) | -| B. Capture surface | IDD-only / IDD+secure-DDA / keep fallbacks | **IDD-push primary for everything (incl. the secure desktop); keep WGC + DDA as fallbacks.** | -| C. Driver binding stack | wdf-umdf vs windows-drivers-rs | **Extend `microsoft/windows-drivers-rs`** with an `iddcx` subset; unify all three drivers on it; **solve `/INTEGRITYCHECK` properly** (§6). | -| D. GameStream on Windows | keep / keep-secure-default / drop | **Keep Moonlight compat; flip the installer/service default to secure `serve`** (GameStream an explicit opt-in). | +> **Single source of truth** for the punktfunk Windows streaming host: the all-Rust **`pf-vdisplay` +> IddCx virtual-display driver** + **IDD-push zero-copy capture** + **NVENC/AMF/QSV encode**, shipped as +> a signed Inno Setup installer with a LocalSystem SCM service. Live-validated on the RTX box through +> 5120×1440@240 HDR, the secure desktop (lock/UAC), and a fullscreen game. +> +> This file **consolidates and replaces** five earlier docs (now retired into it): the rewrite design +> plan, the Goal-1 staged-refactor plan, the audit, the audit-remediation tracker, and the +> fullscreen-game capture-bug analysis. See the [consolidation note](#appendix--consolidation-note) for +> what moved where. **Last updated 2026-06-25.** Work lives on branch **`windows-host-goal1`** (off +> `main`, not yet merged). --- -## 0. Goals (from the brief) - -1. **Clean, stable, well-layered architecture.** Decompose the god-files, give every subsystem one - owner, and replace the ~40-knob `PUNKTFUNK_*` env soup with a typed config resolved once per session. -2. **Drop every trace of SudoVDA back-compat.** We own the driver (`pf-vdisplay`) and the host. The - byte-identical IOCTL ABI, the reused `{e5bcc234}` GUID, the `sudovda` module name, the "SudoVDA - ignores this" conditionals — all pure liability now. -3. **Minimize `unsafe`.** ~480 `unsafe` occurrences across the Windows surface; the large majority are - FFI-mechanical (windows-rs/NVENC/WDK already return `Result`). Target: host ~144→~35, drivers - ~227→~60, with the irreducible floor *contained* in 3–4 named modules under - `deny(unsafe_op_in_unsafe_fn)`. - -### Non-goals / invariants (do not regress) - -- **Linux host behavior is out of scope and must not change.** The host crate is shared; Linux is - validated across KWin/gamescope/Mutter/Sway. Touch only the seams. -- **`punktfunk-core` stays the one linked core.** Protocol/FEC/crypto/QUIC live there behind the C - ABI; the host is a leaf binary. No protocol changes here. -- **No async on the per-frame path.** Native threads only (the existing discipline). - ---- - -## 1. What we KEEP (validated, load-bearing — port, don't rewrite) - -These are expensive empirical wins. The rewrite relocates/wraps them but must preserve behavior -byte-for-byte: - -- **The IDD-push frame transport shape**: host-creates / driver-opens shared keyed-mutex texture ring - with the permissive `D:(A;;GA;;;WD)` SDDL (forced by the restricted WUDFHost token, mirrors the - gamepad drivers); the generation-tagged `latest = gen<<40 | seq<<8 | slot` stale-ring reject (kills - the HDR-flip garbage frame); 0 ms try-acquire / drop-on-full publish (never block the swap-chain - thread); the host output ring `OUT_RING` + `pipeline_depth=2` overlap of convert/copy vs NVENC. -- **The IddCx driver internals that earned their keep**: `edid.rs` in full (128-byte EDID + CTA-861.3 - HDR block, serial-as-index round-trip, dual checksums); the HDR enablement recipe (`CAN_PROCESS_FP16` - + the `*2` mode DDIs + `set_gamma_ramp`/`set_default_hdr_metadata` accept-stubs + `HIGH_COLOR_SPACE` + - 8|10 bpc); `DEVICE_POOL` one-device-per-render-LUID (the NVIDIA UMD-thread/VRAM leak fix); stamping - the OS target id onto the monitor context (the recreated-monitor `target_id=0` fix); the swap-chain - processor's two real leak fixes (borrow `IDXGIDevice` across `SetDevice` retries; check `terminate` - at the loop top during a frame burst). -- **The monitor-lifecycle concurrency correctness**: serialized ADD/REMOVE/teardown, the documented - lock order, the watchdog CAS + re-check-under-lock, the creation grace window, the - generation-stamped lease (a stale lease can't tear down a fresh monitor). *Structure* can change; - these properties must survive. -- **The CCD topology fixes**: `isolate_displays_ccd` (the iGPU-attached-monitor hybrid-box correctness; - the `SDC_FORCE_MODE_ENUMERATION` re-commit that drives `COMMIT_MODES → ASSIGN_SWAPCHAIN`); restore - topology *before* REMOVE. -- **The HDR color math**: `hdr.rs` verbatim (pure, unit-tested, ST.2086 G/B/R + big-endian SEI); - `HdrConverter`/`HdrP010Converter` + the f64 `p010_reference` + `hdr_p010_selftest`; `VideoConverter` - (RGB→NV12/P010 on the video engine — a measured latency win); the cursor decomposition - (`convert_pointer_shape` color/masked/monochrome edge cases). -- **NVENC tuning**: caps-probe-before-configure (disambiguate unsupported-config vs too-high-bitrate; - 10-bit→8-bit graceful downgrade); the bitrate-clamp binary search (finds each GPU's real ceiling); - true RFI over the DPB; the low-latency configs (CBR, infinite GOP, P-only, ~1-frame VBV). -- **The gamepad driver wins**: the SwDeviceCreate identity recipe (enumerator with no `_`; mandatory - completion callback; synthesized `USB\VID_054C&PID_0CE6` compat-ids for native-DS5 detection; the - non-null per-pad `ContainerId` dodging the xinput1_4 slot-skip); one `pf_dualsense` serving - DualSense+DS4 via a `device_type` byte; XUSB declining `WAIT_*` to force synchronous `GET_STATE`; - the static HID descriptors/feature blobs; per-pad index via `pszDeviceLocation`. -- **The session-glue patterns**: the `Capturer`/`VirtualDisplay`/`Encoder` trait seam + RAII keepalive - teardown; host-lifetime shared services (`InjectorService`/`MicService`/`AudioCapSlot`) with - per-session gamepads; the encode|send thread split + microburst pacing; `build_pipeline_with_retry` - + permanent-vs-transient classification; the control-task `select!` + adaptive-FEC; the GameStream - `VideoPacketizer` (GF8 Cauchy, Moonlight byte-exact); the pairing/trust handshake. -- **The SCM supervisor model**: Session-0 LocalSystem supervisor → token-retarget → - `CreateProcessAsUserW` `serve` into the console session, relaunch-on-session-change, kill-on-close - Job Object; the file-append log-mask; the two-tier logging init. -- **Build/CI wins**: the `wdf-umdf-sys` build.rs SDK-version resolution (picks the SDK version that - actually contains `iddcx`, not the max base SDK); the ARM64 cross-compile off the x64 runner; the - thin-.iss / fat-binary installer delegating to `service install`. - ---- - -## 2. Target architecture - -### 2.1 Crate & workspace strategy - -**Keep ONE shared `crates/punktfunk-host` crate** (do *not* split `punktfunk-host-windows`). The host is -a leaf binary consumed by nobody; the "one core, linked everywhere" invariant is already satisfied by -`punktfunk-core`. A split would only fork the genuinely-shared session glue, traits, and `hdr.rs`. The -cfg-sprawl win comes instead from confining all Windows code under one `src/windows/` subtree behind a -single `#[cfg(windows)] mod windows;` seam, with backend impls next to their trait's dispatch point. - -**Pull the three drivers into ONE in-tree driver workspace** (`packaging/windows/drivers/`) on a single -binding stack, one `rust-toolchain.toml`, one signing recipe, one CI build. Today they are 2–3 disjoint -cargo packages on two incompatible WDK stacks (see §6). - -**Add ONE shared `no_std` ABI crate** (`crates/pf-vdisplay-proto`, name TBD) consumed by both the host -crate and the driver workspace. It owns *every* cross-process binary contract that is currently -hand-duplicated with "must match" comments. This is the single highest-value correctness change (§4.1). - -### 2.2 Target file tree (host crate) - -``` -crates/punktfunk-host/src/ - main.rs clap-derive subcommand dispatch only (kills parse_serve/parse_spike/hand --help) - config.rs HostConfig (typed; parsed ONCE from host.env/env/flags) + config_dir - session/ - mod.rs SessionFactory, SessionPlan, SessionContext, Session (the ONLY teardown path) - server.rs QUIC accept loop, handshake, shared-service wiring - serve_session.rs resolve_* → Welcome/Start → spawn → RAII teardown - control.rs mid-stream renegotiation select! loop - pipeline.rs REAL shared encode|send split, send_loop, FrameMsg, pacing (used by native AND GameStream) - capture.rs Capturer trait + CapturedFrame/PixelFormat/FramePayload (platform-neutral) - capture/linux.rs - capture/windows/ mod.rs (dispatch), idd_push.rs, dda.rs, wgc.rs, secure_desktop.rs* - vdisplay.rs VirtualDisplay/VirtualOutput trait + open() dispatch (neutral) - vdisplay/{kwin,gamescope,mutter,wlroots}.rs - vdisplay/windows.rs was sudovda.rs → PfVirtualDisplay + VirtualDisplayManager - encode.rs Encoder trait, EncodedFrame, validate_dimensions, open_encoder dispatch - encode/{linux,vaapi,sw}.rs - encode/windows/ mod.rs (dispatch), nvenc.rs, nvenc_sys.rs, ffmpeg_win/{mod,system,zerocopy,d3d11va_ffi}.rs - hdr.rs PRESERVE VERBATIM - inject.rs / inject/linux/* / inject/windows/{mod,sendinput,pad_manager,xusb,dualsense,dualshock4,swdevice,section}.rs - inject/proto/{dualsense,dualshock4}.rs shared pure codecs (PRESERVE) - audio.rs / audio/linux.rs / audio/windows/{mod,wasapi_cap,wasapi_mic}.rs - windows/ mod.rs, d3d/{mod,texture,ring,convert}.rs, color/{hdr,p010,video_proc}.rs, - cursor.rs, display_ccd.rs, adapter.rs, process.rs (Token/Event/Job/Child/spawn_as_user), - service.rs (SCM; uses process.rs), win32u_hook.rs*, gpu_priority.rs - session_tuning.rs (PRESERVE) / pwinit.rs / discovery.rs / mgmt.rs / native_pairing.rs / library.rs - gamestream/ unchanged module set; stream.rs slims by reusing session/pipeline.rs -``` -`*` = survives only per the secure-desktop / WGC product decisions (§5, §11). - -### 2.3 The seam traits (keep the shape; tighten 3 things) - -```rust -trait VirtualDisplay: Send { - fn name(&self) -> &str; - fn create(&self, mode: Mode) -> Result; - fn set_launch_command(&self, cmd: Option); // per-instance, not a global env var -} -struct VirtualOutput { - node_id: u32, - preferred_mode: Mode, - #[cfg(windows)] win_capture: WinCaptureTarget, // target_id + adapter_luid + monitor_gen (carried, not ambient) - keepalive: Box, -} -trait VirtualLease: Send { // Drop = release; replaces the sudovda free-fns + CURRENT_MON_GEN reach-in - fn set_hdr(&self, on: bool) -> Result<()>; - fn hdr_enabled(&self) -> bool; - fn await_released(&self, timeout: Duration) -> bool; -} - -trait Capturer: Send { - fn next_frame(&mut self) -> Result; - fn try_latest(&mut self) -> Option; - fn set_active(&mut self, a: bool); - fn hdr_meta(&self) -> Option; - fn pipeline_depth(&self) -> usize; -} -fn open_capturer(vout: VirtualOutput, want: OutputFormat) -> Result>; // format+HDR passed IN - -trait Encoder: Send { - fn submit(&mut self, f: &CapturedFrame) -> Result<()>; - fn poll(&mut self) -> Option; - fn flush(&mut self); - fn request_keyframe(&mut self); - fn caps(&self) -> EncoderCaps; // query, don't rely on default no-ops - fn set_hdr_meta(&mut self, m: Option); - fn invalidate_ref_frames(&mut self, lo: u64, hi: u64) -> bool; -} -fn open_encoder(plan: &EncodePlan) -> Result>; - -trait AudioCapturer: Send { fn next_chunk(&mut self) -> Result>; fn channels(&self) -> u16; fn drain(&mut self); } -trait VirtualMic: Send { fn push(&mut self, pcm: &[f32]); fn channels(&self) -> u16; } -trait InputInjector: Send { fn inject(&mut self, e: &InputEvent); } -trait PadManager: Send { /* handle/apply_rich/pump/heartbeat — Box via select(GamepadPref), replaces the PadBackend enum */ } -``` - -The three tightenings: (1) `Capturer` takes the desired `OutputFormat` IN — kills the -`capture → encode::windows_resolved_backend()` back-reference that's recomputed in `dxgi.rs`; (2) HDR -control + monitor-release become `VirtualLease` methods so the session glue never names a concrete -backend and contains zero `unsafe`; (3) optional encoder capabilities are queried via `EncoderCaps`. - -### 2.4 SessionFactory + typed plan (the single biggest clarity lever) - -Today the Windows capture/topology/encoder decision is made by ~40 scattered env reads, recomputed in -THREE places (`capture_virtual_output`, `should_use_helper`, `virtual_stream`) with no single owner and -a latent mirrored-dispatch bug (capture and encode can disagree on the backend). Replace with: - -```rust -struct SessionPlan { - display: DisplayBackend, - capture: CaptureBackend, // IddPush | Dda | Wgc - topology: SessionTopology, // SingleProcess | TwoProcessRelay - encoder: EncoderBackend, // Nvenc | Amf | Qsv | Software - input_format: OutputFormat, - bit_depth: u8, hdr: bool, pipeline_depth: usize, -} -struct SessionFactory { cfg: Arc, vdm: Arc, injector, mic, audio } -impl SessionFactory { - fn plan(&self, welcome: &Welcome) -> SessionPlan; // resolves ONCE from HostConfig; no env reads downstream - fn build(&self, plan: &SessionPlan, ctx: SessionContext) -> Result; // owns the RAII chain -} -``` - -`build()` owns the chain `vdm.lease(mode) → open_capturer(vout, fmt) → open_encoder(plan) → spawn -pipeline`, and `Session::drop` is the only teardown path. This kills the env soup, makes the deployed -path readable, and removes the capture/encode backend-disagreement bug class. It also lets us drop the -12–13-arg `#[allow(too_many_arguments)]` signatures (a `SessionContext` struct) and the dead -`Compositor` ceremony threaded through the Windows path. - -### 2.5 Ownership model — delete the global statics - -> **✅ IMPLEMENTED (2026-06-25, branch `windows-host-goal1`).** Landed as 3 steps + an on-glass -> reconnect-leak test — see [`windows-host-goal1-plan.md`](windows-host-goal1-plan.md) §2.5 for the -> commits + results. One deviation from the sketch below: the 5-agent map found **`CURRENT_MON_GEN` was -> write-only** (the per-frame monitor-gen bail was never wired), so the "generation carried through -> `WinCaptureTarget`" item was unnecessary and dropped; the gen lives on the manager + lease only. - -Today the lifecycle is smeared across `IDD_PERSIST` + `open_or_reuse` (dead code), `CURRENT_MON_GEN` -(read per-frame), `IDD_SETUP_LOCK`/`IDD_SESSION_STOP` (the preempt dance), `MGR: Mutex`, and on -the driver side `ADAPTER`/`MONITOR_MODES`/`NEXT_ID`/`WATCHDOG_*`/`DEVICE_POOL`. Replace with: - -- A host-lifetime **`VirtualDisplayManager`** owning a *typed* `OwnedHandle` device handle (not a raw - `isize` smuggled across threads) and the refcounted Idle/Active/Lingering state machine (preserve the - machine — it's earned). -- A per-session **`MonitorLease`** whose `Drop` releases the refcount; the monitor **generation carried - through `WinCaptureTarget`** instead of the ambient `CURRENT_MON_GEN`. -- On the driver: **wire `EvtCleanupCallback` for `MonitorContext`** (only `DeviceContext` has it today) - so the `SwapChainProcessor` + D3D resources drop via WDF RAII — deleting `free_swap_chain_processor` - and the manual-free-before-departure dance that is the **documented dominant reconnect leak**. Move - the process-global driver state into the `DeviceContext`; collapse the 3-way monitor identity - (`MONITOR_MODES` / EDID serial / context stamp) to one `Monitor` owned by the context. - ---- - -## 3. The host↔driver contract (own it; define once) - -### 3.1 `pf-vdisplay-proto` (no_std, bytemuck/zerocopy) - -One crate, both build graphs (path dep). Owns: - -- **Control plane**: a fresh interface GUID; a contiguous, versioned op enum; `#[repr(C)]` request/reply - structs carrying only used fields. -- **Frame plane**: `SharedHeader`, the `FrameToken { generation, seq, slot }` with `pack`/`unpack` - (replacing the hand-twiddled `gen<<40|seq<<8|slot` on both sides), the `Global\pfvd-*` name helpers. -- **Gamepad sections**: `XusbShm` (64 B) and `PadShm` (256 B, incl. `device_type`) layouts. -- Derive `FromBytes`/`IntoBytes`/`Pod`; `const` size+offset asserts; round-trip tests. **ABI drift - becomes a compile error, not a runtime corruption.** (bytemuck is already a dep in the driver + - wdf-umdf-sys.) This deletes every `OFF_*` constant + `read/write_unaligned` on both sides of every - boundary — the largest single block of shared-memory `unsafe`, and the top drift hazard. - -### 3.2 Control plane — keep DeviceIoControl, redesign the ABI - -`DeviceIoControl` is the correct WDF idiom for a driver with no control device and is low-frequency -(ADD/REMOVE per session + a keepalive); the shared-memory pattern buys nothing here. Keep it; redesign -the surface: - -- Ops actually needed: `Add(mode, identity) → {luid, target_id}`, `Remove`, `SetRenderAdapter` - (now **unconditional** — pf-vdisplay honors it for hybrid-GPU IDD-push; drop the SudoVDA-parity - default-off branch), `ClearAll` (first-class startup orphan reap, not an "ignored by SudoVDA" hack), - `GetInfo` (a real version handshake), and keepalive (see §3.4). -- Drop the SudoVDA-isms: `AddParams.device_name[14]`/`serial[14]` (ignored), the 16-byte GUID → a - monotonic `u64` session id (the refcount manager owns collision safety; retires `next_monitor_guid`'s - pid-mangling), the 4-byte `{major,minor,incr,test}` version tuple → one `u32`, the gappy - `0x800/0x888/0x8FF` func numbering → contiguous. -- One typed IOCTL dispatch helper retrieves+validates+aligns the buffers and hands the body a safe - `&Req` / `&mut MaybeUninit` — collapses ~20 of `control.rs`'s 29 `unsafe` blocks. - -### 3.3 Frame plane — keep the inversion, retire the scaffolding - -Keep the host-creates / driver-opens ring exactly. **Remove the bring-up scaffolding** that diagnosed -the now-solved `run_core=0` mystery: the `DebugBlock` channel + `DBG_MAGIC`, `spawn_observer` / -`PUNKTFUNK_IDD_PUSH_OBSERVE`, the `error!`-as-`info!` logging, the intentional handle leak, and the -20 s blind no-frame deadline (replace with the `DRV_STATUS_OPENED` handshake as a bounded liveness -signal). - -### 3.4 Driver swap-chain reuse — the one open root cause - -Today a *reused* IddCx monitor's swap-chain dies after ~2 sessions (target id resolves to 0, `SetDevice` -fails `0x80070057`, then an access violation), forcing fresh-monitor-per-session + the host-side -preempt/`wait_for_monitor_released` dance + the `IDD_PERSIST` "create once, never recreate" workaround. -The fix is in the **driver**: with `EvtCleanupCallback` wired + state owned by `DeviceContext` + the -identity collapsed to one `Monitor` (the recreate-path bugs are exactly the 3-way identity desync), the -clean recreate should become stable. **If** that holds, delete `IDD_SETUP_LOCK`/`IDD_SESSION_STOP` + -the preempt dance and unblock `max_concurrent>1` on Windows. **If** it can't be fixed cheaply, isolate -the residual serialization inside `VirtualDisplayManager` (not smeared back into the session loop). -Separately, evaluate replacing the polling watchdog (PING/countdown/grace/linger constellation) with a -**WDF file-object `EvtFileClose`** (host holds the control handle open; close = host gone) — feasibility -TBD on UMDF/IddCx. - ---- - -## 4. Capture strategy - -**IDD-push is the universal primary path — normal AND secure desktop (Decision B).** It composes -in-process (cross-session via `Global\` shared textures: driver in WUDFHost/Session 0, `serve` in the -console session), needs no DXGI Desktop Duplication and no `win32u` reparenting hook, is live-validated -at 5K@240 HDR, and (per the owner) also captures the secure desktop (Winlogon/UAC/lock). So there is no -separate "secure capturer" in the primary path: the same `IddPushCapturer` spans the lock screen and -UAC. Capture selection moves into a typed `CaptureBackend` in the `SessionPlan` — replacing the 3-way -env branch with `IddPush` (default) → `Dda`/`Wgc` (explicit fallbacks). - -**WGC + DDA are kept as fallbacks, not deleted (Decision B).** They cover non-IddCx / pre-pf-vdisplay -hardware and act as a safety net if IDD-push fails to attach. But they are **demoted**: they are no -longer the default, no longer entangled with the secure-desktop mux, and selected only via the explicit -`CaptureBackend` fallback in the plan. This lets the DDA module shed the parts that existed *only* to -make virtual-display-over-DDA survive on a hybrid box, while the genuinely-useful capture/recovery core -stays: -- **Scope the `win32u` self-modifying-code hook + the GPU-pref hook to the DDA fallback leg** (one - `win32u_hook::install()`), so the primary IDD-push path never touches them. Re-confirm whether DDA - even needs the `win32u` hook against pf-vdisplay (it may not — open verification item). -- **The two-process WGC relay's secure-desktop mux is retired** — IDD-push handles the secure desktop - directly, so `desktop_watch.rs` + `composed_flip.rs` + the `virtual_stream_relay` monolith are no - longer needed for their original purpose. Keep a **minimal** WGC fallback capturer if the WGC backend - is retained; do not port the 400-line relay state machine. (The cross-session input concern below is - handled by the `InputInjector`/topology abstraction, not the AU video relay.) - -**Shared D3D primitives** move out of `dxgi.rs` (today the de-facto dumping ground that `wgc.rs` and -`idd_push.rs` import from) into `windows/d3d/` (typed `Texture2d`/`Ring`/`CopyResource`/`Map`-as-bytes), -`windows/color/` (the converters + `hdr_p010_selftest` verbatim), and `windows/cursor.rs`. All three -capturers consume them — deletes the duplicated `tex_desc`, cursor, HDR-poll, repeat-last logic. - -**The texture-ownership contract becomes type-level.** NVENC encodes the capturer's texture *in place* -(no copy), sound today only because the IDD-push capturer rotates `OUT_RING` and the loop honors -`pipeline_depth()` — an undocumented cross-module coupling that is *already* a latent corruption risk. -Fix: either the encoder always `CopySubresourceRegion`s (as `ffmpeg_win` does), or the capturer hands an -explicitly-leased ring texture with a documented lifetime. No more relying on the synchronous-loop -assumption. - -**The IDD-push input question** (must confirm on-glass): capture+encode run in `serve`; input must reach -the *streamed* (console-session) desktop. If `serve` runs in the console session, `SendInput` works -directly. A code comment flags "SendInput from Session 0 can't reach Session 1" — so the architecture -must make `InputInjector` satisfiable either by in-session `SendInput` *or* by a tiny **input-only -Session-1 agent** (re-scope the old WGC helper to input only). The `SessionPlan.topology` expresses -this. - ---- - -## 5. Encode layer - -- Resolve backend + input format + pipeline depth **once** into `EncodePlan` and hand it to both the - capturer and the encoder factory — kill the duplicated `windows_resolved_backend()` call in `dxgi.rs` - (the highest-severity coupling). Trim `open_video`'s 8-arg grab-bag (`cuda` is always false on - Windows; `bit_depth` is overridden by the capture format anyway). -- **`nvenc_sys.rs`**: a thin safe wrapper — RAII `NvSession`/`NvBitstream`/`NvRegistration`/ - `NvMappedInput` (Drop = destroy/unregister/unmap) + an `NV_ENC_CONFIG` builder. The public encoder - then has near-zero `unsafe` and no hand-written teardown loops. (The SDK table already returns - `Result` via `result_without_string()`.) This is the single biggest encode-side `unsafe` reduction. -- **`ffmpeg_win`**: RAII `AvFrame`/`SwsCtx`/`HwDeviceCtx`/`HwFramesCtx` delete every manual `av_*_free` - and the error-path cleanup ladders (also the biggest leak-risk reduction); a checked `MappedSurface` - for the staging readback; a `const` size-assert on the hand-mirrored `AVD3D11VA*` structs in a - dedicated `d3d11va_ffi` submodule (silent FFmpeg ABI drift is currently undetectable). Keep - system-readback the default; zero-copy stays opt-in/experimental (no AMD/Intel lab box). -- **HDR symmetry**: make in-band ST.2086/CLL SEI a shared post-encode step so AMF/QSV get the same - mastering metadata as NVENC (today only NVENC attaches it; AMF/QSV rely solely on the 0xCE datagram). - Centralize "when does the client learn HDR metadata" in one owner. -- Keep `hdr.rs`, the `Encoder` trait, `EncodedFrame`, `validate_dimensions`, the caps-probe + RFI logic - verbatim. Delete the `pipeline.rs` `pump_once` doc stub (the real loop is `session/pipeline.rs`). - ---- - -## 6. Drivers — one binding stack (`windows-drivers-rs`), one workspace, one signing recipe - -Today: `pf-vdisplay` on the vendored **`wdf-umdf`** stack; `pf_dualsense` + `pf_xusb` on -**`microsoft/windows-drivers-rs`** (`wdk`/`wdk-sys`/`wdk-build`). Two bindgen passes, two SDK -resolutions, two `NTSTATUS`, two build systems, two signing recipes. - -**Decision C: unify all three on `microsoft/windows-drivers-rs`** (the official Microsoft stack), in one -in-tree `packaging/windows/drivers/` workspace, edition 2024, one `rust-toolchain.toml`, one CI build. -The gamepad drivers already ship on it; the work is to **migrate `pf-vdisplay` onto it** and **add the -IddCx surface** it lacks today. - -**Required pieces of this migration (each a Phase-0/early task):** - -1. **Add an `iddcx` subset to `wdk-sys`.** IddCx DDIs are *not* WDF-table functions — they are direct - `IddCxStub` exports — so the extension is bounded: an `ApiSubset::Iddcx` + `iddcx` feature → - bindgen `IddCx.h` + link `IddCxStub`, then ~15 thin `extern`/wrapper fns. Use the current - `wdf-umdf/src/iddcx.rs` (~345 LOC, validated) as a **line-by-line oracle**, including the IddCx 1.10 - `*2` HDR DDIs (`IddCxSwapChainReleaseAndAcquireBuffer2`, `IDARG_*2`, `_METADATA2`). -2. **Solve `/INTEGRITYCHECK` for self-signed loading — properly.** `wdk-build` links the driver with - `/INTEGRITYCHECK`, which a self-signed cert can't satisfy (CodeIntegrity 3004/3089). Today the - gamepad drivers hand-patch the FORCE_INTEGRITY PE bit post-link. Replace that hack with a robust - solution, in order of preference: (a) **override the linker flag** — drop `/INTEGRITYCHECK` via - `wdk-build` config / `RUSTFLAGS`/`link-args` if it can be suppressed cleanly; else (b) a - **deterministic, tested CI post-link tool** (a small Rust/PowerShell step that clears bit `0x80` at - `e_lfanew+0x5e` and re-signs, run in CI, not by hand) so it's reproducible and not a footgun; (c) for - a public build, real **attestation signing** (Partner Center) satisfies `/INTEGRITYCHECK` - legitimately. Pick (a) if feasible; (b) as the fleet-self-signed fallback. This is the headline cost - of choosing this stack and must be nailed in Phase 0. -3. **Backport the `wdf-umdf-sys` build.rs SDK-resolution fix** into `wdk-build` (or a local override): - resolve `IddCx.h`/`IddCxStub` by the SDK version that *actually contains* `um\x64\iddcx`, not the max - base SDK (the real failure where a newer base SDK shadows the WDK SDK). windows-drivers-rs's default - resolution doesn't exercise IddCx today, so this likely needs porting. -4. **Port `pf-vdisplay`'s typed safety wins** onto the new stack: re-create the - `WDF_DECLARE_CONTEXT_TYPE!` `Arc>` context abstraction (the gold-standard contained - `unsafe`); the version-gate protocol (`IddCxIsFunctionAvailable!` / `IDD_STRUCTURE_SIZE!`); and a - thin safe wrapper layer so the gamepad drivers stop emitting raw `call_unsafe_wdf_function_binding!` - everywhere (the biggest driver-`unsafe` lever). - -While unifying, also: adopt WDF device contexts for per-pad state (drop the -`UmdfHostProcessSharing=ProcessSharingDisabled`-dependent statics → true multi-pad-per-host); replace -`mem::zeroed()` configs with the `WDF_*_CONFIG_INIT` initializers (kills the recurring zeroed-default -bug class that already caused 3 driver bugs); cache the shm view (RAII `ShmView`) instead of -re-mapping ~125×/s; **delete the world-writable `C:\Users\Public\*.log` driver logging** and the "M0 -spike" naming; collapse `is_nt_error()`/`dyn-Any`/`From<()>`-as-error into a typed `IntoDriverResult`; -collapse the per-call dispatch `unsafe` into one generic `dispatch()` helper. - -**Provenance note:** confirm where `wdk`/`wdk-sys`/`wdk-build` come from (the gamepad drivers' Cargo.toml -path-deps `../../crates/wdk*` don't exist in this checkout — they resolve inside a windows-drivers-rs -checkout on the dev box). Pin them as crates.io deps or a vendored, version-pinned copy so the driver -workspace builds reproducibly in CI. - ---- - -## 7. Input, audio, service, packaging - -- **Input**: consolidate the host-side device plumbing (`create_swdevice`/`create_shm_section`/ - `SwDeviceProfile`) into one `inject/windows/swdevice.rs` used by all three managers (XUSB included, - which currently re-implements its own). The shm layouts come from `pf-vdisplay-proto`. Re-scope the - cross-session helper (if any) to input-only. -- **Audio**: small, already fairly clean. Replace the lone `newdev.dll` `LoadLibrary`+`transmute` - (`wasapi_mic.rs`, the audio runtime's *only* `unsafe`) with the windows-rs `DiInstallDriverW` binding - (or move provisioning to the installer) → zero `unsafe` in the audio runtime. -- **Service / process**: one `windows/process.rs` owning RAII `Token`/`Event`/`Job`/`Child` + a single - `spawn_as_user()` used by BOTH the SCM supervisor and any helper — deletes the duplicated - token-dup/`merged_env_block`/`CreateProcessAsUserW` machinery and ~12 manual `CloseHandle` sites. Add - a **cooperative stop**: a named stop event the supervisor sets and `serve` waits on, so Stop runs RAII - teardown (today `TerminateProcess` skips Drop → the virtual monitor lingers, the documented - stale-monitor gotcha); `TerminateProcess` only as a bounded fallback. -- **Packaging/CI**: keep the thin-.iss / fat-binary model; add a `punktfunk-host web install/uninstall` - subcommand to absorb the web-setup PowerShell. **Build + sign the unified driver workspace in CI from - source** (or a CI guard that fails on stale-vendored-DLL / un-bumped DriverVer) so the driver can't - silently drift from its source. Mint the **fresh pf-vdisplay GUID** coordinated across host + driver + - INF. Single source of truth for version → build + ISCC AppVersion + INF DriverVer. Investigate - retiring `nefconc` by creating the ROOT devnode via SwDevice/CM in Rust. Keep the - devgen-never / nefconc-only and DriverVer-bump gotchas codified. - ---- - -## 8. Unsafe-reduction program (run at port time, not as a separate pass) - -- **P0 lints first** (a few lines, before new code): `#![deny(unsafe_op_in_unsafe_fn)]` (host crate has - none today; the driver workspace already has it), `#![warn(clippy::undocumented_unsafe_blocks)]`, - `#![warn(clippy::multiple_unsafe_ops_per_block)]`. Generated bindings keep their opt-out. -- **P0 std handle ownership**: `std::os::windows::io::OwnedHandle` / `std::fs::File::from_raw_handle` - everywhere a raw `HANDLE`/`isize` is held (events/jobs/tokens/sections/pipes). Used in **zero** host - files today — the single biggest cheap win. Deletes the bespoke `unsafe impl Read/Write/Drop` - (`HandleReader`), the never-closed sudovda control handle, the `AtomicIsize` HANDLE globals, ~6 manual - `CloseHandle` sites — and fixes real leaks. -- **P0 the proto crate** (§3.1) — kills the shared-memory pointer-cast `unsafe`. -- **P1 typed wrappers**: `windows/d3d/` (most COM calls already return `Result`; per-frame loop bodies - become `unsafe`-free, the irreducible keyed-mutex/`from_raw_parts` lands in one `frame_xfer` fn); - `nvenc_sys` + RAII ffmpeg (§5); one `windows/process.rs` (§7); collapse the 21 `unsafe impl Send` - onto one audited `SendPtr`/`ThreadBound` (directly de-risks the NVENC in-place coupling). -- **P2 contain the irreducible**: `win32u_hook.rs` (one `install()`; scope to secure-DDA or drop), - `gpu_priority.rs` (the D3DKMT transmute), the WDF context-blob macro, the IddCx swap-chain DDI + - `from_raw_borrowed` (wrap in a typed `SwapChain` guard returning a borrowed `AcquiredSurface<'_>`). - Document a `// SAFETY:` per residual site. -- **P2 delete `unsafe` by deleting code**: the `present_trigger` dead diagnostic, the `DebugBlock` - channel, `spawn_observer`, `IDD_PERSIST`/`open_or_reuse`, `helpers.rs Sendable`, the WGC-open - thread-watchdog hack (gone with WGC), the driver file-logging. - -Estimated: host ~144→~35, drivers ~227→~60, residual concentrated and auditable. (`#![forbid(unsafe)]` -is impossible for the drivers and the per-frame D3D path — the realistic target is *containment*.) - ---- - -## 9. SudoVDA decoupling (mechanical rename + scrub) - -`vdisplay/sudovda.rs` → `vdisplay/windows.rs`; `SudoVdaDisplay` → `PfVirtualDisplay`; scrub "SudoVDA" -from all log/error/doc strings across `capture.rs`/`dxgi.rs`/`wgc*.rs`/`idd_push.rs`/`punktfunk1.rs`/ -`main.rs`/`sendinput.rs` (141 refs / 15 files). **Split the reach-in helpers out** of the vdisplay -backend (they're display-utility, not virtual-display creation): `set_advanced_color`, -`advanced_color_enabled`, `resolve_gdi_name`, `isolate/restore_displays_ccd`, `set_active_mode` → -`windows/display_ccd.rs` (collapsing the 4× copy-pasted `QueryDisplayConfig` preamble into one safe -`query_active_config()`); `resolve_render_adapter_luid` → `windows/adapter.rs`. Both vdisplay and -capture then depend on these as peers, breaking the circular reach-in. `WinCaptureTarget` moves to a -neutral location (defined in `dxgi.rs`, constructed in `sudovda.rs` today). Drop the dual-driver -fallback conditionals. Expose HDR/monitor-release as `VirtualLease` methods (zero `unsafe` in the -session glue). - ---- - -## 10. Build plan (greenfield — Decision A) - -A from-scratch rebuild of the Windows host against the clean architecture, **salvaging the §1 jewels -verbatim** (the already-clean, already-tested modules: `hdr.rs`, `edid.rs`, the `inject/proto` codecs, -the HDR/cursor converters + their self-tests, the GF8 packetizer, the pairing handshake). The old -Windows code stays in-tree, untouched, as the *reference implementation* until the new path reaches -parity on glass, then is deleted. - -**Greenfield-risk mitigation (the survey's strong caveat stands):** almost none of this is -CI-validatable — the Windows backends + drivers need the RTX box (192.168.1.173) + the build VM, and -**AMF/QSV have no lab hardware at all**. A greenfield rewrite therefore carries real risk of silently -dropping a layered bug-fix. Two guardrails are mandatory: -1. **The §1 preservation checklist is a test/assert contract**, not prose: each rebuilt module ports its - hard-won invariants as unit tests or runtime asserts — RAII teardown order (restore displays *before* - REMOVE), keyed-mutex held only across convert/copy, `terminate` checked at the swap-chain loop top, - magic stamped last, `OUT_RING` texture rotation under `pipeline_depth>1`, the NVENC caps-probe - downgrade, the SwDeviceCreate identity recipe. A rebuild that drops one fails its own test. -2. **On-glass A/B gates** at each milestone below, on the RTX box, against the current shipping build: - 1080p60, 5K@240 HDR, reconnect-storm, secure desktop (lock/UAC), multi-pad. Nothing replaces the old - path until its A/B passes. - -### Build order - -- **M0 — Foundations + the `/INTEGRITYCHECK` answer.** Stand up `crates/pf-vdisplay-proto` (the clean, - owned ABI: fresh GUID, the redesigned IOCTL op enum + `#[repr(C)]` structs, `SharedHeader`, - `FrameToken`, the gamepad shm layouts, `const` size-asserts, round-trip tests). Stand up the in-tree - `packaging/windows/drivers/` workspace on `windows-drivers-rs` and **prove the two hard unknowns**: - (a) the `iddcx` `wdk-sys` subset bindgen+links and a trivial IddCx adapter loads; (b) `/INTEGRITYCHECK` - is solved (§6.2) so a self-signed driver loads under Secure Boot with no hand-patching. Add the P0 - lints to the host crate. *No host behavior yet.* -- **M1 — pf-vdisplay on the new stack, first light.** Rebuild the IddCx driver against - `windows-drivers-rs`+`iddcx`, clean from the start: `DeviceContext`-owned state (no process-globals), - one `Monitor` identity, `EvtCleanupCallback` on `MonitorContext`, the ported `Arc>` context, - the EDID + HDR recipe verbatim, the redesigned control plane from the proto crate. *(On-glass: ADD → - monitor arrives → IDD-push ring attaches → frames flow at 1080p; REMOVE clean.)* -- **M2 — IDD-push capture + NVENC, glass-to-glass.** New `src/windows/` tree: `windows/d3d/` typed - wrappers, `windows/color/` (converters + self-tests), `windows/cursor.rs`, `capture/windows/idd_push.rs` - consuming the proto ring with a **type-level texture-ownership contract** (no in-place-encode - assumption), `encode/windows/{nvenc.rs,nvenc_sys.rs}`, `vdisplay/windows.rs` + `windows/display_ccd.rs` - + `windows/adapter.rs`. Wire the `SessionFactory`/`SessionPlan` (M2 only needs the IDD-push+NVENC - plan). *(On-glass A/B: 1080p60 + 5K@240 HDR, latency parity with the current build.)* -- **M3 — Service, input, audio, secure desktop.** `windows/process.rs` (RAII Token/Event/Job/Child + - `spawn_as_user` + cooperative stop) + `windows/service.rs`; `inject/windows/*` on the proto shm + - consolidated `swdevice.rs`; `audio/windows/*` (zero-`unsafe` runtime). Confirm IDD-push captures the - secure desktop (lock/UAC) and input reaches the streamed session (in-session `SendInput`, or the - input-only agent if needed). *(On-glass: full session incl. lock screen + UAC + a real pad.)* -- **M4 — Gamepad drivers onto the unified stack.** Rebuild `pf_dualsense` + `pf_xusb` on - `windows-drivers-rs` in the same workspace, WDF device contexts (true multi-pad), proto shm, - `WDF_*_CONFIG_INIT`, no file logging, no "M0 spike" naming. *(On-glass: 2 XInput + 2 DualSense pads, - rumble/lightbar/adaptive-trigger round-trip.)* -- **M5 — Fallbacks + GameStream + AMF/QSV.** Port the demoted WGC + DDA fallback capturers (minimal, - `win32u` hook scoped to the DDA leg); `encode/windows/ffmpeg_win/*` with RAII FFmpeg + the - `d3d11va_ffi` size-assert (system-readback default; zero-copy experimental); GameStream planes reusing - `session/pipeline.rs`, installer default flipped to secure `serve`. *(On-glass: Moonlight client on - the DDA fallback; AMF/QSV stays CI-only.)* -- **M6 — Cut over + delete.** Flip the default to the new path, run the full A/B matrix, then delete the - old `dxgi.rs`/`wgc*`/`sudovda.rs`/`punktfunk1.rs` Windows monoliths + the bring-up scaffolding - (`DebugBlock`/`spawn_observer`/observe gate) + the old gamepad driver crates. Single source of truth - for version; CI builds+signs all drivers from source. - -Milestones are roughly dependency-ordered; M0 is the long pole (the `/INTEGRITYCHECK` + `iddcx` proof -gates everything else). M5's AMF/QSV cannot be validated without hardware — keep it system-readback-only -and clearly experimental. - ---- - -## 11. Decisions (resolved 2026-06-24) + open verification items - -The five product forks are decided (see the table in §0): **A** greenfield; **B** IDD-push primary for -everything incl. secure desktop, WGC+DDA kept as demoted fallbacks; **C** extend `windows-drivers-rs` + -solve `/INTEGRITYCHECK`; **D** keep GameStream, default secure. On **E (concurrent sessions)**: fix the -driver swap-chain lifecycle regardless (it removes the leak + the preempt dance); treat true -`max_concurrent>1` on Windows as a follow-on once clean reuse is proven on glass. - -What remains are **technical unknowns to confirm on the RTX box** (not user decisions): - -- **`/INTEGRITYCHECK` resolution path (M0 long pole).** Can `wdk-build` suppress `/INTEGRITYCHECK` via - config/link-args (preferred), or must we keep a deterministic CI post-link bit-clear? Decides the - signing story for all three drivers. -- **`iddcx` subset on `wdk-sys`.** Does the bindgen+`IddCxStub` link cleanly, and does the SDK-resolution - fix need backporting? (windows-drivers-rs doesn't exercise IddCx today.) -- **Driver swap-chain reuse.** Does the clean ownership model (`EvtCleanupCallback` + DeviceContext state - + single `Monitor` identity) actually fix the "reused swap-chain dies after ~2 sessions" root cause? If - not, the residual serialization stays inside `VirtualDisplayManager`. -- **IDD-push input + secure desktop. ✅ RESOLVED (owner-confirmed on glass, 2026-06-25).** `serve` runs in - the console session so `SendInput` reaches the streamed desktop, and IDD-push frames flow through the lock - screen / UAC — both confirmed live ("works great"). Locked in as the primary; the DDA secure leg is - demoted to a non-IddCx fallback. (See [§15](#15-current-status-2026-06-25).) -- **Does the demoted DDA fallback still need the `win32u` hook** against pf-vdisplay, or was that purely - a SudoVDA/hybrid pathology? If unneeded, the self-modifying-code hook can be deleted entirely. -- **AMF/QSV** stays CI-only (no hardware) — system-readback default, zero-copy experimental. - ---- - -## 12. Risks - -- **Greenfield with no CI (the dominant risk).** The build VM is headless/WARP; the WinUI/hardware/driver - paths need the RTX box, and AMF/QSV have no hardware. A from-scratch rebuild can silently drop a - layered bug-fix. Mitigation: the §1 preservation checklist is a *test/assert contract* per rebuilt - module; on-glass A/B gates the new path before the old one is deleted (M6); keep the old code in-tree - as the reference until parity. -- **`/INTEGRITYCHECK` (M0 long pole).** Choosing `windows-drivers-rs` means self-signed loading depends - on solving it cleanly (§6.2). If neither linker-flag suppression nor a deterministic CI post-link step - works, drivers can't load self-signed — prove this first, it gates everything. -- **`iddcx` on `wdk-sys`** is new surface (windows-drivers-rs doesn't bind IddCx). Bounded - (`IddCxStub` exports + ~15 wrappers, with the validated `wdf-umdf/iddcx.rs` as oracle) but unproven on - this stack — M0 must light it. -- **`pf-vdisplay-proto` spans two cargo build graphs** (host workspace + the driver workspace). Validate - the path-dep resolves on the Windows build env in M0; pin `wdk*` provenance so the driver workspace - builds reproducibly in CI. -- **Driver swap-chain-reuse root cause still undiagnosed.** The clean ownership model *should* fix it; - if not, residual serialization stays inside `VirtualDisplayManager` and `max_concurrent>1` stays - blocked. Keep `await_released` on the trait until reuse is proven on glass. -- **NVENC in-place encode + `pipeline_depth>1`** is a latent corruption risk; the M2 texture-ownership - contract must be type-level (not the synchronous-loop assumption). Verify the ring on glass. -- **Host/driver version drift in the field.** New host + new driver are always built together (greenfield), - but the installer bundles both — enforce a startup version handshake (proto version in both binaries) - and a CI guarantee they're built from the same revision. -- **Big-bang cutover (M6).** Flipping the default and deleting the old monoliths is the riskiest moment; - it is gated on the full A/B matrix passing, and the old code is recoverable from git if a regression - surfaces post-cutover. - ---- - -## 13. Progress log + M1 IddCx-binding recipe (2026-06-24) - -**M0 COMPLETE** (commits through `f896f70`, on `main`, CI-green + validated on the RTX box): -- `crates/pf-vdisplay-proto` — owned host↔driver ABI (fresh GUID, typed IOCTLs + frame transport, const - size-asserts). Green Linux + MSVC. -- Runner **and** RTX box provisioned: WDK 26100 (WDF 2.31, IddCx 1.10), LLVM **21.1.2** (the runner's - default was a ToT/22-dev build → wdk-sys bindgen `E0080` layout-test overflow; 21.1.2 builds clean — - windows-drivers-rs discussion #591). cargo-wdk on the runner. -- `packaging/windows/drivers/` — unified driver workspace on windows-drivers-rs; `wdk-probe` (minimal - UMDF) builds clean end-to-end (bindgen + WDF link + static-CRT `.cargo/config` + `pf-vdisplay-proto` - path-dep). Build layers solved: in-tree target dir (wdk-build walks OUT_DIR ancestors for `Cargo.lock`); - `[workspace.metadata.wdk.driver-model]` = UMDF 2.31; `target-feature=+crt-static` w/ explicit target; - `Version_Number=10.0.26100.0`; `LIBCLANG_PATH` → LLVM 21.1.2. -- **`/INTEGRITYCHECK` resolved**: wdk-build sets it unconditionally (no opt-out) → `packaging/windows/ - clear-force-integrity.ps1` clears the PE `FORCE_INTEGRITY` bit (0x0080 @ e_lfanew+0x5e) post-link, - before signing. Proven `0x01E0→0x0160` on CI and in PS 5.1 on the box. Self-signed UMDF load itself is - already proven on the box (the gamepad drivers). - -**RTX box** (`ssh "Enrico Bühler"@192.168.1.173`, ENRICOS-DESKTOP, RTX 4090 driver 610.62, PS 5.1 shell): -**ephemeral** — boots to Proxmox on reboot, so unreachable after a reboot. Treat as opportunistic on-glass -(driver load + IDD-push streaming) only; **CI on the windows-amd64 runner is the persistent validator**. -A build clone is at `C:\Users\Public\pf-rewrite`; builds the driver in ~29 s with the box's LLVM 21.1.2. - -### M1 — IddCx binding on windows-drivers-rs (the recipe) - -IddCx DDIs are **function-table dispatched** (`IddFunctions[]` indexed by `IDDFUNCENUM::TableIndex`, -`IddDriverGlobals` implicit first arg) — *exactly* the model wdk-sys already implements for WDF (not direct -`IddCxStub` exports as first assumed). - -**Approach (Option 1, recommended):** vendor windows-drivers-rs **0.5.1** in-tree (pinned; source staged at -`scratchpad/wdr`, commit `0e3499d`), patched via `[patch.crates-io]` for just `wdk-build` + `wdk-sys`, and -add a first-class **`ApiSubset::Iddcx`** that bindgens `iddcx/1.10/IddCx.h` in an extra pass **reusing the -identical `bindgen::Builder::wdk_default(config)` baseline** (so its WDF/DXGI types *resolve to*, not -redefine, wdk-sys's — type identity by construction). This mirrors wdk-sys's existing gpio/hid/spb/usb -versioned-subpath subsets exactly. -- wdk-build: add `ApiSubset::Iddcx`, a `headers` match arm, `iddcx_headers() -> ["iddcx/1.10/IddCx.h"]` - (UMDF-only). -- wdk-sys build.rs: `generate_iddcx` as a copy of `generate_gpio` — `bindgen_header_contents([Base, Wdf, - Iddcx])`, `(TYPES|VARS).complement()`, `.allowlist_file("(?i).*iddcx.*")`; behind an `iddcx` feature; - add to `ENABLED_API_SUBSETS`; `pub mod iddcx` in lib.rs. -- A `wdk-iddcx` wrapper crate (port of `wdf-umdf/src/iddcx.rs`): table dispatch via - `wdk_sys::iddcx::_IDDFUNCENUM::TableIndex as usize` (ModuleConsts const, **not** the oracle's - NewType `.0`); NTSTATUS is plain `i32` in wdk-sys (use `wdk_sys::NT_SUCCESS`, drop the oracle's newtype). -- Driver build.rs: add `link-search` to `Lib//um//iddcx/1.10` (the SDK version that *contains* - iddcx — glob, don't trust max) + `static=IddCxStub`; hand-declare `#[no_mangle] pub static - IddMinimumVersionRequired: ULONG = 4;`; keep the FORCE_INTEGRITY clear. - -**Make-or-break — RESOLVED ✅ (CI-green @ `6d8c7a5`, run 5548, no fallback).** `IddCx.h` bindgens AND the -generated module compiles inside wdk-sys with WDF **type-identity**; the #515/#516 header conflict NEVER -materialized. Vendored the **published windows-drivers-rs 0.5.1** crates (wdk-build + wdk-sys) under -`packaging/windows/drivers/vendor/`, `[patch.crates-io]`'d. The six knobs `generate_iddcx` actually -needed (each a real gotcha, all CI-proven; the recipe above was close but the codegen/scope details -differed): - -1. **`--language=c++`** — `wdk_default` parses **C**; IddCx.h's `IDARG_*` typedefs need C++ or you get a - "must use 'struct' tag" cascade (verified by direct `clang` on the box: 0 errors as C++, fails as C). -2. **`-DIDD_STUB`** — table-dispatch mode; skips `IddCxFuncEnum.h`'s `#error IDDCX_VERSION_MAJOR is not - defined` (it lives inside `#ifndef IDD_STUB`). **Do NOT add `WDF_STUB`** — wdk-sys parses `wdf.h` - non-stubbed, and stubbing it only here would desync the shared WDF types (breaking type-identity). -3. **`allowlist_recursively(false)` + `allowlist_file("(?i).*iddcx.*")`, full codegen (no - `.complement()`)** — emit ONLY IddCx items; WDF/Win types resolve to wdk-sys's via - `use crate::types::*` in `src/iddcx.rs`. No giant blocklist (Option 2 avoided). -4. **`allowlist_type("_?DXGI_.*" / "IDXGI.*" / "_?OPM_.*" / "_?D3DCOLORVALUE")`** — emit the non-WDF types - wdk-sys doesn't bindgen, locally (absent from `crate::types`, so non-conflicting). The `_?` is - load-bearing: `typedef struct _OPM_X {} OPM_X` needs the tag AND the alias (recursively(false) won't - pull the tag from the typedef). -5. **`pub type UINT = ::core::ffi::c_uint;` in `src/iddcx.rs`** — `UINT` (unsigned int) is absent from - `crate::types`; covers the top-level struct-field uses. -6. **`translate_enum_integer_types(true)`** — C++ parsing kept `UINT` as the underlying repr of the - DXGI/OPM ModuleConsts enums (`pub mod _X { pub type Type = UINT; }`), and nested modules can't see the - parent `UINT`. This emits native `u32` reprs → self-contained enum modules. - -The wrapper note still holds: table dispatch via `wdk_sys::iddcx::_IDDFUNCENUM::TableIndex as usize` -(ModuleConsts const, **not** the oracle's NewType `.0`); NTSTATUS = plain `i32` (`wdk_sys::NT_SUCCESS`). -Driver build.rs will add the IddCxStub link-search + `IddMinimumVersionRequired` + keep the -FORCE_INTEGRITY clear. **Option 2 stays rejected; the `wdf-umdf-sys` fallback is unneeded.** - -**NEXT (M1 cont.):** port the full ~30-DDI / ~40-struct surface (incl. the HDR `*2` DDIs) + the -swap-chain processor + frame transport, with the clean ownership model (DeviceContext-owned state, -`EvtCleanupCallback` on `MonitorContext`, single `Monitor` identity, the owned `pf-vdisplay-proto` plane). -First gate: a probe linking `IddCxStub` and calling `IddCxDeviceInitConfig`/`…Initialize`/ -`…AdapterInitAsync` (CI = compile+link). On-glass load + IDD-push stream needs the RTX box (ephemeral — -currently down/Proxmox). - ---- - -## 14. M1 step 2 — pf-vdisplay driver port plan (2026-06-24, workflow-mapped + critiqued) - -**Status of the binding (DONE, CI-green):** the wdk-sys `iddcx` binding is proven *complete for the whole -driver*, not just init. `wdk-probe/src/iddcx_surface_assert.rs` (commit `ae803b2`) CI-asserts every `*2`/HDR -struct (`IDDCX_TARGET_MODE2`/`PATH2`/`METADATA2`, `IDARG_*RELEASEANDACQUIREBUFFER2` — which embed -`DISPLAYCONFIG_*`/`LUID`, both of which **resolve from `crate::types`** — no allowlist gap), all 14 inbound -`PFN_IDD_CX_*` callbacks, the `.Size` machinery (`IddStructures`/`IddStructureCount`/ -`IddClientVersionHigherThanFramework`/`_IDDSTRUCTENUM::INDEX_*` — so `IDD_STRUCTURE_SIZE!` is portable), and -`IDDCX_ADAPTER_FLAGS::…CAN_PROCESS_FP16` + `IDDCX_TARGET_CAPS::…HIGH_COLOR_SPACE`. ModuleConsts module -naming: the func/struct enums are `_IDDFUNCENUM`/`_IDDSTRUCTENUM` (underscored tag), but the flag/cap enums -are `IDDCX_ADAPTER_FLAGS`/`IDDCX_TARGET_CAPS` (no underscore). - -### DDIs to wrap (11 — graduate `wdk-probe/src/iddcx_rt.rs` → a `wdk-iddcx` crate) -DeviceInitConfig, DeviceInitialize, AdapterInitAsync (done), MonitorCreate, MonitorArrival, -MonitorDeparture, AdapterSetRenderAdapter, SwapChainSetDevice (`other_is_error`; 0x887A0026→retry), -**SwapChainReleaseAndAcquireBuffer2** (HDR variant only; `other_is_error`; E_PENDING 0x8000000A → wait on -the surface event), SwapChainFinishedProcessingFrame. **Drop** the v1 `ReleaseAndAcquireBuffer` (adapter -always sets FP16). **Defer** the hardware-cursor DDIs (cursor baked into video). - -### Callbacks (15 in `IDD_CX_CLIENT_CONFIG`; `*2` mandatory because FP16) -parse_monitor_description (+`2`), monitor_query_target_modes (+`2`), adapter_commit_modes (+`2`), -adapter_init_finished (stash IDDCX_ADAPTER + start watchdog), monitor_get_default_modes (→NOT_IMPLEMENTED, -we always carry EDID), **query_target_info (→HIGH_COLOR_SPACE), set_gamma_ramp (accept-stub — WITHOUT it -the adapter fails to init), set_default_hdr_metadata (accept-stub)** — the last three are mandatory under -FP16, assign_swap_chain, unassign_swap_chain, device_io_control (the pf-vdisplay-proto control plane). -Plus `EvtDeviceD0Entry` (adapter created HERE, not in DeviceAdd) and two `EvtCleanupCallback`s. - -### State model (the rewrite's core change) -`DeviceContext` OWNS all state — IDDCX_ADAPTER, session_id-keyed monitor map, watchdog, the per-render-LUID -`Direct3DDevice` pool — replacing the oracle's process globals. Reachable from BOTH the WDFDEVICE (strong) -and the IDDCX_ADAPTER object (the adapter-side callbacks need it). `MonitorContext` owns the -`SwapChainProcessor` + `target_id`; **wire `EvtCleanupCallback` on the IDDCX_MONITOR object** so RAII Drop -joins the worker thread + frees D3D (the oracle lacked this → the dominant reconnect leak). **Single Monitor -identity** keyed by `session_id` (collapses the oracle's 3-way EDID-serial/map/stamp desync that caused the -`target_id=0` recreate bug); `assign_swap_chain` reads `target_id` from the context, never a map lookup. -The HOST still owns the control-device handle, the linger/reuse state machine, and ALL `Global\` shared -objects (created `D:(A;;GA;;;WD)`); the driver only OPENS them. - -### Frame transport (single-source on `pf_vdisplay_proto::frame::*`) -Acquire via `ReleaseAndAcquireBuffer2{AcquireSystemMemoryBuffer=0}` → GPU `ID3D11Texture2D`; borrow -`out.MetaData.pSurface` with `IDXGIResource::from_raw_borrowed` (do NOT steal IddCx's refcount), publish -BEFORE `FinishedProcessingFrame`. Ring = `RING_LEN`(6) keyed-mutex shared textures opened by name -(`frame::{header_name,event_name,texture_name}`); per-frame: GetDesc format-guard (drop on FP16↔BGRA -mismatch), `AcquireSync(0,0ms)`, `CopyResource`, `ReleaseSync(0)`, store `FrameToken{gen,seq,slot}.pack()` -(Release), `SetEvent`. All-slots-busy → drop, never block. `is_stale()` (header.generation Acquire) → reattach -on host ring recreate. Write `DRV_STATUS_OPENED` + render LUID into the header. Drop the old DebugBlock + -the locally-duplicated header/MAGIC/name consts. - -### Implementation checklist (each step CI- or box-gated) -0. workspace `pf-vdisplay`(cdylib)+`wdk-iddcx` members — **STEP-0 gate must pull in `std::thread`+`OwnedHandle`** (critique: prove std links under the UMDF toolchain *here*, not at STEP 5). CI. -1. graduate `iddcx_rt.rs` → `wdk-iddcx` (11 DDIs + `is_nt_error`/`other_is_error`) + **re-export the inbound PFN types**. CI link. -1.5 (critique add) the surface-assert (DONE @ `ae803b2`) lives on so the full PFN/`*2`/`DISPLAYCONFIG` surface stays a CI gate. -2. DriverEntry + driver_add: full `IDD_CX_CLIENT_CONFIG` (15 callbacks as stubs) + DeviceInitConfig + WdfDeviceCreate(+cleanup) + CreateDeviceInterface(`PF_VDISPLAY_INTERFACE_GUID`) + DeviceInitialize + D0Entry stub; salvage `edid.rs` verbatim. **Resolve `.Size` via `IDD_STRUCTURE_SIZE!` (machinery confirmed present).** CI link + FORCE_INTEGRITY clear. -3. DeviceContext + `WDF_DECLARE_CONTEXT_TYPE` Arc blob; init_adapter in D0Entry (caps+FP16) → AdapterInitAsync; the `*2` mode DDIs + query_target_info + gamma/hdr accept-stubs. **Box gate:** loads under Secure Boot, enumerates as IddCx adapter, Status OK (no "Failed to get adapter"). -4. control plane (GET_INFO version handshake — **host MUST assert `protocol_version`**, ADD/REMOVE/SET_RENDER_ADAPTER/PING/CLEAR_ALL) + create_monitor + real mode DDIs + watchdog + MONITOR_OP_LOCK; **switch host `sudovda.rs`/`idd_push.rs` to `pf_vdisplay_proto` (GUID e5bcc234→70667664, IOCTL 0x800→0x900, GUID-key→session_id) — lockstep**. CI (host build) + box (monitor appears at WxH@Hz). -5. Direct3DDevice + assign/unassign + SwapChainProcessor (worker thread, SetDevice 60×@50ms single-borrow retry, top-of-loop terminate, Buffer2 acquire, from_raw_borrowed) WITHOUT publisher; wire monitor `EvtCleanupCallback`. **Box:** swap-chain assigns, acquire loop runs, RAII teardown (no thread/VRAM leak). **Critique: instrument that MonitorContext::Drop actually RAN; if the monitor-object cleanup callback does not fire, keep the oracle's explicit free-before-departure path as the fallback.** -6. FramePublisher on `pf_vdisplay_proto::frame::*` + keyed-mutex RAII guard + OwnedHandle/ShmView; wire into run_core. **Box:** full IDD-push glass-to-glass, A/B vs the shipping driver. **Critique: add a BLOCKING secure-desktop gate here** — lock (Win+L)+UAC with serve in the console session / driver in Session 0, confirm frames keep flowing AND input reaches the desktop; until it passes, do NOT delete the WGC-relay/DDA secure path. -7. HDR ring-recreate + repeated session recreate (confirm the recreate-crash is gone). **Critique: define the failure branch** — if recreate isn't stable, keep IDD_PERSIST + state that mid-stream Reconfigure stays unsupported on Windows IDD-push (host rejects, as today) rather than crashing; keep `max_concurrent=1`. **Specify the concurrent-monitor D3D model before enabling >1** (two worker threads must not share one SINGLETHREADED immediate context — give each monitor its own device or a deferred/multithreaded context). -8. unsafe-reduction pass (one audited `SendPtr`/`ThreadBound`; per-site `// SAFETY`; `AcquiredSurface<'_>` + `KeyedMutexGuard` RAII so the hot loop has zero raw Finish/ReleaseSync) + **delete the old `packaging/windows/vdisplay-driver/` tree only after the secure-desktop gate (step 6) passes**. CI clippy -D warnings + final box A/B. - -### Critique verdict + the big risk -Plan is implementation-ready once the 4 CI-checkable unknowns are gates (3 now resolved by the surface-assert -+ `.Size` machinery presence; std-under-UMDF is the STEP-0 gate). **SINGLE BIGGEST RISK: the secure-desktop -claim** — ~~the plan retires the proven two-process WGC relay + DDA on the *unproven* assertion that one -IddPushCapturer captures the lock/UAC secure desktop directly~~ → **✅ RESOLVED (owner-confirmed on glass, -2026-06-25): the IddPushCapturer captures the lock/UAC secure desktop AND input reaches it — "works -great."** The assertion held; this risk is retired (see [§15](#15-current-status-2026-06-25)). The WGC relay -stays only as a non-IddCx-hardware fallback. Other defined-failure-branch items: monitor `EvtCleanupCallback` firing, IDD_PERSIST/Reconfigure, -concurrent-monitor device sharing, host↔driver `protocol_version` lockstep. - ---- - -## 15. Current status (2026-06-25) - -The rewrite is **largely implemented**. The new all-Rust `pf-vdisplay` driver (the M0 long pole — `iddcx` -on `windows-drivers-rs` + `/INTEGRITYCHECK` — and the §14 STEP 0–8 port) **landed on `main`, on-glass HDR -validated**, and the host was decomposed into the clean layered architecture. One important deviation from -the plan: **the host was refactored *in place* via a staged, behavior-preserving plan -([`windows-host-goal1-plan.md`](windows-host-goal1-plan.md)), not greenfield-rebuilt** — the §10 "rebuild -fresh, keep old as reference" framing was superseded because staging preserved the live-validated host at -every step (lower regression risk than a big-bang M2 rebuild). The §2.3/§2.4/§2.5 design (seam traits, -`SessionPlan`/`SessionFactory`/`SessionContext`, the `VirtualDisplayManager` ownership model) is realized in -that branch's commits, not the M2 greenfield tree the build order imagined. - -### Milestone / step status +## 1. Status at a glance + +The Windows host is **functionally complete and validated on glass.** The hard, high-risk proofs are done: +a clean all-Rust IddCx driver on the unified `windows-drivers-rs` stack (the `/INTEGRITYCHECK` answer + +the `iddcx` `wdk-sys` binding), IDD-push zero-copy capture at 5K@240 HDR, the secure desktop (Winlogon / +UAC / lock), and the host re-architected into a clean, typed, layered shape. What remains is +**non-blocking**: hygiene (host `unsafe` lints, a few `OwnedHandle` rollouts), the SudoVDA backend +deletion (decoupled, not yet removed), a driver robustness gap (slot reclaim), the gamepad-driver +unification (M4), and old-monolith cleanup (M6) — plus the merge to `main`. + +One framing correction baked into this doc: the host was **not** greenfield-rebuilt as the original plan +imagined. It was **refactored in place** via a staged, behavior-preserving sequence (the "Goal-1" plan), +which kept the live-validated host working at every step. The driver, by contrast, *was* rebuilt fresh +(the new `packaging/windows/drivers/pf-vdisplay/` tree). + +### Scorecard (verified against `windows-host-goal1` HEAD, 2026-06-25) | Item | Status | Evidence | |---|---|---| -| **M0** — proto crate, driver workspace, `iddcx` binding, `/INTEGRITYCHECK` | ✅ **DONE** | `pf-vdisplay-proto`; `packaging/windows/drivers/`; `clear-force-integrity.ps1`; CI-green (§13) | -| **§14 STEP 0–8** — pf-vdisplay driver port (device→adapter→control→swap-chain→frame transport→HDR→.inx→unsafe pass) | ✅ **DONE** | `d7a9fbf`…`cd59151`; on-glass HDR (`6399d28`: "Mac connects WITH HDR") | -| **M1/M2** — IDD-push capture + NVENC glass-to-glass | ✅ **DONE** | new driver tree + the existing host IDD-push path; 5K@240 HDR zero-copy on-glass | -| **§2.5** — ownership-model rewrite (`VirtualDisplayManager`/`MonitorLease`); swap-chain-reuse / monitor-leak | ✅ **DONE / RESOLVED** | `windows-host-goal1` §2.5 (`1520201`…`683c81b`); reconnect-leak A/B: 0 leaked monitors | -| **Goal-1 host refactor** (the in-place §2.2–2.5 realization, incl. `EncoderCaps`) | ✅ **DONE** | `windows-host-goal1` branch — all 6 stages + §2.5 + 3 seam tightenings | -| **Game-capture bug (GB1)** — fullscreen game breaks IDD-push | ✅ **FIXED** | `c87bfe0`/`f98ab07`/`789ad49`; see [game-capture-bug.md](windows-host-rewrite-game-capture-bug.md) | -| **M3** — service / input / audio / **secure desktop** | ✅ **DONE** — secure desktop (lock/UAC) on-glass validated | owner-confirmed 2026-06-25: IDD-push captures the secure desktop + input reaches it | -| **M4** — gamepad drivers (`pf_dualsense`/`pf_xusb`) onto the unified stack, WDF device contexts (true multi-pad) | ❌ **NOT STARTED** | old gamepad-driver crates still separate | -| **M5** — demoted WGC/DDA fallback port + GameStream-on-`session/pipeline` + AMF/QSV (no hw) | 🟡 **PARTIAL** | fallbacks exist; not re-shaped onto the new seams | -| **M6** — cut over + delete the old monoliths | 🟡 **PARTIAL** | old `vdisplay-driver/` tree deleted (`a2bd0cd`); host monoliths remain | +| **Goal 1** — clean, layered host architecture | ✅ **DONE** | `config.rs` (`HostConfig`), `session_plan.rs` (`SessionPlan`), `SessionContext`, `windows/`+`linux/` confinement (`38c68c3`), `VirtualDisplayManager` (§2.5), `EncoderCaps` (`0ccd0fe`) | +| **Goal 2** — drop every trace of SudoVDA | 🟡 **PARTIAL** | reach-in **decoupled** (F1: `d638a93`/`e60cda3` → `win_adapter`/`win_display`); `sudovda.rs` still present as a fallback backend — deletable now, not yet deleted | +| **Goal 3** — minimize `unsafe` + P0 lints | 🟡 **PARTIAL** | driver `deny(unsafe_op_in_unsafe_fn)` (`a755d6e`); host crate has **no** P0 lints yet; `OwnedHandle` adopted in `manager.rs`/`pf_vdisplay.rs`/`sudovda.rs`, **not** `idd_push.rs` | +| **M0** — proto ABI + driver toolchain + `/INTEGRITYCHECK` + `iddcx` | ✅ **DONE** | `pf-vdisplay-proto`; vendored `windows-drivers-rs` 0.5.1; `clear-force-integrity.ps1`; CI-green | +| **M1** — new IddCx driver, first light + HDR | ✅ **DONE (on-glass)** | STEP 0–8 (`d7a9fbf`…`cd59151`); HDR live ("Mac connects WITH HDR", `6399d28`) | +| **M2** — IDD-push capture + NVENC, glass-to-glass | ✅ **DONE (on-glass)** | 5120×1440@240 HDR zero-copy; integrated into the host path | +| **M3** — service / input / audio / **secure desktop** | ✅ **DONE (on-glass)** | secure desktop (lock/UAC) **owner-confirmed 2026-06-25** — IDD-push captures it + input reaches it | +| **M4** — gamepad drivers onto the unified stack | ❌ **OPEN** | `pf_dualsense`/`pf_xusb` still standalone (`packaging/windows/{dualsense,xusb}-driver/`), not in `drivers/` workspace | +| **M5** — WGC/DDA fallback reshape + GameStream-on-pipeline + AMF/QSV | 🟡 **PARTIAL** | fallbacks exist (`wgc.rs`/`wgc_relay.rs`/`dxgi.rs`), not reshaped onto the new seams; AMF/QSV CI-only (no lab hw) | +| **M6** — cut over + delete the old monoliths | 🟡 **PARTIAL** | old `vdisplay-driver/` tree deleted (`a2bd0cd`); host monoliths + bring-up scaffolding (`spawn_observer`/`DebugBlock`) remain | +| **Game-capture bug (GB1)** — fullscreen game breaks IDD-push | ✅ **FIXED** | resolution-listening recovery (`c87bfe0`) + open-time DDA failover (`f98ab07`) + driver guard/log (`789ad49`) | +| **Audit P0/P1/P2** | ✅ mostly **RESOLVED** | watchdog, `SET_RENDER_ADAPTER`, log gate, mode bounds, IDD-push fallback, F1, out-ring/HDR-ring, proto asserts — all landed; **open:** host hygiene (§8), E1 completion, slot-reclaim | -### What genuinely remains +--- -With the secure-desktop gate passed (below), the primary-path risk is retired. What's left is migration / -cleanup / a driver robustness gap — none of it blocking the validated streaming path: +## 2. Architecture (what is on disk) -1. **M4 — gamepad-driver migration** onto `windows-drivers-rs` (WDF device contexts → true multi-pad). The - proven recipe exists; ~2–3 days, hardware-gated. -2. **M5/M6 cleanup** — re-shape the WGC/DDA fallback + GameStream onto `session/pipeline`, then delete the - old Windows monoliths. Low priority; AMF/QSV stays CI-only (no lab hw). -3. **pf-vdisplay driver slot reclaim** — sustained ADD/REMOVE churn wedges the driver (`ADD → - 0x80070490 ERROR_NOT_FOUND`): it doesn't reclaim IddCx monitor slots on REMOVE (ghost nodes accumulate). - Recovery today is `packaging/windows/reset-pf-vdisplay.ps1`; the real fix is in the driver - (`control.rs`/`adapter.rs`). Dev helpers `reset-pf-vdisplay.ps1` + `redeploy-pf-vdisplay.ps1` are committed. +### 2.1 Layering & crates -### Resolved since the original §11 open items +- **`crates/punktfunk-host`** — one shared host crate (Linux + Windows; not split). Platform code is + confined under per-module `windows/`+`linux/` folders behind `#[cfg]` seams (`capture/{windows,linux}/`, + `encode/{windows,linux}/`, `inject/{windows,linux}/`, `audio/{windows,linux}/`, `vdisplay/{windows,linux}/`, + and top-level `src/windows/`+`src/linux/`). Module names stay flat (`#[path]`), so caller paths are + platform-agnostic. +- **`crates/punktfunk-core`** — the one linked protocol/FEC/crypto/QUIC core (unchanged here). +- **`crates/pf-vdisplay-proto`** — the owned, `no_std` host↔driver ABI (frame ring + control plane + + gamepad SHM), consumed by both the host crate and the driver workspace (§2.7). +- **`packaging/windows/drivers/`** — the unified driver workspace on `microsoft/windows-drivers-rs` + (vendored 0.5.1 + an `iddcx` subset): members `pf-vdisplay` (the IddCx display driver), `wdk-iddcx` + (the typed IddCx DDI wrappers), `wdk-probe` (the CI link/surface gate), `vendor/{wdk-build,wdk-sys}`. -- **Secure desktop (the single biggest open risk; §14 STEP 6 / "biggest risk").** ✅ **Confirmed on glass - (owner, 2026-06-25): the IDD-push primary path captures the lock screen / UAC secure desktop AND input - reaches the streamed console session — "works great."** The core assertion the whole capture strategy - (Decision B) rested on is now proven, not asserted; the WGC-relay / secure-DDA path is no longer load- - bearing (kept only as a non-IddCx-hardware fallback). -- **Driver swap-chain reuse** — the clean ownership model (`EvtCleanupCallback` + DeviceContext-owned state + - single `Monitor` identity) is in; §2.5's reconnect-leak A/B shows **0 leaked active monitors**. The - per-frame `CURRENT_MON_GEN` "monitor-gen bail" turned out to have been **write-only** (never wired), so the - "carry the gen through `WinCaptureTarget`" item was dropped; the gen lives on the manager + lease only. -- **`/INTEGRITYCHECK` + `iddcx` on `wdk-sys`** — both proven CI-green (§13). +### 2.2 Session resolution — `HostConfig → SessionPlan → SessionContext` (Goal-1 realized) -Box reminder: the RTX box (`ssh "Enrico Bühler"@…`) is **ephemeral** (boots to Proxmox on reboot; IP floats -on DHCP — has been `.173`/`.158`); the windows-amd64 CI runner is the persistent validator. On-glass gates -are opportunistic. +The old ~40-knob `PUNKTFUNK_*` env soup, re-read and recomputed in three places, is replaced by a +resolve-once pipeline: + +- **`config.rs` `HostConfig`** — typed config parsed **once** from `host.env`/env/flags + (`idd_push`/`encoder_pref`/`no_wgc`/`capture_backend`/`render_adapter`/`secure_dda`/`ten_bit`/`zerocopy`/…). + Each field's parser is byte-identical to the read it replaced. (Runtime-mutated Linux session vars from + `vdisplay::apply_session_env`, and single-use local tuning knobs, are deliberately kept live — see the + `config.rs` header.) +- **`session_plan.rs` `SessionPlan { display, capture, topology, encoder, input_format, bit_depth, hdr, + pipeline_depth }`** — a `Copy` plan resolved **once** per session from `HostConfig` + the negotiated + bit-depth, logged, and threaded through `build_pipeline`. `CaptureBackend::resolve()` is the one + resolver (`IddPush | Dda | Wgc`); `resolve_topology` decides `SingleProcess | TwoProcessRelay`. This + killed the latent capture/encode backend-disagreement bug. +- **`SessionContext`** — bundles the session entry's ~13 args (was `#[allow(too_many_arguments)]`) and the + plane receivers into one owned struct moved into the stream thread. + +### 2.3 Ownership model — `VirtualDisplayManager` + `MonitorLease` (§2.5 realized) + +A single **OnceLock `VirtualDisplayManager`** (`vdisplay/windows/manager.rs`) owns a *typed* +`Arc` control-device handle (no raw-`isize` cross-thread smuggle), the refcounted +Idle/Active/Lingering state machine, and the monitor generation (`AtomicU64`). Both Windows backends +(`pf_vdisplay`, `sudovda`) shrank to thin `VdisplayDriver` impls (`open`/`add_monitor`/`remove_monitor`/ +`ping`) behind it; `MonitorKey = Guid | Session(u64)`. A per-session `MonitorLease`'s `Drop` releases the +refcount (a stale lease can't tear down a fresh monitor). This deleted the old `CURRENT_MON_GEN`/`MON_GEN`/ +two-`MGR`/`IDD_PERSIST`/`IDD_SETUP_LOCK`/`IDD_SESSION_STOP` globals. Validated on glass: **0 leaked active +monitors across a reconnect storm**, A/B-equivalent to the shipping host. (The 5-agent map found +`CURRENT_MON_GEN` had been **write-only** — the per-frame "monitor-gen bail" was never wired — so the gen +lives on the manager + lease only.) + +### 2.4 The seam traits + +`VirtualDisplay`/`VirtualOutput`/`VirtualLease` (RAII keepalive = release), `Capturer` +(`next_frame`/`try_latest`/`set_active`/`hdr_meta`/`pipeline_depth`), `Encoder` +(`submit`/`caps`/`request_keyframe`/`set_hdr_meta`/`invalidate_ref_frames`/`poll`/`flush`), +`AudioCapturer`/`VirtualMic`/`InputInjector`/`PadManager`. Realized tightenings: the capturer takes the +desired `OutputFormat { gpu, hdr }` **in** (killed the `capture → encode::windows_resolved_backend()` +back-reference recomputed in `dxgi.rs`); and `Encoder::caps() -> EncoderCaps { supports_rfi, +supports_hdr_metadata }` lets the session glue route loss-recovery by query (only Windows direct-NVENC +overrides it; the GameStream loop gates the RFI path on `supports_rfi`). + +### 2.5 Capture — IDD-push primary (normal **and** secure desktop), WGC/DDA fallback, GB1 recovery + +**IDD-push is the universal primary path.** Capture comes straight from the driver's shared keyed-mutex +texture ring (`capture/windows/idd_push.rs`) — no Desktop Duplication, no `win32u` reparenting hook. The +host creates the ring; the driver opens it (permissive `D:(A;;GA;;;WD)` SDDL). The generation-tagged +`latest = gen<<40 | seq<<8 | slot` stale-ring reject kills the HDR-flip garbage frame; a host-owned +3-slot `OUT_RING` rotated per frame is the texture-ownership contract that enables `pipeline_depth=2` +(convert/copy on the 3D engine overlapping NVENC on the ASIC). It captures the **secure desktop** +(Winlogon/UAC/lock) directly (validated 2026-06-25), so there is no separate secure capturer in the +primary path. + +- **Open-time fallback:** `IddPushCapturer::open` waits a bounded ~4 s for a *first frame* (not just + `DRV_STATUS_OPENED`); on attach failure it returns the keepalive back so `capture.rs` opens **DDA** on + the same `WinCaptureTarget` — never a 20 s black bail (audit §5.1, `ed58365`/`f98ab07`). +- **Mid-session game mode-set recovery (GB1, fixed):** the 250 ms poll follows the display's *actual* + resolution (`win_display::active_resolution`, CCD/GDI) and recreates the ring on any descriptor change + (size **or** HDR) → the driver re-attaches → frames resume at the game's mode, **no reconnect**. If a + change is unrecoverable (e.g. an exclusive flip), a `recovering_since` clock drops the session after 3 s + so the client reconnects cleanly. No protocol bump was needed — the host reads the resolution straight + from Windows (`c87bfe0`; the driver's `publish()` width/height guard + flushed log is `789ad49`). +- **WGC + DDA** stay as demoted fallbacks for non-IddCx hardware (`wgc.rs`/`dxgi.rs`). The two-process WGC + secure-desktop relay (`wgc_relay.rs`) is no longer load-bearing now that IDD-push handles the secure + desktop; it is kept recoverable but slated for M5/M6 cleanup. + +### 2.6 Encode — NVENC / AMF / QSV / software; `EncoderCaps`; HDR + +`encode/windows/` dispatches per DXGI adapter vendor (`open_video`): **NVENC** (NVIDIA, direct SDK, +`nvenc.rs` — caps-probe-before-configure, bitrate-clamp binary search, true RFI over the DPB, in-band +ST.2086/CLL SEI), **AMF**/**QSV** (AMD/Intel via libavcodec, `ffmpeg_win.rs` — system-readback default, +opt-in zero-copy D3D11; CI-only, no lab hardware), or **software** H.264 (`sw.rs`). HDR (10-bit) forces +HEVC Main10 + BT.2020 PQ; the client auto-detects PQ from the VUI. The encoder adapts to a mid-session +size/format/HDR change per frame (tears down + re-inits), so the GB1 capturer's resolution changes are +handled downstream with no API change. + +### 2.7 Host↔driver ABI — `pf-vdisplay-proto` + +One `no_std` crate, both build graphs. Owns the **frame plane** (`SharedHeader`, `FrameToken { generation, +seq, slot }` with `pack`/`unpack`, `Global\pfvd-*` name helpers), the **control plane** (fresh interface +GUID — not SudoVDA's `e5bcc234`; contiguous `0x900` IOCTL ops; `u64` session id; a real `GET_INFO` version +handshake the host **asserts** + bails on mismatch), and the **gamepad SHM** (`XusbShm` 64 B, `PadShm` +256 B incl. `device_type`). `bytemuck`-`Pod` + `size_of` **and** `offset_of!` asserts make ABI drift a +**compile error** (`95dcef3`). The host-side gamepad consumers derive their layouts from here; the +**driver-side** gamepad drivers do not yet (M4). + +### 2.8 The `pf-vdisplay` IddCx driver + +All-Rust UMDF IddCx driver on `windows-drivers-rs` + the `iddcx` `wdk-sys` subset. STEP 0–8 landed +(`packaging/windows/drivers/pf-vdisplay/src/`): `entry.rs` (DriverEntry + `IDD_CX_CLIENT_CONFIG`, 15 +callbacks), `adapter.rs` (caps + FP16 + `SET_RENDER_ADAPTER`), `monitor.rs`/`callbacks.rs` (the `*2` HDR +mode DDIs, EDID verbatim), `swap_chain_processor.rs` (the worker, `SetDevice`-retry + top-of-loop +`terminate`), `frame_transport.rs` (the `FramePublisher` on `pf_vdisplay_proto::frame`), `control.rs` (the +typed IOCTL dispatch + host-gone **watchdog** + mode bounds). Self-signed-loadable under Secure Boot +(FORCE_INTEGRITY cleared post-link). **Known gaps:** ownership state is still partly process-global +(`MONITOR_MODES`/`NEXT_ID`/`ADAPTER`/`DEVICE_POOL`) with `EvtCleanupCallback` on the **WDFDEVICE** (not +per-`IDDCX_MONITOR`) — see E1 in §4; and it does not reclaim IddCx monitor **slots** on REMOVE (the +ghost-monitor wedge, §4). + +### 2.9 Service, packaging, installer + +A `LocalSystem` SCM supervisor (`service.rs`) token-retargets and `CreateProcessAsUserW`s `serve` into the +console session (so `SendInput` reaches the streamed desktop + the secure desktop), relaunches on +session-change, and kills-on-close via a Job Object. Shipped as a **signed Inno Setup** `setup.exe` +(`packaging/windows/`, `windows-host.yml`) that bundles the **new** `pf-vdisplay` driver +(`pf_vdisplay.inx` in-tree, old `vdisplay-driver/` tree deleted) + FFmpeg DLLs and delegates to `service +install`. GameStream (Moonlight) is kept but the installer/service default to secure `serve` (GameStream +opt-in). + +--- + +## 3. Validated invariants — preserve, do not regress + +These are expensive empirical wins; keep them intact when touching the code: + +- **Frame transport:** host-creates/driver-opens keyed-mutex ring; generation-tagged stale-ring reject; + 0 ms try-acquire / drop-on-full publish (never block the swap-chain thread); the `OUT_RING` rotation + + `pipeline_depth=2` overlap; `repeat_last` rotates into a fresh out-ring slot (depth-safe). +- **Driver internals:** `edid.rs` (128-byte EDID + CTA-861.3 HDR block, dual checksums); the FP16 HDR + recipe (`CAN_PROCESS_FP16` + the `*2` DDIs + gamma/HDR accept-stubs + `HIGH_COLOR_SPACE`); `DEVICE_POOL` + per render-LUID (NVIDIA UMD/VRAM leak fix); target-id stamped on the monitor context; the two swap-chain + leak fixes (borrow `IDXGIDevice` across `SetDevice` retries; check `terminate` at the loop top). +- **Monitor lifecycle:** serialized ADD/REMOVE/teardown; restore CCD topology **before** REMOVE; the + generation-stamped lease (a stale lease can't tear down a fresh monitor); 0-leak across reconnects. +- **HDR color math:** `hdr.rs` (pure, unit-tested, ST.2086 + big-endian SEI); the FP16→P010/Rgb10a2 + converters + `hdr_p010_selftest`; the cursor decomposition. +- **NVENC tuning:** caps-probe-before-configure (10-bit→8-bit graceful downgrade); bitrate-clamp binary + search (each GPU's real ceiling); true RFI over the DPB; CBR / infinite-GOP / P-only / ~1-frame VBV. +- **Gamepad recipe:** the SwDeviceCreate identity (enumerator with no `_`; mandatory completion callback; + synthesized DS5 compat-ids; non-null per-pad `ContainerId`); one `pf_dualsense` serving DualSense+DS4 + via a `device_type` byte; XUSB declining `WAIT_*`; per-pad index via `pszDeviceLocation`. +- **Session glue:** the trait seam + RAII keepalive teardown; host-lifetime shared services + per-session + gamepads; the encode|send split + microburst pacing; `build_pipeline_with_retry` permanent-vs-transient + classification; the GameStream `VideoPacketizer` (GF8 Cauchy, Moonlight byte-exact); the pairing/trust + handshake. +- **Core discipline:** no async on the per-frame path; `pf-vdisplay-proto` is the single ABI source + (drift = compile error); the version handshake the host asserts. + +--- + +## 4. Open work / next tasks (prioritized) + +**P1 — ship-readiness / correctness** +1. **Merge `windows-host-goal1` → `main` + push** (outward-facing → confirm first). Pushing also runs the + full Windows CI matrix incl. the `amf-qsv` encode path, which local checks skip. +2. **Make IDD-push the default** — today it is gated behind `PUNKTFUNK_IDD_PUSH` (`config.rs` default + `false`); deployment sets it in `host.env`. Flip the code default (with the WGC/DDA fallback already in + place) so a fresh install runs the validated path, or document the `host.env` requirement explicitly. +3. **pf-vdisplay slot reclaim on REMOVE** (driver robustness) — sustained ADD/REMOVE churn wedges the + driver (`ADD → 0x80070490 ERROR_NOT_FOUND`) because IddCx monitor slots aren't reclaimed (ghost nodes + accumulate). Workaround today: `packaging/windows/reset-pf-vdisplay.ps1`. Real fix in `control.rs`/ + `monitor.rs`. On-glass-gated. + +**P2 — hygiene / architecture completion** +4. **D1-host — host-crate P0 lints.** Add `#![deny(unsafe_op_in_unsafe_fn)]` + + `#![warn(clippy::undocumented_unsafe_blocks)]` to the host crate and fix the fallout (large, mechanical, + touches Linux + Windows `unsafe`). Do it incrementally per-subsystem. The driver already has the deny. +5. **D2 — `OwnedHandle` in `idd_push.rs`.** `map`/`event`/`dbg_map` are still raw `HANDLE` closed in `Drop`; + wrap in `std::os::windows::io::OwnedHandle` (RAII close, fixes leak-on-error). `manager.rs` already shows + the pattern. +6. **Goal 2 — delete `sudovda.rs`.** The reach-in is fully decoupled (F1); the backend is now a thin, + deletable fallback. Retire it (and the `PUNKTFUNK_VDISPLAY=sudovda` path) to finish "drop SudoVDA." +7. **E1 — finish the driver ownership refactor.** Move the process-globals + (`MONITOR_MODES`/`NEXT_ID`/`ADAPTER`/`DEVICE_POOL`) into a `DeviceContext`; wire `EvtCleanupCallback` on + the `IDDCX_MONITOR` object (today only the WDFDEVICE has it); collapse the 3-key monitor identity. This + is the prerequisite to `max_concurrent>1` on Windows + removes the host-side preempt dance. **On-glass + gated** (must instrument that `MonitorContext::Drop` actually fires on this UMDF/IddCx stack; keep the + explicit REMOVE path as fallback if it doesn't). +8. **M6 scaffolding cleanup** — delete the bring-up diagnostics (`spawn_observer`/`DebugBlock` in + `idd_push.rs`) and, once full parity is proven on glass, the host monoliths. + +**P3 — larger, mostly hardware-gated** +9. **M4 — gamepad-driver unification.** Fold `pf_dualsense` + `pf_xusb` (standalone + `packaging/windows/{dualsense,xusb}-driver/` on the old WDF stack) into the unified `drivers/` workspace + on `windows-drivers-rs` with WDF device contexts (true multi-pad), and point the **driver side** at + `pf_vdisplay_proto::gamepad::{PadShm,XusbShm}` (host side already does — the `device_type`-at-offset-140 + hand-duplication is the last ABI-drift hazard). Largest item. +10. **M5 — reshape WGC/DDA + GameStream onto `session/pipeline`**, then delete the old relay/monoliths. + AMF/QSV stays CI-only (no lab hardware). +11. **On-glass behavioral validation** of the committed-but-unexercised fixes: the watchdog reaping on + host-kill, `SET_RENDER_ADAPTER` on a **hybrid** box (the lab box is single-dGPU), the IDD-push→DDA + fallback trigger, HDR-ring sizing + out-ring repeat under real HDR/static-desktop pipelining. + +--- + +## 5. Operations + +### 5.1 RTX box on-glass recipe + +The persistent on-glass validator is the **RTX box** (`ssh "Enrico Bühler"@`, ENRICOS-DESKTOP, RTX +4090, PS shell). **The IP FLOATS** (DHCP; boots to **Proxmox** on reboot → ephemeral, unreachable after a +reboot; recently `.173`/`.158` — confirm current first; **never reboot it, never depend on it surviving**). +It has WDK 26100 + LLVM 21.1.2 + the Rust toolchain; build clone at `C:\Users\Public\pf-rewrite` (the +user's active driver-dev tree — **don't clobber uncommitted WIP**; use a worktree). Username has a `ü` → +quote it; it only breaks SDL3/client builds, not the host. To validate a host branch: worktree-checkout, +build with `CARGO_TARGET_DIR=C:\t-goal1`, then stop the **PunktfunkHost** service, back up the binary + +`%ProgramData%\punktfunk\host.env`, copy your build in, restart, drive `punktfunk-probe.exe` loopback, +then restore + `git worktree remove`. Drive over ssh via `powershell -EncodedCommand ` +(plain quoting mangles; prefer `Write-Output`/file-redirect for clean output). Driver redeploy: +`packaging/windows/redeploy-pf-vdisplay.ps1`; ghost-monitor recovery: `reset-pf-vdisplay.ps1`. + +### 5.2 CI / validation + +The persistent build validator is the **windows-amd64 CI runner** (no GPU — fine for builds / `iddcx` +link / `/INTEGRITYCHECK` self-sign / the surface-asserts; live NVENC encode + on-glass defers to the RTX +box). Workflows: `windows-host.yml` (the host installer), `windows-drivers.yml` (the driver workspace +build + FORCE_INTEGRITY clear), `windows-drivers-provision.yml` (WDK/LLVM toolchain), `windows-msix.yml` +(the client). A single Windows runner serializes the whole fleet; a `Cargo.toml` touch costs ~25 min of +queue, so driver pushes that avoid `Cargo.toml` skip the fleet serialization. + +Local pre-push checks (this Linux box can't compile the Windows paths): +```sh +cargo test -p pf-vdisplay-proto # the ABI crate (cross-platform) +cargo check -p punktfunk-host # Linux paths; win_* mods are #[cfg(windows)] +cargo clippy -p punktfunk-host --all-targets -- -D warnings +# Windows host clippy (on the box): PUNKTFUNK_NVENC_LIB_DIR=C:\t\nvenc; +# cargo clippy -p punktfunk-host --features nvenc --target x86_64-pc-windows-msvc -- -D warnings +# Driver build (on the box): cd packaging/windows/drivers; Version_Number=10.0.26100.0; +# LIBCLANG_PATH='C:\Program Files\LLVM\bin'; cargo build +``` +Note: a pre-existing rustfmt-version drift exists in some Windows-only files (this box's rustfmt 1.9.0 +wraps `offset_of!`/`unsafe fn` differently than the runner's) — don't reformat unrelated files to chase it. + +### 5.3 Env knobs (Windows host) + +`PUNKTFUNK_IDD_PUSH=1` (capture from the driver ring; default off), `PUNKTFUNK_VDISPLAY=pf|sudovda`, +`PUNKTFUNK_ENCODER=auto|nvenc` (auto → vendor-detect), `PUNKTFUNK_10BIT=1` + `PUNKTFUNK_HDR_SHADER_P010=1` +(HDR), `PUNKTFUNK_SECURE_DDA=1`, `PUNKTFUNK_NO_WGC=1` (pure DDA), `PUNKTFUNK_ZEROCOPY=1`, +`PUNKTFUNK_MONITOR_LINGER_MS`, `PFVD_DEBUG_LOG=1` (driver file log — release builds are silent without it). +Config lives in `%ProgramData%\punktfunk\host.env`; logs in `%ProgramData%\punktfunk\logs\host.log`. + +### 5.4 Build / deploy / packaging + +x64-only by design (no ARM64 NVIDIA driver / SudoVDA). The installer is the thin-`.iss` / fat-binary model +delegating to `service install`; tag `host-win-vX.Y.Z`. The driver is built + FORCE_INTEGRITY-cleared + +signed + `Inf2Cat`'d in CI from source. DriverVer must bump on any driver change; create the ROOT devnode +via nefcon (devgen is forbidden). + +--- + +## 6. Reference (hard-won — keep) + +### 6.1 The `/INTEGRITYCHECK` answer + +`wdk-build` emits `cargo::rustc-cdylib-link-arg=/INTEGRITYCHECK` **unconditionally** (no cfg/env/Config +opt-out), so a self-signed driver can't load (CodeIntegrity 3004/3089). The fix: a deterministic, +idempotent post-link step `packaging/windows/clear-force-integrity.ps1` clears the PE FORCE_INTEGRITY bit +(`0x0080 @ e_lfanew+0x5e`) + verifies (CI-proven `0x01E0 → 0x0160`), **before** signing. Packaging order: +`cargo build` → clear-force-integrity → sign `.dll` → `Inf2Cat` → sign `.cat`. (A public build would use +real attestation signing, which satisfies `/INTEGRITYCHECK` legitimately.) + +### 6.2 The `iddcx` binding on `wdk-sys` (the make-or-break — proven, the 6 bindgen knobs) + +IddCx DDIs are **function-table dispatched** (`IddFunctions[]` indexed by `_IDDFUNCENUM::TableIndex`, +`IddDriverGlobals` implicit arg 1) — the same model `wdk-sys` already implements for WDF. The vendored +`windows-drivers-rs` 0.5.1 (`packaging/windows/drivers/vendor/`, `[patch.crates-io]`'d) gets a first-class +`ApiSubset::Iddcx` that bindgens `iddcx/1.10/IddCx.h` reusing the identical `wdk_default(config)` baseline +(so WDF/DXGI types **resolve to**, not redefine, `wdk-sys`'s — type-identity by construction). The six +knobs `generate_iddcx` needed (each a real gotcha, all CI-proven): + +1. **`--language=c++`** — `wdk_default` parses C; `IddCx.h`'s `IDARG_*` typedefs need C++ (else a "must use + 'struct' tag" cascade). +2. **`-DIDD_STUB`** — table-dispatch mode; skips `IddCxFuncEnum.h`'s `#error IDDCX_VERSION_MAJOR not + defined`. **Do NOT add `WDF_STUB`** (would desync the shared WDF type-identity). +3. **`allowlist_recursively(false)` + `allowlist_file("(?i).*iddcx.*")`, full codegen (no `.complement()`)** + — emit ONLY IddCx items; WDF/Win types resolve via `use crate::types::*`. +4. **`allowlist_type("_?DXGI_.*" / "IDXGI.*" / "_?OPM_.*" / "_?D3DCOLORVALUE")`** — emit the non-WDF types + `wdk-sys` doesn't bindgen, locally. The `_?` is load-bearing (`typedef struct _OPM_X {} OPM_X` needs the + tag AND the alias). +5. **`pub type UINT = ::core::ffi::c_uint;` in `src/iddcx.rs`** — `UINT` is absent from `crate::types`. +6. **`translate_enum_integer_types(true)`** — emit native `u32` reprs for the DXGI/OPM ModuleConsts enums + (nested modules can't see a parent `UINT`). + +Wrapper note: table dispatch via `_IDDFUNCENUM::TableIndex as usize` (the ModuleConsts const, **not** +a NewType `.0`); NTSTATUS is plain `i32` (`wdk_sys::NT_SUCCESS`). The driver `build.rs` adds the IddCxStub +link-search (the import lib is under `iddcx\1.0\` even though headers are `1.10`) + `#[no_mangle] pub static +IddMinimumVersionRequired: ULONG = 4`. The versioned `IDD_STRUCTURE_SIZE!` path is dropped — the WDK links +the iddcx **1.0** stub (lacks the version table); we target 1.10 vs a current framework, so `size_of` is +exactly correct. + +### 6.3 Driver port checklist (STEP 0–8, as landed) + +0. workspace `pf-vdisplay`(cdylib)+`wdk-iddcx`; prove `std::thread`+`OwnedHandle` link under UMDF (done). +1. `wdk-iddcx`: 11 typed DDI wrappers via one dispatch macro + re-export the inbound `PFN_*` types. +2. DriverEntry + `IDD_CX_CLIENT_CONFIG` (15 callbacks) + DeviceInitConfig + WdfDeviceCreate + + CreateDeviceInterface (the owned pf GUID) + DeviceInitialize; `edid.rs` salvaged verbatim. +3. DeviceContext + `WDF_DECLARE_CONTEXT_TYPE` blob; `init_adapter` in D0Entry (caps + FP16) → + AdapterInitAsync; the `*2` mode DDIs + `query_target_info` + gamma/HDR accept-stubs. (Box gate: loads + under Secure Boot, enumerates as an IddCx adapter, Status OK.) +4. control plane (`GET_INFO` version handshake the host asserts, ADD/REMOVE/SET_RENDER_ADAPTER/PING/ + CLEAR_ALL) + create_monitor + real mode DDIs + watchdog + mode bounds; host switched to + `pf_vdisplay_proto`. +5. `Direct3DDevice` + assign/unassign + `SwapChainProcessor` (worker, `SetDevice` 60×@50 ms single-borrow + retry, top-of-loop `terminate`, `ReleaseAndAcquireBuffer2`, `from_raw_borrowed`). +6. `FramePublisher` on `pf_vdisplay_proto::frame` + keyed-mutex RAII guard; wire into `run_core`. (Box: + full IDD-push glass-to-glass + the **secure-desktop** gate — validated 2026-06-25.) +7. HDR / FP16 ring (validated: Mac connects WITH HDR). +8. its own `.inx` + an `unsafe`-reduction pass (`deny(unsafe_op_in_unsafe_fn)`, per-site `// SAFETY:`). + +**Remaining driver work** beyond STEP 8: E1 (DeviceContext-owned state + per-`IDDCX_MONITOR` +`EvtCleanupCallback` → unblock `max_concurrent>1`), the slot-reclaim-on-REMOVE fix, and M4 (fold the +gamepad drivers in). See §4. + +### 6.4 Resolved product decisions (the five forks) + +**A** the host was refactored **in place** (staged, behavior-preserving), not greenfield-rebuilt — the +driver *was* rebuilt fresh. **B** IDD-push primary for everything incl. the **secure desktop** (validated); +WGC+DDA demoted to non-IddCx fallbacks. **C** all drivers on `microsoft/windows-drivers-rs` (+ the `iddcx` +subset; `/INTEGRITYCHECK` solved) — done for `pf-vdisplay`, **pending for the gamepad drivers (M4)**. +**D** keep GameStream (Moonlight), default to secure `serve`. **E** concurrent sessions: the host-side +preempt dance was removed by §2.5, but true `max_concurrent>1` on Windows stays blocked on the E1 driver +swap-chain-reuse work. + +--- + +## Appendix — consolidation note + +This file replaces five docs (recoverable from git history): + +- `windows-host-rewrite.md` (the original design + plan, §0–§15) — its current status, architecture, the + jewels, the seam traits, and the deep reference (§6) are folded in here. +- `windows-host-goal1-plan.md` (the 6-stage in-place host refactor) — **complete**; its outcome is §2.2–2.4 + and the Goal-1 scorecard row. +- `windows-host-rewrite-audit.md` (the 2026-06-25 audit) — its findings are reconciled to current reality + in §1 (scorecard) and §4 (only the still-open items survive: host hygiene, E1, slot-reclaim). +- `windows-host-rewrite-remediation.md` (the audit-remediation tracker) — its landed items are in §1; its + remaining items (D1-host, D2, E1, G) are §4 P2/P3. +- `windows-host-rewrite-game-capture-bug.md` (the GB1 investigation + fix) — **fixed**; the resolution is + §2.5 (capture). The full investigation narrative is in git history. + +(The older `docs/windows-host.md`, a pre-rewrite implementation plan from 2026-06-22, is a separate +lineage and is left as-is.) diff --git a/packaging/windows/drivers/pf-vdisplay/src/adapter.rs b/packaging/windows/drivers/pf-vdisplay/src/adapter.rs index 6d98ada..070beb1 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/adapter.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/adapter.rs @@ -147,7 +147,7 @@ pub(crate) fn adapter() -> Option { /// iGPU+dGPU box the OS may otherwise pick the iGPU to render the virtual monitor, so the host's shared /// ring textures (created on the NVENC dGPU) can't be opened → `DRV_STATUS_TEX_FAIL` → the host's 20 s /// black bail. Pinning the render adapter to the encode GPU fixes that. Unconditional — NOT the -/// SudoVDA-parity default-off branch (`docs/windows-host-rewrite-audit.md` §4.2). Returns +/// SudoVDA-parity default-off branch (`docs/windows-host-rewrite.md` §2.8). Returns /// `STATUS_NOT_FOUND` if called before the adapter exists. pub fn set_render_adapter(luid_low: u32, luid_high: i32) -> NTSTATUS { let Some(adapter) = adapter() else { diff --git a/packaging/windows/drivers/pf-vdisplay/src/control.rs b/packaging/windows/drivers/pf-vdisplay/src/control.rs index 7aa5d39..49ed727 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/control.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/control.rs @@ -27,7 +27,7 @@ static WATCHDOG_STARTED: AtomicBool = AtomicBool::new(false); /// without a cooperative REMOVE (crash / `TerminateProcess`) left its virtual monitor + swap-chain /// worker + pooled D3D device wedged in WUDFHost until the next host start's CLEAR_ALL, and a /// not-restarted host left the orphan monitor in the desktop topology indefinitely -/// (`docs/windows-host-rewrite-audit.md` §4.1). This thread closes that: if no IOCTL arrives for +/// (`docs/windows-host-rewrite.md` §2.8). This thread closes that: if no IOCTL arrives for /// `WATCHDOG_TIMEOUT_S` while monitors exist, it departs them all. /// /// (A WDF `EvtFileClose` on the control handle would be more immediate — the plan's preferred §3.4