Supersedes the check77797a9eshipped an hour ago. The suppression, the log-once, and the "unknown must not suppress" rule all stand; the field it reads does not.77797a9eread `Welcome.gamepad` — the backend the host resolved for the SESSION — and stopped sending motion when it had no motion plane. But the host does not build pads from that. It builds each virtual device from that pad's own `GamepadArrival` (`Pads::set_kind`) and falls back to the session default only for a pad that never declares one, which is precisely why `declared_kind` exists and why its doc comment says an explicit setting has to be re-declared per pad. So the check had a false negative, and it is an ordinary living-room setup. Under "Automatic" the Hello carries the ACTIVE pad's kind (`auto_pref`), so a couch with an X-Box pad on slot 0 and a DualSense on slot 1 echoes Xbox360 — while the host, reading pad 1's arrival, builds it a DualSense with a working motion plane. The old check read the echo, saw no motion plane, and killed pad 1's gyro. That is the exact failure 77797a9e's own commit message names as the worse of the two ("a false negative kills working motion"), introduced by the fix for the other one. The question is per pad, so the slot now carries what it declared, beside the physical `pref` it already held. The two are deliberately separate fields answering different questions: `pref` is the controller in the user's hands, which is what the local feedback paths must keep reading, and `declared` is the one the host is pretending to have. Three facts decide the predicate, and they are written out in `pad_motion_reaches` rather than at the call site because all three clients need the same reasoning: - the echo is not this pad's answer when the pad declared something else; - the host FOLDS what it cannot build — a Switch Pro on Windows, any UHID backend on a host whose /dev/uhid is unusable — and nothing client-side can predict it; - but the echo IS one observed sample of that fold, for the kind the Hello asked about, so it is authoritative for a pad that declared exactly that. Hence: trust the echo when declared == asked, else fall back to the declaration. That keeps both motivating cases — a generic pad under Automatic (declares X-Box 360, suppressed, the sweep's H5c) and an explicit Switch Pro folded to X-Box 360 by a Windows host (declared == asked, so the echo catches it, H5d) — where either field alone gets one of them wrong. `requested_gamepad` is kept on the client next to `resolved_gamepad` for this: the pair is what makes the echo usable per pad, and a lone field would only tempt the next reader back into the session-level question. The residual gap is a pad whose declared kind differs from the session's AND gets folded: we keep sending and the host keeps dropping. That is the direction to be wrong in, and it is what the session-level check was worth in the first place — wasted datagrams, not a dead gyro. Non-vacuity proven both directions rather than assumed. Reverting to `resolved.has_motion()` fails on the mixed-pad row; reverting to `declared.has_motion()` (no echo at all) fails on the Switch-Pro-on-Windows row. Each case in the table is a session someone can actually sit down to, and the comment on each says which of the three inputs decides it. Gate (Linux CI image, pf-lxcheck2): fmt, `build -p punktfunk-core`, `build -p pf-client-core`, `clippy --locked --all-targets -D warnings`, and both test suites — green, with the new case observed in the run's own `... ok` line rather than inferred from a green gate, and pf-client-core's 163 unchanged.
punktfunk-core
The shared protocol core — the one place where punktfunk's transport, forward error correction, and crypto live. It's linked into the host and every native client, so there's exactly one implementation of the wire format everywhere.
Written in Rust with no async on the per-frame path (native threads only). It exposes both a normal Rust API and a stable, versioned C ABI, so the Swift and Kotlin clients — and any C embedder — link the same code as the Rust ones.
What's in here
- Transport & session (
session.rs,transport/,packet.rs) — thepunktfunk/1data plane over raw UDP: packetization, reassembly (with attacker-bounded limits), pacing, and socket tuning. - FEC (
fec/) — the wall-breaker. Two codes:- GF(2⁸) classic Reed–Solomon with the Cauchy generator matrix — byte-identical to the
nanorslibrary Moonlight uses, so our parity is decodable by a stock Moonlight client. - GF(2¹⁶) Leopard-RS (SIMD, O(n log n)) — up to 65535 shards/block, which removes the ~1 Gbps
FEC ceiling.
punktfunk/1negotiates this one.
- GF(2⁸) classic Reed–Solomon with the Cauchy generator matrix — byte-identical to the
- Crypto (
crypto.rs) — AES-128-GCM session encryption with per-direction nonce salts and sequence-as-AAD; SPAKE2 PIN pairing lives behind thequicfeature. - QUIC control plane (
quic.rs,client.rs, featurequic) — the Hello/Welcome/Start handshake, cert pinning/TOFU, reverse audio, and the embeddableNativeClientconnector. This is the only placetokio/quinnare allowed; the feature is off by default so the core stays runtime-free. - C ABI (
abi.rs) — the versioned surface (punktfunk_abi_version(),PunktfunkConfigcarrying its ownstruct_size) that generatesinclude/punktfunk_core.hvia cbindgen at build time.
Build outputs
The crate builds three ways at once (crate-type = ["lib", "cdylib", "staticlib"]):
| Output | Used by |
|---|---|
lib (rlib) |
the host, probe, and tools link it as a normal Rust crate |
cdylib (.so/.dylib) |
the Swift / Kotlin clients via the C ABI |
staticlib (.a) |
the C test harness and static embedding |
Test
cargo test -p punktfunk-core # unit + proptest + loopback
cargo run -p loss-harness # FEC loss-resilience sweep (no network needed)
bash crates/punktfunk-core/tests/c/run.sh # standalone C-ABI link + round-trip proof
Design invariants (do not regress)
- One core, linked everywhere — protocol/FEC/crypto live only here, behind the stable C ABI.
- No async on the hot path — the per-frame pipeline is native threads only;
quic(tokio/quinn) is control-plane only, feature-gated, off by default. - Security hardening stays intact — the reassembler bounds attacker-controlled fields before
allocating; AES-GCM keeps per-direction nonce salts + seq-as-AAD; the ABI checks
struct_size. Regression tests exist — keep them green.
Related
punktfunk-host— the streaming host built on this core- Clients — the apps that link this core over the C ABI (or directly, in Rust)
- punktfunk-planning:
implementation-plan.md(internal planning repo) — why GF(2¹⁶) FEC, the latency budget, and the architecture thesis