# Windows virtual display — a Rust port of SudoVDA (investigation & plan) > **Status:** SHIPPED (P1, 2026-06-22) + P2 CLOSED as a dead end. The all-Rust IddCx driver > `pf-vdisplay` (`packaging/windows/drivers/pf-vdisplay/`) replaced the vendored SudoVDA C++ driver > (SudoVDA backend deleted in `84a3b95`) and is the **sole** Windows vdisplay backend; the host drives it > via `crates/punktfunk-host/src/vdisplay/windows/pf_vdisplay.rs`. Live-validated streaming on the RTX box > at 5120×1440@240. The current consolidated Windows-host architecture lives in > [`windows-host-rewrite.md`](windows-host-rewrite.md). This doc is trimmed to the two things git history > can't replace: the **on-glass driver-iteration gotchas**, and the **P2 decision record** proving > direct-frame-push (IDD-push) is architecturally impossible for bare-metal capture — *do not re-attempt it.* All the P1 planning/feasibility/decision content (signing tier, Rust prior art, binding-stack choice, IOCTL contract, phased plan) executed as designed and now lives in the code + `windows-host-rewrite.md`; it has been cut. What remains below is the durable record. ## Driver-iteration gotchas (hard-won, on-glass) These cost real time during P1 bring-up and apply to **any** future IddCx/UMDF driver work on this box. - **INF DriverVer gate.** Updating an installed UMDF driver only takes if the INF **DriverVer changes** — `deploy-dev.ps1` stamps a date.time `-v` on every run; without a bump the **old binary keeps running (silently)**. - **Devnode hygiene — `nefconc`, never `devgen`.** Create the root devnode with `nefconc --create-device-node` (a clean `ROOT\DISPLAY` node), **NOT** `devgen /add` — devgen makes **persistent `SWD\DEVGEN` software devices** that survive reboot *and* registry deletion and resurrect on every `pnputil /add-driver` (they carry `hwid root\pf_vdisplay`, so the driver install re-materializes them). The production installer must use a single `nefconc`/INF-created node and never `devgen`. - **Session-0 vs Session-1 observability.** Every standalone probe (`vdtest`, the host's `live_create_drop` test) runs in **Session 0** — the services session, whose desktop is a throwaway **1024×768** basic display. IddCx activation happens in the **console Session 1**, where the GPU drives the real desktop. So `Screen.AllScreens`/CCD queries from Session 0 *can never* see the virtual monitor activate — they report the wrong desktop. The only valid way to drive + observe it is the **host service** (SYSTEM, which targets Session 1) plus the driver's own `OutputDebugString` (system-wide, session-agnostic). (An early "monitor arrives but never gets a swap-chain / no DXGI output" symptom was this measurement artifact, not a driver bug.) - **Accumulated device-state damage.** Repeated reinstalls + `Disable`/`Enable-PnpDevice` cycles + a control handle the host **cached across all of it** wedge the device tree (stale handle → the host's PINGs fail → the 3 s watchdog tears the monitor down mid-session → capture opens a dying display → "no DXGI output"). A **reboot** clears it and it works on the first connect. Lesson: after device churn, restart the host service (fresh handle) — and when in doubt, reboot. - **Hot-reload is unreliable; deploy = install + reboot.** `pnputil /restart-device` does **NOT** restart WUDFHost (old image stays mapped), `Disable/Enable-PnpDevice` errors on the root-enumerated IDD, and **killing WUDFHost invalidates the host's cached `{e5bcc234}` control handle** (every ADD then fails `0x80070006`, and the device can wedge to `FAILED_POST_START`). A **reboot** loads a freshly-installed build cleanly. **Recovery** from a broken build is clean and reboot-free: `pnputil /delete-driver .inf /uninstall` removes the bad package and the device rebinds the previous (validated) package in the DriverStore. - **`FAILED_POST_START` is usually churn, not the binary.** Comparing a working vs. a suspect DLL's import tables came out **identical** (same DLLs; the size/hash delta is just the Authenticode signature). A clean install **+ reboot** (no `restart-device`/`disable-enable`/kill in between) loads to `OK`. - **The swap-chain drain is required.** The swap-chain processor is a faithful port of virtual-display-rs's — it drains correctly via `ReleaseAndAcquireBuffer` + `FinishedProcessingFrame`. The drain is *required*; a true no-op stalls DWM and freezes the captured image. - **`pf-vdisplay` can't coexist with SudoVDA.** They register the same control-interface GUID, so two IddCx adapters claiming `{e5bcc234}` → `FAILED_POST_START`. pf-vdisplay *replaces* SudoVDA (now moot — SudoVDA is deleted — but the same rule binds any second IDD that claims the GUID). ## P2 — direct frame push (kill DDA): decision record — DEAD END, DO NOT PURSUE P2 wanted the driver to *publish* each swap-chain frame to the host directly (Looking-Glass style), to retire DXGI Desktop Duplication and its multi-GPU survival code (`capture/dxgi.rs`'s `DXGI_ERROR_ACCESS_LOST`/`MODE_CHANGE_IN_PROGRESS` re-duplication churn and the `win32u.dll` `install_gpu_pref_hook()` patch). **It cannot work for bare-metal console-desktop capture.** All the IDD-push code stays in-tree, compiles, and is gated **off** behind `PUNKTFUNK_IDD_PUSH` — dormant and harmless — as the documented record so it isn't re-tried. ### What was proven sound (so the failure is *not* a transport bug) - **Producer and consumer are both in Session 0.** The pf-vdisplay host process is `WUDFHost.exe` (`-DeviceGroupId:pfVDisplayGroup`) and the punktfunk host service is `LocalSystem` — **both Session 0**. So a D3D11 **shared keyed-mutex texture** created in the driver can be opened by name in the host (`ID3D11Device1::OpenSharedResourceByName`) with both devices on the **same render-adapter LUID** (the driver reports it out of the `ADD` IOCTL via `OsAdapterLuid`). Named kernel objects resolve through Session 0's shared `\BaseNamedObjects`, so no `Global\` prefix / `SeCreateGlobalPrivilege` gymnastics are needed for same-session use. The Looking-Glass cross-*VM* shared-memory device is unnecessary — this is cross-*process*, same-session, one GPU. - **Transport shape (built):** a **ring** of N (default 3) shared keyed-mutex textures (newest-wins, so the swap-chain thread never blocks — a stalled `IddCxSwapChainReleaseAndAcquire` loop freezes DWM compositing system-wide) + a named metadata header (`{magic, version, generation, width, height, dxgi_format, ring_len, latest}`) + a frame-ready auto-reset event. A **generation** counter bumps on a mode change so the host re-opens the ring. - **The inversion (required) — host creates, driver opens.** **WUDFHost runs with a restricted token: it can neither write the filesystem nor create named kernel objects** (`CreateFileMappingW`/`CreateEventW`/ `CreateSharedHandle` all fail silently), which a file-logging driver build confirmed (it wrote no file at all even though `init()` runs in `DriverEntry` and the device is `OK`). This is exactly why the gamepad UMDF drivers invert it (`inject/dualsense_windows.rs`): **the HOST creates the section** (privileged → a permissive `Global\` name + SDDL `D:(A;;GA;;;WD)`) and **the DRIVER only OPENS it**. The host-created-ring / restricted-open split was implemented and **works every time** (`created shared ring … render_luid=…`, no name collisions after the per-attempt generation fix). The gamepad drivers independently prove a UMDF driver *can* open + write a host-created `Global\` section on this box — so the driver writing nothing is **not** an access problem. ### Root cause — the swap-chain is never assigned (fundamental, not fixable) Across **every** configuration tested, the driver's `run_core` swap-chain processor is **never entered** (`run_core_entries=0`): - in-process (Session 0) and WGC-triggered (Session 1 helper) sessions, - a user-created ring AND a host-created (LocalSystem) ring with the permissive `D:(A;;GA;;;WD)` SDDL, - with and without a Low-IL (`S:(ML;;NW;;;LW)`) mandatory label, - with WUDFHost confirmed **not** an AppContainer (`IsAppContainer=0`), — even while WGC simultaneously captured the same virtual monitor's composition and streamed multi-MB of HEVC. **An IddCx virtual monitor only receives a swap-chain (`EVT_IDD_CX_MONITOR_ASSIGN_SWAPCHAIN`) when the OS presents/scans-out to it, which requires a real presentation consumer. WGC/DDA capture of the composed desktop does NOT count** — it reads DWM's composition, bypassing the driver's swap-chain. With no physical scanout and no consumer that routes *through the driver*, the path stays inactive (`IDDCX_PATH_FLAGS=0`) and `ASSIGN_SWAPCHAIN` never fires. Session 0 additionally has no DWM/compositor at all. Ecosystem + first-party confirmation: - **Every bare-metal virtual-display capture project uses WGC/DDA, not the driver swap-chain:** SudoVDA (its swap-chain loop acquires-and-discards), Apollo/Sunshine (DDA + WGC backends), virtual-display-rs (discards), parsec-vdd (no frame path). Only **Looking Glass** consumes the driver swap-chain — and only because a **VM guest scans out** the display (the consumer). Bare metal has no equivalent. - Microsoft's own unanswered Q&A (learn.microsoft.com/answers 4096179) reports the identical symptom for the IddSampleDriver: virtual display "always inactive," `ASSIGN_SWAPCHAIN` never runs. ### Both remaining escape hatches tested and closed - **Option 3 — a present *source* on the display — TESTED, failed.** A present-trigger added to the Session-1 WGC helper successfully created a D3D11 swapchain on the virtual display and presented continuously (WGC even captured the flashing window). The driver stayed `run_core_entries=0` / `frames_acquired=0`. So an active present *source* does NOT make the OS assign the driver's swap-chain — DWM composes the present onto the display (capturable) without routing it through the driver. - **Option 2 — a driver flag — closed by analysis.** The present-trigger succeeding proves the **path is already active**; the missing piece is **scanout routed through the driver**, which the OS does only for a real consumer (physical display / VM guest / RDP). The one IddCx flag for that — `IDDCX_ADAPTER_FLAGS_REMOTE_SESSION_DRIVER` — requires the **RDP protocol stack** as the consumer, which bare-metal console capture has no equivalent of. ### Verdict (final) IDD-push needs a presentation consumer (scanout / VM guest / RDP) that bare-metal console desktop-capture fundamentally cannot provide. No host-side capture, no in-process path, no present source, and no available driver flag overcomes it. **WGC (normal desktop) + DDA (secure desktop) is the only viable Windows capture path — as the entire ecosystem already does.** Any future "lower overhead" must come from optimizing the WGC/DDA path (trimming the Session-0↔Session-1 relay, zero-copy encode), **not** from IDD-push. The remaining gaps a hypothetical IDD-push would also have had (cursor delivered separately via `IddCxMonitorSetupHardwareCursor`/`QueryHardwareCursor`; HDR needing the IddCx **1.11 D3D12 acquire path** `SetDevice2`/`ReleaseAndAcquireBuffer2` → `ID3D12Resource`, since the default swap-chain surface is 8-bit) are moot for the same reason. ## Open items **None.** P1 shipped; P2 is a permanent *do-not-pursue* record (no pending work). WGC/DDA is the shipping capture path.