refactor(core/W7): split packet.rs into packet/ facade + submodules
Turn the 1446-line packet.rs into a packet/ directory module (mod.rs facade + header/packetize/reassemble/tests) behind glob re-exports, so every crate::packet::X path stays byte-stable. Pure move: the header consts + PacketHeader -> header.rs; Packetizer -> packetize.rs; the Reassembler cluster (kept WHOLE -- disjoint-borrow hot path) + loss-window consts -> reassemble.rs; the inline #[cfg(test)] block -> tests.rs. Sole visibility change: LOSS_WINDOW_NS -> pub(super) (a test imports it). No behavior change. Verified on both platforms from a clean HEAD snapshot: Linux clippy (--features quic and --no-default-features, --all-targets -D warnings) + full cargo test; Windows clippy (both feature sets) + cargo test --lib (156 pass). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
//! Host side: split an access unit into FEC-protected shard packets.
|
||||
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::error::{PunktfunkError, Result};
|
||||
use crate::fec::ErasureCoder;
|
||||
use zerocopy::IntoBytes;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Host side: packetization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Splits encoded access units into FEC-protected shard packets. Host-side only.
|
||||
///
|
||||
/// Frame numbering: a caller can pass an **explicit** `frame_index` to
|
||||
/// [`packetize_each`](Self::packetize_each) (the punktfunk/1 encode loop owns the video numbering
|
||||
/// so the encoder's reference-frame-invalidation bookkeeping stays 1:1 with the wire across
|
||||
/// encoder rebuilds/resets), or pass `None` to draw from the internal counter (the legacy path —
|
||||
/// synthetic/spike/ABI sessions where no encoder cares). Speed-test probe filler draws from a
|
||||
/// **separate** index space ([`alloc_probe_index`](Self::alloc_probe_index)) so a burst never
|
||||
/// consumes video indexes — see [`crate::quic::VIDEO_CAP_PROBE_SEQ`].
|
||||
pub struct Packetizer {
|
||||
next_frame_index: u32,
|
||||
/// Probe-space frame counter (see [`alloc_probe_index`](Self::alloc_probe_index)).
|
||||
next_probe_index: u32,
|
||||
next_seq: u32,
|
||||
shard_payload: usize,
|
||||
fec: crate::config::FecConfig,
|
||||
version: u8,
|
||||
/// Reusable zero-padded scratch for the frame's final data shard when the frame isn't an
|
||||
/// exact `shard_payload` multiple (and for the single all-zero shard of an empty frame).
|
||||
/// Every other data shard is a `shard_payload`-sized slice straight into the frame buffer —
|
||||
/// blocks are consecutive shard ranges, so only the frame's last shard can be partial.
|
||||
tail: Vec<u8>,
|
||||
/// Reusable parity buffers for [`ErasureCoder::encode_into`] (plan Phase 1.4): grows once
|
||||
/// to the session's high-water recovery count, then every block's parity is generated
|
||||
/// into it with zero allocations.
|
||||
recovery: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Packetizer {
|
||||
pub fn new(config: &Config) -> Self {
|
||||
Packetizer {
|
||||
next_frame_index: 0,
|
||||
next_probe_index: 0,
|
||||
next_seq: 0,
|
||||
shard_payload: config.shard_payload,
|
||||
fec: config.fec,
|
||||
version: config.phase as u8,
|
||||
tail: Vec::new(),
|
||||
recovery: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Allocate the next **probe-space** frame index (speed-test filler). A separate counter from
|
||||
/// the video `frame_index`es so a multi-thousand-AU probe burst never advances the video
|
||||
/// numbering — the client routes [`FLAG_PROBE`]-flagged shards into its own reassembly window
|
||||
/// (see [`Reassembler`]), so the two spaces never collide. Only used against clients that
|
||||
/// advertise [`crate::quic::VIDEO_CAP_PROBE_SEQ`].
|
||||
pub fn alloc_probe_index(&mut self) -> u32 {
|
||||
let i = self.next_probe_index;
|
||||
self.next_probe_index = i.wrapping_add(1);
|
||||
i
|
||||
}
|
||||
|
||||
/// Live-adjust the FEC recovery percentage (adaptive FEC). Takes effect on the next
|
||||
/// [`packetize`](Self::packetize); the wire is self-describing (each packet carries its block's
|
||||
/// data/recovery counts), so the receiver needs no notification. Clamped to ≤ 90.
|
||||
pub fn set_fec_percent(&mut self, pct: u8) {
|
||||
self.fec.fec_percent = pct.min(90);
|
||||
}
|
||||
|
||||
/// The current FEC recovery percentage.
|
||||
pub fn fec_percent(&self) -> u8 {
|
||||
self.fec.fec_percent
|
||||
}
|
||||
|
||||
/// Packetize one access unit into owned wire packets (header ++ shard payload each).
|
||||
/// Thin wrapper over [`packetize_each`](Self::packetize_each) — the allocation-free
|
||||
/// streaming path's reference implementation (tests and the loss harness use this).
|
||||
pub fn packetize(
|
||||
&mut self,
|
||||
frame: &[u8],
|
||||
pts_ns: u64,
|
||||
user_flags: u32,
|
||||
coder: &dyn ErasureCoder,
|
||||
) -> Result<Vec<Vec<u8>>> {
|
||||
let mut packets = Vec::new();
|
||||
self.packetize_each(frame, pts_ns, user_flags, None, coder, |hdr, body| {
|
||||
let mut pkt = Vec::with_capacity(HEADER_LEN + body.len());
|
||||
pkt.extend_from_slice(hdr.as_bytes());
|
||||
pkt.extend_from_slice(body);
|
||||
packets.push(pkt);
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(packets)
|
||||
}
|
||||
|
||||
/// Packetize one access unit, yielding each packet to `emit` as a `(header, shard bytes)`
|
||||
/// pair — in exact wire order, which is also the order the session's nonce counter
|
||||
/// advances. No per-packet allocation happens here, so the caller can write header and
|
||||
/// shard straight into a pooled wire buffer and seal in place
|
||||
/// ([`Session::seal_frame`](crate::session::Session::seal_frame)). An `emit` error aborts
|
||||
/// the frame mid-way (packet numbering has already advanced — callers treat it as fatal).
|
||||
///
|
||||
/// `frame_index`: `Some(i)` stamps the AU with the caller's index — the punktfunk/1 encode
|
||||
/// loop numbers video AUs itself so the encoder's RFI bookkeeping (LTR marks, DPB timestamps)
|
||||
/// is 1:1 with what the client sees, surviving encoder rebuilds/resets that restart internal
|
||||
/// counters. `None` draws from the internal counter (the legacy/self-numbering path). A
|
||||
/// session must not mix the two styles for the same index space.
|
||||
pub fn packetize_each(
|
||||
&mut self,
|
||||
frame: &[u8],
|
||||
pts_ns: u64,
|
||||
user_flags: u32,
|
||||
frame_index: Option<u32>,
|
||||
coder: &dyn ErasureCoder,
|
||||
mut emit: impl FnMut(&PacketHeader, &[u8]) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let payload = self.shard_payload;
|
||||
let frame_index = frame_index.unwrap_or_else(|| {
|
||||
let i = self.next_frame_index;
|
||||
self.next_frame_index = i.wrapping_add(1);
|
||||
i
|
||||
});
|
||||
|
||||
// At least one (zero-padded) data shard even for an empty frame.
|
||||
let total_data = frame.len().div_ceil(payload).max(1);
|
||||
let max_block = self.fec.max_data_per_block as usize;
|
||||
let block_count = total_data.div_ceil(max_block).max(1);
|
||||
let frame_bytes = frame.len() as u32;
|
||||
|
||||
// Defend the u16 wire fields against silent truncation. `Config::validate`
|
||||
// already rejects configs that could reach these for valid frame sizes; this is
|
||||
// the belt-and-suspenders for a frame larger than the negotiated maximum.
|
||||
if payload > u16::MAX as usize {
|
||||
return Err(PunktfunkError::InvalidArg("shard_payload exceeds u16"));
|
||||
}
|
||||
if block_count > u16::MAX as usize {
|
||||
return Err(PunktfunkError::Unsupported(
|
||||
"frame too large: block count exceeds u16",
|
||||
));
|
||||
}
|
||||
|
||||
// Stage the frame's one possibly-partial shard (the last) in the reusable
|
||||
// zero-padded scratch; every full shard is referenced in place below.
|
||||
let full_shards = frame.len() / payload;
|
||||
self.tail.clear();
|
||||
self.tail.resize(payload, 0);
|
||||
let rem = frame.len() % payload;
|
||||
if rem > 0 {
|
||||
self.tail[..rem].copy_from_slice(&frame[full_shards * payload..]);
|
||||
}
|
||||
let tail = &self.tail;
|
||||
let recovery_pool = &mut self.recovery;
|
||||
let shard_at = |s: usize| -> &[u8] {
|
||||
if s < full_shards {
|
||||
&frame[s * payload..(s + 1) * payload]
|
||||
} else {
|
||||
tail.as_slice()
|
||||
}
|
||||
};
|
||||
|
||||
for b in 0..block_count {
|
||||
let first = b * max_block;
|
||||
let last = ((b + 1) * max_block).min(total_data);
|
||||
let block_data_count = last - first;
|
||||
|
||||
// This block's data shards: references into `frame` (plus the staged tail).
|
||||
let data_shards: Vec<&[u8]> = (first..last).map(shard_at).collect();
|
||||
|
||||
let recovery_count = self.fec.recovery_for(block_data_count);
|
||||
coder.encode_into(&data_shards, recovery_count, recovery_pool)?;
|
||||
let recovery = &*recovery_pool;
|
||||
let total_shards = block_data_count + recovery_count;
|
||||
if total_shards > u16::MAX as usize {
|
||||
return Err(PunktfunkError::Unsupported("block shard count exceeds u16"));
|
||||
}
|
||||
|
||||
for shard_index in 0..total_shards {
|
||||
let body: &[u8] = if shard_index < block_data_count {
|
||||
data_shards[shard_index]
|
||||
} else {
|
||||
&recovery[shard_index - block_data_count]
|
||||
};
|
||||
|
||||
let seq = self.next_seq;
|
||||
self.next_seq = self.next_seq.wrapping_add(1);
|
||||
|
||||
let mut flags = FLAG_PIC;
|
||||
if b == 0 && shard_index == 0 {
|
||||
flags |= FLAG_SOF;
|
||||
}
|
||||
if b + 1 == block_count && shard_index + 1 == total_shards {
|
||||
flags |= FLAG_EOF;
|
||||
}
|
||||
|
||||
let hdr = PacketHeader {
|
||||
pts_ns,
|
||||
frame_index,
|
||||
stream_seq: seq,
|
||||
frame_bytes,
|
||||
user_flags,
|
||||
block_index: b as u16,
|
||||
block_count: block_count as u16,
|
||||
data_shards: block_data_count as u16,
|
||||
recovery_shards: recovery_count as u16,
|
||||
shard_index: shard_index as u16,
|
||||
shard_bytes: payload as u16,
|
||||
magic: PUNKTFUNK_MAGIC,
|
||||
version: self.version,
|
||||
fec_scheme: coder.scheme() as u8,
|
||||
flags,
|
||||
};
|
||||
emit(&hdr, body)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user