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>
punktfunk — Linux client
The native Linux app for streaming a punktfunk host to your desktop, laptop, or Steam Deck.
It's a clean relm4/GTK4/libadwaita shell that finds hosts on your network, pairs with a PIN,
and manages your settings and library — the stream itself runs in the sibling
punktfunk-session Vulkan binary (clients/session), which the shell
spawns, putting the picture on glass at your display's own resolution and refresh rate.
Built in Rust end to end (no C ABI): the shell shares its plumbing with the session binary through
crates/pf-client-core, which links the punktfunk-core protocol crate and speaks the fast
punktfunk/1 protocol — QUIC control plane, GF(2¹⁶) FEC + AES-GCM data plane.
Features
- Zero-copy hardware decode — the session presenter decodes via Vulkan Video on every GPU vendor (including NVIDIA), falling back to FFmpeg VAAPI → DRM-PRIME dmabuf and then software when Vulkan Video is unavailable.
- Your display's native mode — the host builds a virtual output at exactly your WxH@Hz; no scaling, no letterboxing. Steady 60 fps at 1080p60, ~6 ms capture→decoded on the LAN.
- Audio both ways — PipeWire playback with a jitter ring, plus mic uplink to the host.
- Full controller support — SDL3 gamepads with rumble and DualSense fidelity (lightbar, player LEDs, touchpad, motion, adaptive-trigger replay). Click-to-capture keyboard and mouse, with a release chord (Ctrl+Alt+Shift+Q) and focus-loss release.
- Find hosts automatically — mDNS discovery lists hosts on your LAN; saved hosts persist. First connect does a one-time SPAKE2 PIN pairing (or TOFU on trusted LANs), then reconnects on a pinned identity.
- Per-host speed test to pick a bitrate, plus compositor and mode preferences in Settings.
- Game library browser (experimental, off by default) — "Browse library…" on a saved host shows its games (Steam + custom) as a poster grid; click one to launch it in the session. Fetched from the host's management API over mTLS — paired devices are authorized by their certificate, no extra host setup.
- Gamepad library launcher (
--browse host) — a console-style, controller-driven library view of a paired host's games, rendered by the session binary's Skia console UI: A plays the focused title, B quits, L1/R1 jump. Built for the Steam Deck plugin's "Open library" launch; session end returns to the launcher. Arrow keys/Enter/Esc drive it too (no pad needed).
Get it
Most people should install a package rather than build from source:
| Distro | Install |
|---|---|
| Flatpak (any distro, Steam Deck) | io.unom.Punktfunk — see packaging/flatpak |
| Ubuntu / Debian (apt) | sudo apt install punktfunk-client (after adding the repo) |
| Fedora / Bazzite (rpm) | rpm-ostree install punktfunk-client |
| Arch (PKGBUILD) | see packaging/arch |
Per-device install steps and pairing walkthrough: docs.punktfunk.unom.io/docs/install-client.
Build & run from source
Requires GTK ≥ 4.16, libadwaita ≥ 1.5, FFmpeg 7 or 8 (with VAAPI for hardware decode), PipeWire, and SDL3 (with hidapi) development packages.
# from the repo root
cargo run -p punktfunk-client-linux # launch the app
cargo run -p punktfunk-client-linux -- --connect HOST[:PORT] # skip the host list and connect
cargo run -p punktfunk-client-linux -- --browse HOST # the gamepad library launcher
The binary is named punktfunk-client — the relm4/libadwaita desktop shell (hosts,
pairing/trust, settings, the desktop library page). Every stream and the console game
library run in the sibling punktfunk-session Vulkan binary; the shell spawns it
for connects, and --connect/--browse on the shell exec it directly (so the Decky
wrapper keeps working unchanged). Headless flags stay in the shell:
--pair <PIN> --connect host[:port] (pairing ceremony), --wake host[:port], and
--library host[:mgmt_port] (print a host's game library).
Layout
src/
main.rs · app.rs entry point, relm4 AppModel (window, trust gate, session child
lifecycle, typed messages), primary menu, CSS
cli.rs headless paths (--pair/--wake/--library), the --connect/--browse
exec handoff to punktfunk-session, screenshot scenes
ui_hosts.rs hosts page component (FactoryVecDeque cards, saved + discovered
grids, add-host dialog, banner)
ui_library.rs game-library poster grid (per-host, launches titles)
ui_trust.rs TOFU / PIN-pairing / request-access dialogs
ui_settings.rs resolution · refresh · decoder · bitrate · compositor · mic
spawn.rs the session-child plumbing (stdout contract → AppMsg)
tools/screenshots.sh store screenshot capture (app self-capture; Xvfb fallback)
The UI-agnostic plumbing — session pump, FFmpeg decode, PipeWire audio, SDL3 gamepads +
keymap, trust store, mDNS discovery, library client, Wake-on-LAN — lives in
crates/pf-client-core, shared with the Vulkan session binary.
Related
- Documentation — quick start, pairing, troubleshooting
- Steam Deck plugin — launches this client fullscreen in Gaming Mode
- Project README — the host, the other clients, and how it all fits together