Files
punktfunk/crates/punktfunk-core
enricobuehler d0d2399476 feat(client/present): Apple reads the cadence statistic — WP1 complete
The third and last leg of WP1. All three clients now publish the same
judder number, which was the point: one ruler, so a smoothness A/B can be
compared across platforms instead of argued about.

A verbatim Swift port of punktfunk_core::phase::PresentIntervals, in the
same spirit as PhaseReporter.circularLatch alongside it, with a test file
that runs the SAME vectors as the Rust unit tests. A hand-written port is
exactly where "all three emit the same numbers" quietly stops being true,
so it is pinned rather than trusted.

Porting it found a real cross-client hazard. The modal spacing was read
with max_by_key, which returns the LAST maximum, while Swift's max(by:)
returns the FIRST — so a 50/50 window (the classic 1-and-3 sawtooth) would
have reported the same judder but a different mode on Android and Apple.
Both sides now spell the rule out: ties resolve to the smallest spacing.
The Rust test that previously accepted either answer now pins it.

Two Apple-specific decisions:

  - the stats object is built for EVERY session, not just under the debug
    env var or deadline pacing. A smoothness defect produces no drops and
    healthy percentiles, so gating the one statistic that could see it
    behind an env var means it is off exactly when it matters. A `verbose`
    flag preserves the old behaviour for the wordy counters line; the
    cadence line always emits.
  - the panel period comes from the link's own reported period (glass
    pacing) or is learned from the link's target instants (deadline
    pacing). Those tick at the panel rate whether or not WE present, which
    is what makes the window minimum the true period — the same reasoning
    PhaseReporter already documents. Learning it from on-glass spacings
    instead would read a 60-on-120 stream as a 60 Hz panel and mislabel the
    cadence mode.

A dropped drawable splits the run rather than scoring the gap: it never
reached glass, so it is not a cadence event, and the next present does not
continue the previous interval either.

Gates: punktfunk-core 21 phase tests green; the Swift port verified against
all 11 Rust vectors via a standalone harness (identical mode/judder/samples/
stalls/disordered on every case, incl. the tie-break); both edited Swift
files parse clean; fmt clean.

⚠ The Swift INTEGRATION is not compiler-verified locally: building
PunktfunkCore.xcframework on this machine fails a pre-existing deployment-
target guard (objects at minos 26 survive a cache wipe and an exported
MACOSX_DEPLOYMENT_TARGET). Source-only change, so it cannot be the cause.
CI's Apple leg owns that check — treat it as owed, not passed.
2026-08-05 23:14:28 +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