---
title: "Windows build & packaging"
description: "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`](windows-host-rewrite.md); deployment/runtime in
[`windows-service.md`](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 `CreateProcessAsUserW`s into the
interactive session for secure-desktop capture (why MSIX is unusable - see
[`windows-service.md`](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 `cdylib`s 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 build`s 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`](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 a self-hosted `windows-amd64` runner (provisioned by unom/infra's `windows-runner/`
Packer template + Terraform clone, `home-windows-runner-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.yml` / `windows-msix.yml` | client | build the Windows *client* + its signed MSIX (x64 + ARM64) |

Every workflow above self-provisions its own toolchain at job start via `scripts/ci/
ensure-windows-toolchain.ps1` (WDK/cargo-wdk, FFmpeg, Inno Setup, the ARM64 rustup target) - a
fast no-op once already present, so no separate one-shot provisioning workflow/dispatch step is
needed, and it works the same on any runner sharing the `windows-amd64` label.

`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`](windows-host-rewrite.md) - host architecture (capture/encode/vdisplay
  backends, IDD-push, the rewrite milestones). The architecture source of truth.
- [`windows-service.md`](windows-service.md) - the SYSTEM service + secure-desktop deployment model.
- [`windows-virtual-display-rust-port.md`](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/`.
