A session ending was a single bit. A player quitting their game, an operator ending the session from the console, a stop the client itself asked for, a host crashing and a Wi-Fi drop all arrived as the same "closed" — so every client had to write one message covering all of them, and every client picked an error. That is how quitting your own game came to be reported as trouble on all three. The information was already there and thrown away: the host closes with APP_EXITED when a launched game exits, with 0 when it ends the session cleanly and 1 when it fails, and a link that simply dies never closes at all. The connection watcher now classifies that into a PunktfunkEndReason — local, game exited, host ended, host error, lost — and latches it before the shutdown flag, since the two are read by different threads and the reason must never arrive second. Exposed as punktfunk_connection_end_reason. This replaces the game-exited flag added a moment ago rather than joining it: that question is one row of this table, and it was never released. Still additive to any embedder that ignores it, and the host sends the same bytes either way, so the wire is untouched. `is_normal()` is the question nearly every caller actually has, so both the Rust and C surfaces answer it directly rather than making each client re-derive which of five values are worth alarming a user about.
157 lines
10 KiB
Rust
157 lines
10 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`.
|
||
//! - [`config`] / [`error`] / [`stats`] — session configuration, the shared error/status
|
||
//! vocabulary, and the counters snapshot.
|
||
//! - [`input`] — the wire input-event vocabulary (keyboard/mouse/touch, gamepad snapshots).
|
||
//! - [`reject`] — typed application-close rejection codes · [`reanchor`] — the post-loss
|
||
//! freeze-until-reanchor client gate · [`render_scale`] — the shared render-scale setting ·
|
||
//! [`audio`] — Opus PCM decode for C-ABI embedders · [`wol`] — Wake-on-LAN.
|
||
//! - `quic` (feature `quic`) — the punktfunk/1 control plane: handshake, typed control
|
||
//! messages, pairing (SPAKE2), the datagram plane codecs, and clock sync. With it come
|
||
//! `client` (the embeddable NativeClient worker), `abr` (the adaptive-bitrate
|
||
//! controller), and `clipboard` (the shared-clipboard transport task). `tls`
|
||
//! (feature `tls`) — the pinned-fingerprint certificate verifier.
|
||
//!
|
||
//! ## 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.
|
||
|
||
// Unsafe-proof program: every `unsafe {}` / `unsafe impl` in this crate carries a `// SAFETY:`
|
||
// proof. The bulk lives in `abi.rs`, whose sites are instances of the ABI contract stated once at
|
||
// the top of that file rather than 141 independent arguments.
|
||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||
#![forbid(unsafe_op_in_unsafe_fn)]
|
||
|
||
pub mod abi;
|
||
#[cfg(feature = "quic")]
|
||
mod abr;
|
||
pub mod audio;
|
||
#[cfg(feature = "quic")]
|
||
pub mod client;
|
||
/// Client-side shared-clipboard transport: the per-session task that runs the fetch-stream accept
|
||
/// loop, drives outbound fetches, and serves inbound ones — surfaced to the embedder as poll
|
||
/// events. Wire codecs live in [`quic`]; the OS pasteboard integration lives in the native client.
|
||
#[cfg(feature = "quic")]
|
||
pub mod clipboard;
|
||
pub mod config;
|
||
pub mod crypto;
|
||
pub mod error;
|
||
pub mod fec;
|
||
pub mod input;
|
||
pub mod packet;
|
||
pub mod phase;
|
||
#[cfg(feature = "quic")]
|
||
pub mod quic;
|
||
pub mod reanchor;
|
||
pub mod reject;
|
||
pub mod render_scale;
|
||
pub mod session;
|
||
pub mod stats;
|
||
#[cfg(feature = "tls")]
|
||
pub mod tls;
|
||
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.
|
||
/// v6: added the `punktfunk_reanchor_gate_*` surface (post-loss freeze-until-reanchor gate for the
|
||
/// Swift client; Rust embedders use [`reanchor::ReanchorGate`] directly). Additive, client-local —
|
||
/// no wire change, so [`WIRE_VERSION`] is unchanged.
|
||
/// v7: added `punktfunk_connect_ex8` (`status_out` — typed connect-failure reporting, including
|
||
/// the host-rejection block `PUNKTFUNK_STATUS_REJECTED_*` decoded from the host's QUIC
|
||
/// application close) and the `PunktfunkStatus` −20 block itself. Additive — the close codes are
|
||
/// new application-close vocabulary an old peer simply never sends/reads, so [`WIRE_VERSION`] is
|
||
/// unchanged.
|
||
/// v8: added the shared-clipboard client surface — `punktfunk_connection_host_caps` and
|
||
/// `punktfunk_connection_clipboard_{control,offer,fetch,serve,cancel}` +
|
||
/// `punktfunk_connection_next_clipboard`. Additive; the wire grows only backward-compatible control
|
||
/// messages (0x40-0x44) and a new `Welcome::host_caps` bit, so [`WIRE_VERSION`] is unchanged.
|
||
/// v9: `PunktfunkFrame` grew `received_ns` — the reassembly-completion receipt stamp, so
|
||
/// embedders stop stamping receipt at the hand-off pull (which folds the pre-decode queue wait
|
||
/// into apparent network latency). Struct-size change on the frame poll surface = a hard ABI
|
||
/// break for embedders reading `PunktfunkFrame`; nothing on the wire moved, so [`WIRE_VERSION`]
|
||
/// is unchanged.
|
||
/// v10: added `punktfunk_connection_clock_offset_now_ns` — the LIVE (mid-stream re-synced)
|
||
/// clock offset ongoing latency math must use; the connect-time getter stays frozen by
|
||
/// contract. Additive, client-local — no wire change, so [`WIRE_VERSION`] is unchanged.
|
||
/// v11: added `punktfunk_connect_ex9` — `connect_ex8` plus a `client_caps` bitfield
|
||
/// (`PUNKTFUNK_CLIENT_CAP_CURSOR`, later `…_PHASE_LOCK`), which is how a client tells the host it
|
||
/// renders the pointer itself. Additive; the caps ride the existing Hello, so [`WIRE_VERSION`] is
|
||
/// unchanged. (Documented late — the bump shipped without its line here.)
|
||
/// v12: added `punktfunk_connection_set_cursor_render` — the mid-stream cursor-render flip
|
||
/// (design/remote-desktop-sweep.md §8): the client's mouse-model chord tells the host who
|
||
/// renders the pointer. Additive; rides the existing control stream (a new message TYPE, which
|
||
/// pre-§8 hosts ignore), so [`WIRE_VERSION`] is unchanged.
|
||
/// v13: added `punktfunk_connection_send_pen` — the stylus wire plane
|
||
/// (design/pen-tablet-input.md): a client sends `RICH_PEN` sample batches once the host
|
||
/// advertises `HOST_CAP_PEN`. Additive and capability-gated, so [`WIRE_VERSION`] is unchanged.
|
||
/// v14: added `punktfunk_connection_report_phase` + the `PUNKTFUNK_CLIENT_CAP_PHASE_LOCK` mirror
|
||
/// — the phase-locked capture reporter (design/phase-locked-capture.md): a client that advertises
|
||
/// the cap reports its next display latch (already converted to host clock), the panel period, an
|
||
/// uncertainty and the circular arrival-lead statistic the host's controller steers on. Additive;
|
||
/// the wire grows only a new control message (`PhaseReport`, 0x32) an old host never reads and a
|
||
/// strict-prefix append on the 0xCF host-timing tail, so [`WIRE_VERSION`] is unchanged.
|
||
/// v15: versions the shared rumble policy engine's C surface —
|
||
/// `punktfunk_connection_next_rumble_cmd`, `punktfunk_connection_set_rumble_quirks` and the
|
||
/// `PUNKTFUNK_RUMBLE_QUIRK_*` bits. These symbols are NOT new: they landed while this constant
|
||
/// still read 7 and no bump was made, so every core since has exported them while advertising a
|
||
/// version that never promised them. That cannot be corrected retroactively — a shipped binary
|
||
/// says what it says — so v15 is the floor that *guarantees* them: at or above it the surface is
|
||
/// present, below it an embedder must probe for the symbol. Purely a version statement; no code
|
||
/// changed with this bump, and no wire change, so [`WIRE_VERSION`] is unchanged.
|
||
/// v16: added the pad-audio client surface — `punktfunk_connection_next_pad_audio` (the 0xD1
|
||
/// per-gamepad DualSense haptics/speaker plane) + `punktfunk_connection_set_pad_audio_caps` and
|
||
/// the `PUNKTFUNK_CLIENT_CAP_PAD_AUDIO` / `PUNKTFUNK_HOST_CAP_PAD_AUDIO` mirrors. Additive and
|
||
/// capability-gated end to end: the wire grows a new datagram tag (0xD1) an old client never
|
||
/// receives (double-gated caps), a new 0xCD kind (0x06, dropped as unknown by old clients) and
|
||
/// arrival flag bits 8/9 sent only toward a capable host, so [`WIRE_VERSION`] is unchanged.
|
||
/// v17: added `punktfunk_connection_end_reason` + the `PUNKTFUNK_END_REASON_*` vocabulary — asks,
|
||
/// once a session has ended, WHY: this client closed it, the host's launched game exited (its close
|
||
/// carried [`quic::APP_EXITED_CLOSE_CODE`], which the host has sent since long before this bump
|
||
/// with nothing consuming it), the host ended it cleanly, the host reported a failure, or the
|
||
/// connection was simply lost. Purely a read of state the core already had: no new call is required
|
||
/// of an embedder, a client that never calls it is unchanged, and the host sends exactly the same
|
||
/// bytes either way, so [`WIRE_VERSION`] is unchanged.
|
||
pub const ABI_VERSION: u32 = 17;
|
||
|
||
/// 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;
|