The IDD-push consume loop acquired the slot's keyed mutex by hand
(`AcquireSync(0,8)` … work … `ReleaseSync(0)`), with a comment warning that a
`?`-return between acquire and release would leak the lock and stall the driver
on that slot — the reason the HDR converter is built *before* the acquire.
Replace with a `KeyedMutexGuard` RAII (acquire → `ReleaseSync` on drop), scoped
to JUST the convert/copy block so the lock releases at the EXACT same point as
before (the driver gets the slot back immediately; not held across the rest of
`try_consume`). Now the release can't be skipped on any early return/panic — the
leak footgun is gone by construction, and the hot loop has no raw `ReleaseSync`.
Behavior/latency-equivalent (same acquire params, same release point). Windows-
only (CI + on-glass gated); to be validated on the RTX box (host clippy build +
a PERF=1 latency A/B vs the shipping binary — the change should show no delta).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pre-existing working-tree changes committed to the branch on request: the
gpu-contention investigation doc, host-latency-plan additions, and small
pack-host-installer / stage-pf-vdisplay packaging-script edits.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the no-op Windows `set_launch_command` real. New `windows/interactive.rs`
`spawn_in_active_session` (WTSGetActiveConsoleSessionId → WTSQueryUserToken →
CreateProcessAsUserW(winsta0\default) under the LOGGED-IN USER token, factored from
the wgc_relay primitive) + `library::launch_title` resolving a store-qualified id to
a concrete process via `windows_launch_for` (steam_appid → Steam.exe/explorer.exe
steam:// URI; command → cmd.exe /c). Threaded as `SessionContext.launch` into both
native data-plane paths (`virtual_stream`, `virtual_stream_relay`) and fired after
capture is live so the title renders onto the captured desktop and grabs foreground.
Security invariant intact: the client sends only the store-qualified id; the host
resolves the recipe from its own library and the URI/flags are handed to a concrete
EXE as plain args (never cmd /c of a client string). Linux unchanged (gamescope
nesting via the handshake PUNKTFUNK_GAMESCOPE_APP path).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The DualSense, DualShock 4, and XUSB Windows pad backends each hand-rolled the
SAME per-pad resource handling: a `CreateFileMappingW` + `MapViewOfFile` shared
section (with the permissive D:(A;;GA;;;WD) SDDL the restricted-token driver
needs) and an identical `Drop` doing `SwDeviceClose` + `UnmapViewOfFile` +
`CloseHandle` — three copies, each a chance to drift or leak on an error path.
New `inject/windows/gamepad_raii.rs` owns both resources with RAII:
- `Shm` — the section handle (`OwnedHandle`) + its view; `Shm::create(name, size)`
does the SDDL + map + zero-fill leak-safely, `base()` gives the mapped pointer,
`Drop` unmaps then closes (in that order).
- `SwDevice` — the `SwDeviceCreate`'d devnode; `Drop` calls `SwDeviceClose`.
All three backends now hold `_sw: Option<SwDevice>` + `shm: Shm` instead of raw
`hsw`/`map`/`view`, access the section via `self.shm.base()`, and have NO manual
`Drop`. Deletes the duplicated `create_shm_section` (DualSense/DS4 now use
`Shm::create`) and the three hand-written Drops; the DS4 device-type byte is still
written before the magic, the SwDeviceCreate `None` fallback still works, and the
field drop order (devnode removed, then section unmapped+closed) matches the old
manual order.
Net: 3 manual `Drop`s + a duplicated section-creation path → one shared RAII
module; fewer unsafe ops, leak-on-error fixed by construction. Linux `cargo check`
clean (the inject mod wiring); the backends are #[cfg(windows)] → CI-gated.
Drafted + adversarially verified (no double-free, imports correct under
-D warnings, behavior preserved); my own spot-checks confirm.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The driver zero-initialised C POD structs (IddCx/WDF descriptors) with 27
scattered `let mut x: T = unsafe { core::mem::zeroed() };`, each carrying its own
`// SAFETY` about the all-zero bit pattern being valid + the caller setting `.Size`
etc. right after.
Replace with one `pod_init!(T)` macro (in log.rs, reachable everywhere via the
existing `#[macro_use] mod log;` — same mechanism as `dbglog!`) that owns the
single `unsafe { zeroed::<T>() }` + the SAFETY rationale. All 27 sites
(adapter 6, callbacks 3, entry 4, monitor 10, swap_chain_processor 4) now read
`let mut x = pod_init!(T)`. Zero behavior change (mem::zeroed semantics identical);
the type is passed explicitly so no inference depends on the removed annotation.
27 `unsafe` blocks → 1. Driver still `deny(unsafe_op_in_unsafe_fn)`-clean (the
macro expands to an explicit `unsafe {}`; the one nested-in-user-unsafe site is
fine — no `unused_unsafe` for macro-generated blocks). Driver-only (CI-gated);
adversarially reviewed (macro scoping, all sites, no leftover raw zeroed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SCM supervisor scattered manual `CloseHandle(pi.hProcess)`/`(pi.hThread)`
across ~5 supervise-loop match arms and hand-closed the job object — easy to miss
an arm (leak) or double-close.
- `spawn_host` returns an owned `Child { process: OwnedHandle, _thread: OwnedHandle,
pid }` instead of raw `PROCESS_INFORMATION`; the supervise loop borrows
`child.process` (`HANDLE(as_raw_handle() as *mut c_void)`) for wait/Terminate and
the `Child` auto-closes both handles when it drops / is replaced each iteration.
- The job object → `OwnedHandle` (borrowed for AssignProcessToJobObject), auto-closed.
- Deletes ~9 manual `CloseHandle` calls. The `_thread` handle is RAII-only (`_`-prefixed
so `dead_code`/`-D warnings` doesn't flag it).
Deliberately LEFT the `STOP_EVENT`/`SESSION_EVENT` `AtomicIsize` statics as-is — they
are smuggled into the C SCM control handler, so `OwnedHandle`-ifying them is a separate,
riskier supervisor redesign out of scope here (noted in a comment).
Behavior preserved (the supervise state machine / wait semantics / restart-on-
session-change / kill-on-close are unchanged). Windows-only (CI-gated); adversarially
reviewed (no double-close, handles outlive their borrows, idiom matches manager.rs).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The IDD-push capturer held raw `HANDLE`s for the shared header mapping, the
frame-ready event, the debug section, and each ring slot's shared texture, with
manual `CloseHandle` scattered across two `Drop` impls — and the MapViewOfFile
VIEWS (header/dbg_block) were never UnmapViewOfFile'd (a real view leak).
- New `MappedSection { handle: OwnedHandle, view }` RAII: `Drop` UnmapViewOfFile's
the view THEN the `OwnedHandle` closes the mapping (unmap-before-close).
- `map`+`header` → `section: MappedSection` (+ a cached `header` ptr borrowing into
it, declared after `section` for drop order); same for `dbg_map`+`dbg_block`.
- `event: HANDLE` → `OwnedHandle` (borrowed as `HANDLE(as_raw_handle() as *mut
c_void)` for WaitForSingleObject); `HostSlot.shared` → `OwnedHandle` (its manual
`Drop` deleted). Removed the manual `CloseHandle`s + the `CloseHandle` import.
Net: deletes two `Drop` impls' worth of manual handle/view teardown and fixes the
view leak — fewer unsafe ops, RAII-correct. Behavior preserved (recreate_ring
writes the header in place; the keepalive still drops last so REMOVE is last).
Windows-only (CI-gated); adversarially reviewed (no double-free / UAF / dangling
header; handle interop matches manager.rs). Linux check unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shared host<->driver ABI crate already contains more than the virtual
display: the IDD-push frame ring + control plane AND the gamepad shared-memory
layouts (XusbShm / PadShm). "pf-vdisplay-proto" was a misnomer — the name now
represents all the drivers it serves.
Mechanical rename, no behavior change:
- git mv crates/pf-vdisplay-proto -> crates/pf-driver-proto (package name +
path-deps in the host crate and the driver workspace).
- pf_vdisplay_proto -> pf_driver_proto across host + driver Rust, both Cargo.lock
files, the workspace members, the CI path triggers (windows-drivers.yml), and
the docs/INF comments. The runtime Global\pfvd-* shared-object names are a
SEPARATE contract and are deliberately untouched (host<->driver name matching).
- The pf-vdisplay DRIVER crate + its INF service name (Root\pf_vdisplay,
UmdfService=pf_vdisplay, pf_vdisplay.dll) are unchanged — only the full
`pf_vdisplay_proto` token was replaced, never the `pf_vdisplay` driver name.
Linux-verified: cargo test -p pf-driver-proto (const size-asserts compile) +
cargo clippy -p punktfunk-host -D warnings clean; Cargo.lock regenerated. The
driver-workspace side (path-dep + imports + its Cargo.lock) is Windows-CI-gated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Goal 2 ("drop every trace of SudoVDA") is done. The SudoVDA driver is no longer
shipped (only pf-vdisplay; the old vdisplay-driver tree was deleted in 416862a),
and F1 (d5c6b7e/03da47b) already moved the display-utility helpers out of the
backend into neutral modules (win_adapter/win_display), breaking the reach-in.
So the backend is now cleanly removable:
- Deleted crates/punktfunk-host/src/vdisplay/windows/sudovda.rs (350 lines: the
SudoVdaDisplay VirtualDisplay impl + its VdisplayDriver/probe).
- vdisplay::open()/probe() are now unconditional pf-vdisplay; deleted the
windows_use_pf_vdisplay() backend selector. open() now ensure!s
pf_vdisplay::is_available() with a clear "driver not installed" error instead
of the old silent SudoVDA fallback (no fallback driver exists anymore).
- Scrubbed the dangling references to the deleted symbols (manager/sendinput/dxgi
comments, the config + host.env PUNKTFUNK_VDISPLAY docs); the var stays as an
informational forward-seam. Updated the F1 module docs (Goal 2 now done).
All changes are #[cfg(windows)] except the config doc; Linux clippy
-p punktfunk-host -D warnings clean; zero `sudovda::`/`SudoVdaDisplay` code refs
remain (comments only). Windows build is CI-gated.
Scorecard Goal 2 -> DONE; recorded the E1 "do NOT do it" stability decision in
windows-host-rewrite.md §4 (the process-global driver design is sound given
ProcessSharingDisabled; a device-owned variant adds a use-after-free window for
no gain).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The driver assigned each virtual monitor a monotonically-increasing NEXT_ID used
as the EDID serial / IddCx ConnectorIndex / container GUID, and never reclaimed
it on REMOVE. Under sustained ADD/REMOVE churn the connector index kept climbing,
so IddCx/PnP allocated a NEW OS target slot every cycle and orphaned the old one
(ghost "Generic Monitor (punktfunk)" nodes) until the adapter's target capacity
was exhausted and ADD failed 0x80070490 ERROR_NOT_FOUND.
Fix: `create_monitor` now allocates the LOWEST free id (`alloc_monitor_id`,
computed under the MONITOR_MODES lock with the push) instead of a counter, so a
departed monitor's id is reclaimed and a fresh ADD reuses its target slot rather
than orphaning it. With <= N live monitors the id stays bounded to 1..=N+1.
Deleted the now-unused NEXT_ID + AtomicU32/Ordering import.
CI-compile-gated only — the wedge reproduces solely under sustained churn on the
RTX box, so this needs an on-glass reconnect-storm A/B to confirm (box is
ephemeral/down). Marked on-glass-pending in windows-host-rewrite.md §4; keep
reset-pf-vdisplay.ps1 as the recovery until validated. NOT to be relied on (or
merged to main) until that A/B passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the validated IDD-push zero-copy path the default for a fresh install,
without penalising dev / non-pf-driver runs:
- The shipped default config now enables it. Both seed sites set
`PUNKTFUNK_VDISPLAY=pf` + `PUNKTFUNK_IDD_PUSH=1`: the hardcoded default the
service writes on `service install` (`ensure_default_host_env`) AND the
`host.env.example` template the installer bundles. A fresh install therefore
runs the validated path (the installer also bundles the pf-vdisplay driver);
it falls back to DDA if the driver can't attach.
- `idd_push` is now **value-aware** instead of a bare presence flag, so an
operator can turn it OFF with `PUNKTFUNK_IDD_PUSH=0` in host.env — a `var_os`
presence check read `=0` as "on". Unset still ⇒ off (the code default is
unchanged, so existing host.env files and dev/CI runs are unaffected; only the
shipped default config opts in).
Also scrubbed the stale "SudoVDA" wording in host.env.example. Linux cargo
clippy -p punktfunk-host -D warnings clean; the service.rs default string is
Windows-only (CI-gated).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stand up Storybook so the management console can be designed without a running
host, plus the design-system work that surfaced along the way.
Storybook (@storybook/react-vite):
- Slim Start/Nitro-free vite config; the preview imports the app's real
src/styles.css directly so the design tokens stay single-sourced (no mirror).
- Stories for the @unom/ui primitives (Button/Card/Inputs/Badge), brand marks,
the AppShell (throwaway in-memory TanStack router), and every data-driven page
(Dashboard/Host/Clients/Library/Settings) rendered offline via a window.fetch
stub + typed fixtures. The route page components are exported so stories can
render them.
Light theme:
- styles.css now carries a light :root (lavender, from the docs palette) with the
existing violet chrome moved to .dark; the live console still pins html.dark by
default, so this only adds the option (Storybook's toolbar toggles it).
- Fixes a stray `*/` inside a comment that prematurely closed it and silently
broke Tailwind's @theme processing.
Spinner:
- The punktfunk lens recreated with motion/react: two circles surge through one
another in depth (JS perspective scale + z-index — robust where mix-blend-mode
flattens CSS preserve-3d) with a screen-blend lens highlight. Replaces the
skeleton loading state in QueryState; removes ui/skeleton.tsx.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Windows-host docs were scattered across a design plan, a staged-refactor
plan, an audit, an audit-remediation tracker, and a game-capture-bug analysis —
several badly stale (the audit/remediation predate the Goal-1 branch landing and
call DONE items "not started"). Verified the true state of every audit finding /
goal / milestone against current code+git (4-agent workflow), then rewrote
windows-host-rewrite.md as ONE consolidated, accurate doc:
- §1 Status scorecard (Goals 1-3, M0-M6, GB1, audit P0/P1/P2) with DONE/PARTIAL/
OPEN + commit evidence.
- §2 Architecture as-built (layering, HostConfig→SessionPlan→SessionContext, the
VirtualDisplayManager ownership model, IDD-push-primary capture incl. secure
desktop + GB1 recovery, encode/EncoderCaps, pf-vdisplay-proto, the driver,
service/packaging).
- §3 Validated invariants (the jewels).
- §4 Prioritized open tasks (the genuine remaining work).
- §5 Operations (RTX-box recipe, CI, env, build).
- §6 Deep reference (/INTEGRITYCHECK answer, the 6 iddcx bindgen knobs, the driver
port checklist, resolved decisions).
Deleted the four now-redundant docs (content folded in; history in git):
windows-host-goal1-plan.md, windows-host-rewrite-audit.md,
windows-host-rewrite-remediation.md, windows-host-rewrite-game-capture-bug.md.
Repointed the 6 code/proto/driver doc-comment refs that targeted them at the
consolidated windows-host-rewrite.md sections. Linux cargo check clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The last §2.3 seam-trait tightening: give `Encoder` a `caps() -> EncoderCaps`
so the session glue routes by *query* instead of relying on the no-op/`false`
defaults of `invalidate_ref_frames`/`set_hdr_meta`.
`EncoderCaps { supports_rfi, supports_hdr_metadata }` is a cheap `Copy` struct.
The trait gains a default `caps()` returning `EncoderCaps::default()` (all
false) — correct for every SDR/libavcodec backend (Linux NVENC, VAAPI, AMF/QSV,
software openh264), so they need no change. Only the Windows direct-NVENC path
(`NvencD3d11Encoder`) overrides it, reporting the real `rfi_supported` (probed
once at open via `nvEncGetEncodeCaps`) and `hdr` (HDR-SEI on keyframes).
Consumer: the GameStream encode loop (`gamestream/stream.rs`) hoists
`supports_rfi` once before the loop and gates the loss-recovery path on it —
`!(supports_rfi && enc.invalidate_ref_frames(..))` forces a keyframe directly
on non-RFI encoders instead of making an always-`false` call every loss event.
Behaviour-preserving (same keyframe/RFI outcome), one fewer no-op call, intent
explicit. The native host (punktfunk1) uses FEC+keyframes, no RFI consumer.
Linux `cargo clippy -p punktfunk-host --all-targets -D warnings` clean; the
three edited files are rustfmt-clean. The NVENC override is Windows-only
(1:1 with the existing impl style) → CI/on-glass gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Today's manual driver recovery (wedged under ADD/REMOVE churn → ERROR_NOT_FOUND) and the manual
host-stop/install/host-start dance around drivers/deploy-dev.ps1 are now two scripts:
* reset-pf-vdisplay.ps1 — recover a wedged driver: stop host → pnputil /remove-device the ghost
"Generic Monitor (punktfunk)" nodes → Disable+Enable the adapter
(Restart-PnpDevice doesn't exist on the box PS) → start host. No reboot
(the box boots to Proxmox). -Verify probes to confirm ADD recovered.
* redeploy-pf-vdisplay.ps1 — one-shot dev redeploy wrapping deploy-dev.ps1 with the host stop/start
(the running host holds the driver DLL) + a post-install adapter reload
(pnputil updates the store but the live device keeps the old binary).
Both standalone (don't touch deploy-dev.ps1). README gains a "Dev iteration on the test box" section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On-glass caught a runtime panic the box compile couldn't: `VirtualDisplayManager used before a
backend initialised it`. Step 3 put the preempt (`vdm().begin_idd_setup`) BEFORE
`vdisplay::open` in virtual_stream, but vdisplay::open is what constructs the backend that calls
manager::init() — so vdm() was reached before init and panicked on the first IDD-push session.
(The old IDD_SETUP_LOCK/IDD_SESSION_STOP globals needed no init, so the prior ordering was fine.)
Fix: open the backend first (it does no monitor work — just constructs the marker + opens the
control device, initialising the manager), THEN run the preempt, THEN build the pipeline (which
creates the monitor). The preempt still precedes this session's monitor creation, so the
semantics are unchanged. Validates why §2.5 needs the on-glass gate, not just the compile.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The last two virtual-display globals lived in punktfunk1: IDD_SETUP_LOCK (serialize IDD-push
setup against a reconnect flood) + IDD_SESSION_STOP (the prior session's stop flag, signalled +
waited-on so a reconnect preempts the stale session cleanly). Both move onto VirtualDisplayManager
as fields, behind one `vdm().begin_idd_setup(stop)` method that locks the setup gate, registers
this session's stop while signalling the prior one, waits for the monitor to release, and hands
back the setup guard the session holds across the pipeline build. punktfunk1 no longer reaches
into vdisplay internals for the preempt — it just calls the manager and holds the guard.
Behaviour-identical (same lock/signal/wait order, same guard lifetime). Completes §2.5's
"delete the smeared globals": CURRENT_MON_GEN/MON_GEN/MGR x2/IDD_PERSIST/IDD_SETUP_LOCK/
IDD_SESSION_STOP are all gone, replaced by the one OnceLock VirtualDisplayManager with a typed
OwnedHandle device. Box build to follow; on-glass reconnect-leak test pending.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The two Windows virtual-display backends (sudovda + pf_vdisplay) carried VERBATIM-DUPLICATED
~250-line Idle/Active/Lingering refcount state machines in two `MGR: Mutex<Mgr>` globals, each
smuggling the control HANDLE across the pinger/linger threads as a raw `isize` (HANDLE is !Send).
New `vdisplay/windows/manager.rs`: one host-lifetime `VirtualDisplayManager` (OnceLock singleton,
user-approved) owns the earned state machine + the linger timer + a TYPED `Arc<OwnedHandle>`
control device (the raw-isize smuggle is gone — OwnedHandle is Send+Sync and also CloseHandle's
the device on drop, fixing a latent leak). The only backend-specific code left is the IOCTL
surface behind a small `VdisplayDriver` trait (open/add_monitor/remove_monitor/ping) + the
per-monitor REMOVE key (`MonitorKey::Guid` for sudovda, `::Session(u64)` for pf-vdisplay). The
render-adapter pin decision, the GDI/CCD glue (crate::win_display), and the gen-stamped
MonitorLease are backend-neutral and live once in the manager.
* sudovda.rs / pf_vdisplay.rs: shrink to a `VdisplayDriver` impl + a thin `VirtualDisplay`
wrapper (new() -> manager::init(driver); create() -> manager::vdm().acquire(mode)). Their
IOCTL ops + structs + open_device stay in place (no transcription).
* MON_GEN -> a manager field; the preempt's wait_for_monitor_released moves onto the manager
(punktfunk1 calls vdm().wait_for_monitor_released). MonitorLease.drop -> vdm().release(gen),
with the stale-lease no-op preserved verbatim.
Behaviour-preserving: the state machine (acquire/release/reconfigure/teardown/linger/preempt) is
the canonical sudovda copy with the IOCTLs routed through the driver seam. Box build to follow
(Windows-only; Linux check is a no-op for these files).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removes the cruft the §2.5 ownership-model rewrite would otherwise carry forward, and corrects a
false invariant the docs described:
* CURRENT_MON_GEN (sudovda) — the "current monitor generation" global was WRITE-ONLY. It was
stored on every mgr_acquire (both backends) but its only reader, idd_push's `my_gen`, was set
and NEVER read. The "session capturer re-checks the monitor gen each frame and bails on a
reconnect" behaviour the doc describes was never wired — per-frame staleness is the SEPARATE
ring FrameToken.generation / IDD_GENERATION mechanism (which works and is untouched). So the
monitor-gen-via-WinCaptureTarget carry the design proposed is unnecessary. Deleted the static,
its stores in both backends, the pf_vdisplay import, and idd_push's dead `my_gen` field/read.
(MON_GEN — the lease-generation counter behind the stale-lease no-op — is REAL and kept.)
* IDD_PERSIST + open_or_reuse + IddReuseHandle (idd_push) — a persistent-capturer reuse path
from an early prototype, defined but with ZERO callers across the crate. Deleted, plus the now
-orphaned `use std::sync::Mutex` and the now-dead `set_client_10bit` setter.
Windows-only; grep confirms no remaining references to any deleted symbol. Box build to follow.
First of the incremental §2.5 steps (user-approved OnceLock VirtualDisplayManager design).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move 36 platform-specific files into per-module `windows/` and `linux/` subfolders (and the
shared HID codecs into `inject/proto/`):
capture/{windows,linux}/ encode/{windows,linux}/ inject/{windows,linux,proto}/
audio/{windows,linux}/ vdisplay/{windows,linux}/
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 cfg-sprawl folder confinement the stage is about). Done LAST, after the semantic stages, so
the path churn didn't fight them.
Verified: Linux cargo check + clippy (-D warnings) clean; my mod-decl changes fmt-clean (the 3
remaining fmt diffs are pre-existing local-rustfmt-version skew that moved with their files); all
36 `#[path]` targets exist; no internal `#[path]`/`include!`/file-child-mod in any moved file
(the inline `mod X {` blocks are self-contained). Box build to follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The headline §2.3 seam tightening (the explicit Stage-3 deferral; §5's "highest-severity
coupling"): the capturer is now TOLD its output format instead of re-deriving the encode backend.
New `capture::OutputFormat { gpu, hdr }`, resolved once per session and passed INTO
capture_virtual_output:
* native punktfunk/1 path: `SessionPlan::output_format()` (gpu = encoder.is_gpu(), from the
already-resolved plan.encoder — no second probe; hdr = plan.hdr).
* GameStream + spike paths: `OutputFormat::resolve(hdr)` (gpu from the single `gpu_encode()`
source, which maps windows_resolved_backend()).
`capture/dxgi.rs DuplCapturer::open` takes `gpu` in and its internal
`!matches!(windows_resolved_backend(), Software)` recompute is DELETED — the capture layer no
longer re-calls the encode layer (the back-reference that could let capture and encode disagree
on whether frames are GPU-resident, plan §2.3/§5). The relay's secure-desktop DDA passes
`gpu_encode()` likewise.
Behavior-preserving: the `gpu` passed in equals the value the capturer used to compute (same
encode-backend resolution). The DDA opens keep `want_hdr=false` (the SDR fallback, unchanged).
Tightenings 2 (HDR/release -> VirtualLease) and 3 (EncoderCaps) split off: (2) needs the
monitor-generation carried on the lease + the keepalive becoming Box<dyn VirtualLease> — that's
the §2.5 ownership-model change (CURRENT_MON_GEN / sudovda::wait_for_monitor_released), so it
moves there; (3) is a small additive follow-on. Documented in the plan.
Verified: Linux cargo check + clippy (-D warnings) + fmt clean. Box build to follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bundle 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 reconfig/keyframe 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. Both `#[allow(too_many_arguments)]` attrs removed.
Scoped deliberately: the plan's SessionFactory.build() owning a `vdm.lease -> 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 doesn't
exist yet (the lifecycle still lives in 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 is the validated shipping path, so wrapping the deployed
reconfig/switch/rebuild loop in a Session::drop for a behavior-preserving change would add real
regression risk for marginal gain. The SessionFactory/Session::drop/vdm.lease work folds into
§2.5; this stage delivers the concrete, safe arg-bundling.
Verified: Linux cargo check + clippy (-D warnings) + fmt clean. Box build to follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New src/session_plan.rs: a Copy `SessionPlan { capture, topology, encoder, bit_depth, hdr }`
resolved ONCE from HostConfig (+ the negotiated bit_depth) at the top of `virtual_stream`,
logged, and threaded through build_pipeline_with_retry/build_pipeline. The three scattered
Windows dispatch points now read this one typed artifact instead of re-deriving from config
(plan §2.4, the "capture and encode disagree on the backend" hazard):
* capture: capture::capture_virtual_output takes a CaptureBackend IN (was re-reading
config().idd_push / capture_backend / no_wgc internally). CaptureBackend::resolve() is the
single resolver, shared with the GameStream + spike call sites.
* topology: virtual_stream reads plan.topology; should_use_helper is deleted (its body is
session_plan::resolve_topology, verbatim). The IDD-push reconnect-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 capture->windows_resolved_backend()
back-reference recomputed in dxgi.rs) is stage 5.
Behavior-preserving by construction: each resolved decision is provably equivalent to the
pre-stage-3 reads (same config() + the same cached running_as_system()/GPU-vendor probes), so
old==new. SessionPlan is platform-neutral so it threads the shared virtual_stream/build_pipeline
signatures; on Linux it resolves to the single portal/single-process path.
Also fixes a pre-existing mod-ordering fmt drift in main.rs (mod config; / mod capture;).
Verified: Linux cargo check + clippy (-D warnings) + fmt clean on the touched files. Box build
(Windows compile) + on-glass (NVENC + IDD-push + mode switch) pending on the RTX box.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate 31 genuinely-constant operator/dispatch env::var sites onto HostConfig, so the
capture/topology/encoder decision reads ONE owner instead of being recomputed at each call
site (the latent bug where capture and encode could disagree on the resolved backend, plan §2.4):
idd_push x7, no_wgc, capture_backend, render_adapter, encoder_pref (Linux open_video +
linux_zero_copy_is_vaapi), the Windows vdisplay-backend select, plus the plan-named
secure_dda/idd_depth/zerocopy/ten_bit and the multi-site perf x4 / compositor x5 /
video_source x3 / gamepad. Each HostConfig field's parser is byte-identical to the read it
replaced, so old==new by construction (the plan's "a flipped bool is a silent regression" guard).
Scope correction — the plan's "~64 sites / Linux XDG+compositor included / grep env::var -> 0"
was unsafe as written. Two classes are deliberately KEPT as live reads and documented in config.rs:
* Runtime-mutated session vars. 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,
GAMESCOPE_SESSION/NODE, KWIN/MUTTER_VIRTUAL_PRIMARY, FORCE_SHM. Parsing these once would
freeze them at startup and silently break session-following — they are NOT constant.
* Single-use local tuning with no resolve-once benefit (and FEC_PCT even has two different
semantics): FEC_PCT, VIDEO_DROP, VBV_FRAMES, SPLIT_ENCODE, PACE_BURST_KB, the dxgi 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 (1|true|yes|on) parser.
Verified: Linux cargo check + clippy (-D warnings) + fmt clean on the touched files. The
Windows-only edits are 1:1 substitutions; they get a real Windows compile on the box with Stage 3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
config.rs: typed HostConfig parsed ONCE from env (idd_push/encoder_pref/no_helper/force_helper), replacing per-call env::var re-reads (PUNKTFUNK_ENCODER was re-read on EVERY windows_resolved_backend() call; PUNKTFUNK_IDD_PUSH is read 8x across the host — the recompute that lets capture + encode disagree on the backend, plan §2.4). Migrated the two highest-churn dispatch reads onto it (encode::windows_resolved_backend, punktfunk1::should_use_helper). Behavior-identical: the env is constant for the process lifetime (the service loads host.env before launch), so a lazily-parsed global == parsed-once-at-startup.
docs/windows-host-goal1-plan.md: the ORDERED, independently-shippable execution plan for Goal-1 (the plan's biggest unstarted goal — a from-scratch layered host architecture). Six behavior-preserving, box-verified stages (HostConfig -> SessionPlan -> SessionContext/SessionFactory -> seam-trait tightenings -> src/windows tree), because the host is live-validated and a monolithic rewrite would strand it broken. Stage 1 done here; stages 3-5 rewire the deployed path and require on-glass re-test.
Verified: Linux + box (--features nvenc) cargo check clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EvtCleanupCallback on the WDFDEVICE (entry.rs + callbacks::device_cleanup): on device removal (PnP/unload) drop every monitor's swap-chain worker via monitor::cleanup_for_device_removal (joins threads, IddCx-free — the framework tears the monitors down with the device). Worker threads no longer linger into teardown.
Single identity per session (create_monitor): a re-ADD of a still-live session_id departs the stale monitor first, so one session maps to exactly one monitor (no duplicate EDID/target).
DeviceContext-owned state (audit §2.5): documented decision NOT to migrate the globals to a Box/AtomicPtr device-owned allocation. The IddCx monitor/mode DDIs receive only an IddCx handle (never the WDFDEVICE/context), so the state MUST be globally reachable (upstream virtual-display-rs is a process-static for the same reason); the globals are already module-encapsulated; and with one devnode + UmdfHostProcessSharing=ProcessSharingDisabled they die with the host process on removal anyway. A pointer variant would only add a host-gone-watchdog-race use-after-free for zero benefit.
Verified: driver workspace builds clean on the RTX box (.173).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The old all-Rust IddCx driver tree (packaging/windows/vdisplay-driver/ — the wdf-umdf-sys 'oracle', 7896 lines) is fully superseded by packaging/windows/drivers/ (wdk-sys / windows-drivers-rs + the owned pf-vdisplay-proto ABI), which is the source of the vendored + installed driver. It was in NO cargo workspace (never built) and NO CI workflow; only stale doc/script refs pointed at it (the confusion the audit + game-capture-bug doc both flagged).
Delete it + repoint the build-relevant refs (packaging/windows/README.md, stage-pf-vdisplay.ps1, pack-host-installer.ps1) at drivers/ + drivers/deploy-dev.ps1. The vendored driver (packaging/windows/pf-vdisplay/) is unaffected; docs/windows-virtual-display-rust-port.md keeps its historical mentions as narrative.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Build/sign/install script for the wdk-sys/windows-drivers-rs driver in packaging/windows/drivers/ (the new tree lacked one). Like the old vdisplay-driver/deploy-dev.ps1 but adds the FORCE_INTEGRITY clear (this tree links /INTEGRITYCHECK) and a 9.9.MMdd.HHmm DriverVer (the vendored build is 9.5.*). Verified: deployed the rebuilt driver to the RTX box (.173).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
publish() now guards width/height alongside format (CopyResource needs matching DIMS too, else garbage): drops a surface whose descriptor no longer matches the host ring (a fullscreen game mode-set the display) AND logs the actual descriptor once per mismatch episode, so a repro shows exactly what changed (GB1/Stage-0 diagnostic + the Stage-2 width/height guard).
log.rs: a process-lifetime, flushed, Mutex-shared append handle (opened ONCE) replaces the per-call open/append — so the swap-chain WORKER thread's lines land. They were hidden (per-call open raced the control thread / could fail under the worker's restricted token), which is exactly why a game-break repro showed no swap-chain-processor lines (bug doc S3). This is the observability foundation the bug doc gates Stage S (S1/S2 driver resilience) on.
Needs a driver rebuild + re-vendor to deploy (separate from the GB1 host-only fix). Stage 3 (trim default_modes) deprioritized: GB1 recovers from mode-sets, and trimming risks the live display-activation path.
Verified: driver workspace builds clean on the RTX box (.173).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bug: a fullscreen game mode-sets the virtual display (format/size); the driver's publish() guard then drops every frame; the host's ring — fixed at the session-negotiated mode — never adapts -> frozen picture, then black on reconnect.
RECOVER (no DDA, per the chosen design): the ring now TRACKS the display's actual mode. At open it is sized to the display's actual resolution (new win_display::active_resolution, CCD/GDI) — so reconnecting while a game holds a different mode just works. Mid-session, the 250ms poll (was HDR-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 -> frames resume at the game's mode. No freeze, no reconnect needed.
DROP if unrecoverable: a descriptor change starts a recovery clock (recovering_since); if no fresh frame resumes within 3s (e.g. an exclusive-flip the host can't follow), try_consume bails -> the session ends cleanly -> the client reconnects, instead of freezing forever. A pure idle desktop (no mode change) never triggers this.
Verified: host clippy (nvenc) clean on the RTX box. NEEDS ON-GLASS (Doom repro on .158): confirm the poll sees the mode-set, the ring recreates + recovers, the encoder+client adapt to the size change; tune the 3s window.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
wait_for_attach now requires the driver to publish a FIRST frame, not just attach (DRV_STATUS_OPENED). A fullscreen game can leave the virtual display in a format/size the driver's publish() guard rejects -> the driver ATTACHES but silently drops every frame; previously the host sailed past open() and only died on next_frame's 20s deadline (the 'reconnect = black + working audio' symptom). Now open() fails -> capture.rs falls back to DDA (reusing the C1 fallback) -> the game is captured + visible after a reconnect.
Safe at open: the OS composites the freshly-activated virtual display, so a frame arrives within ~1s — a normal/idle open isn't false-failed; only a genuinely-broken display (no frame in 4s) falls back (and DDA is a working path, so even a false-positive degrades gracefully).
GB1 Stage 1a (docs/windows-host-rewrite-game-capture-bug.md P3). The mid-session-without-reconnect live failover (composing capturer) is the next piece.
Verified: host clippy (nvenc) clean on the RTX box.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Moved the remaining 6 SudoVDA reach-in helpers + SavedConfig (resolve_gdi_name, set_advanced_color, advanced_color_enabled, set_active_mode, isolate/restore_displays_ccd) verbatim from vdisplay::sudovda into a backend-neutral crate::win_display module (the plan's windows/display_ccd.rs). The capturers (idd_push/dxgi/wgc), pf_vdisplay, and punktfunk1 now depend on these as PEERS via crate::win_display instead of reaching into the SudoVDA backend.
With win_adapter (F1 pt1), all 7 reach-in helpers are now neutral — the circular reach-in is broken, so SudoVDA can eventually be deleted (Goal 2) without losing the display utilities. sudovda re-exports the ones it still uses internally; its now-unused CCD/GDI imports were removed.
Verified: host clippy (nvenc) clean on the RTX box; Linux check clean (the new modules are #[cfg(windows)]).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The discrete-render-GPU LUID picker was display-utility living in the SudoVDA backend; moved it verbatim to a backend-neutral crate::win_adapter module (the plan's windows/adapter.rs). The IDD-push capturer + pf-vdisplay backend now depend on it as a PEER instead of reaching into vdisplay::sudovda — the first step in breaking the circular reach-in so SudoVDA can eventually be dropped (Goal 2). sudovda re-exports it for its own callers.
Remaining F1 increments: the CCD/HDR helpers (resolve_gdi_name, set_advanced_color, advanced_color_enabled, set_active_mode, isolate/restore_displays_ccd) → a neutral win_display module.
Verified: host clippy (nvenc) clean on the RTX box.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lock in the explicit-unsafe-block discipline so a fn-level 'unsafe' never silently blesses its whole body (the per-site // SAFETY: comments already landed in STEP 8). Builds clean on the RTX box — no fallout. The host-wide unsafe-lint sweep + clippy::undocumented_unsafe_blocks (hundreds of blocks across Linux+Windows) are a larger dedicated follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
§5.3 (C3): repeat_last() now copies the last frame into a FRESH rotated out-ring slot instead of re-handing last_present's slot, so a repeat (static desktop) never re-hands a slot still encoding under pipeline_depth>1. OUT_RING(3) > max depth(2) keeps the rotated slot free — the out-ring rotation contract now holds for repeats too, not just the synchronous-loop assumption.
§5.4 (C4): when enabling advanced color for a 10-bit client, trust set_advanced_color success and size the ring FP16 directly, instead of racing the advanced_color_enabled poll (which could size SDR while the driver composes FP16 -> format mismatch -> an immediate ring recreate + dropped first frames).
Verified: host clippy (nvenc) clean on the RTX box. On-glass to confirm: HDR-client first-frame + static-desktop pipelining.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
open() now hands the keepalive BACK on failure (the WGC attach_keepalive pattern) so the caller can fall back instead of tearing the virtual display down. Added a bounded wait_for_attach() that polls the driver's DRV_STATUS_OPENED — it checks ATTACH status, not frame arrival, so it never false-fails on an idle desktop that has composed no frame yet.
An attach failure (e.g. a hybrid-GPU render mismatch -> DRV_STATUS_TEX_FAIL, or the driver never opening the ring within 4s) now fails open() -> capture.rs falls back to DDA, instead of next_frame's 20s deadline leaving the session black. Pairs with the driver SET_RENDER_ADAPTER fix (cdccec6).
Verified: host clippy (nvenc) clean on the RTX box. Behavioral validation (fallback trigger + happy-path attach timing) needs an on-glass session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
§4.2h (C2): the host already pins the discrete GPU via IOCTL_SET_RENDER_ADAPTER on the IDD-push path; now that the pf-vdisplay driver implements it (cdccec6), correct the stale 'driver returns STATUS_NOT_IMPLEMENTED / STEP-4 stub' comments. Hybrid iGPU+dGPU boxes now actually pin the NVENC GPU.
§6.1 (C5): switch the host gamepad SHM consumers (inject/{dualsense,gamepad}_windows.rs) to derive size/offsets/magic/name from pf_vdisplay_proto::gamepad::{PadShm,XusbShm} via offset_of!/size_of!/helpers, instead of hand-literal OFF_*/140 — proto is now the single source of truth (driver-side switch follows with the gamepad-driver unification). The DualShock4 backend reuses the same pub(super) consts unchanged.
Verified: host clippy (nvenc) clean on the RTX box (x86_64-pc-windows-msvc).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit §4.1: implement the host-gone watchdog — it was dead code (WATCHDOG_PINGS bumped but never sampled, no thread). Every IOCTL now bumps a liveness counter; a watchdog thread reap_orphaned()s monitors (created_at grace) if no IOCTL arrives within WATCHDOG_TIMEOUT_S, so a crashed/TerminateProcess'd host no longer leaves its virtual monitor + swap-chain worker + pooled D3D device wedged until the next CLEAR_ALL. Removes the false 'watchdog thread' comments.
Audit §4.2: implement SET_RENDER_ADAPTER (was STATUS_NOT_IMPLEMENTED) via IddCxAdapterSetRenderAdapter, so the host can pin the IDD render to the NVENC GPU on a hybrid iGPU+dGPU box (else the OS-picked iGPU makes the host ring textures un-openable -> DRV_STATUS_TEX_FAIL).
Audit §4.4: gate the world-writable C:\Users\Public\pfvd-driver.log behind debug builds / PFVD_DEBUG_LOG (a release build never writes it).
Audit §4.5: bounds-check the requested mode in IOCTL_ADD; compute display_info clock_rate in u64 + saturate (the old u32 refresh*(h+4)^2 overflowed/aborted the mode DDI for large modes).
Verified: driver workspace builds clean on the RTX box (WDK 26100 + LLVM 21.1.2, MSVC). On-glass functional validation of the watchdog/render-pin is a follow-up (needs a driver reinstall + session).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
§6.2: add offset_of! asserts to SharedHeader/AddReply/control structs so a same-size field reorder is a compile error, not silent corruption (size+Pod alone miss it).
§6.1: add XusbShm (64B) + PadShm (256B, incl device_type@140) layouts + Global\ name helpers + magics to the proto crate as the single source of truth, with offset asserts pinned to the shipped wire layout — kills the hand-duplicated literal-140 host/driver drift hazard. Enables bytemuck min_const_generics for the >32-byte reserved tails. Host + driver consumers switch in a follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The installer's vendored driver binary (packaging/windows/pf-vdisplay/) was STALE — built from the OLD
oracle tree (packaging/windows/vdisplay-driver/, wdf-umdf, SudoVDA-compat GUID), so it was
ABI-mismatched with the host (which opens the owned proto GUID 70667664). Re-vendor it from the NEW
drivers/ tree so the rewrite's ACTUAL driver is what the installer ships.
Built RELEASE on the RTX box from the new tree + the new .inx: cargo build --release -p pf-vdisplay ->
FORCE_INTEGRITY clear -> stampinf (DriverVer 06/25/2026,9.5.0625.1614, > the old 06/22) -> Inf2Cat
/os:10_X64 -> signtool sign the .cat with punktfunk-ds-test (.cat sig Valid). Replaces the stale
.dll/.inf/.cat; the .cer is unchanged (same cert).
ON-GLASS VALIDATED (install-test): pnputil /add-driver /install the release package -> clean WUDFHost
reload -> Status=OK, init_adapter -> IddCxAdapterInitAsync -> 0x0 (FP16 accepted),
IddCxMonitorCreate(id=1) -> 0x0. The shipping installer now installs + loads the real wdk-sys
proto-GUID driver, FP16/HDR-capable, monitor-create working.
Remaining STEP 8 (recorded in memory, deferred): re-point the stale "built from vdisplay-driver/"
comments in stage-pf-vdisplay.ps1 / pack-host-installer.ps1 / packaging README; selector default ->
pf-vdisplay unconditional; CI build-sign-or-stale-vendored drift guard; then DELETE the oracle tree.
KEEP sudovda.rs (runtime fallback + the backend-neutral CCD helpers pf_vdisplay.rs reuses) and the
WGC-relay/DDA secure path (the secure-desktop lock/UAC gate is not yet proven on glass for IDD-push).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The new wdk-sys driver tree (packaging/windows/drivers/pf-vdisplay/) had no INF — it borrowed the
oracle's (packaging/windows/vdisplay-driver/.../pf_vdisplay.inx), which blocked deleting the oracle.
Port it verbatim: the proto-vs-SudoVDA control GUID is registered in CODE
(WdfDeviceCreateDeviceInterface), so the INF is GUID-agnostic and identical — HWID Root\pf_vdisplay,
UmdfExtensions=IddCx0102, the control-device security DACL, UpperFilters=IndirectKmd,
UmdfHostProcessSharing=ProcessSharingDisabled. Prerequisite for the STEP-8 re-vendor (build ->
stampinf -> Inf2Cat -> sign the .dll/.cat from the NEW tree into packaging/windows/pf-vdisplay/,
replacing the stale oracle-built binary) and for deleting the oracle tree.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit pass over the new pf-vdisplay driver's unsafe surface: 92 per-site // SAFETY comments added
across adapter.rs / monitor.rs / entry.rs / callbacks.rs / swap_chain_processor.rs /
frame_transport.rs / direct_3d_device.rs (control.rs already had full coverage). COMMENTS ONLY — zero
logic, signature, or control-flow change (verified via git diff: every added line is a // SAFETY
comment or blank).
The dominant gap was the pervasive `core::mem::zeroed()` FFI-struct builds (IDDCX_*/WDF_*/
DISPLAYCONFIG_* C PODs whose all-zero bit pattern is a valid uninitialized/Invalid state, with the
required .Size/fields set immediately after) — each now carries a one-line // SAFETY. Plus explicit
notes on the two stack/local-pointer-into-FFI hazards (adapter.rs `version` ptr into
IddCxAdapterInitAsync; monitor.rs `edid` Vec ptr into IddCxMonitorCreate — both read synchronously
before the local drops) and the frame_transport.rs raw-HANDLE / mapped-header derefs + cleanup paths.
The already-justified Send/Sync wrappers (SendAdapter, CtxTypeInfo/DevCtxInfo, MonitorObject,
Sendable, FramePublisher) were audited — each already carried a // SAFETY. No site needed a code
change.
First slice of STEP 8 (the SudoVDA drop). Comments-only ⇒ build-neutral; windows-drivers.yml verifies
on the next runner build. Remaining STEP 8: re-vendor the installer's driver binary from the new
drivers/ tree (the shipping packaging/windows/pf-vdisplay/ binary is still built from the OLD oracle
tree with the SudoVDA-compat GUID — ABI-mismatched with the host's proto GUID), add an .inx to the
new tree, re-point scripts/README from vdisplay-driver/ to drivers/, flip the selector default to
pf-vdisplay, then delete the old oracle tree. Keep sudovda.rs (the runtime fallback + the
backend-neutral CCD helpers pf_vdisplay.rs reuses) and the WGC-relay/DDA secure path (the
secure-desktop gate is not yet passed on glass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pf-vdisplay driver now advertises HDR/FP16 and the full glass-to-glass HDR path works
end-to-end — validated LIVE: the Mac client connected to the .173 host WITH HDR (display_hdr=true,
FP16 ring -> NVENC P010). The STEP-3 assumption that FP16 needs a higher UmdfExtensions was WRONG:
IddCx0102 + CAN_PROCESS_FP16 + the *2 DDIs works (the oracle proved it; confirmed on-glass
IddCxAdapterInitAsync -> 0x0 WITH the FP16 cap set). Driver-only change — the host FP16-ring ->
NVENC-P010 path and the HDR EDID were already in place.
- adapter.rs: caps.Flags = IDDCX_ADAPTER_FLAGS_CAN_PROCESS_FP16.
- entry.rs: register the 6 *2/HDR callbacks (ParseMonitorDescription2, MonitorQueryTargetModes2,
AdapterCommitModes2, AdapterQueryTargetInfo, MonitorSetDefaultHdrMetaData, MonitorSetGammaRamp)
ALONGSIDE the v1 set (matching the oracle — CAN_PROCESS_FP16 OBLIGATES the *2 DDIs or the
framework rejects the adapter at init; STEP 3 rejected FP16 only because they weren't registered).
- callbacks.rs: parse_monitor_description2 + monitor_query_modes2 now fill IDDCX_MONITOR_MODE2 /
IDDCX_TARGET_MODE2 with BitsPerComponent (8|10 bpc RGB); query_target_info already reports
IDDCX_TARGET_CAPS_HIGH_COLOR_SPACE; set_default_hdr_metadata + set_gamma_ramp accept (the gamma
one is mandatory under FP16).
- monitor.rs: wire_bits() (Rgb 8|10, no YCbCr) + target_mode2().
- EDID + INF UNCHANGED (the EDID already carries the CTA-861.3 BT.2020 + ST.2084/PQ block; the INF
stays UmdfExtensions=IddCx0102).
Built via the ultracode flow (STEP-7 map workflow -> agent-implement -> box build [driver green] ->
deploy -> on-glass HDR). OPERATIONAL NOTE: do NOT Disable/Enable the IddCx devnode to reload it —
that leaves the adapter STOPPED in the persisted WUDFHost process (ADAPTER OnceLock survives), so
monitor-create then fails with 0xc00002b6 (INDIRECT_DISPLAY_DEVICE_STOPPED). Kill the pf_vdisplay
WUDFHost process (or reboot) for a clean adapter re-init.
This completes the pf-vdisplay rewrite STEP 0-7, all on-glass validated (loads, adapter inits,
monitor appears, swap-chain drain, IDD-push frames at ~235fps, and HDR). Remaining: STEP 8 (unsafe-
reduction + delete the old vdisplay-driver tree + the vendored SudoVDA driver + unbundle from the
installer = the SudoVDA drop).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The driver now publishes each acquired swap-chain surface into the host-created shared ring (the
IDD-push path) — the full glass-to-glass transport is code-complete. Both sides use the canonical
pf_vdisplay_proto::frame layout (lockstep by compile-error, not "must match" comments). Driver compiles
+ LOADS on-glass (adapter inits, Status=OK; no regression — the publisher is dormant until a frame is
acquired); host cargo check green; adversarially reviewed (no blockers — token layout, keyed-mutex key 0,
names by target_id, and the format guard all match the host consumer).
- new driver frame_transport.rs: FramePublisher OPENS the host ring by target_id (OpenFileMapping header
+ magic Acquire readiness gate + OpenEvent + OpenSharedResourceByName RING_LEN keyed-mutex textures),
writes its render LUID + DRV_STATUS back into the header; publish() is NON-BLOCKING (round-robin 0ms
try-acquire -> CopyResource -> ReleaseSync -> FrameToken::pack store Release -> SetEvent; drops the
frame if every slot is busy or the surface format != the ring format). Manual handle/view cleanup on
every try_open early return; RAII Drop (slots -> unmap -> CloseHandle). Layout/consts/names/token all
from pf_vdisplay_proto::frame.
- swap_chain_processor.rs run_core: lazy rate-limited attach (every ~30 frames) + is_stale re-attach
(mid-session HDR ring recreate); publishes buffer.MetaData.pSurface via IDXGIResource::from_raw_borrowed
(preserves IddCx's refcount) BEFORE IddCxSwapChainFinishedProcessingFrame. run/run_core gain the render
LUID; callbacks.rs assign_swap_chain passes it.
- host idd_push.rs migrated onto pf_vdisplay_proto::frame (deleted the hand-rolled SharedHeader / MAGIC /
VERSION / RING_LEN / DRV_STATUS_* / name fns / token packing) — pure refactor, byte-identical, no
behavior or gating change. DebugBlock + DXGI_SHARED_RESOURCE_RW kept local (not in the proto).
- driver windows crate gains Win32_System_Memory (MapViewOfFile/OpenFileMappingW/...); rustfmt'd the whole
driver workspace (incl. wdk-probe — fmt-only).
Built via the ultracode flow: STEP-6 map workflow -> agent-implement -> box build (driver + host both
green; caught nothing this time) -> adversarial-verify-agent (no blockers) -> FrameToken::pack hardening
-> deploy (loads). Glass-to-glass frame validation awaits a composited session (per the parity finding:
this headless box yields 0 frames for the proven SudoVDA path too). FOLLOW-UPs: port the optional
Global\pfvd-dbg DebugBlock triage channel to the new driver; STEP 7 HDR; STEP 8 drop SudoVDA.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
STEP 5 (b6486b3) added the windows + thiserror deps to pf-vdisplay/Cargo.toml but the
workspace lock was not updated (driver is windows-only, cant build on the Linux dev box).
Regenerated on the RTX box. Both crates were already resolved in the lock (pulled by
wdk-build), so this is purely the pf-vdisplay dependency edges.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pf-vdisplay driver now consumes the OS swap-chain so a virtual monitor is a usable
display rather than a stalled one. Compiles + loads on-glass (no regression: adapter still
inits, Status=OK); adversarially reviewed — no blockers, the leak/deadlock invariants preserved.
- new swap_chain_processor.rs: a worker thread (MMCSS "Distribution") that binds the render D3D
device (IddCxSwapChainSetDevice, single-borrow 60x@50ms retry) then drains the swap-chain
(ReleaseAndAcquireBuffer2 -> FinishedProcessingFrame; E_PENDING waits 16ms on the surface
event). NO frame publisher yet (STEP 6). RAII terminate+join Drop; the load-bearing
top-of-loop terminate check (the oracle's reconnect-leak fix). Fixed a Rust-2021 disjoint-
capture bug: `.0` field access bypassed the Sendable Send wrapper -> rebind the whole wrappers.
- new direct_3d_device.rs: CreateDXGIFactory2 -> EnumAdapterByLuid(render LUID) -> D3D11CreateDevice;
a DEVICE_POOL of one Arc<Direct3DDevice> per render LUID (the NVIDIA-UMD-worker-thread leak fix).
- monitor.rs: MonitorObject gains swap_chain_processor; set/take helpers return it for the caller
to drop OUTSIDE the MONITOR_MODES lock (dropping joins the worker — must never happen under the
lock); remove_monitor/clear_all drop it before IddCxMonitorDeparture.
- callbacks.rs: assign_swap_chain spawns the processor (pooled device per RenderAdapterLuid;
WdfObjectDelete on D3D-init failure so the OS retries); unassign_swap_chain drops it. Fixed the
stale `panic = "abort"` doc (workspace is unwind; the extern "C" boundary aborts on unwind).
- Cargo.toml: windows 0.58 + thiserror (both already resolved in the driver lock). The 3 needed
swap-chain DDIs were already wrapped in wdk-iddcx; their HRESULT-shaped NTSTATUS is classified
by hand (hr>=0 success, 0x8000000A E_PENDING).
- Also rustfmt'd the whole driver workspace (it had never been driver-fmt'd).
Built via the ultracode flow: STEP-5 map workflow -> agent-implement -> box build (caught the
Send-capture bug) -> adversarial-verify-agent -> deploy (loads). Session-1 on-glass validation
(the drain loop servicing an ACTIVE monitor) is the next gate — assign_swap_chain only fires
under an interactive session. Note for STEP 6: target_id_for_object uses the MONITOR_MODES handle
lookup the oracle moved to a WDF context; revisit before target_id keys the shared frame ring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1a86f16 shipped the agent-written pf_vdisplay.rs unformatted (cargo fmt --all --check
gate) and omitted the Cargo.lock edges for the new windows-only deps (pf-vdisplay-proto +
bytemuck). cargo fmt --all is now clean; Cargo.lock records the host dep edges.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>