forked from unom/punktfunk
Field report: since 0.17.0 a stream to a Chromecast with Google TV 4K freezes on the first frame and ~80% of the time crashes + reboots the DEVICE — with both the Punktfunk app and Moonlight, while an Xbox Series S is fine. Root cause: LN1 Phase 3 (67b79810) defaulted Linux direct-NVENC to 4 slices per frame for EVERY session. Amlogic HEVC decoders wedge on multi-slice AUs — exactly why moonlight-android requests slicesPerFrame=1 for every hardware decoder (4 only for software slice-threading) — and our RTSP parser never read the request. The Phase-3 commit recorded the untested leg ("a live Moonlight re-test joins the standing owed Moonlight item"); this report is that re-test. The slicing ceiling now belongs to the CLIENT, threaded as open_video's new max_slices from both planes: - GameStream: parse x-nv-video[0].videoEncoderSlicesPerFrame into StreamConfig and honor it; absent/out-of-range (pre-auth input) => 1. - punktfunk/1: new Hello cap VIDEO_CAP_MULTI_SLICE (0x80 — the byte's LAST free bit; the next cap needs a second byte + ABI bump). SessionPlan.max_slices = 32 with the bit, 1 without, applied to every encoder the plan opens so rebuilds can't change the wire shape. The desktop session client advertises it (FFmpeg/D3D11VA/Vulkan decode stacks are fine); Android/Apple stay off until they can decide per-decoder like Moonlight does — the cap is embedder-set decoder truth, never OR'd in by the shared pump. - Linux direct-NVENC clamps its Phase-3 default to the ceiling (resolve_slices(codec, 4.min(max_slices))) and logs slices/max_slices in the caps-probe line; PUNKTFUNK_NVENC_SLICES stays the explicit operator override in both directions. Windows keeps its single-slice default untouched. Also repairs the nvenc_cuda #[ignore] hardware tests:d2c46eafadded open()'s cursor_blend param without updating them, invisible because CI never compiles tests with the nvenc feature. Verified on .21 (RTX 5070 Ti): pf-encode + host check/clippy clean with nvenc; host unit suite 263/0; rtsp announce tests 7/7 incl. the new slicesPerFrame coverage; on-hardware smokes — default e2e still 4 chunks/frame, NEW single-slice client-ceiling test clamps + disarms chunked poll with no env involved, env escape unchanged. rustfmt clean. Windows leg (prepare_display param) is CI-only. Co-Authored-By: Claude Fable 5 <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