Files
punktfunk/clients/windows
enricobuehler cc8eb7df08
ci / bun-nix (pull_request) Successful in 26s
ci / web (pull_request) Successful in 1m4s
ci / docs-site (pull_request) Successful in 1m13s
apple / swift (pull_request) Successful in 1m44s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 3m10s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 4m39s
android / android (pull_request) Successful in 7m12s
ci / rust (pull_request) Successful in 7m58s
windows / build (aarch64-pc-windows-msvc) (pull_request) Failing after 11m18s
feat(clients): every pinned card gets a library, and it launches with that card's profile
The console fix before this one closed the leak on one client. The same question
has a different wrong answer on each of the others, so this closes it everywhere:
a pinned host+profile card can be browsed, and every title launched off a pinned
card's shelf streams with that card's profile.

Two shapes of bug, one per client:

**The library was not offered on a pinned card at all** — Apple (both UIs:
`hasLibrary: profile == nil` on the console tile, no menu item on the touch card),
Android (`hasLibrary` required `pinnedProfileId == null`, and the console host
options gated Library on `pin == null`), GTK (the pinned card's menu had Connect,
Copy link, Create shortcut, Unpin — no library) and Windows (pinned tiles had no
menu whatsoever). Each was justified in a comment as "a pin is a shortcut, not a
second host, so the host-level actions stay on the host's tile" — right about
wake, pair, edit and forget, wrong about the library. Browsing is not a property
of the machine: it is this card's connect with a title picked first, which is
exactly what a shortcut is for. So the library joins Connect on every pinned
surface, and the host-level actions stay where they were.

**The launch dropped the profile.** GTK already carried it (its library clones
the card's whole ConnectRequest) and Windows already carried it (its library page
launches through the shared target, which the tile parks). Apple did not: the
library was keyed on a bare `StoredHost`, so `launchTitle` connected with
`.inherit` and the host's binding won even from a pinned card. Android was worse
still, and not only for pins — `LibraryScreen` dialled `connectToHost` with the
RAW settings, so a library launch applied NO profile at all, not even the host's
binding, on every host. Its host list resolves
`settings.effectiveFor(profileStore.resolveFor(kh, oneOff))`; its library resolved
nothing.

So the shelf, not the host, is now the unit both clients navigate by. Apple gets
a `LibraryTarget` (host + `ProfileSelection`) threaded through `libraryTarget`,
the shell's screen enum, both presentations and `SessionModel`; Android passes
the pinned id into `LibraryScreen` and resolves it there through the same
`ProfileStore.resolveFor` rule the connect path uses. Falling out of that:

  * a game that exits returns to the shelf it was launched from, pin and all,
    rather than to the host's default one — `SessionModel.launchedShelf` on Apple,
    `ActiveSession.libraryProfileId` → `LibraryReturn` on Android. Android also
    drops a pin that was unpinned while the game ran, rather than reopening a card
    that no longer exists.
  * Android's stats overlay finally names the profile a library launch used
    (`profileName` was never set on that path).
  * Apple's `punktfunk://browse/<host>` honours `profile=`, which it parsed and
    ignored — and refuses an unknown or ambiguous reference exactly as the connect
    route does (§10.6) rather than degrading to the binding.
  * every shelf says which one it is, in the card's own `host · profile` shape:
    the console's title, GTK's page title, Android's ConsoleHeader, Apple's
    navigation title and its console heading.

Verified per platform, none of it on trust:
  * console + GTK: fmt, build, clippy `-D warnings` and 85 tests green in the
    pf-lxcheck2 container (a Mac `cargo test -p pf-console-ui` compiles nothing).
  * Apple: `swift build` green for macOS, iOS and tvOS. Worth all three — the tvOS
    pass alone caught `navigationDestination(item:)` needing Hashable, and an
    iOS-only screen was confirmed genuinely compiled by planting a type error.
  * Android: `compileDebugKotlin` + unit tests, with HomeTilesTest's pin
    expectation flipped to match.
  * Windows: `cargo check --all-targets` + clippy `-D warnings` on the CI runner,
    cold (3m10s) — that client cannot compile on a Mac.
2026-08-11 22:13:54 +02:00
..

punktfunk — Windows client

The native Windows app for streaming a punktfunk host to your PC. A modern WinUI 3 app that discovers hosts on your network, pairs with a PIN, and streams at your display's own resolution and refresh rate — with hardware-accelerated video decode and HDR.

It's pure Rust: the UI is WinUI 3 driven through windows-reactor (a declarative, React-like framework), and it links the shared punktfunk-core directly to speak the fast punktfunk/1 protocol.

Features

  • Hardware decode, GPU present — Punktfunk's own decoders, no FFmpeg anywhere in the client: Vulkan Video (pf-vkdecode) leads on NVIDIA and AMD, D3D11VA (pf-dxvadec driving ID3D11VideoDecoder) leads on Intel, whichever isn't first is the fallback, and an OpenH264/rav1d CPU rung is last. Either hardware rung hands its surface to the Vulkan presenter without a CPU copy.
  • HDR10 — advertise 10-bit/HDR, detect PQ in-band, and flip the swapchain to R10G10B10A2 + ST.2084 with HDR10 metadata.
  • Your display's native mode — the host builds a virtual display at exactly your WxH@Hz.
  • Audio both ways — WASAPI render + mic capture.
  • Full controller support — SDL3 gamepads with rumble, lightbar, and DualSense feedback.
  • Your display's native mode, really — "Native display" resolves the actual size + refresh of the monitor the window is on at connect time.
  • Find hosts automatically — mDNS discovery lists hosts on your LAN, alongside saved and manual entries. First connect does a one-time SPAKE2 PIN pairing (or TOFU on trusted LANs), then reconnects on a pinned identity. Saved hosts carry per-host actions: a network speed test (probe burst over the real data plane → recommended bitrate, applied in one tap) and forget.
  • Polished shell — host cards, settings (resolution / refresh / host compositor / decoder / codec / bitrate / HDR / forwarded controller / gamepad type / system shortcuts / audio channels / mic / stats-overlay level), the tiered stats overlay (Off / Compact / Normal / Detailed — Ctrl+Alt+Shift+S cycles it live in the session window), and the full trust surface. Stream input uses Win32 low-level hooks with Moonlight-style capture: Ctrl+Alt+Shift+Q releases the pointer, a click on the stream re-captures it, and system shortcuts (Alt+Tab, Win, …) can act locally or forward to the host.

Builds and ships for both x64 and ARM64 as a signed MSIX.

Get it

Install the signed MSIX from the package registry — see docs.punktfunk.unom.io/docs/install-client. A stock Moonlight client also works over GameStream if you prefer.

Build from source

Windows-only (the crate builds as a stub on other platforms so the workspace stays green). You need the MSVC toolchain and CMake (SDL3 builds from source) — nothing else: decode is native since M10, so there is no FFMPEG_DIR to point anywhere, and the Windows App SDK runtime bootstrap is staged next to the exe by windows-reactor-setup from this crate's own build.rs.

cargo build -p punktfunk-client-windows --target x86_64-pc-windows-msvc

# CLI paths for testing (no window):
punktfunk-client --discover                                   # list hosts on the LAN
punktfunk-client --headless --connect host[:port] [--pin HEX] # connect, count frames, print stats
punktfunk-client --headless --speed-test --connect host[:port]  # probe burst → recommended bitrate

CARGO_HOME must be an ASCII path — non-ASCII characters break SDL3's MSVC precompiled-header build. Packaging (MSIX manifest, signing) lives in packaging/.

Layout

Decode/present/input live in the spawned punktfunk-session binary (clients/session), not here — this crate is the shell that discovers, pairs, and launches it.

src/
  main.rs                 entry point + CLI paths (--discover · --headless · --speed-test)
  bin/punktfunk-console.rs  the couch/HTPC Start-menu entry (re-execs with --console)
  app/                    WinUI 3 shell (windows-reactor), one module per screen:
                          mod (root/router) · hosts · connect · pair · speed · settings ·
                          library · help · licenses · stream · style (shared cards/pills)
  deeplink.rs             punktfunk:// activation, single-instance hand-off, shortcut writer
  spawn.rs                punktfunk-session child process + its stdout event contract
  shell_window.rs         hide/restore the shell HWND around a session
  gpu.rs                  DXGI adapter enumeration for the GPU picker
  trust.rs · discovery.rs persistent identity, TOFU/PIN pairing, mDNS browse
  probe.rs · wol.rs       speed probe · Wake-on-LAN
  logfile.rs              log tee to %LOCALAPPDATA%
packaging/                MSIX manifest, signing, pack script

Manual smoke checklist

The windows-reactor pin is a moving target and WinUI regressions rarely show up in cargo check — walk this after a reactor bump or a change to the render/state architecture:

  • Hosts — discovery populates tiles; tile hover fill; "…" menu → Forget and Rename; add-host modal connects; WOL wait screen cancels.
  • Settings — every section renders; combos still show their selection after a section switch AND a scope switch (the historic blank-combo reconciler bug); profile create / rename / delete (with confirm); colour swatches repaint; the Overridden marker appears on edit and clears on Reset; GPU combo lists adapters.
  • Pair — PIN entry pairs (the typed PIN must reach the Connect click — use_ref mirror path).
  • Session — connect → session spawns → HUD stats tick; Ctrl+Alt+Shift+Q releases the pointer; shell window restores on exit.
  • Shell — speed test completes; library grid loads; punktfunk:// deep link routes (second instance hands off and exits); window icon appears; screen-entrance animations play.