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
+46
View File
@@ -86,6 +86,17 @@ pub struct Stats {
/// `host + network`. An old host never emits 0xCF, so this stays false and the
/// combined stage renders unchanged.
pub split: bool,
/// p50 host STAGE split (latency plan T0.1), valid only when `staged`: capture→submit
/// queue age, encoder submit→bitstream, seal/FEC + send-channel wait (the residual
/// `host queue encode pace`), and the paced-send spread. Together they tile
/// `host_ms`, giving per-stage attribution without a host-side log in hand.
pub host_queue_ms: f32,
pub host_encode_ms: f32,
pub host_xfer_ms: f32,
pub host_pace_ms: f32,
/// The window had extended (staged) 0xCF timings — a host older than the stage tail
/// sends the 13-byte form and the OSD keeps the plain `host` figure.
pub staged: bool,
/// p50 `decode` stage: received → decode COMPLETE, single-clock client-local (ms).
/// Hardware paths measure GPU completion via the frame's timeline fence (an async
/// decoder's submission returning in ~0.1 ms is not "decoded"); software measures
@@ -361,6 +372,11 @@ fn pump(
std::collections::VecDeque::with_capacity(PENDING_SPLIT_CAP);
let mut host_us_win: Vec<u64> = Vec::with_capacity(256);
let mut net_us_win: Vec<u64> = Vec::with_capacity(256);
// T0.1 host-stage windows (extended 0xCF only; empty against an older host).
let mut queue_us_win: Vec<u64> = Vec::with_capacity(256);
let mut enc_us_win: Vec<u64> = Vec::with_capacity(256);
let mut xfer_us_win: Vec<u64> = Vec::with_capacity(256);
let mut pace_us_win: Vec<u64> = Vec::with_capacity(256);
// What actually decoded the last frame — a VAAPI failure demotes mid-session, so
// this is read off each frame's image variant rather than fixed at startup.
let mut dec_path: &'static str = "";
@@ -658,6 +674,18 @@ fn pump(
let (_, hn_us) = pending_split.remove(i).unwrap();
host_us_win.push(t.host_us as u64);
net_us_win.push(hn_us.saturating_sub(t.host_us as u64));
// Extended 0xCF (T0.1): per-stage host split; the seal/FEC + channel-wait
// residual is derived so the four stages tile host_us exactly.
if let Some(s) = t.stages {
queue_us_win.push(s.queue_us as u64);
enc_us_win.push(s.encode_us as u64);
pace_us_win.push(s.pace_us as u64);
xfer_us_win.push(
(t.host_us as u64).saturating_sub(
s.queue_us as u64 + s.encode_us as u64 + s.pace_us as u64,
),
);
}
}
}
@@ -691,6 +719,11 @@ fn pump(
let split = !host_us_win.is_empty();
let (host_p50, _) = window_percentiles(&mut host_us_win);
let (net_p50, _) = window_percentiles(&mut net_us_win);
let staged = !queue_us_win.is_empty();
let (queue_p50, _) = window_percentiles(&mut queue_us_win);
let (enc_p50, _) = window_percentiles(&mut enc_us_win);
let (xfer_p50, _) = window_percentiles(&mut xfer_us_win);
let (pace_p50, _) = window_percentiles(&mut pace_us_win);
let lost = dropped.saturating_sub(window_dropped) as u32;
window_dropped = dropped;
tracing::debug!(
@@ -698,6 +731,10 @@ fn pump(
hostnet_p50_us = hn_p50,
host_p50_us = host_p50,
net_p50_us = net_p50,
queue_p50_us = queue_p50,
encode_p50_us = enc_p50,
xfer_p50_us = xfer_p50,
pace_p50_us = pace_p50,
decode_p50_us = dec_p50,
lost,
total_frames,
@@ -710,6 +747,11 @@ fn pump(
host_ms: host_p50 as f32 / 1000.0,
net_ms: net_p50 as f32 / 1000.0,
split,
host_queue_ms: queue_p50 as f32 / 1000.0,
host_encode_ms: enc_p50 as f32 / 1000.0,
host_xfer_ms: xfer_p50 as f32 / 1000.0,
host_pace_ms: pace_p50 as f32 / 1000.0,
staged,
decode_ms: dec_p50 as f32 / 1000.0,
lost,
lost_pct: if lost > 0 {
@@ -726,6 +768,10 @@ fn pump(
decode_us.clear();
host_us_win.clear();
net_us_win.clear();
queue_us_win.clear();
enc_us_win.clear();
xfer_us_win.clear();
pace_us_win.clear();
}
};