docs(windows-host): consolidate 5 scattered docs into one current source of truth
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>
This commit is contained in:
@@ -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<dyn VirtualLease>` 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<Mgr>` globals
|
||||
collapse into one OnceLock `VirtualDisplayManager` { `Box<dyn VdisplayDriver>`, `Arc<OwnedHandle>` device
|
||||
(typed — kills the raw-`isize` cross-thread smuggle **and** fixes a latent control-handle leak),
|
||||
`Mutex<MgrState>`, `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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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"@<ip>` (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 <rev>"
|
||||
# scp <changed files> "Enrico Bühler@<ip>:C:/Users/Public/pf-rewrite/<same rel path>"
|
||||
|
||||
# 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).
|
||||
+384
-830
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user