Files
punktfunk/clients/windows/packaging
enricobuehler d6647b9183 feat(clients/windows): port the Vulkan session client to Windows — session-always
The punktfunk-session Vulkan client (clients/linux-session, now clients/session)
builds and runs on Windows; the WinUI shell spawns it for every stream. Verified
live: 10-bit HEVC via Vulkan Video on both AMD (iGPU) and NVIDIA, 5120x1440 at
130 fps / 8 ms end-to-end on the RTX 4090.

- pf-ffvk: Windows bindgen branch (FFMPEG_DIR + PF_FFVK_VULKAN_INCLUDE, no
  pkg-config); provisioning fetches Vulkan-Headers (pinned v1.4.309).
- pf-client-core: builds on Windows — WASAPI audio (audio_wasapi.rs, cfg-swapped
  via #[path], same surface as the PipeWire twin), VAAPI/dmabuf gated inline
  (chain = vulkan -> software), trust reads the WinUI shell's %APPDATA% stores
  (parity tests pin both serialized shapes), Settings gains adapter/hdr_enabled
  (serde-defaulted; Linux stores unaffected).
- pf-presenter: builds on Windows — dmabuf module Linux-gated; SDL keyboard grab
  while captured (Alt+Tab/Win reach the host); pick_device ranks discrete over
  integrated (device 0 was the iGPU on hybrid boxes — the silent footgun) and
  honors PUNKTFUNK_VK_ADAPTER (the Settings GPU pick, exported by the session).
- run loop: block in one SDL wait woken by input AND decoded frames (a per-
  session forwarder pushes a FrameWake user event) instead of a 1 ms poll —
  measured 111%% -> 5%% of a core (NVIDIA), 86%% -> 3.5%% (AMD), stats unchanged.
  The pump's decode-fence wait became once-per-window sampling (no per-frame
  pipeline stall; the stat now shows true backlog).
- pf-console-ui: builds on Windows (skia-safe msvc prebuilts); font lookup falls
  through fontconfig aliases to concrete DirectWrite families (Consolas/Segoe UI)
  — browse/coverflow works, verified against a live host.
- WinUI shell: session-always via new src/spawn.rs (GTK spawn.rs port —
  CREATE_NO_WINDOW, stdout contract, kill handle); the Stream screen is a status
  card (chips + stage lines from the child's stats). The legacy in-process
  D3D11VA path stays behind Settings "Streaming engine" / PUNKTFUNK_BUILTIN_
  STREAM=1 as the A/B baseline until Phase 8 deletes it. SessionParams.video_caps
  makes the HDR toggle real.
- clients/linux-session renamed to clients/session (builds for both OSes).
- CI/MSIX: both workflows build/test both bins with widened path filters; the
  MSIX ships punktfunk-session.exe. ARM64 session builds --no-default-features
  (rust-skia has no aarch64-pc-windows-msvc prebuilts; flip when it does).

A/B on this box (5120x1440 HEVC vs home-worker-5): NVIDIA Vulkan 130 fps / 8 ms
e2e / 1.6 ms decode — clearly better than the built-in path. The AMD iGPU VCN
saturates at ~52 fps where its own D3D11VA does ~70 — Adrenalin Vulkan decode is
slower on APU silicon; discrete RDNA validation gates Phase 8.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 23:21:36 +02:00
..

punktfunk Windows client — MSIX packaging

The Windows client ships as signed MSIX packages so Windows boxes get a real package (Start tile, clean install/uninstall) instead of a loose exe. CI builds + publishes them from .gitea/workflows/windows-msix.yml to Gitea's generic package registry (https://git.unom.io/unom/-/packages), on every main push that touches the client (canary) and on vX.Y.Z release tags (stable) — see Release Channels.

Two architectures, one x64 runner. Both x64 and arm64 packages are produced off the single x64 Windows runner — x86_64-pc-windows-msvc builds natively, aarch64-pc-windows-msvc is cross-compiled (the x64 MSVC toolset ships the ARM64 cross compiler; the matrix points FFMPEG_DIR at the runner's ARM64 FFmpeg tree, C:\Users\Public\ffmpeg-arm64). Artifacts are arch-suffixed (..._x64.msix / ..._arm64.msix, each with its matching .cer); pack-msix.ps1 -Arch x64|arm64 stamps the manifest ProcessorArchitecture and names the output. See windows.yml for the cross-build rationale.

What's in the package

pack-msix.ps1 assembles a layout from a cargo build --release and runs makeappx + signtool:

File Source
punktfunk-client.exe the release build (the WinUI shell)
punktfunk-session.exe the release build — the Vulkan session client the shell spawns for every stream (sibling resolution, src/spawn.rs). Skia links statically; vulkan-1.dll is a GPU-driver component, never bundled. ARM64 builds it --no-default-features (no Skia console UI) until rust-skia ships aarch64-pc-windows-msvc prebuilts
Microsoft.WindowsAppRuntime.Bootstrap.dll, resources.pri staged by the client's build.rs via windows-reactor-setup::as_framework_dependent()
SDL3.dll auto-staged by the sdl3 crate
avcodec/avformat/avutil/swscale/swresample/...-*.dll FFMPEG_DIR\bin
Assets\*.png checked-in tile/store logos (rasterized from packaging/flatpak/io.unom.Punktfunk.svg)
AppxManifest.xml the template here, with {VERSION}/{PUBLISHER} substituted

Why an "unpackaged" WinUI app packages cleanly

main calls windows_reactor::bootstrap(), which runs MddBootstrapInitialize2 with OnPackageIdentity_NOOP (crates/libs/reactor/src/bootstrap.rs), so under MSIX package identity the App SDK bootstrapper is a no-op and the runtime is resolved from the manifest's <PackageDependency> on Microsoft.WindowsAppRuntime.2 instead (reactor pins WINDOWSAPPSDK_RELEASE_MAJORMINOR = 0x20000 = 2.0). It's a full-trust Win32 app (EntryPoint="Windows.FullTrustApplication" + runFullTrust) because it owns raw D3D11, Win32 low-level input hooks, WASAPI and SDL3.

Versioning

MSIX requires a strictly 4-part numeric version. The workflow computes:

  • vX.Y.Z tag → X.Y.Z.0 (THE release; any -rc/+meta suffix is dropped for MSIX). Published to the stable latest/ alias and attached to the unified Gitea Release.
  • main push / workflow_dispatch0.3.<run_number>.0 (canary, climbs by run number; canary/ alias).

Signing & install

CI signs every build with a stable self-signed code-signing cert (CN=unom, SHA-1 CD1EFDEEEC9743AFC38F56C5AF30C5A3009BE941, valid to 2036). Its public half is checked in as punktfunk-codesign.cer; the private .pfx + password live in the MSIX_CERT_PFX_B64 / MSIX_CERT_PASSWORD Actions secrets. Because it's the same cert every build, trusting it is one-time, per machine — once imported, every future build and in-place upgrade is trusted with no further prompt:

# once per machine (elevated): trust the publisher
Import-Certificate -FilePath .\punktfunk-codesign.cer -CertStoreLocation Cert:\LocalMachine\TrustedPeople
# then install the package for your CPU (and re-run for each upgrade — no re-trust needed)
Add-AppxPackage -Path .\punktfunk-client-windows_<ver>_x64.msix     # Intel/AMD
Add-AppxPackage -Path .\punktfunk-client-windows_<ver>_arm64.msix   # ARM64 (Snapdragon, etc.)

The matching .cer is also published next to each .msix in the registry, so it's always at hand.

The MSIX declares a dependency on the Windows App SDK 2.x runtime; install the App SDK runtime if Add-AppxPackage reports a missing Microsoft.WindowsAppRuntime.2 framework.

pack-msix.ps1 signing precedence: it uses the MSIX_CERT_PFX_B64 / MSIX_CERT_PASSWORD secrets when present (the stable cert above), else generates an ephemeral self-signed cert (forks / local builds without the secrets). Either way it exports the signing cert's public .cer for the import. To move to a publicly-trusted (no-import) cert — Azure Artifact Signing or a public OV cert — replace the two secrets with the new .pfx; the cert's subject DN must equal the manifest Publisher, so pass a matching -Publisher (it's stamped into the package Identity, and changing it changes the package identity → a one-time reinstall).

Building locally

On the Windows runner / dev VM (MSVC + Windows SDK present), after a release build:

# x64
cargo build --release -p punktfunk-client-windows --target x86_64-pc-windows-msvc
pwsh -File clients/windows/packaging/pack-msix.ps1 `
  -Version 0.2.0.0 -TargetDir C:\t\x86_64-pc-windows-msvc\release -OutDir C:\t\msix

# arm64 (cross-compiled; point FFMPEG_DIR at the ARM64 tree)
$env:FFMPEG_DIR = 'C:\Users\Public\ffmpeg-arm64'
cargo build --release -p punktfunk-client-windows --target aarch64-pc-windows-msvc
pwsh -File clients/windows/packaging/pack-msix.ps1 `
  -Version 0.2.0.0 -Arch arm64 -TargetDir C:\t\aarch64-pc-windows-msvc\release -OutDir C:\t\msix

Validated end-to-end on the build VM (pack → sign → Add-AppxPackage → framework-dependency resolution). The only step that needs a real display is launching the WinUI window (same on-glass constraint as the rest of the client).