ci / web (pull_request) Successful in 1m4s
ci / rust-arm64 (pull_request) Successful in 1m42s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m5s
android / android (pull_request) Successful in 2m50s
ci / docs-site (pull_request) Successful in 2m45s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m2s
apple / swift (pull_request) Successful in 1m18s
apple / screenshots (pull_request) Skipped
ci / rust (pull_request) Failing after 12m58s
The 0.23.0 slice wire flushes a block every MIN_STREAM_BLOCK_SHARDS, so every ordinary access unit is now opened by a SENTINEL — a header with no totals. The reassembler sized those frames at `max_frame_bytes`, which the QUIC handshake clamps to 8-64 MiB. That was survivable while sentinels were rare (the streamed path emitted one only for an AU exceeding a whole FEC block, ~281 KB); it is not survivable now that every frame is one. Two consequences, both measured: each access unit allocated and ZEROED a multi-megabyte buffer, and the in-flight budget (IN_FLIGHT_BUF_FACTOR x max_frame_bytes) was spent after ~3 concurrent frames — with production geometry, 12 ordinary AUs in flight lost 9 of them outright, every packet dropped before it could be placed. On a link with normal reorder that is a permanent loss storm: frames never complete, the re-anchor gate freezes the picture, and the client begs for keyframes. Only clients advertising VIDEO_CAP_MULTI_SLICE reach this path — Android and the Linux/Windows session client; Apple and the Windows in-process client never did, which is why it read as a platform-specific "video pipeline" fault in the field. A sentinel carries no total but does pin its own block's extent: a slice sentinel by its wire base, a legacy one by its full-K position. Size the buffer to that and grow as later blocks (or the final block's totals) reveal more. The budget is re-checked on growth for the same reason it is checked at open. The same flush also drained `pending` to empty whenever the AU's length was an exact multiple of the shard payload, leaving `finish_streamed` to seal a final block of one zero-padded FILLER shard. Its derived base overlapped the block flushed a moment earlier, retro-validation correctly read that as a lying header, and the whole AU died — one frame in every 1408 on a 1500-MTU link, ~12 s apart at 120 fps, each costing a freeze and a recovery keyframe. A flush now keeps one whole shard back, restoring the invariant `StreamedAu::pending` already documented. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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