perf(latency): T1.1 frame-driven encode trigger + T1.4 time-based flush thresholds
ci / web (push) Successful in 1m11s
ci / docs-site (push) Successful in 1m14s
apple / swift (push) Successful in 1m24s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
decky / build-publish (push) Successful in 22s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 25s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 25s
docker / deploy-docs (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
release / apple (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
windows-host / package (push) Has been cancelled
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Has been cancelled
windows / build (aarch64-pc-windows-msvc) (push) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
deb / build-publish (push) Failing after 4m39s
arch / build-publish (push) Failing after 4m46s
ci / rust (push) Failing after 4m53s
ci / bench (push) Successful in 6m8s
android / android (push) Successful in 13m40s
apple / screenshots (push) Successful in 6m26s

design/latency-reduction-2026-07.md tier 1, remaining halves:

- T1.1: the native encode loop wakes on the capture's ACTUAL arrival instead of
  sampling at a free-running tick — deletes the sample-and-hold (~half a frame
  interval on average, a full one worst-case: ~8ms avg @60fps). New
  Capturer::supports_arrival_wait/wait_arrival pair (IDD-push waits its
  frame-ready event against the shared-header token; the PipeWire portal blocks
  its channel with a pending stash); backends without an arrival signal — and
  PUNKTFUNK_FRAME_DRIVEN=0 — keep the legacy tick bit-identically. A
  0.9x-interval rate floor caps encode at ~1.11x target when the compositor
  outruns the session; a +0.5x-interval keepalive keeps static desktops
  re-encoding at 1.5x-interval cadence. Pacing deadlines re-anchor to the
  actual submit so they can't drift against the arrival clock. GameStream
  plane untouched.
- T1.4: the jump-to-live detectors run on WALL-CLOCK now (STANDING_TIME /
  FLUSH_AFTER = 250ms) instead of 30-frame counts whose meaning scaled with
  fps (500ms @60 but 125ms @240 — and stretching further under T1.1's slower
  static-scene repeats). The queue trip also requires depth still >= high, so
  a hysteresis-band hover can't fire on elapsed time alone.

Validated: .21 Linux 185 core + 177 host + pf-capture tests, clippy
-D warnings; .133 Windows cargo check of pf-capture + punktfunk-host green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 19:31:49 +02:00
parent aedee2a4e3
commit bbe4380b41
6 changed files with 152 additions and 41 deletions
+34 -4
View File
@@ -193,6 +193,14 @@ fn service_probes(
}
}
/// T1.1 frame-driven encode trigger (latency plan): `PUNKTFUNK_FRAME_DRIVEN=0` restores the
/// legacy fixed-cadence tick everywhere (backends without an arrival wait keep it regardless —
/// see [`pf_capture::Capturer::supports_arrival_wait`]).
fn frame_driven_enabled() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ON.get_or_init(|| std::env::var("PUNKTFUNK_FRAME_DRIVEN").as_deref() != Ok("0"))
}
/// Seal one access unit and send it with MICROBURST pacing (the shared
/// [`send_pacing`](crate::send_pacing) policy, native parameterization): the first `burst_cap`
/// bytes go out immediately (one absorbed burst the NIC / socket tx-buffer can swallow), and
@@ -1784,7 +1792,14 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
}
// This frame's pacing deadline (the next frame's due time); the send thread spreads a big frame
// up to here. Each in-flight frame carries its own (capture_ns, deadline) for when it's polled.
next += interval;
// Frame-driven mode (T1.1) re-anchors to the ACTUAL submit — arrivals are the clock, and a
// fixed `+= interval` grid would drift against them and squeeze the pacing budget; the
// legacy tick keeps its fixed grid (with the catch-up reset in the tail).
next = if frame_driven_enabled() && capturer.supports_arrival_wait() {
std::time::Instant::now() + interval
} else {
next + interval
};
inflight.push_back((capture_ns, submit_ns, next));
// Drain the OLDEST in-flight frames, keeping at most depth-1 deferred. At depth 1 this polls
// immediately after every submit (synchronous); at depth 2 it polls N right after submitting N+1,
@@ -1919,9 +1934,24 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
"encode stall detected — encoder rebuilt in place, forcing an IDR");
last_au_at = std::time::Instant::now();
}
match next.checked_duration_since(std::time::Instant::now()) {
Some(d) => std::thread::sleep(d),
None => next = std::time::Instant::now(),
if frame_driven_enabled() && capturer.supports_arrival_wait() {
// T1.1 frame-driven trigger: instead of sleeping out the whole tick and then
// SAMPLING (which holds a frame that arrived just after the previous sample for up
// to a full interval — ~half on average), sleep only to the rate floor and then
// wake on the capture's actual arrival. The 0.9×interval floor caps the encode
// rate at ~1.11× target when the source runs faster (compositor Hz > session fps);
// the +0.5×interval keepalive keeps a static desktop re-encoding (bitrate shape,
// client liveness) at 1.5×interval cadence and bounds control-servicing latency.
let earliest = next - interval.mul_f32(0.1);
if let Some(d) = earliest.checked_duration_since(std::time::Instant::now()) {
std::thread::sleep(d);
}
capturer.wait_arrival(next + interval.mul_f32(0.5));
} else {
match next.checked_duration_since(std::time::Instant::now()) {
Some(d) => std::thread::sleep(d),
None => next = std::time::Instant::now(),
}
}
}
// Drain the in-flight tail (the depth-1 frames submitted but not yet polled) so the last frames still