Field report within hours of 0.20.0 (HitFrostbite): `winget install unom.PunktfunkHost` fails with "Installer failed with exit code: 1", 0x8A150006, after the hash verifies and the install starts. That is Setup aborting on purpose. `StreamHostPresent` counted a conflicting host as present if its SCM service key existed at ANY start type, OR if a bare Program Files\<Name> directory existed — so a disabled service, or a folder left behind by an uninstall, read as a live conflict.3d893013then made the warning a SuppressibleMsgBox defaulting to IDNO so unattended installs could not hang on it; under winget's /VERYSILENT /SUPPRESSMSGBOXES that default aborts InitializeSetup, and Inno exits "failed to initialize" = code 1. Net effect: anyone with Sunshine on disk — the exact audience most likely to migrate — could not install via winget at all, with no visible reason. The other candidate for exit 1 is excluded rather than assumed: the manifest declares MinimumOSVersion 10.0.22621.0 matching the script's MinVersion, so winget refuses an unsupported OS BEFORE downloading, and the report shows the hash verified and the install starting. The conflict check is also the only `Result := False` in InitializeSetup. Detection now keys on the service's `Start` value (0 boot / 1 system / 2 automatic / 3 manual / 4 disabled), counting only 0-2 — a host that comes up on its own is the only kind that can take the GameStream ports or load a second virtual-display driver. Directories no longer count at all: they were the noisiest signal and the one that survives an uninstall. This makes the installer agree with the tray, which dropped its always-on warning over a merely-INSTALLED Sunshine in this same release (3e782852) for exactly the same reason. The message now says "installed and set to start automatically" and offers disabling the service as a remedy, both of which are what is actually detected. A conflicting host running as a plain user process is deliberately not detected: pure Pascal cannot enumerate processes, it is a runtime rather than install-time condition, and the host already reports it via the `punktfunk::detect` startup warning, `detect-conflicts`, and /api/v1/local/summary. Verified by compiling the [Code] section with ISCC 6.7.1 on the windows-amd64 runner (WithWeb defined, as CI does). That caught two defects in my own first draft that review would not have: a continuation line STARTING with #13#10, which ISPP reads as a preprocessor directive ("Unknown preprocessor directive", compile aborted), and `{commonpf}` written inside a `{ }` comment, which closes it early since Inno comments do not nest. Both are noted at the sites so they are not reintroduced.
Windows host packaging — signed Inno Setup installer
A one-file, signed setup.exe for the punktfunk streaming host on Windows, published to Gitea's
generic package registry (punktfunk-host-windows) by .gitea/workflows/windows-host.yml.
Full picture (drivers-from-source, toolchain, CI, dev loop): punktfunk-planning:
windows-build-and-packaging.md(internal planning repo). This README is thepackaging/windows/file index.
Windows 11 22H2+ only (no Windows 10)
The installer refuses anything below Windows 11 22H2 (build 22621) — MinVersion=10.0.22621 in
punktfunk-host.iss, with a [Messages] override naming the requirement. The floor comes from the
pf-vdisplay driver: it is built against the IddCx 1.10 class extension (the HDR *2 DDIs +
the FP16 adapter cap, linked via the 1.10 IddCxStub, no runtime IddCxGetVersion downgrade), and
IddCx 1.10 first shipped in Windows 11 22H2. On older Windows — all of Windows 10 including LTSC,
and Windows 11 21H2 — the driver package installs fine, but the device then fails to start with
Code 10 STATUS_DEVICE_POWER_FAILURE in Device Manager and every session dies with "pf-vdisplay
driver interface not found". Gating the installer turns that late, confusing failure into an upfront
message. (Down-level SDR-only support would need a runtime IddCx version check in the driver —
tracked as a possible future feature, not planned.)
x64 only (no ARM64)
Unlike the client (which ships x64 + ARM64 MSIX), the host is x64-only by design. It is coupled to
an NVIDIA GPU (NVENC, via nvEncodeAPI64.dll from the driver) and the pf-vdisplay virtual-display
driver — neither exists on Windows ARM64 (no ARM64 NVIDIA driver; the driver builds x64-only). An
ARM64 host would install but couldn't encode or create a virtual display, so we don't build one.
Revisit if NVIDIA-ARM Windows PCs ever ship.
Why not MSIX (like the client)
The host installs a LocalSystem SCM service that CreateProcessAsUserW's from Session 0 into the
interactive session for secure-desktop (UAC / lock screen) capture, adds firewall rules, and depends
on the pf-vdisplay UMDF/IDD virtual-display driver. MSIX's sandbox can install neither a SYSTEM
service of this kind nor a driver. So the host ships as a classic elevated installer.
The installer is deliberately thin: the real install logic lives in punktfunk-host subcommands, not
in PowerShell — service install (SCM registration, firewall rules, the default host.env, the
SYSTEM→interactive-session supervisor; service.rs), driver install [--gamepad] and web setup
(driver/console provisioning; windows/install.rs). The installer lays the exe into
C:\Program Files\punktfunk\ and calls those subcommands elevated. Keeping the logic in the compiled
exe — not a .ps1 file PowerShell reads in the machine codepage — is the fix for the ANSI-codepage
parse breakage that silently failed installs on non-English boxes.
What the installer does
- Installs
punktfunk-host.exe(+host.env.example, this README) to{app}(C:\Program Files\punktfunk). - Optional task Install the pf-vdisplay virtual display driver —
punktfunk-host.exe driver installimports the driver's self-signed cert (machineRoot+TrustedPublisher), creates theroot\pf_vdisplaydevice node (only if absent, via nefconc — never devgen), and stages the driver withpnputil /add-driver /install. Best-effort: a driver failure warns but never aborts the install (the host degrades to a physical display without it). - Runs
punktfunk-host service install(idempotent; writes a defaulthost.envonly if absent, so user config survives upgrades) and, by the Start service now task,service start. - Web management console (bundled when packed with
-WebDir/-BunExe, which the CI always is): lays down the built self-contained.outputserver (NitronoExternals— deps bundled + tree-shaken, ~75 files, nonode_modules) + a portable bun, prompts for a console login password (pre-filled with a secure random default, shown again on the final page; kept on upgrade), thenpunktfunk-host.exe web setupwrites the ACL'd%ProgramData%\punktfunk\web-password, registers thePunktfunkWebscheduled task (boot, SYSTEM, restart-on-failure →web-run.cmd→bunon:47992), opens TCP 47992, and starts it. It proxies the host's loopback mgmt API with the host's own%ProgramData%\punktfunk\mgmt-token. - GameStream (Moonlight) compatibility is a wizard task (checked by default): the choice is passed
to
service install --gamestream=on|off, which writesPUNKTFUNK_HOST_CMD=serve --gamestream(orserve, the secure native-only host) intohost.env. Upgrade-safe: a hand-customizedPUNKTFUNK_HOST_CMDis never overwritten. - Branded, modern wizard:
WizardStyle=modern dynamic windows11(Inno ≥ 6.6 — Windows-11-style controls following the system light/dark theme; pre-6.6 compilers fall back to plainmodern), with the punktfunk lens mark on the side panel / header tile and a multi-sizepunktfunk.ico(SetupIconFile+ the Apps & features entry). Assets are generated and committed bybranding/gen-branding.ps1from the canonical brand geometry (web/src/components/brand-mark.tsx); re-run it only when the brand changes. - Upgrade: stops a running
PunktfunkHostservice and waits forSTOPPEDbefore replacing files (otherwise the locked exe / respawning supervisor would block the copy), then re-points the service; the existing console password is kept (the wizard page is skipped). - Uninstall (Add/Remove Programs): runs
service uninstall(stop + delete service + remove firewall rules), removes thePunktfunkWebtask + its firewall rule, thendriver uninstall(+--gamepad) removes the punktfunk virtual-device drivers — the pf-vdisplay device node(s) and the pf-vdisplay / pf-dualsense / pf-xusb driver-store packages (the field report was that they survived uninstall). VB-CABLE is intentionally NOT removed (a third-party shared component the user may use elsewhere — its own uninstaller isVBCABLE_Setup_x64.exe -u -h); the%ProgramData%\punktfunkconfig (incl.web-password) is also left in place.
Silent install: punktfunk-host-setup-<ver>.exe /VERYSILENT (omit the driver with
/MERGETASKS="!installdriver"; disable Moonlight compat with /MERGETASKS="!gamestream"). A silent
fresh install uses the generated random console password — read it from
%ProgramData%\punktfunk\web-password.
Prerequisites on the target box
- A GPU for hardware encode: an NVIDIA GPU + driver (NVENC), an AMD GPU (native AMF), or an
Intel GPU (native QSV via the statically linked VPL dispatcher; the runtime ships in the Intel
driver) — the CI exe is built
--features nvenc,amf-qsv,qsv. Software H.264 is the GPU-less fallback. - Virtual gamepads need no prerequisite. The DualSense / DualShock 4 / Xbox 360 (XUSB) UMDF drivers
are bundled in the installer (the Install the virtual gamepad drivers task) and
pnputil-installed. ViGEmBus is no longer used. - The streaming microphone uses VB-CABLE, bundled + silently installed by the installer (the Install
VB-CABLE virtual audio task). The host writes the client's mic into VB-CABLE's input; its
CABLE Outputcapture endpoint surfaces as a host mic. A Windows audio device can only be created by a kernel-mode driver (no UMDF path exists), so unlike our self-signed UMDF drivers we cannot ship our own — VB-CABLE is a vendor-signed cable that loads with no test-signing. It is donationware by VB-Audio, redistributed under VB-Audio's bundling grant (only the single base cable) — the grant requires the end user to see VB-CABLE's origin + donationware status, which the wizard task text andlicenses/VB-CABLE-NOTICE.txtsurface. The package binary is not in the repo — CI provisions the pinned, SHA-256-verified official package onto the runner (scripts/ci/provision-windows-punktfunk-extras.ps1→C:\Users\Public\vbcable) andwindows-host.ymlpasses it via$env:VBCABLE_DIR, so published installers always bundle it; locally supply-VbCableDir/$env:VBCABLE_DIR(the extracted official package, containingVBCABLE_Setup_x64.exe). Unset → the installer is built without it and the host falls back to auto-installing the Steam Streaming pair; set-but-invalid → the pack fails (a broken provisioning must not silently ship a mic-less installer again). (Endgame: attestation-sign our own MIT virtual-audio driver to drop this dependency.)
Files here
| File | Role |
|---|---|
punktfunk-host.iss |
Inno Setup script (the installer definition). |
branding/ |
Wizard branding: gen-branding.ps1 renders the brand mark into the committed wizard-image-*.bmp / wizard-small-*.bmp (100–200% DPI) + punktfunk.ico. Re-run only on a brand change. |
pack-host-installer.ps1 |
Orchestrator: cert + sign exe, build + sign the drivers from source, stage them + FFmpeg + VB-CABLE + the web console (.output + bun) + the HDR layer + branding, run ISCC, sign setup.exe. |
build-pf-vdisplay.ps1 |
Build pf-vdisplay from source (the drivers/ workspace) + clear FORCE_INTEGRITY + sign .dll/.cat + export .cer. |
build-gamepad-drivers.ps1 |
Sign + catalog the gamepad drivers (pf-dualsense + pf-xusb) from the same workspace build (-SkipBuild), one shared cert. |
install-vbcable.ps1 |
On-target: seed VB-Audio's cert into TrustedPublisher, silently install the bundled VB-CABLE (-i -h). Run by the installer's Install VB-CABLE virtual audio task; idempotent + always exits 0 (non-fatal). |
clear-force-integrity.ps1 |
Clear the /INTEGRITYCHECK PE bit so a self-signed driver loads (reused by every driver build). |
stage-pf-vdisplay.ps1 |
Stage the just-built pf-vdisplay bundle + fetch/verify the pinned nefcon release. |
../../scripts/windows/web-run.cmd |
The PunktfunkWeb task action: loads the mgmt token + login password env, runs the bundled bun on the Nitro server (:47992). |
drivers/ |
The all-Rust IddCx driver source workspace: the pf-vdisplay crate on wdk-sys / windows-drivers-rs + the owned pf-driver-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. |
pf-vkhdr-layer/ |
HDR Vulkan layer (standalone cdylib): lets Vulkan games (Doom: The Dark Ages, etc.) enable HDR over the virtual display by advertising the HDR surface formats the NVIDIA/AMD ICDs hide on an indirect display. Built by the packer, laid into {app}\vklayer, registered under HKLM64\…\Khronos\Vulkan\ImplicitLayers (opt-out Install the HDR Vulkan layer task). Self-gated on the display's HDR state. See its README. |
Drivers are built from source, not vendored. All three (pf-vdisplay + the gamepad pf-dualsense / pf-xusb) are members of the all-Rust
drivers/workspace (windows-drivers-rs / IddCx) and are rebuilt + signed every release bybuild-pf-vdisplay.ps1+build-gamepad-drivers.ps1- the checked-in prebuilt binaries were deleted (a stale.catonce stopped covering its.inf→SPAPI_E_FILE_HASH_NOT_IN_CATALOGon every box, and a frozen binary predated a driver IOCTL the host needed). Building from source keeps.dll/.inf/.catin lockstep. nefcon (the device-node tool - the install creates theroot\pf_vdisplaynode with it, neverdevgen, which leaves persistent phantom devices) is fetched + SHA-256-verified from its pinned release instage-pf-vdisplay.ps1. See punktfunk-planning:windows-build-and-packaging.md(internal planning repo) for the toolchain
- signing details.
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. (The C:\t-goal1\... probe
path below is the maintainer's test box — substitute your own punktfunk-probe.exe build.)
# 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)
# 1. build the host (NVENC needs no import lib — its entry points are runtime-loaded; `qsv`
# statically links the vendored VPL dispatcher — needs cmake + a libclang, no FFmpeg)
cargo build --release -p punktfunk-host --features nvenc,qsv
# 2. pack (self-signed unless MSIX_CERT_PFX_B64/MSIX_CERT_PASSWORD are set; -NoDriver to skip pf-vdisplay)
pwsh -File packaging\windows\pack-host-installer.ps1 -Version 0.0.0-dev -TargetDir C:\t\release -OutDir C:\t\out
Release
Push a vX.Y.Z tag — one tag releases every platform (see
Release Channels). The workflow builds, signs, and
publishes punktfunk-host-setup-X.Y.Z.exe + the public .cer, refreshes the stable latest/
alias, and attaches the installer to the unified Gitea Release. Main pushes publish rolling
<next-minor>.<run> canary builds (base derived from the latest stable tag by
scripts/ci/pf-version.ps1) to the canary/ alias.