Files
punktfunk/crates/punktfunk-core
enricobuehler fc5b6296e3 feat(client/present): the cadence statistic — measure judder, not just latency
WP1 of design/presenter-cadence-rework-implementation-plan.md. Shared core
type plus the Android binding; desktop and Apple follow.

Every stat we publish is a latency — a difference between two points on one
frame. No latency can see judder, because judder is a property of the
SEQUENCE. A stream that shows each frame one refresh early and the next one
late has excellent percentiles and looks broken; a stream whose every
interval is exactly two refreshes has worse latency than one alternating 1
and 3, and looks perfect. That blind spot is why a smoothness complaint
could not be confirmed or refuted from our own telemetry.

PresentIntervals quantises the spacing between consecutive on-glass instants
onto the learned panel grid and reports the modal spacing plus the fraction
of intervals that miss it — the judder number, in permille to match the
phase coherence already next to it. Scale-free: the mode absorbs the cadence
ratio, so 60-on-120 and 120-on-120 are both "one tall bucket" and directly
comparable. That is what makes it usable as one ruler across clients,
refresh rates and stream rates, and for a feature-on/off A/B.

Deliberate choices, each with a test:

  - fed the MEASURED on-glass instant, never the requested present time,
    which would measure our own intent and always look perfect
  - fed SurfaceFlinger's raw CLOCK_MONOTONIC render stamp, not the
    realtime-rebased one the latency stats use: cadence is about spacing,
    and a realtime clock step would forge a hitch that never happened
  - stalls (>8 refreshes) and out-of-order callbacks counted apart from the
    ratio, so a window that looks smooth because the stream was PAUSED
    cannot be mistaken for a good one
  - sub-refresh jitter is not judder: the display quantises it away, so the
    metric must too
  - the predecessor survives a window drain, else one interval per window
    would go unscored forever

Always-on via the 1 Hz pf.present line, so the HUD-off wireless A/B the
baseline measurement needs is readable from logcat. HUD surfacing waits on
the stats-unification spec amendment (plan S3) and on the in-flight HUD work.

Gates: punktfunk-core 189 tests green (10 new); cargo ndk check + clippy
arm64 clean — the 5 remaining warnings are pre-existing and in other files.
2026-08-05 22:40:43 +02:00
..

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) — the punktfunk/1 data plane over raw UDP: packetization, reassembly (with attacker-bounded limits), pacing, and socket tuning.
  • FEC (fec/) — the wall-breaker. Two codes:
    • GF(2⁸) classic ReedSolomon with the Cauchy generator matrix — byte-identical to the nanors library 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/1 negotiates this one.
  • Crypto (crypto.rs) — AES-128-GCM session encryption with per-direction nonce salts and sequence-as-AAD; SPAKE2 PIN pairing lives behind the quic feature.
  • QUIC control plane (quic.rs, client.rs, feature quic) — the Hello/Welcome/Start handshake, cert pinning/TOFU, reverse audio, and the embeddable NativeClient connector. This is the only place tokio/quinn are allowed; the feature is off by default so the core stays runtime-free.
  • C ABI (abi.rs) — the versioned surface (punktfunk_abi_version(), PunktfunkConfig carrying its own struct_size) that generates include/punktfunk_core.h via 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.
  • 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