The safety half of the rust-safety programme's §8.4: `std::env::set_var`/`remove_var` are
`unsafe fn` in edition 2024, converting the class of bug the programme found the hard way
(the 972af299 environ data race lived in a file with ZERO occurrences of the word
`unsafe`) from invisible to counted and compiler-enforced.
Manifests: [workspace.package] edition 2021→2024, rust-version 1.82→1.85 (the pinned
toolchain is 1.96.0, so no toolchain bump — only the declared floor rises); the 13 crates
pinning `edition = "2021"` literally now inherit it (Trap 1: the root bump alone reaches
only `edition.workspace = true` crates and would have left pf-encode/pf-capture/pf-inject
et al. on 2021 while reading as complete); pf-driver-proto's stale rust-version 1.82 pin
now inherits; pf-vkhdr-layer (a separate workspace, inherits nothing) bumped to 2024. The
four vendored crates (fec-rs, cros-codecs, usbip-sim, the patched ndk) stay on 2021
deliberately — upstream code stays pristine. The excluded usbip-poc standalone PoC is
untouched.
Mechanical, done textually across ALL cfg branches so no platform's half is left behind
(Trap 3 — 44% of the host's unsafe is Windows-only and a one-platform `cargo fix` misses
it): 148 `#[no_mangle]` → `#[unsafe(no_mangle)]` (83 in abi.rs); 12 bare extern blocks →
`unsafe extern`; `gen` is a reserved keyword, so pf-vdisplay's generation stamps
(registry.rs, windows/manager.rs) and the WinUI shell's animation counters rename
gen → generation (internal identifiers only, no serde/wire surface); two
match-ergonomics patterns take the compiler's suggested reference form.
env mutation: every `set_var`/`remove_var` site (20 files) now sits in an `unsafe` block
whose SAFETY comment states the real serialization argument (pf-vdisplay's ENV_LOCK,
CONFIG_DIR_TEST_LOCK, ART_ROOTS_LOCK, vkdecode's gpu_lock, the `--test-threads=1`
contracts of the hardware spikes, or single-threaded startup). Two genuine hazards
surfaced en route — exactly the WP3b-class finds this migration exists to make visible —
and are fixed here:
- windows/service.rs spawned the network-profile warner thread BEFORE `load_host_env()`,
so a child-spawning thread (child spawn snapshots the env block) was live while
`set_var` ran in a loop; the load now precedes the spawn.
- pf-console-ui's `fake_home()` re-set HOME outside its OnceLock on EVERY call, so two
parallel tests could race the write; the set now happens exactly once inside
`get_or_init`.
cbindgen (Trap 2): 0.29.4 parses `#[unsafe(no_mangle)]` — verified empirically; the
header regenerates byte-identical. The ci.yml drift check could never catch "failed to
regenerate" (build.rs demotes a cbindgen failure to a warning and writes nothing, leaving
the checked-in header untouched and the diff clean), so the step now first asserts the
"punktfunk-core: wrote" line and the absence of "cbindgen failed" (sh -e safe: no `!`
pipeline, no tee-masked exit).
rustfmt: style_edition pinned to 2021 at the root — edition 2024 would otherwise flip the
style edition and reformat ~370 untouched files inside this same commit, burying the
migration diff. The drivers workspace pins its already-current 2024 style. Adopting the
2024 style tree-wide is its own future one-line-plus-reformat commit.
Census: the primary metric moves UP BY DESIGN — 2435 → 2453 operations, unsafe blocks
1534 → 1577, and env_set_var is now a counted category (45 ops). The newly counted env
sites are a truer number, not a regression; baseline snapshot saved as punktfunk-planning
design/rust-safety-census-baseline-2026-08-12-edition-2024.txt. Gate C's env ratchet is
now compiler-enforced (the hygiene-script header says so); the two shrunk file counts
(nvenc_cuda 49→2 via the test helpers, shell/tests 2→1) are lowered in the same commit
per the gate's own rule.
Drop order (the semantic change most likely to bite this codebase): the migration lint
`-W tail-expr-drop-order` reports zero findings on the macOS-visible halves of
pf-encode / pf-zerocopy / pf-capture / pf-frame; the Linux and Windows halves run the
same lint on the gate boxes. The four #[ignore]d alloc/drop-cycle tests on the hardware
boxes remain owed, as before this change.
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 (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, 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