Files
punktfunk/clients/windows
enricobuehlerandClaude Opus 5 e35981d70f
apple / swift (pull_request) Successful in 1m15s
apple / screenshots (pull_request) Skipped
windows / build (aarch64-pc-windows-msvc) (pull_request) Failing after 2m50s
android / android (pull_request) Successful in 2m49s
ci / web (pull_request) Successful in 2m35s
ci / docs-site (pull_request) Successful in 3m17s
ci / rust-arm64 (pull_request) Successful in 3m51s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 3m54s
ci / rust (pull_request) Successful in 9m23s
feat(client/present): V-Sync and VRR become real settings, and VRR is measured
WP3 of design/desktop-presentation-rebuild.md. The `vsync` and `allow_vrr`
settings have existed since WP1 but nothing consumed them — the swapchain picked
MAILBOX-or-FIFO once, from an env var, and froze. This makes them mean
something, which is also what unblocks their settings rows (deliberately
withheld from WP5 rather than shipped as dead switches).

Present-mode selection is now a preference ladder, not a constant:

* V-Sync off — IMMEDIATE, then FIFO_RELAXED, then the tear-free modes. Asking
  to tear and silently getting vsync is a lie, so the mode that actually took is
  named in the stats line and a refused preference is logged requested-vs-active.
* V-Sync on + VRR allowed + fullscreen — FIFO first. On a variable-refresh panel
  with direct scanout the FIFO present IS the flip, so the panel follows the
  stream's cadence instead of a fixed grid; MAILBOX would decouple presents from
  scanout and re-quantize to the compositor's clock. This is only safe because
  WP2's glass gate bounds the standing queue that historically made FIFO costly.
* Otherwise — MAILBOX then FIFO, the shipped default, unchanged.

`PUNKTFUNK_PRESENT_MODE` still pins a mode outright and now falls back to the
settings (rather than to mailbox) when the name is unknown.

VRR detection is MEASURED, never queried. No portable query exists — SDL exposes
none, Wayland does not report adaptive-sync state, Windows surfaces nothing
through Vulkan — and the platforms that do answer have been caught lying (see
the Android per-uid refresh-rate finding). The discriminator is quantization: on
a fixed-refresh panel every on-glass instant lands on the vblank grid, so the
spacing between presents is ~k×period for whole k even when the stream runs
slower than the panel (it just picks a larger k); under real VRR the panel
refreshes when we present, so the spacing follows our own cadence and sits off
the grid. `CadenceProbe` folds each delta to its distance from the nearest
multiple of the learned period and takes the median. Tri-state: it stays Unknown
below 24 deltas and after a display change, so `vrr` is reported only when it
has been measured — never inferred from what the display claims.

Also fixes the read-once refresh rate: `native.refresh_hz` was sampled at
startup and never revisited, so dragging the window to another monitor left a
60 Hz-seeded clock pacing a 144 Hz panel. `WindowEvent::DisplayChanged` now
relearns the latch grid, resets the cadence verdict, and clears the served-slot
latch.

Settings rows for both, on all three surfaces (GTK, WinUI, console). The
console's V-Sync row is reachable in Gaming Mode, which is the only editor a
Deck user has.

Gates: punktfunk-rust-ci linux/amd64 — fmt, clippy -D warnings over
pf-client-core, pf-presenter, pf-console-ui, the session binary and the GTK
client, 160 tests (the two new ones cover every ladder and both cadence
regimes, including the case that matters most: a stream slower than a FIXED
panel must still read as fixed). WinUI leg on the Windows runner .133:
clippy=0 tests=0, against a tree proven by content to contain the edit.

⚠ On-glass validation is still owed and is NOT claimed here: every box with a
real display was powered off when this landed, so the VRR ladder and the
detector have been exercised only against synthetic stamps in unit tests.

Rebase follow-up: `20de58a7` landed the same "panel grid can be wrong in both
directions" defect fix on Android and extracted the corrected learner into
`punktfunk_core::phase::PanelGrid` for the iOS and desktop presenters to share.
This clock had the identical bug — it capped the learned period at the display
mode's refresh, and the mode is only a CLAIM, so a display really running slower
than it advertises pinned a grid whose instants never arrive, for the session,
with no way back. Adopted the shared learner rather than carrying a second,
buggier copy; still fed the window's MIN spacing, which preserves the k×period
resistance the cap was actually aimed at while the streak requirement lets a
genuinely slower panel be discovered. New test: seed 120 Hz, real panel 60 Hz,
the clock must climb back out.

Took the same commit's third lesson too: the adaptive margin widened on a
latch over 1.5×period (a number picked here), and now widens on the latch
exceeding one period plus the lead already applied — the slot actually aimed at.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 22:02: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 a hardware-accelerated D3D11 video path 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 — FFmpeg HEVC with a D3D11VA zero-copy path (decoder and presenter share one D3D11 device; NV12/P010 textures sampled straight into a SwapChainPanel composition swapchain), with a robust software-decode fallback.
  • 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, an FFMPEG_DIR FFmpeg tree, and CMake (SDL3 builds from source). The Windows App SDK runtime bootstrap is staged next to the exe by windows-reactor-setup from this crate's own build.rs — no extra environment needed.

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.