perf(latency): tier-0 attribution + tier-1 send-path levers from the latency plan
ci / web (push) Successful in 49s
ci / docs-site (push) Successful in 54s
decky / build-publish (push) Successful in 18s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
apple / swift (push) Successful in 1m23s
ci / rust (push) Failing after 4m15s
arch / build-publish (push) Failing after 4m35s
deb / build-publish (push) Failing after 4m4s
docker / deploy-docs (push) Successful in 24s
ci / bench (push) Successful in 5m35s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 3m45s
windows-host / package (push) Failing after 9m8s
release / apple (push) Successful in 5m49s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 5m25s
flatpak / build-publish (push) Failing after 8m3s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m58s
android / android (push) Successful in 14m9s
apple / screenshots (push) Successful in 6m28s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 7m0s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 9m28s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 12m26s

design/latency-reduction-2026-07.md T0.1/T0.2/T1.2/T1.3:

- T1.2 rate-capped front-loaded pacing: the paced overflow's budget is now
  min(0.9x slack, overflow wire time at ~3x the live encoder bitrate)
  (PUNKTFUNK_PACE_FACTOR, 0 = legacy deadline-only spread). A 300 KB-1 MB
  frame's tail leaves in ~2-5 ms instead of smearing across ~15 ms at 60 fps;
  GameStream schedule byte-identical (pins unchanged).
- T1.3 data-first wire order: packetize emits every block's data shards before
  any parity (per-block parity pools keep all blocks' parity alive for the
  second pass), so lossless completion stops waiting behind the parity tail.
  EOF = last emitted packet; receiver already order-agnostic.
- T0.1 staged 0xCF: HostTiming gains an append-extensible per-stage tail
  (queue/encode/pace us; seal+channel-wait derived as residual) - no cap bit
  needed, old peers read the 13-byte prefix. Joined client-side into
  Stats::host_{queue,encode,xfer,pace}_ms, the OSD detailed tier, and the
  probe's report.
- T0.2 true on-glass present timing: VK_KHR_present_id/present_wait enabled
  when supported; a PresentTimer waiter thread resolves each present id to
  real visibility, replacing the submit-time display stamp (which undercounts
  by up to a refresh and hides a silent-FIFO standing queue).

Validated on .21: core 185 + host 185 tests, pf-presenter 19, clippy
-D warnings across all five touched crates; loss-harness recovery curve
unchanged; C ABI harness round-trips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 19:12:36 +02:00
parent c28b10a5b9
commit aedee2a4e3
15 changed files with 779 additions and 94 deletions
+77 -38
View File
@@ -32,10 +32,13 @@ pub struct Packetizer {
/// 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>>,
/// Reusable PER-BLOCK parity buffers for [`ErasureCoder::encode_into`] (plan Phase 1.4):
/// one pool per block index, each growing once to its high-water recovery count, then
/// every frame's parity is generated into them with zero allocations. Per-block (not one
/// shared pool) because the data-first wire order (latency plan T1.3) emits every block's
/// DATA shards before any block's parity — all blocks' parity must stay alive until the
/// frame's second emission pass.
recovery: Vec<Vec<Vec<u8>>>,
}
impl Packetizer {
@@ -103,6 +106,15 @@ impl Packetizer {
/// ([`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).
///
/// Wire order is DATA-FIRST (latency plan T1.3): every block's data shards in block order,
/// then every block's parity shards in block order. In the lossless case a frame completes
/// the moment its last DATA shard arrives, so the completion-gating packet no longer sits
/// behind the parity tail of the paced spread (~fec% of the spread saved). The receiver is
/// order-agnostic (headers are self-describing; the reassembler completes each block on
/// `data + recovery ≥ k`), so this is not a wire-format change. `FLAG_SOF` stays on the
/// first emitted packet (block 0, shard 0); `FLAG_EOF` marks the last emitted packet —
/// the final parity shard, or the final data shard of a FEC-free frame.
///
/// `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
@@ -152,7 +164,6 @@ impl Packetizer {
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]
@@ -160,41 +171,30 @@ impl Packetizer {
tail.as_slice()
}
};
// Per-block shard geometry (deterministic — recomputed in both passes).
let block_data_count = |b: usize| ((b + 1) * max_block).min(total_data) - b * max_block;
// One parity pool per block, reused across frames (steady-state zero-alloc).
if self.recovery.len() < block_count {
self.recovery.resize_with(block_count, Vec::new);
}
// Total parity across the frame decides where FLAG_EOF lands (the last emitted packet).
let mut total_recovery = 0usize;
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 {
let k = block_data_count(b);
let m = self.fec.recovery_for(k);
if k + m > u16::MAX as usize {
return Err(PunktfunkError::Unsupported("block shard count exceeds u16"));
}
total_recovery += m;
}
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 mut emit_one =
|next_seq: &mut u32, b: usize, shard_index: usize, body: &[u8], flags: u8| {
let seq = *next_seq;
*next_seq = next_seq.wrapping_add(1);
let k = block_data_count(b);
let hdr = PacketHeader {
pts_ns,
frame_index,
@@ -203,8 +203,8 @@ impl Packetizer {
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,
data_shards: k as u16,
recovery_shards: self.fec.recovery_for(k) as u16,
shard_index: shard_index as u16,
shard_bytes: payload as u16,
magic: PUNKTFUNK_MAGIC,
@@ -212,9 +212,48 @@ impl Packetizer {
fec_scheme: coder.scheme() as u8,
flags,
};
emit(&hdr, body)?;
emit(&hdr, body)
};
let mut next_seq = self.next_seq;
// Pass 1 — per block: generate parity into the block's pool, emit the DATA shards.
for b in 0..block_count {
let first = b * max_block;
let k = block_data_count(b);
// This block's data shards: references into `frame` (plus the staged tail).
let data_shards: Vec<&[u8]> = (first..first + k).map(shard_at).collect();
let recovery_count = self.fec.recovery_for(k);
coder.encode_into(&data_shards, recovery_count, &mut self.recovery[b])?;
for (shard_index, body) in data_shards.iter().enumerate() {
let mut flags = FLAG_PIC;
if b == 0 && shard_index == 0 {
flags |= FLAG_SOF;
}
if total_recovery == 0 && b + 1 == block_count && shard_index + 1 == k {
flags |= FLAG_EOF;
}
emit_one(&mut next_seq, b, shard_index, body, flags)?;
}
}
// Pass 2 — per block: emit the parity shards (the frame's tail on the wire).
let mut parity_left = total_recovery;
for b in 0..block_count {
let k = block_data_count(b);
let recovery_count = self.fec.recovery_for(k);
for r in 0..recovery_count {
parity_left -= 1;
let mut flags = FLAG_PIC;
if parity_left == 0 {
flags |= FLAG_EOF;
}
let body: &[u8] = &self.recovery[b][r];
emit_one(&mut next_seq, b, k + r, body, flags)?;
}
}
self.next_seq = next_seq;
Ok(())
}
}
+83 -5
View File
@@ -370,16 +370,94 @@ fn e2e_roundtrip(
/// 100 bytes / 16 = 7 shards → blocks of (4 data + 2 rec) and (3 data + 2 rec).
#[test]
fn e2e_multiblock_loss_reorder_dup_gf16() {
// Packet order: blk0 = idx 0..6 (4 data + 2 rec), blk1 = idx 6..11 (3 data + 2 rec).
// Data-first wire order (T1.3): blk0 data = idx 0..4, blk1 data = idx 4..7,
// blk0 rec = idx 7..9, blk1 rec = idx 9..11.
// Kill 2 data in block 0 and 1 data in block 1 — all within the 50% budget.
e2e_roundtrip(FecScheme::Gf16, 100, 50, &[0, 2, 7], false);
e2e_roundtrip(FecScheme::Gf16, 100, 50, &[0, 2, 7], true);
e2e_roundtrip(FecScheme::Gf16, 100, 50, &[0, 2, 5], false);
e2e_roundtrip(FecScheme::Gf16, 100, 50, &[0, 2, 5], true);
}
#[test]
fn e2e_multiblock_loss_reorder_dup_gf8() {
e2e_roundtrip(FecScheme::Gf8, 100, 50, &[1, 3, 8], false);
e2e_roundtrip(FecScheme::Gf8, 100, 50, &[1, 3, 8], true);
e2e_roundtrip(FecScheme::Gf8, 100, 50, &[1, 3, 6], false);
e2e_roundtrip(FecScheme::Gf8, 100, 50, &[1, 3, 6], true);
}
/// T1.3 pin: the wire order is DATA-FIRST — every block's data shards in block order, then
/// every block's parity in block order — so the lossless-completion-gating packet (the last
/// data shard) never sits behind parity in the paced spread. SOF on the first emitted packet,
/// EOF on the last (a parity shard whenever the frame carries FEC).
#[test]
fn packetize_emits_all_data_before_any_parity() {
use zerocopy::FromBytes;
let cfg = e2e_config(FecScheme::Gf16, 50);
let coder = coder_for(FecScheme::Gf16);
let mut pk = Packetizer::new(&cfg);
// 100 B / 16 → 7 data shards → blocks (4 data + 2 rec) + (3 data + 2 rec).
let src: Vec<u8> = (0..100).map(|i| (i * 31 + 3) as u8).collect();
let pkts = pk.packetize(&src, 1, 0, coder.as_ref()).unwrap();
assert_eq!(pkts.len(), 11);
let hdrs: Vec<PacketHeader> = pkts
.iter()
.map(|p| PacketHeader::read_from_bytes(&p[..HEADER_LEN]).unwrap())
.collect();
// (block_index, shard_index) in emission order.
let layout: Vec<(u16, u16)> = hdrs
.iter()
.map(|h| (h.block_index, h.shard_index))
.collect();
assert_eq!(
layout,
vec![
(0, 0),
(0, 1),
(0, 2),
(0, 3), // blk0 data
(1, 0),
(1, 1),
(1, 2), // blk1 data
(0, 4),
(0, 5), // blk0 parity
(1, 3),
(1, 4), // blk1 parity
],
"data-first wire order"
);
// A shard is parity iff shard_index >= data_shards; no parity may precede any data.
let first_parity = hdrs
.iter()
.position(|h| h.shard_index >= h.data_shards)
.unwrap();
assert!(
hdrs[first_parity..]
.iter()
.all(|h| h.shard_index >= h.data_shards),
"no data shard after the first parity shard"
);
// Stream seqs stay strictly sequential in emission order (the nonce contract).
for (i, w) in hdrs.windows(2).enumerate() {
assert_eq!(w[1].stream_seq, w[0].stream_seq + 1, "seq gap at {i}");
}
assert_eq!(hdrs[0].flags & FLAG_SOF, FLAG_SOF, "SOF on first packet");
assert_eq!(
hdrs.last().unwrap().flags & FLAG_EOF,
FLAG_EOF,
"EOF on last (parity) packet"
);
assert_eq!(
hdrs.iter().filter(|h| h.flags & FLAG_EOF != 0).count(),
1,
"exactly one EOF"
);
// FEC-free frame: EOF falls on the last data shard instead.
let cfg0 = e2e_config(FecScheme::Gf16, 0);
let mut pk0 = Packetizer::new(&cfg0);
let pkts0 = pk0.packetize(&src, 2, 0, coder.as_ref()).unwrap();
assert_eq!(pkts0.len(), 7, "no parity at 0% FEC");
let last = PacketHeader::read_from_bytes(&pkts0.last().unwrap()[..HEADER_LEN]).unwrap();
assert_eq!(last.flags & FLAG_EOF, FLAG_EOF, "EOF on last data shard");
assert!(last.shard_index < last.data_shards, "last packet is data");
}
/// Zero losses, in order: the pure fast path (no codec call, recovered == 0) must still
+40 -5
View File
@@ -545,28 +545,63 @@ pub struct HostTiming {
/// Host capture→sent duration, µs (saturated at `u32::MAX` ≈ 71 min — far past the 10 s
/// client-side sanity clamp anyway).
pub host_us: u32,
/// Per-stage split of `host_us` (latency plan T0.1). `None` from a host that predates the
/// extended datagram — the 0xCF wire is APPEND-extensible (decode reads the 13-byte prefix
/// and takes the stage tail only when present), so no capability bit is needed in either
/// direction: old client + new host reads the prefix, new client + old host gets `None`.
pub stages: Option<HostStages>,
}
/// Wire length of a [`HOST_TIMING_MAGIC`] datagram: tag + u64 pts + u32 µs = 13 bytes.
const HOST_TIMING_LEN: usize = 1 + 8 + 4;
/// The extended 0xCF's per-stage split of [`HostTiming::host_us`], all µs against the same
/// capture anchor. The stages tile the host pipeline as
/// `host_us = queue + encode + (seal/FEC + channel-wait = the residual) + pace`, so the client
/// derives the residual as `host_us queue_us encode_us pace_us` — no fifth field needed.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HostStages {
/// Capture delivery → encoder submit (the capture ring / channel-queue age; 0 for
/// re-encoded hold frames, which never waited).
pub queue_us: u32,
/// Encoder submit → bitstream ready (scheduling wait + ASIC time).
pub encode_us: u32,
/// Paced send: first byte handed to the socket → last packet sent (the microburst spread).
pub pace_us: u32,
}
/// Encode a [`HostTiming`] into a [`HOST_TIMING_MAGIC`] datagram.
/// Wire length of a legacy [`HOST_TIMING_MAGIC`] datagram: tag + u64 pts + u32 µs = 13 bytes.
const HOST_TIMING_LEN: usize = 1 + 8 + 4;
/// Wire length with the [`HostStages`] tail appended: + 3 × u32 = 25 bytes.
const HOST_TIMING_STAGES_LEN: usize = HOST_TIMING_LEN + 12;
/// Encode a [`HostTiming`] into a [`HOST_TIMING_MAGIC`] datagram (extended form when `stages`
/// is set — an older client parses the prefix and ignores the tail).
pub fn encode_host_timing_datagram(t: &HostTiming) -> Vec<u8> {
let mut b = Vec::with_capacity(HOST_TIMING_LEN);
let mut b = Vec::with_capacity(HOST_TIMING_STAGES_LEN);
b.push(HOST_TIMING_MAGIC);
b.extend_from_slice(&t.pts_ns.to_le_bytes());
b.extend_from_slice(&t.host_us.to_le_bytes());
if let Some(s) = &t.stages {
b.extend_from_slice(&s.queue_us.to_le_bytes());
b.extend_from_slice(&s.encode_us.to_le_bytes());
b.extend_from_slice(&s.pace_us.to_le_bytes());
}
b
}
/// Parse a [`HOST_TIMING_MAGIC`] datagram → [`HostTiming`]. `None` on bad tag or a short buffer
/// (the fixed length bounds every read before it happens).
/// (the fixed lengths bound every read before it happens). A datagram carrying only the 13-byte
/// prefix (an older host) yields `stages: None`.
pub fn decode_host_timing_datagram(b: &[u8]) -> Option<HostTiming> {
if b.len() < HOST_TIMING_LEN || b[0] != HOST_TIMING_MAGIC {
return None;
}
let stages = (b.len() >= HOST_TIMING_STAGES_LEN).then(|| HostStages {
queue_us: u32::from_le_bytes(b[13..17].try_into().unwrap()),
encode_us: u32::from_le_bytes(b[17..21].try_into().unwrap()),
pace_us: u32::from_le_bytes(b[21..25].try_into().unwrap()),
});
Some(HostTiming {
pts_ns: u64::from_le_bytes(b[1..9].try_into().unwrap()),
host_us: u32::from_le_bytes(b[9..13].try_into().unwrap()),
stages,
})
}
+28
View File
@@ -266,6 +266,7 @@ fn host_timing_datagram_roundtrip_and_truncation() {
let t = HostTiming {
pts_ns: 1_751_500_000_123_456_789, // a realistic 2026 CLOCK_REALTIME capture stamp
host_us: 4_321,
stages: None,
};
let d = encode_host_timing_datagram(&t);
assert_eq!(d[0], HOST_TIMING_MAGIC);
@@ -278,6 +279,33 @@ fn host_timing_datagram_roundtrip_and_truncation() {
let mut bad = d.clone();
bad[0] = HDR_META_MAGIC;
assert_eq!(decode_host_timing_datagram(&bad), None);
// Extended form (T0.1): the stage tail roundtrips; a truncated tail (an old host's 13-byte
// datagram, or anything short of the full 25) degrades to `stages: None`, never a partial
// read; the prefix fields stay identical in both forms (the append-extensibility contract).
let ts = HostTiming {
stages: Some(HostStages {
queue_us: 900,
encode_us: 3_100,
pace_us: 2_500,
}),
..t
};
let ds = encode_host_timing_datagram(&ts);
assert_eq!(ds.len(), 25);
assert_eq!(
&ds[..13],
&d[..13],
"prefix is byte-identical to the legacy form"
);
assert_eq!(decode_host_timing_datagram(&ds), Some(ts));
for n in 13..ds.len() {
assert_eq!(
decode_host_timing_datagram(&ds[..n]),
Some(t),
"partial stage tail ({n} B) must degrade to the legacy decode"
);
}
}
#[test]