Stage A of the zero-copy host packetize path (networking-audit deferred
plan §1): ErasureCoder::encode now takes &[&[u8]], so Packetizer::packetize
builds each block's data shards as slices straight into the frame buffer
instead of allocating + copying a Vec per data shard. Only the frame's
final (possibly partial) shard is staged in a reusable zero-padded scratch;
blocks are consecutive shard ranges, so every other shard is a full
payload-sized slice.
- gf8: encode_sep() over the same Cauchy codec — parity byte-identical to
nanors/Moonlight (nanors_exact_parity_vectors unchanged and green)
- gf16: reed_solomon_simd::encode is already generic over AsRef<[u8]>
- loss-harness sweep: recovery rates identical before/after
- bench pipeline (end-to-end, host+client): gf8/64K -3.0%, gf16/64K -2.2%,
gf16/1M -3.4%, gf8/1M -0.7%
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root-caused live on a phone at 100 Mbps (stream stuck seconds behind, then
oscillating): a stack of transport defects, each amplifying the next.
- MTU-safe shards: shard_payload 1452 overshot the IPv4/1500 budget (the old
math forgot the 40 B header + 24 B crypto ride inside the UDP payload and
counted IP+UDP as 8 B) — the kernel silently split EVERY video datagram into
two IP fragments, doubling per-datagram loss on Wi-Fi. New
config::mtu1500_shard_payload() = 1408 (1472 sealed = the exact ceiling),
negotiated in the Welcome, pinned by a unit test.
- Android batched I/O: recv/send batching was cfg(linux); Android is
target_os="android" and silently fell back to a syscall per datagram. The
libc crate binds neither recvmmsg/sendmmsg nor mmsghdr for Android, so a
local bionic extern binding provides them (API 21+, floor is 28); cbindgen
excludes them from the C header. The pump/runtime threads also get the
Apple-QoS analogue on Android: nice −8 (below the decode thread's −10).
- Latency-bounded receive: packets are consumed strictly in order at exactly
the arrival rate, so a standing queue (Wi-Fi stall, power-save clumping)
NEVER drains — observed as a stream permanently 6-7 s behind with both 32 MB
socket buffers full. The pump now flushes the entire backlog
(Session::flush_backlog: discard ring + kernel queue at memcpy speed, reset
the reassembler) and requests a keyframe when frames keep completing > 400 ms
behind the skew-corrected capture clock (30 consecutive, 2 s cooldown,
logged).
- Time-based loss window: the reassembler declared an incomplete frame lost a
fixed 4 INDICES behind the newest — 33 ms at 120 fps, inside normal Wi-Fi
retry/reorder timescales, so merely-late frames were pruned every few
seconds, each costing a recovery-IDR burst + an inflated loss report.
Now 120 ms of capture time (LOSS_WINDOW_NS), same fuse at every refresh
rate, with a 64-index hard cap bounding memory against hostile pts.
- Adaptive-FEC hysteresis: the controller was memoryless — one clean 750 ms
report dropped FEC from 8 % straight back to the 1 % floor, so periodic burst
loss (Wi-Fi scan / BT coexistence beats) always hit an unprotected stream and
ping-ponged 1↔8 % with a frozen frame per cycle (observed in the host log as
alternating loss_ppm=0/50000). Attack stays instant; decay is now one point
per clean report.
Verified: full core suite (incl. new flush + time-window tests) on macOS +
Linux, host release build, arm64 cargo-ndk build, and a 30 s wired probe run
at 2800x1260@120 — 3559/3559 frames, zero loss, capture→received p50 5.3 ms
(host 5.1 + network 0.3).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two bodies of work in one commit (the rename moved files the fixes also touched).
Naming/structure cleanup (pre-launch):
- Host modules m3.rs->punktfunk1.rs, m0.rs->spike.rs; CLI m3-host->punktfunk1-host,
m0->spike; bare `punktfunk-host` now prints help. Types M3Options/M3Source->
Punktfunk1Options/Punktfunk1Source.
- Clients consolidated out of crates/ into clients/: punktfunk-client-rs->
clients/probe (crate punktfunk-probe), client-linux->clients/linux,
client-windows->clients/windows, punktfunk-android->clients/android/native
(crate punktfunk-client-android; kept [lib] name=punktfunk_android so the JNI
contract is unchanged). crates/ now holds only core + host.
- Milestone codes M0-M4 purged from code/CLI/CLAUDE.md/README/docs/docs-site,
kept only in docs/implementation-plan.md. docs/m2-plan.md->
docs/gamestream-host-plan.md. CI/gradle/flatpak paths updated.
Client loss-recovery (video froze and never recovered after a brief drop):
- Export punktfunk_connection_frames_dropped through the C ABI (the core already
tracked it for the client keyframe-recovery loop; it was never reachable from
the ABI clients). Regenerated punktfunk_core.h.
- Apple (StreamPump + Stage2Pipeline) and Android (decode.rs) now poll
frames_dropped and request a keyframe when it climbs -- the same loss-driven
recovery Linux/Windows already had. Under infinite GOP the decoder silently
conceals reference-missing frames, so the decode-error trigger rarely fires.
Apple rumble robustness (worked then went spotty -- DualSense + Xbox):
- Add CHHapticEngine stopped/reset handlers (rebuild on app background / audio
interruption / server reset) and drop the permanent `broken` latch on a
transient drive failure; latch only when the controller truly has no haptics.
- Surface swallowed SDL set_rumble errors on Linux/Windows + diagnostic logging.
Verified: cargo build/clippy/fmt --workspace, C-ABI harness, header drift.
Not runnable on this box (verify in CI): Gitea workflows, gradle/Android,
flatpak, Swift/decky.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Full project rename, decided 2026-06-10:
- Crates/binaries: punktfunk-core / punktfunk-host / punktfunk-client-rs.
- C ABI: punktfunk_* symbols, Punktfunk* types, include/punktfunk_core.h,
PUNKTFUNK_FEATURE_QUIC guard (header regenerated; cbindgen renames updated, incl.
PUNKTFUNK_BTN_*/PUNKTFUNK_AXIS_* wire constants).
- Protocol: punktfunk/1 — control-plane magic LMN1 → PKF1, nonce salt lmn1 → pkf1.
WIRE BREAK: clients must be rebuilt from this revision.
- Env knobs: PUNKTFUNK_VIDEO_SOURCE / PUNKTFUNK_COMPOSITOR / PUNKTFUNK_ZEROCOPY / ….
- Host config dir: ~/.config/punktfunk (the box's dir was migrated in place — the
persistent identity is unchanged, pinned fingerprints stay valid).
- Swift package: PunktfunkKit + PunktfunkCore.xcframework + PunktfunkConnection
(Sources/PunktfunkClient app + tests renamed with it); build-xcframework.sh updated.
- scripts/: 60-punktfunk.rules, punktfunk-host.service; OpenAPI doc regenerated.
Also: scripts/headless/run-headless-kde.sh — full headless Plasma bringup. Root cause of
"desktop but no apps/settings" over the stream: plasmashell launched without
XDG_MENU_PREFIX=plasma-, so the launcher resolved a nonexistent applications.menu and
rendered an empty menu. The script sets the complete KDE session env (menu prefix,
KDE_FULL_SESSION, session version) and rebuilds ksycoca before starting plasmashell.
Gate: 97/97 tests, clippy -D warnings (both feature sets), fmt, C-ABI harness PASS,
zero lumen references left outside .git.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>