ci / docs-site (pull_request) Successful in 1m14s
apple / swift (pull_request) Successful in 1m28s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 2m54s
ci / rust-arm64 (pull_request) Successful in 4m8s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 4m46s
android / android (pull_request) Successful in 5m31s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 4m11s
ci / rust (pull_request) Successful in 9m39s
Three faults in the shared rumble policy engine, all answered by one change of shape: the free-running jitter phase becomes `last_emit` — the exact value last handed to an embedder — and every emit routes through one helper. That single field answers all three live questions: would re-sending this be a no-op device write, is this stop redundant, and would the nudge invent a stop. The Steam Deck declares a 40 ms keepalive with a 1-LSB nudge, because an SDL-class layer discards a write identical to the last one. But the nudge lived only in the keepalive branch, so every host renewal re-emitted the raw level, collided with the last jittered write, was discarded, AND re-anchored the keepalive timer. The gap between distinct device writes stretched to 80 ms at the 400 ms default TTL and 100 ms at the hatch floor — two to two and a half times the cadence the quirk exists to guarantee. Nudging on any repeat closes it: 40 ms throughout. Level (1, 0) turned that nudge into (0, 0) — the value the engine reserves for "stop now" — and handed it out with a non-zero backstop, under a live lease. It is the only such level: high must already be zero, and low ^ 1 == 0 implies low == 1. The nudge now steps the LSB up instead, so the phase still alternates and no stop is ever invented. A zero for a pad the engine already believes silent is now dropped. Under the legacy hatch the host re-sends zeros for every latched pad every 500 ms for the rest of the session, which cost Android an unconditional log line and a binder cancel() at 2 Hz per pad. The deliberate stop-burst heal is untouched, because a stop that was LOST leaves the pad buzzing, and that is exactly the guard's pass condition. The client also now bounds the lease it will honour. RUMBLE_TTL_CEIL_MS is sender-side only, so a modified or third-party host could stamp a long TTL and wedge its pump, leaving Apple — whose renderer deliberately keeps no staleness policy of its own — and a Deck slot buzzing for all of it. Every new test was proven to fail with its own fix reverted, including the two that guard against over-reach: a default-quirks pad must still get the level verbatim, or an off-by-one amplitude would land in Apple's identical-target comparison and Android's one-shots. One suspicion from the audit did NOT survive: a v2 envelope carrying ttl_ms 0 cannot take the legacy backstop, because the expiry check preempts the relay branch. No fix; pinned with a test so that ordering stays load-bearing. Verified: 17/17 rumble tests, clippy --all-targets --features quic -D warnings = 0, fmt clean, generated C header unchanged. (`c_abi_harness_round_trips` fails on this Mac with a linker error, identically on an unmodified tree.) From the 2026-08-03 force-feedback sweep (B12, B22, R9, T1).
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