docs(windows): add design/windows-build-and-packaging.md + refresh packaging README
apple / swift (push) Successful in 1m0s
apple / screenshots (push) Successful in 5m19s
windows-host / package (push) Successful in 6m20s
android / android (push) Successful in 4m42s
ci / rust (push) Successful in 4m47s
ci / web (push) Successful in 50s
ci / docs-site (push) Successful in 58s
deb / build-publish (push) Successful in 2m30s
decky / build-publish (push) Successful in 23s
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 3s
ci / bench (push) Successful in 4m40s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m16s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m3s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m0s
docker / deploy-docs (push) Successful in 22s
apple / swift (push) Successful in 1m0s
apple / screenshots (push) Successful in 5m19s
windows-host / package (push) Successful in 6m20s
android / android (push) Successful in 4m42s
ci / rust (push) Successful in 4m47s
ci / web (push) Successful in 50s
ci / docs-site (push) Successful in 58s
deb / build-publish (push) Successful in 2m30s
decky / build-publish (push) Successful in 23s
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 3s
ci / bench (push) Successful in 4m40s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m16s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m3s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m0s
docker / deploy-docs (push) Successful in 22s
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>
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
---
|
||||
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 (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`](windows-service.md) for the `service install` precedent):
|
||||
|
||||
- **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`.
|
||||
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 the `PunktfunkWeb` task (boot, SYSTEM,
|
||||
restart-on-failure -> `bun` on `:3000`), open TCP 3000, start it. Upgrade-safe: stop + reap any old
|
||||
console (by the `:3000` owner, 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-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/`.
|
||||
Reference in New Issue
Block a user