Phase 0 of mid-session shard-payload renegotiation (planning design/shard-payload-reneg.md), stacked on the leg-1 MTU resilience. All three legs are client-side and forward-compatible: deployed clients that carry them accept a mid-session shard change the moment a future host sends one, and nothing changes on the wire until then. - W0.1 — the reassembler's strict shard_bytes firewall becomes per-frame pinning: a frame's first-arriving packet pins that frame's shard size (bounds-checked to [min_shard_bytes, max_shard_bytes], even), later packets must match the pin, and the per-frame block ceiling derives from the pinned size (a session-level cap would reject legitimate post-shrink frames). The reorder race between an ordered control message and unordered video dies structurally: old-geometry frames in flight complete under their own pin while new frames arrive under the new one, and no cross-geometry splice can land in one buffer. The in-flight budget stays byte-based and exact. - W0.2 — MAX_DATAGRAM_BYTES 2048 → 9216: every receive path (transport RECV_BUF, the recvmmsg ring) now accepts sealed jumbo datagrams (9000-MTU LAN ≈ 8908-byte shards). Static buffers over resize-on-ack: the ring delta is 128 × ~7 KiB ≈ 896 KiB per client session, lazily allocated, hosts unaffected. Grep verdict: no embedder uses the constant directly, so no C ABI bump — the regenerated header rides along (drift gate). - W0.3 — trailing Hello field max_shard_payload: u16 (0/absent = legacy), the append-with-placeholder discipline of video_caps/ client_caps. One field is both the renegotiation capability flag and the jumbo ceiling; core's pump advertises it for all client families, the probe too. - Host seam for Phase 1, dead until wired: Packetizer::set_shard_payload (re-derives the block ceilings; construction delegates to it) + Session::set_shard_payload (host-only, Config::validate parity). Verification (the 0.23.0 lesson — geometry changes breed sizing bugs): the slice-wire suite re-runs at shard 512/1216/1408/8908 (exact-multiple sweep, lossy + reversed roundtrips, sentinel path, in-flight budget); mid-stream shrink→grow→revert delivery; the old-geometry reorder race; cross-geometry splice rejection; firewall bounds non-vacuous both ways; a 48-case mixed-geometry reorder-torture proptest asserting per-frame byte-identical DELIVERY and an exactly-zero final budget; and a sealed loopback session test (continuous crypto/replay) delivering frames across live re-keys — every test asserts delivered frames, never the absence of errors. core: 294/294 --features quic + clippy -D warnings (macOS), fmt.
110 lines
6.5 KiB
Rust
110 lines
6.5 KiB
Rust
//! Wire packet header: the fixed [`PacketHeader`] + the flag/geometry consts every
|
||
//! packet carries. Zero-copy (de)serializable; 40 bytes, unpadded.
|
||
|
||
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
|
||
|
||
/// Identifies a punktfunk video packet (vs. an input datagram, see [`crate::input`]).
|
||
pub const PUNKTFUNK_MAGIC: u8 = 0xC9;
|
||
|
||
// Frame flags (mirroring GameStream's FLAG_*).
|
||
pub const FLAG_PIC: u8 = 0x1;
|
||
pub const FLAG_EOF: u8 = 0x2;
|
||
pub const FLAG_SOF: u8 = 0x4;
|
||
/// Bandwidth-probe filler, not decodable video: a [`crate::quic::ProbeRequest`] speed test makes
|
||
/// the host burst access units carrying this flag so the client measures throughput/loss without
|
||
/// feeding them to the decoder. Punktfunk/1 only (GameStream never sets it).
|
||
pub const FLAG_PROBE: u8 = 0x8;
|
||
|
||
/// Application `user_flags` bit (the u32 [`PacketHeader::user_flags`] word, surfaced to the client
|
||
/// as [`crate::session::Frame::flags`]) — NOT a transport packet flag. Marks the access unit that
|
||
/// **completes an intra-refresh wave**: the picture is loss-free from here even though the frame is
|
||
/// a coded `P` (no IDR, so the decoder never sets `AV_FRAME_FLAG_KEY`). The client lifts its
|
||
/// post-loss display freeze on this bit as well as on a real keyframe — the only bitstream-invisible
|
||
/// clean point it can honor without forcing a full IDR. Lives above the low nibble because the host
|
||
/// reuses `FLAG_PIC`/`FLAG_SOF`/`FLAG_PROBE` bit values inside `user_flags`; `0x10` clears all four.
|
||
pub const USER_FLAG_RECOVERY_POINT: u32 = 0x10;
|
||
|
||
/// Application `user_flags` bit — a **definitive single-frame clean re-anchor**. Unlike
|
||
/// [`USER_FLAG_RECOVERY_POINT`] (an intra-refresh wave boundary, where the first boundary after a loss
|
||
/// is only half-healed so the client waits for the second), this marks an access unit the host coded
|
||
/// to reference a **known-good** picture on purpose — an AMD **LTR reference-frame-invalidation**
|
||
/// recovery frame (`ForceLTRReferenceBitfield`): a clean P-frame off a long-term reference the client
|
||
/// already has, not an IDR. The picture is loss-free the instant this AU decodes, so the client lifts
|
||
/// its post-loss freeze on the **first** such mark. Coded `P` (no IDR), so the decoder never sets
|
||
/// `AV_FRAME_FLAG_KEY` — this host flag is the only signal.
|
||
pub const USER_FLAG_RECOVERY_ANCHOR: u32 = 0x20;
|
||
|
||
/// `user_flags` bit: the AU's content is **shard-aligned self-delimiting chunks** — every
|
||
/// `shard_payload`-sized window of the frame buffer starts a fresh codec packet, padded to the
|
||
/// window with zeros (PyroWave datagram-aligned mode, design/pyrowave-codec-plan.md §4.4). Two
|
||
/// consequences: a receiver that opted into partial delivery can use an aged-out frame's buffer
|
||
/// AS-IS (missing shards stay zeroed; the codec's block walk skips zero windows), and even a
|
||
/// COMPLETE frame must be consumed window-by-window (the padding is not part of the stream).
|
||
pub const USER_FLAG_CHUNK_ALIGNED: u32 = 0x40;
|
||
|
||
/// `user_flags` bit: this AU was packetized as a **slice-streamed** frame (the P2 slice
|
||
/// pipeline): its sentinel blocks (`block_count == 0`) are SLICE-granularity and carry their
|
||
/// shard-aligned BASE byte offset in `frame_bytes` (the legacy fixed-geometry sentinel is the
|
||
/// degenerate base-0 case), and its FINAL block's base derives from the totals as
|
||
/// `(total_data_shards − final_data_shards) × shard_bytes` — variable-size blocks tile the
|
||
/// frame in shard units, so the uniform-geometry offset formula does not apply to ANY of its
|
||
/// blocks. On every packet of the AU (not just sentinels) because reorder can deliver the final
|
||
/// block first and its placement rule differs. Only emitted toward peers advertising
|
||
/// [`VIDEO_CAP_STREAMED_AU`](crate::quic::VIDEO_CAP_STREAMED_AU) ∧
|
||
/// [`VIDEO_CAP_MULTI_SLICE`](crate::quic::VIDEO_CAP_MULTI_SLICE) — the pair whose receivers
|
||
/// know this contract.
|
||
pub const USER_FLAG_SLICE_STREAM: u32 = 0x80;
|
||
|
||
/// Widest lost-frame range (frames, wrapping `last - first`) a reference-frame-invalidation
|
||
/// recovery may be asked to repair; anything wider goes straight to the keyframe path on BOTH
|
||
/// ends. RFI can only re-reference history the encoder still holds — NVENC keeps a 5-frame DPB,
|
||
/// AMD LTR ~1 s of marks — and a genuine loss this wide (>1 s even at 240 fps) has no valid
|
||
/// reference anywhere, so an RFI request for it is either hopeless or (worse) a phantom range
|
||
/// from a desynced counter. Shared by the host's RFI dispatch (range → keyframe fallback) and the
|
||
/// client-side gap detectors (huge gap → resync + keyframe request, no RFI).
|
||
pub const RFI_MAX_RANGE: u32 = 256;
|
||
|
||
/// Crypto framing overhead [`Session`](crate::session::Session) adds when encrypting:
|
||
/// an 8-byte sequence prefix plus the GCM tag.
|
||
pub const CRYPTO_OVERHEAD: usize = 8 + crate::crypto::TAG_LEN;
|
||
|
||
/// Largest UDP datagram the core will send or accept. `Config::validate` bounds
|
||
/// `shard_payload` so `HEADER_LEN + shard_payload + CRYPTO_OVERHEAD ≤ MAX_DATAGRAM_BYTES`.
|
||
///
|
||
/// Sized for **jumbo frames** (design/shard-payload-reneg.md W0.2): a 9000-MTU LAN carries
|
||
/// ~8908-byte shards (sealed 8972-byte UDP payloads), and every receive path — the transport
|
||
/// `RECV_BUF`, the session's `recvmmsg` ring — is sized from this constant, so a deployed
|
||
/// client can accept a jumbo geometry the moment its host negotiates one. The ring cost is
|
||
/// 128 × ~9 KiB ≈ 1.1 MiB per **client** session (lazily allocated on first poll; hosts never
|
||
/// allocate it) — measured against the ~256 KiB it was at 2048, an acceptable static price
|
||
/// for never having to resize buffers on a mid-session grow. Senders still derive their
|
||
/// shard payload from the path MTU (`config::mtu1500_shard_payload*`, the wire-MTU clamps);
|
||
/// this is the acceptance ceiling, not a transmit size.
|
||
pub const MAX_DATAGRAM_BYTES: usize = 9216;
|
||
|
||
/// Fixed per-packet header. `#[repr(C)]`, no padding, zero-copy (de)serializable.
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, KnownLayout, Immutable)]
|
||
pub struct PacketHeader {
|
||
pub pts_ns: u64,
|
||
pub frame_index: u32,
|
||
pub stream_seq: u32,
|
||
pub frame_bytes: u32,
|
||
pub user_flags: u32,
|
||
pub block_index: u16,
|
||
pub block_count: u16,
|
||
pub data_shards: u16,
|
||
pub recovery_shards: u16,
|
||
pub shard_index: u16,
|
||
pub shard_bytes: u16,
|
||
pub magic: u8,
|
||
pub version: u8,
|
||
pub fec_scheme: u8,
|
||
pub flags: u8,
|
||
}
|
||
|
||
/// Size of [`PacketHeader`] on the wire (40 bytes).
|
||
pub const HEADER_LEN: usize = std::mem::size_of::<PacketHeader>();
|
||
|
||
const _: () = assert!(HEADER_LEN == 40, "PacketHeader must be 40 bytes / unpadded");
|