Files
punktfunk/design/windows-build-and-packaging.md
enricobuehler dac0fee4e3
apple / swift (push) Successful in 1m3s
apple / screenshots (push) Successful in 5m31s
ci / web (push) Successful in 49s
decky / build-publish (push) Successful in 14s
ci / rust (push) Failing after 32s
ci / docs-site (push) Successful in 1m1s
android / android (push) Successful in 3m21s
deb / build-publish (push) Successful in 2m30s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 13s
ci / bench (push) Successful in 4m49s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m32s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m47s
docker / deploy-docs (push) Successful in 6s
docs(windows): reflect the install-via-exe (Option A) landing in the build/packaging doc
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 16:44:47 +00:00

16 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 (the iddcx ApiSubset bindgen reuses wdk_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 added iddcx ApiSubset. A [patch.crates-io] redirects every wdk-sys/wdk-build reference (incl. wdk 0.4.1's transitive deps) to these copies, so the graph has exactly one iddcx-capable wdk-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-build links /INTEGRITYCHECK, which a non-EV (self-signed) cert can't satisfy, so the driver won't load. clear-force-integrity.ps1 clears the PE DllCharacteristics bit (offset 0x5e) before signing.
  • Self-signed cert. The drivers are signed with a self-signed CodeSigning cert; the installer trusts the bundled .cer (machine Root + TrustedPublisher) at install time so PnP loads them silently. Validated to load under Secure Boot on. (CI can use a stable DRIVER_CERT_PFX_B64 secret instead.)
  • Device node via nefcon, never devgen. The root\pf_vdisplay node is created with nefconc (a clean ROOT\DISPLAY node). devgen leaves persistent SWD\DEVGEN phantoms that survive reboot + registry deletion. The gamepad drivers create their per-session nodes from the host via SwDeviceCreate (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:

  1. A repo-wide rename edited pf_vdisplay.inf (a comment) but never re-signed pf_vdisplay.cat. A catalog hashes the INF+DLL byte-for-byte, so pnputil /add-driver failed SPAPI_E_FILE_HASH_NOT_IN_CATALOG on every box - the driver never installed, every session died "pf-vdisplay driver interface not found".
  2. 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.0 pins the SDK version wdk-build uses (it otherwise picks 10.0.28000.0, which has no km/crt, and bindgen fails). Provisioned by scripts/ci/provision-windows-wdk.ps1 (iddcx headers are the "WDK present" signal).
  • clang 22 + bindgen 0.72 - the vendored bindgen is 0.72.1, which builds clean on the runner's default LLVM (C:\Program Files\LLVM, currently clang 22). LIBCLANG_PATH is 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 and windows-drivers.yml).
  • NVENC import lib synthesised from a 2-export .def via llvm-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.5 for the CMake-from-source deps (aws-lc, opus).
  • Gotcha: CARGO_HOME must be an ASCII path (a non-ASCII username breaks SDL3's MSVC precompiled header). The runner uses C:\Users\Public\.cargo.
  • CARGO_TARGET_DIR for the driver build must be the DEFAULT (in-tree) dir. wdk-build's find_top_level_cargo_manifest() walks up from OUT_DIR to the first ancestor with a Cargo.lock; a relocated C:\t target 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 to C:\t to 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 runs from punktfunk-host.exe subcommands, not locale-parsed PowerShell files - the [Run] section calls driver install [--gamepad] --dir <stage> and web setup --app-dir <app> [--password-file <f>] (crates/punktfunk-host/src/windows/install.rs). This is the ANSI-codepage root fix: PowerShell 5.1 reads a BOM-less .ps1 file in the machine codepage, so a stray non-ASCII byte aborted the install on a non-English box; a compiled subcommand drives the same external tools as fixed string literals (the service install precedent, see windows-service.md). The .iss's inline -Command PowerShell is a command-line string, not a file read, so it's unaffected and stays. Each subcommand is best-effort (a hiccup warns, never aborts the installer):

  • Driver install: trust the bundled .cer (Root + TrustedPublisher), create the root\pf_vdisplay node if absent (nefconc, gated so a re-create can't spawn a phantom), pnputil /add-driver /install (pf-vdisplay) or pnputil /add-driver per-inf (gamepads - the host SwDeviceCreate's the devnodes). A driver hiccup never aborts the install (the host degrades to a physical display).
  • Web console (web setup): write the ACL'd web-password, register the PunktfunkWeb task (boot, SYSTEM, restart-on-failure -> bun on :3000, via a generated UTF-16 Task Scheduler XML), open TCP 3000, start it. Upgrade-safe: stop + reap any old console (by the :3000 listener owner, runtime- agnostic - identified by the wildcard foreign address, so the localized state word is never parsed) 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-increasing DriverVer -> 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 the PunktfunkHost service.
  • 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 for packaging/windows/.