A single repo-internal source of truth for the Windows build/packaging: what ships, the all-Rust driver workspace built FROM SOURCE in CI (+ the anti-stale rationale), the toolchain (clang 22 + bindgen 0.72, no LLVM pin), the Inno installer, the web console bundle, the CI workflows, signing, and the dev loop. (design/, not the docs-site.) packaging/windows/README.md: drop the deleted vendored-driver dir + its "Vendored driver" callout, add the build-* / install-gamepad / clear-force-integrity rows, point at the new design doc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
15 KiB
title, description
| title | description |
|---|---|
| Windows build & packaging | How the punktfunk Windows host is built, signed, and packaged: the all-Rust driver workspace built from source in CI, the Inno Setup installer, the web console bundle, the CI workflows, and the dev-iteration helpers. Repo-internal source of truth - not part of the user-facing docs-site. |
Windows build & packaging
Single source of truth for how the Windows host ships: what artifacts are built, the all-Rust
driver workspace and why we build it from source in CI, the Inno Setup installer, the web console
bundle, the CI workflows, signing, and the dev loop. Architecture lives in
windows-host-rewrite.md; deployment/runtime in
windows-service.md. This doc is repo-internal (do not mirror into
docs-site/).
x64-only by design. The host is coupled to NVENC (
nvEncodeAPI64.dll) and the pf-vdisplay IddCx driver, neither of which exists on Windows ARM64 (no ARM64 NVIDIA driver / IddCx path). The client ships x64 + ARM64 MSIX; the host does not.
1. What ships
The signed punktfunk-host-setup-<ver>.exe (Inno Setup) lays down, under C:\Program Files\punktfunk\:
| Component | What it is |
|---|---|
punktfunk-host.exe |
the host binary (--features nvenc,amf-qsv = NVIDIA + AMD/Intel in one build) |
pf-vdisplay driver |
all-Rust UMDF IddCx virtual display (per-session client-resolution output) |
pf-dualsense driver |
virtual DualSense / DualShock 4 (one type-aware HID minidriver) |
pf-xusb driver |
virtual Xbox 360 / XInput companion |
pf-vkhdr-layer |
Vulkan implicit layer that advertises HDR formats on the virtual display |
| web console | self-contained Nitro .output + a portable bun runtime (the PunktfunkWeb task) |
| FFmpeg DLLs | avcodec/avutil/swscale/... - the AMD/Intel (AMF/QSV) encode backend link-imports them |
nefconc.exe |
nefarius' nefcon (creates the root\pf_vdisplay device node; pnputil can't) |
All three drivers and the HDR layer are bundled, not external - no ViGEmBus, no SudoVDA, no separate
driver download. The host installs a LocalSystem SCM service that CreateProcessAsUserWs into the
interactive session for secure-desktop capture (why MSIX is unusable - see
windows-service.md).
2. Component map (source -> artifact)
| Source | Built by | Artifact |
|---|---|---|
crates/punktfunk-host/ |
cargo build --release -p punktfunk-host --features nvenc,amf-qsv |
punktfunk-host.exe |
packaging/windows/drivers/pf-vdisplay/ |
build-pf-vdisplay.ps1 (workspace cargo build + sign) |
pf_vdisplay.{dll,inf,cat} + .cer |
packaging/windows/drivers/pf-dualsense/ pf-xusb/ |
build-gamepad-drivers.ps1 (sign the workspace build) |
pf_{dualsense,xusb}.{dll,inf,cat} + shared .cer |
packaging/windows/pf-vkhdr-layer/ |
pack-host-installer.ps1 (cargo build --release) |
pf_vkhdr_layer.dll + .json |
web/ |
scripts/windows/build-web.ps1 (bun run build) |
self-contained .output |
packaging/windows/nvenc/nvenc.def |
gen-nvenc-importlib.ps1 (llvm-dlltool) |
nvencodeapi.lib (link import, no GPU/SDK) |
3. The driver workspace - packaging/windows/drivers/
A separate cargo workspace (its own [workspace] root) because driver crates are cdylibs built
with the WDK toolchain on Windows only. Members:
pf-vdisplay- the IddCx virtual display (the real driver).pf-dualsense,pf-xusb- the virtual gamepad HID/XUSB minidrivers.wdk-iddcx- hand-written IddCx DDI wrappers (theiddcxApiSubset bindgen reuseswdk_default).wdk-probe- a toolchain/surface-assert probe crate.vendor/wdk-sys+vendor/wdk-build- vendored microsoft/windows-drivers-rs 0.5.1 (the published crates) + an addediddcxApiSubset. A[patch.crates-io]redirects everywdk-sys/wdk-buildreference (incl.wdk0.4.1's transitive deps) to these copies, so the graph has exactly one iddcx-capablewdk-sys. Pinned - do not chase upstream.
Path-deps the owned ABI crate crates/pf-driver-proto (the host<->driver control protocol). .cargo/ config.toml sets an explicit --target x86_64-pc-windows-msvc + target-feature=+crt-static (UMDF
needs the static CRT; the explicit target keeps crt-static off host build-scripts/proc-macros).
[workspace.metadata.wdk.driver-model] sets UMDF 2.31 once for all members.
Driver-specific gotchas (handled by the build scripts):
/INTEGRITYCHECK(FORCE_INTEGRITY).wdk-buildlinks/INTEGRITYCHECK, which a non-EV (self-signed) cert can't satisfy, so the driver won't load.clear-force-integrity.ps1clears the PEDllCharacteristicsbit (offset0x5e) before signing.- Self-signed cert. The drivers are signed with a self-signed CodeSigning cert; the installer trusts
the bundled
.cer(machineRoot+TrustedPublisher) at install time so PnP loads them silently. Validated to load under Secure Boot on. (CI can use a stableDRIVER_CERT_PFX_B64secret instead.) - Device node via nefcon, never devgen. The
root\pf_vdisplaynode is created withnefconc(a cleanROOT\DISPLAYnode).devgenleaves persistentSWD\DEVGENphantoms that survive reboot + registry deletion. The gamepad drivers create their per-session nodes from the host viaSwDeviceCreate(no install-time node). - Strictly-increasing
DriverVer.9.9.MMdd.HHmm(stampinf). pnputil silently keeps the old binary on a non-increasing version; a later-minute redeploy always wins.
4. Drivers are BUILT FROM SOURCE - the anti-stale decision
The drivers used to ship as checked-in prebuilt binaries (packaging/windows/pf-vdisplay/ +
gamepad-drivers/). That model went stale and shipped two field bugs on a fresh install:
- A repo-wide rename edited
pf_vdisplay.inf(a comment) but never re-signedpf_vdisplay.cat. A catalog hashes the INF+DLL byte-for-byte, sopnputil /add-driverfailedSPAPI_E_FILE_HASH_NOT_IN_CATALOGon every box - the driver never installed, every session died "pf-vdisplay driver interface not found". - The frozen binary predated
IOCTL_SET_RENDER_ADAPTER, which the host needs to pin the IddCx render GPU on hybrid/Optimus boxes.
Fix: build from source every release. pack-host-installer.ps1 calls build-pf-vdisplay.ps1 (which
cargo builds the whole workspace) then build-gamepad-drivers.ps1 -SkipBuild (sign the already-built
gamepad cdylibs), so .dll/.inf/.cat are always in lockstep and current driver features ship. The
checked-in binaries were deleted. Re-introducing a vendored binary is the bug; if you must, a catalog
guard (Test-FileCatalog hash-membership) belongs in the build script.
The build scripts share the same shape (WDK env -> build -> clear FORCE_INTEGRITY -> sign DLL ->
stampinf -> Inf2Cat -> sign cat -> export .cer); build-gamepad-drivers.ps1 loops over the two gamepad
drivers and signs both with one shared cert. (A _driver-pack-common.ps1 helper to dedup the ~90% they
share is a known TODO - keep behavior identical and re-run windows-host if you do it.)
5. Toolchain / build env
The drivers build with plain cargo build against the vendored windows-drivers-rs - no cargo-make,
no cargo-wdk for the build (cargo-wdk is only provisioned + probed by windows-drivers.yml). The build
needs, on the runner:
- WDK 26100 -
Version_Number=10.0.26100.0pins the SDK versionwdk-builduses (it otherwise picks10.0.28000.0, which has nokm/crt, and bindgen fails). Provisioned byscripts/ci/provision-windows-wdk.ps1(iddcx headers are the "WDK present" signal). - clang 22 + bindgen 0.72 - the vendored
bindgenis0.72.1, which builds clean on the runner's default LLVM (C:\Program Files\LLVM, currently clang 22).LIBCLANG_PATHis left unset (defaults to the runner default). History: LLVM 21.1.2 was briefly pinned (C:\llvm-21) to dodge a bindgen-0.71 layout-test overflow on clang 22; the 0.72 bump retired that pin, so there's now one toolchain for both driver builds (the pack andwindows-drivers.yml). - NVENC import lib synthesised from a 2-export
.defviallvm-dlltool(gen-nvenc-importlib.ps1) - no GPU or NVIDIA SDK at build time. FFMPEG_DIR(the BtbN gpl-shared x64 tree) for the AMD/Intel AMF/QSV link; NASM + CMake +CMAKE_POLICY_VERSION_MINIMUM=3.5for the CMake-from-source deps (aws-lc, opus).- Gotcha:
CARGO_HOMEmust be an ASCII path (a non-ASCII username breaks SDL3's MSVC precompiled header). The runner usesC:\Users\Public\.cargo. CARGO_TARGET_DIRfor the driver build must be the DEFAULT (in-tree) dir.wdk-build'sfind_top_level_cargo_manifest()walks up fromOUT_DIRto the first ancestor with aCargo.lock; a relocatedC:\ttarget dir hides the workspace lock and the build-script panics "a Cargo.lock file should exist...". The driver deps have no deep CMake crates, so the in-tree target stays under MAX_PATH. (The host/client builds do relocate toC:\tto dodge MAX_PATH - that's the opposite need.)
6. The installer - Inno Setup
pack-host-installer.ps1 orchestrates, in order: resolve a code-signing cert -> sign punktfunk-host.exe
-> build + sign the drivers from source (build-pf-vdisplay.ps1 + build-gamepad-drivers.ps1,
staged via stage-pf-vdisplay.ps1 which also fetches/verifies pinned nefcon) -> stage FFmpeg DLLs + the
web console + a portable bun -> build + sign the HDR Vulkan layer -> run ISCC on punktfunk-host.iss
-> sign setup.exe.
punktfunk-host.iss (Inno) lays down {app}, runs the install steps, and registers things. Optional
tasks (all default-checked): install the pf-vdisplay driver, install the gamepad drivers, install the
HDR Vulkan layer, start the service. Silent install: /VERYSILENT (omit a task with
/MERGETASKS="!installdriver").
Install-time work (currently [Run] -> powershell.exe -File install-*.ps1 / web-setup.ps1; being
moved into punktfunk-host.exe subcommands so there are no locale-parsed PowerShell scripts on the
end-user box - the root fix for the recurring ANSI-codepage parse breakage, see
windows-service.md for the service install precedent):
- Driver install: trust the bundled
.cer(Root + TrustedPublisher), create theroot\pf_vdisplaynode if absent (nefconc, gated so a re-create can't spawn a phantom),pnputil /add-driver /install. Best-effort - a driver hiccup never aborts the install (the host degrades to a physical display). - Web console: write the ACL'd
web-password, register thePunktfunkWebtask (boot, SYSTEM, restart-on-failure ->bunon:3000), open TCP 3000, start it. Upgrade-safe: stop + reap any old console (by the:3000owner, runtime-agnostic) before re-registering so the new one can bind.
Signing: the exe/setup/HDR-layer use the MSIX_CERT_PFX_B64/MSIX_CERT_PASSWORD secrets
(CN=unom, shared with the client); the drivers use a separate cert (self-signed per build, or a
stable DRIVER_CERT_PFX_B64) and their own bundled .cer - the two never collide. Without the MSIX
secrets, an ephemeral self-signed cert is generated and its .cer published next to the installer.
7. The web console bundle
The console is a TanStack Start / Nitro SSR app (web/). vite.config.ts sets noExternals: true, so
bun run build emits a self-contained .output (~75 files, deps bundled + tree-shaken, no
node_modules/.npmrc). The installer ships that .output + a portable bun.exe; the PunktfunkWeb
task runs bun .output/server/index.mjs on :3000, auto-wired to the host's loopback mgmt API via
web-run.cmd (sources %ProgramData%\punktfunk\mgmt-token + web-password). No node, no node_modules
forest. (build-web.ps1 is the dev-box rebuild-and-restart helper.)
8. CI workflows (.gitea/workflows/)
All run on the single self-hosted windows-amd64 runner (home-windows-1), which serializes the
whole Windows fleet - a Cargo.lock/packaging/windows/** touch queues several builds back-to-back.
| Workflow | Trigger | Does |
|---|---|---|
windows-host.yml |
crates/punktfunk-host, packaging/windows, scripts/windows, web, tags v* |
build host + clippy + HDR layer + web smoke-boot -> pack + sign installer -> publish (canary/latest) |
windows-drivers.yml |
packaging/windows/drivers, crates/pf-driver-proto |
probe the driver toolchain + build/test/clippy pf-driver-proto + cargo build the driver workspace + inspect FORCE_INTEGRITY (the fast driver-only gate; coverage the pack lacks) |
windows-drivers-provision.yml |
provision-windows-wdk.ps1 |
one-shot WDK + cargo-wdk provisioning onto the persistent runner |
windows.yml / windows-msix.yml |
client | build the Windows client + its signed MSIX (x64 + ARM64) |
windows-host.yml also builds the drivers from source (in pack), so it overlaps windows-drivers.yml on
a drivers/** edit (two driver builds on the serialized runner). They're kept separate on purpose -
windows-drivers.yml is the fast pre-pack gate. CI builds, never launches the exe (no GPU on the
runner), so AMF/QSV + on-glass behavior are validated on a real box, not in CI.
9. Dev iteration
- Host:
scripts/windows/deploy-host.ps1(build + redeploy the exe to a box),build-web.ps1(rebuild + restart the console). - pf-vdisplay driver:
packaging/windows/drivers/deploy-dev.ps1(build -> clear FORCE_INTEGRITY -> sign -> stampinf a strictly-increasingDriverVer-> Inf2Cat -> sign ->-Install);redeploy-pf-vdisplay.ps1(one-shot: stop host -> install -> reload adapter -> start);reset-pf-vdisplay.ps1(recover a wedged driver: reap ghost monitor nodes + cycle the adapter, no reboot). Run elevated; default to thePunktfunkHostservice. - Drive any of these from Linux over SSH:
ssh user@box 'powershell -ExecutionPolicy Bypass -File C:\...\reset-pf-vdisplay.ps1'. - The RTX/on-glass box is where NVENC encode + IDD-push frame flow are validated (CI can't).
10. Release
Push a vX.Y.Z tag (one tag releases every platform): windows-host.yml builds + signs
punktfunk-host-setup-X.Y.Z.exe + the public .cer, refreshes the latest/ alias, and attaches them to
the unified Gitea Release. Main pushes publish rolling 0.3.<run> canary builds to canary/.
Download: https://git.unom.io/api/packages/unom/generic/punktfunk-host-windows/{latest,canary}/punktfunk-host-setup.exe.
11. See also
windows-host-rewrite.md- host architecture (capture/encode/vdisplay backends, IDD-push, the rewrite milestones). The architecture source of truth.windows-service.md- the SYSTEM service + secure-desktop deployment model.windows-virtual-display-rust-port.md- history of the all-Rust IddCx driver port (SUPERSEDED in its conclusion: IDD-push became the primary capture path).packaging/windows/pf-vkhdr-layer/README.md- the HDR Vulkan layer.packaging/windows/README.md- the file index forpackaging/windows/.