73c911cae4
Rumble was level-triggered, unbounded state on a lossy channel: a non-zero level meant "buzz until further notice", healed only by the host re-sending state every 500 ms, and every client guessed when the host had died with its own magic timeout (SDL 1.5 s, Apple 1.6 s, Android up to 60 s). A lost stop, a reordered start, or a dead host could drone the motor for seconds. Make "stuck rumble" inexpressible on the wire. The 0xCA datagram grows a length-tolerant tail — [u8 seq][u16 ttl_ms] — so it self-terminates: the host authorizes a level for at most ttl_ms and renews it (~120 ms) while it holds, letting an abandoned one lapse client-side. seq is a per-pad wrapping reorder gate (reusing GamepadSnapshot::seq_newer) so a reordered stale start can't re-light a stopped motor. Decoders read the first 7 bytes as a plain level and ignore the tail, so no wire-version bump: an old client renders a new host's levels, and a new client falls back to its prior staleness heuristic against an old host (ttl = None). All four generation pairings render correctly. - core: encode_rumble_datagram_v2 / decode_rumble_envelope (datagram.rs); the client demux applies the seq gate then forwards (pad, low, high, Option<ttl>); next_rumble is unchanged (drops ttl), next_rumble_ttl keeps it; ABI adds punktfunk_connection_next_rumble2 + PUNKTFUNK_RUMBLE_NO_TTL, ABI_VERSION 4->5 (WIRE_VERSION unchanged — the tail is backward-compatible). - host (punktfunk1.rs): the flat 500 ms refresh becomes a renewal loop that bumps seq + stamps a fresh TTL on active pads and drains a short post-stop zero burst, then goes quiet. Hatches: PUNKTFUNK_RUMBLE_ENVELOPE=0 (legacy v1 + flat refresh, a bisect switch), PUNKTFUNK_RUMBLE_TTL_MS (clamped [150, 5000]). - renderers honor the TTL as their playback duration/deadline and keep their old heuristic only for a legacy (ttl=None) update: pf-client-core (the Deck haptic keep-alive is now deadline-bounded so it can't sustain a host-stopped rumble), clients/windows (SDL duration), android (JNI packs the lease out-of-band in bit 48 so any u16 ttl is unambiguous; Kotlin createOneShot(ttl)), apple (RumbleRenderer.envelopeDeadline + nextRumble2; sessionStaleSeconds demoted to the legacy fallback). - tests: codec round-trip + tail tolerance + seq-gate reorder (Rust); the probe asserts the v2 tail arrived under PUNKTFUNK_TEST_FEEDBACK; the Apple loopback asserts ttlMs round-trips end to end; RumbleTuning lease-decision cases. The host-side idle-timeout from the previous commit is defense in depth on the game side; this is the guarantee on the client side. Design: punktfunk-planning/design/rumble-envelope-plan.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
73 lines
3.5 KiB
Rust
73 lines
3.5 KiB
Rust
//! # punktfunk-core
|
|
//!
|
|
//! The shared protocol / transport / FEC core for the punktfunk low-latency streaming
|
|
//! stack. It is compiled exactly once and linked by every host and client — directly
|
|
//! as a Rust `lib`, or across the [C ABI](crate::abi) by Swift / Kotlin / C clients.
|
|
//!
|
|
//! Everything platform-specific (capture, encode, decode, present, input injection)
|
|
//! lives *outside* this crate. What lives *here*:
|
|
//!
|
|
//! - [`fec`] — erasure coding. GF(2⁸) for GameStream/Moonlight compatibility (P1) and
|
|
//! GF(2¹⁶) Leopard-RS (P2) which removes the ~1 Gbps per-frame shard-count ceiling.
|
|
//! - [`packet`] — `#[repr(C)]` zero-copy wire framing: splitting an access unit into
|
|
//! FEC blocks of MTU-sized shards and reassembling them on the far side.
|
|
//! - [`crypto`] — AES-128-GCM session sealing, matching GameStream in P1.
|
|
//! - [`session`] — the host (submit frame → FEC → packetize → seal → send) and client
|
|
//! (recv → open → reorder → FEC recover → reassemble) state machines.
|
|
//! - [`transport`] — pluggable packet I/O (in-process loopback for tests; UDP for real).
|
|
//! - [`abi`] — the `extern "C"` surface and `cbindgen`-generated `punktfunk_core.h`.
|
|
//!
|
|
//! ## Threading contract
|
|
//!
|
|
//! Nothing in the per-frame path touches an async runtime. `tokio`/`quinn` are gated
|
|
//! behind the off-by-default `quic` feature and used only for the control plane.
|
|
|
|
#![forbid(unsafe_op_in_unsafe_fn)]
|
|
|
|
pub mod abi;
|
|
#[cfg(feature = "quic")]
|
|
mod abr;
|
|
pub mod audio;
|
|
#[cfg(feature = "quic")]
|
|
pub mod client;
|
|
pub mod config;
|
|
pub mod crypto;
|
|
pub mod error;
|
|
pub mod fec;
|
|
pub mod input;
|
|
pub mod packet;
|
|
#[cfg(feature = "quic")]
|
|
pub mod quic;
|
|
pub mod session;
|
|
pub mod stats;
|
|
pub mod transport;
|
|
pub mod wol;
|
|
|
|
pub use config::{CompositorPref, Config, FecConfig, FecScheme, Mode, ProtocolPhase, Role};
|
|
pub use error::{PunktfunkError, PunktfunkStatus, Result};
|
|
pub use session::{Frame, Session};
|
|
pub use stats::Stats;
|
|
|
|
/// Bump on any breaking change to the [C ABI](crate::abi). Mirrors
|
|
/// `punktfunk_abi_version()` and is checked by clients before use.
|
|
///
|
|
/// v2: `punktfunk_connect` gained `client_cert_pem`/`client_key_pem` (pairing identities);
|
|
/// added `punktfunk_pair` / `punktfunk_generate_identity` / `punktfunk_connection_request_mode`.
|
|
/// v3: added `punktfunk_wake_on_lan` (Wake-on-LAN magic packet; the host's wake MAC(s) reach
|
|
/// clients out-of-band via the mDNS `mac` TXT record, so no connection is required to wake).
|
|
/// v4: added `punktfunk_probe` (bounded, trust-agnostic, mDNS-independent reachability handshake —
|
|
/// the display-side companion to dial-first, so saved-host "online" pips reflect real reachability).
|
|
/// v5: added `punktfunk_connection_next_rumble2` (rumble pull that also yields the self-terminating
|
|
/// TTL of a v2 envelope; `punktfunk_connection_next_rumble` is unchanged and drops it). Additive —
|
|
/// the wire is backward-compatible (the envelope is a length-tolerant tail on 0xCA), so
|
|
/// [`WIRE_VERSION`] is unchanged.
|
|
pub const ABI_VERSION: u32 = 5;
|
|
|
|
/// The punktfunk/1 **wire** version — what `Hello`/`Welcome` carry and hosts equality-check.
|
|
/// Deliberately its own constant: [`ABI_VERSION`] tracks the embeddable **C surface**
|
|
/// (functions a client links), which can grow without changing a single wire byte — v3's
|
|
/// `punktfunk_wake_on_lan` is client-local, and riding the C-ABI bump onto the wire locked
|
|
/// every new client out of every deployed host ("ABI mismatch: client 3 host 2", observed
|
|
/// live). Bump this ONLY when the handshake/planes actually change incompatibly.
|
|
pub const WIRE_VERSION: u32 = 2;
|