feat(windows-packaging): dev-iteration scripts — reset + redeploy pf-vdisplay driver

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>
This commit is contained in:
2026-06-25 20:45:14 +00:00
parent 683c81be03
commit c206e19ce5
3 changed files with 259 additions and 4 deletions
+30 -4
View File
@@ -71,21 +71,47 @@ read it from `%ProgramData%\punktfunk\web-password`.
| `install-pf-vdisplay.ps1` | Runs at install time (elevated): trust cert → gated device-node create (nefconc) → `pnputil` install. |
| `../../scripts/windows/web-run.cmd` | The `PunktfunkWeb` task action: loads the mgmt token + login password env, runs the bundled `bun` on the Nitro server (`:3000`). |
| `../../scripts/windows/web-setup.ps1` | Install-time (elevated): write the ACL'd console password, register the `PunktfunkWeb` task + firewall rule, start it. |
| `pf-vdisplay/` | **Vendored** signed pf-vdisplay driver: `pf_vdisplay.inf` / `pf_vdisplay.cat` / `pf_vdisplay.dll` / `punktfunk-driver.cer`. Built from `vdisplay-driver/`. |
| `vdisplay-driver/` | The all-Rust IddCx **driver source** (`pf-vdisplay` crate + vendored `wdf-umdf*` bindings) + `deploy-dev.ps1` (build/sign/install for dev). |
| `pf-vdisplay/` | **Vendored** signed pf-vdisplay driver: `pf_vdisplay.inf` / `pf_vdisplay.cat` / `pf_vdisplay.dll` / `punktfunk-driver.cer`. Built from `drivers/`. |
| `drivers/` | The all-Rust IddCx **driver source** workspace: the `pf-vdisplay` crate on `wdk-sys` / windows-drivers-rs + the owned `pf-vdisplay-proto` ABI + `wdk-iddcx` / `wdk-probe`, plus `deploy-dev.ps1` (build/sign/install for dev). |
| `reset-pf-vdisplay.ps1` | **Dev:** recover a wedged driver — stop host → reap ghost monitor nodes → reload the adapter → start host (no reboot). See *Dev iteration* below. |
| `redeploy-pf-vdisplay.ps1` | **Dev:** one-shot redeploy — (optional) build → stop host → `deploy-dev.ps1 -Install` → reload adapter → start host. |
| `nvenc/nvenc.def`, `nvenc/gen-nvenc-importlib.ps1` | Synthesise `nvencodeapi.lib` for the `--features nvenc` link (llvm-dlltool / lib.exe). |
> **Vendored driver:** pf-vdisplay is our **all-Rust IddCx** virtual display (UMDF2), built from
> `packaging/windows/vdisplay-driver/`. It replaced the vendored SudoVDA C++ driver — full story in
> `packaging/windows/drivers/`. It replaced the vendored SudoVDA C++ driver — full story in
> [`docs/windows-virtual-display-rust-port.md`](../../docs/windows-virtual-display-rust-port.md). The
> **signed** output (`pf_vdisplay.dll`/`.inf`/`.cat` + `punktfunk-driver.cer`; signer
> `punktfunk-ds-test` — the same cert the gamepad drivers ship, Class=Display, HWID `root\pf_vdisplay`)
> is checked in under `pf-vdisplay/`. To refresh it after a driver-source change, rebuild + re-sign with
> `vdisplay-driver/deploy-dev.ps1` and copy the staged `pf_vdisplay.{dll,inf,cat}` over the vendored
> `drivers/deploy-dev.ps1` and copy the staged `pf_vdisplay.{dll,inf,cat}` over the vendored
> copies. nefcon (the device-node tool — the install creates the node with it, **never** `devgen`, which
> leaves persistent phantom devices) **is** fetched + SHA-256-verified from its pinned release in
> `stage-pf-vdisplay.ps1`.
## Dev iteration on the test box (driver)
Two helpers wrap the painful manual steps of iterating on the pf-vdisplay driver against a live host
service. Run **elevated**; both default to the `PunktfunkHost` service.
```powershell
# Recover a WEDGED driver. Symptom: every session fails with
# create virtual output: pf-vdisplay ADD ...: DeviceIoControl(0x222400): Element nicht gefunden (0x80070490)
# i.e. ERROR_NOT_FOUND — sustained ADD/REMOVE churn exhausted the IddCx monitor slots (ghost
# "Generic Monitor (punktfunk)" nodes pile up, target_ids climb). A host restart's CLEAR_ALL does NOT
# fix it; the driver instance must be reloaded. This clears the ghosts + cycles the adapter (no reboot —
# this box boots to Proxmox).
powershell -ExecutionPolicy Bypass -File reset-pf-vdisplay.ps1 -Verify -Probe C:\t-goal1\debug\punktfunk-probe.exe
# Redeploy a driver build cleanly (stop host → install with a strictly-increasing DriverVer → reload
# adapter → start host). -Build runs `cargo build` first, but ONLY from an MSVC dev shell
# (LIBCLANG_PATH + Version_Number=10.0.26100.0); otherwise build separately and omit -Build.
powershell -ExecutionPolicy Bypass -File redeploy-pf-vdisplay.ps1 -Build -Verify -Probe C:\t-goal1\debug\punktfunk-probe.exe
```
The driver should reclaim monitor slots on REMOVE so churn can't wedge it; until it does, `reset` is
the recovery. From a Linux box drive either over SSH, e.g.
`ssh user@box 'powershell -ExecutionPolicy Bypass -File C:\...\reset-pf-vdisplay.ps1'`.
## Build locally (Windows, MSVC + Windows SDK + Inno Setup)
```powershell
@@ -0,0 +1,99 @@
#requires -Version 5.1
<#
.SYNOPSIS
One-shot DEV redeploy of the pf-vdisplay (punktfunk) virtual-display driver on the test box:
(optional) build -> stop host -> stage+sign+install -> reload the adapter -> start host.
.DESCRIPTION
Wraps drivers/deploy-dev.ps1 (which stages the freshly built pf_vdisplay.dll, clears its
FORCE_INTEGRITY PE bit, signs it, stamps a STRICTLY-INCREASING DriverVer, builds+signs the catalog,
and pnputil-installs it) with the two things the dev loop always needs around it:
* The running host service HOLDS the driver's control device, and pnputil can't replace a busy
DLL — so the host must be stopped across the install. This stops it first and starts it after.
* pnputil /add-driver /install updates the driver STORE, but the OS keeps the LIVE adapter on the
old binary until the device is reloaded — so this cycles the adapter (reset-pf-vdisplay.ps1)
after install, which also clears the ghost monitor nodes for a clean slate.
Run ELEVATED. Use -Build only from an MSVC dev shell (the driver's cargo build needs LIBCLANG_PATH
+ Version_Number=10.0.26100.0, per drivers/deploy-dev.ps1); otherwise build separately and omit it.
.PARAMETER Build Run `cargo build` in packaging/windows/drivers first (needs the MSVC env).
.PARAMETER Service Host service name. Default PunktfunkHost.
.PARAMETER Thumbprint Passthrough to deploy-dev.ps1 (test-cert SHA-1). Omit to use its default.
.PARAMETER Nefconc Passthrough to deploy-dev.ps1 (nefconc.exe path). Omit to use its default.
.PARAMETER Verify After redeploy, probe to confirm ADD works (passes through to the reset's
-Verify; needs -Probe or punktfunk-probe.exe on PATH).
.PARAMETER Probe Path to punktfunk-probe.exe for -Verify.
.EXAMPLE
# already built the driver in an MSVC shell -> deploy it cleanly:
powershell -ExecutionPolicy Bypass -File redeploy-pf-vdisplay.ps1
.EXAMPLE
# build + deploy + verify, from an MSVC dev shell:
powershell -ExecutionPolicy Bypass -File redeploy-pf-vdisplay.ps1 -Build -Verify -Probe C:\t-goal1\debug\punktfunk-probe.exe
#>
[CmdletBinding()]
param(
[switch]$Build,
[string]$Service = 'PunktfunkHost',
[string]$Thumbprint,
[string]$Nefconc,
[switch]$Verify,
[string]$Probe
)
$ErrorActionPreference = 'Stop'
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$driversDir = Join-Path $here 'drivers'
$deploy = Join-Path $driversDir 'deploy-dev.ps1'
$reset = Join-Path $here 'reset-pf-vdisplay.ps1'
foreach ($f in @($deploy, $reset)) { if (-not (Test-Path $f)) { throw "missing helper: $f" } }
# 1) Optional rebuild (MSVC dev shell only).
if ($Build) {
Write-Host "==> cargo build (pf-vdisplay driver, $driversDir)"
Push-Location $driversDir
try {
cargo build
if ($LASTEXITCODE -ne 0) { throw "cargo build failed ($LASTEXITCODE) — is this an MSVC dev shell with LIBCLANG_PATH + Version_Number set?" }
}
finally { Pop-Location }
}
# 2) Stop the host (it holds the driver DLL; pnputil can't replace a busy binary).
$svc = Get-Service $Service -ErrorAction SilentlyContinue
if ($svc -and $svc.Status -eq 'Running') {
Write-Host "==> stopping $Service"
Stop-Service $Service -Force
Start-Sleep -Seconds 2
}
# 3) Stage + sign + install (strictly-increasing DriverVer so pnputil takes the new binary).
$deployArgs = @{ Install = $true }
if ($Thumbprint) { $deployArgs.Thumbprint = $Thumbprint }
if ($Nefconc) { $deployArgs.Nefconc = $Nefconc }
Write-Host "==> deploy-dev.ps1 -Install"
& $deploy @deployArgs
# 4) Reload the adapter so the OS loads the freshly-installed binary (+ clear ghost nodes). The reset
# leaves the host alone (-NoHost) — we own the service lifecycle here.
Write-Host "==> reloading the pf-vdisplay adapter (clean slate)"
& $reset -NoHost
# 5) Start the host.
if ($svc) {
Write-Host "==> starting $Service"
Start-Service $Service
Start-Sleep -Seconds 3
Write-Host " $Service status: $((Get-Service $Service -ErrorAction SilentlyContinue).Status)"
}
# 6) Optional verification probe.
if ($Verify) {
$vArgs = @{ NoHost = $true; KeepGhosts = $true; Verify = $true }
if ($Probe) { $vArgs.Probe = $Probe }
& $reset @vArgs
}
Write-Host "pf-vdisplay redeploy done."
+130
View File
@@ -0,0 +1,130 @@
#requires -Version 5.1
<#
.SYNOPSIS
Recover the pf-vdisplay (punktfunk) virtual-display driver after it WEDGES under rapid ADD/REMOVE
churn — no reboot. The dev-iteration counterpart to redeploy-pf-vdisplay.ps1.
.DESCRIPTION
Sustained connect/disconnect churn (e.g. a client reconnect loop × the host's 8 pipeline-build
retries — ~100 ADD/REMOVE cycles) exhausts the driver's IddCx monitor slots: the per-monitor
target_ids climb, ghost "Generic Monitor (punktfunk)" device nodes pile up, and eventually
IOCTL_ADD returns 0x80070490 ERROR_NOT_FOUND ("Element nicht gefunden"). Every session then fails
to create a virtual output -> the client gets a hard blackscreen. A host-service restart's
IOCTL_CLEAR_ALL does NOT recover it; the driver instance itself must be reloaded.
Steps (run ELEVATED):
1. Stop the host service (it holds the driver's control device).
2. pnputil /remove-device the GHOST (Status != OK = not-present) punktfunk virtual-monitor nodes
that accumulated — the root of the slot exhaustion.
3. Disable + Enable the pf-vdisplay adapter (ROOT\DISPLAY\*, "punktfunk Virtual Display") to
reload the IddCx driver instance and reset its monitor list. (Restart-PnpDevice does NOT exist
on this box's PowerShell, so we disable+enable explicitly.)
4. Restart the host service.
Avoids a reboot on purpose (this box boots to Proxmox).
.PARAMETER Service Host service name. Default PunktfunkHost.
.PARAMETER AdapterName FriendlyName substring of the IddCx adapter to cycle. Default "punktfunk
Virtual Display" (NOT SudoVDA's "SudoMaker Virtual Display Adapter").
.PARAMETER GhostMatch FriendlyName substring of the virtual monitors to reap. Default "punktfunk".
.PARAMETER KeepGhosts Skip the ghost-node cleanup; only cycle the adapter.
.PARAMETER NoHost Don't stop/start the host service (just reset the driver) — used by
redeploy-pf-vdisplay.ps1, which manages the service itself.
.PARAMETER Verify After recovery, run a punktfunk-probe loopback and report whether ADD works
again (best-effort; needs punktfunk-probe.exe on PATH or via -Probe).
.PARAMETER Probe Path to punktfunk-probe.exe for -Verify.
.EXAMPLE
powershell -ExecutionPolicy Bypass -File reset-pf-vdisplay.ps1
.EXAMPLE
powershell -ExecutionPolicy Bypass -File reset-pf-vdisplay.ps1 -Verify -Probe C:\t-goal1\debug\punktfunk-probe.exe
#>
[CmdletBinding()]
param(
[string]$Service = 'PunktfunkHost',
[string]$AdapterName = 'punktfunk Virtual Display',
[string]$GhostMatch = 'punktfunk',
[switch]$KeepGhosts,
[switch]$NoHost,
[switch]$Verify,
[string]$Probe
)
$ErrorActionPreference = 'Continue'
function Get-PfAdapter {
Get-PnpDevice -Class Display -ErrorAction SilentlyContinue |
Where-Object { $_.FriendlyName -match $AdapterName } | Select-Object -First 1
}
# 1) Stop the host so it isn't mid-IOCTL during the reset (it holds the control device).
$svc = Get-Service $Service -ErrorAction SilentlyContinue
$hostWasRunning = $svc -and $svc.Status -eq 'Running'
if (-not $NoHost -and $hostWasRunning) {
Write-Host "==> stopping $Service"
Stop-Service $Service -Force
Start-Sleep -Seconds 2
}
# 2) Reap the ghost (not-present) punktfunk virtual-monitor device nodes.
if (-not $KeepGhosts) {
$ghosts = Get-PnpDevice -Class Monitor -ErrorAction SilentlyContinue |
Where-Object { $_.Status -ne 'OK' -and $_.FriendlyName -match $GhostMatch }
Write-Host "==> removing $($ghosts.Count) ghost virtual-monitor node(s)"
$removed = 0
foreach ($g in $ghosts) {
pnputil /remove-device $g.InstanceId *> $null
if ($LASTEXITCODE -eq 0) { $removed++ }
}
Write-Host " removed $removed"
}
# 3) Reload the IddCx adapter instance (disable + enable) to clear its monitor list.
$ad = Get-PfAdapter
if (-not $ad) {
Write-Warning "pf-vdisplay adapter '$AdapterName' not found (Class Display) — is the driver installed?"
}
else {
Write-Host "==> cycling adapter $($ad.InstanceId)"
Disable-PnpDevice -InstanceId $ad.InstanceId -Confirm:$false -ErrorAction Continue
Start-Sleep -Seconds 3
Enable-PnpDevice -InstanceId $ad.InstanceId -Confirm:$false -ErrorAction Continue
Start-Sleep -Seconds 3
$st = (Get-PnpDevice -InstanceId $ad.InstanceId -ErrorAction SilentlyContinue).Status
if ($st -ne 'OK') {
# One retry — a disabled root device occasionally needs a second enable to come back OK.
Enable-PnpDevice -InstanceId $ad.InstanceId -Confirm:$false -ErrorAction Continue
Start-Sleep -Seconds 2
$st = (Get-PnpDevice -InstanceId $ad.InstanceId -ErrorAction SilentlyContinue).Status
}
Write-Host " adapter status: $st"
}
# 4) Restart the host.
if (-not $NoHost -and $svc) {
Write-Host "==> starting $Service"
Start-Service $Service
Start-Sleep -Seconds 3
Write-Host " $Service status: $((Get-Service $Service -ErrorAction SilentlyContinue).Status)"
}
# 5) Optional: probe to confirm ADD recovers.
if ($Verify) {
if (-not $Probe) {
$Probe = (Get-Command punktfunk-probe.exe -ErrorAction SilentlyContinue).Source
}
if (-not $Probe -or -not (Test-Path $Probe)) {
Write-Warning "-Verify: punktfunk-probe.exe not found (pass -Probe <path>); skipping verification."
}
else {
$log = Join-Path $env:ProgramData 'punktfunk\logs\host.log'
Write-Host "==> verifying with $Probe"
& $Probe *> $null
Start-Sleep -Seconds 2
$last = Get-Content $log -Tail 80 -ErrorAction SilentlyContinue |
Select-String -Pattern 'pf-vdisplay created|Element nicht|0x80070490' | Select-Object -Last 1
if ($last -match 'created') { Write-Host " OK: ADD succeeded after reset." }
elseif ($last) { Write-Warning " ADD still failing after reset: $($last.Line.Trim())" }
else { Write-Warning " no ADD outcome found in the log; check $log." }
}
}
Write-Host "pf-vdisplay reset done."