Five field requests from one returning user, traced and fixed together
(report: the "Daily-Driver Five" triage).
**10-bit SDR, independent of HDR.** VIDEO_CAP_10BIT and VIDEO_CAP_HDR were
always separate wire bits, but everything above the wire welded them: the
client advertised both from one "HDR" switch, the handshake required an HDR
capture source for any 10-bit depth, SessionPlan derived hdr from bit_depth,
and the encoder stamped BT.2020 PQ on every 10-bit format. Now a client
setting ("10-bit SDR", default off, tier-P) advertises the depth bit alone;
the handshake resolves bit_depth=10 without the HDR gates where the SDR-10
chain exists (Windows IDD-push + direct NVENC + HEVC — everywhere else the
session stays 8-bit, said honestly in the Welcome); the Welcome's colour
label, the virtual display's HDR bring-up and the capturer's want-HDR flag
all follow the new session-hdr verdict instead of the depth. The capture
side grows PixelFormat::Rgb10a2Sdr — the BGRA slot expanded 8→10 by a
trivial full-res pass (HdrRgb10Converter::new_sdr_expand) — which NVENC
ingests as ABGR10 and encodes Main10 under the ordinary BT.709 SDR VUI,
both 4:2:0 and 4:4:4. The win is the ENCODER's precision (compression
stops adding banding to gradients); the desktop stays 8-bit and neither
display's colour state is touched. The client already decodes by the VUI,
so no decode-side change was needed.
**"Capture system shortcuts" now works in Desktop mouse mode.** The setting
was wired end to end and then discarded by one `!desktop` term in the
presenter's grab condition — documented as "a remote desktop is something
you Alt-Tab away from", which made the toggle inert for exactly the
remote-desktop use it matters most for. The keyboard grab now follows the
setting in BOTH mouse models; desktop mode keeps an escape capture mode
never had (the unlocked pointer clicks any other window, focus drops, the
chords come back). Docs updated; the Mac's ⌘-chord behaviour is unchanged.
**Keep host audio playing (Moonlight's "Mute host PC speakers", unchecked,
per session).** New client setting + CLIENT_CAP_KEEP_HOST_AUDIO (0x20,
request-only, no HOST_CAP echo — an older host just re-routes as always).
A session holding the bit gets the host's audio devices left alone for its
lifetime (RAII guard): Windows skips the IPolicyConfig default parking and
loopbacks the operator's own default device; Linux taps the default sink's
monitor instead of claiming the default. Host-global best-effort across
concurrent sessions, as the cap documents. Rows in the GTK, Windows and
console-UI settings (desktop-only until Android advertises the bit).
**Prep commands carry the negotiated mode.** A per-app do/undo step's whole
environment was PF_APP_ID; the width/height/refresh/HDR it needs for a
per-mode frame cap were resolved 80 lines above and written only to the
unix-only marker file. Both serving planes now pass PF_STREAM_WIDTH /
_HEIGHT / _REFRESH / _HDR (the marker file's vocabulary, via one shared
hooks::prep_mode_env) — so an RTSS cap is one step instead of one
hard-coded entry per device, on Windows hosts too.
**The forwarded cursor matches the client's DPI.** The desktop-mode pointer
was resampled by the video fit factor alone, and SDL shows a custom cursor
surface at ~1:1 physical pixels on every backend — so on a 200% client our
pointer was half the size of every native one (and X11 the same; Wayland
escapes via buffer scale). The resample factor now folds in
SDL_GetWindowDisplayScale, the same model the Apple client gets for free
from point-sized NSCursors.
Verified: workspace clippy --locked -D warnings + targeted tests (hooks,
profiles, caps, console-ui rows, capture-policy) green on Linux
(pf-gtkflow); pf-capture/pf-frame windows-msvc green via xcheck; rustfmt
clean. NOT verified: the Windows-only halves of pf-encode/punktfunk-host
(no reachable Windows toolchain — .133 down; windows-host.yml is
dispatch-only, see the preceding commit), and everything on-glass: the
SDR-10 stream end to end, the desktop-mode grab, keep-host-audio wiring,
prep env on a live launch, and the cursor size at 200%.
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, and it's ours — the session presenter decodes with Punktfunk's own
decoders; no FFmpeg is linked or bundled. Vulkan Video (
pf-vkdecode, decoding onto the presenter's own device) leads on NVIDIA and AMD, VAAPI (pf-vaadecdriving a dlopen'd libva, exporting DRM-PRIME dmabufs) leads on Intel, whichever isn't first is the fallback, and an OpenH264/rav1d CPU rung is last. - 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 — "Browse library…" on a paired 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, PipeWire, and SDL3 (with hidapi) development packages, plus a C compiler (the CPU rung builds OpenH264 from source). No decoder development package is needed: libva and the Vulkan loader are both opened at runtime rather than linked, so hardware decode is a fact about the box you run on — a Vulkan loader and your GPU's driver, and libva for the VAAPI rung — not about the one you build on.
# 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, the native decode ladder (Vulkan Video · VAAPI ·
OpenH264/rav1d), 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