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
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:
@@ -15,13 +15,16 @@ pub(crate) const QUEUE_HIGH: usize = 6;
|
||||
/// A true standing queue never falls back to this; a clump does within a few frames.
|
||||
pub(crate) const QUEUE_LOW: usize = 2;
|
||||
|
||||
/// Consecutive frames the hand-off queue must sit ≥ [`QUEUE_HIGH`] (never dropping to [`QUEUE_LOW`])
|
||||
/// before the pump declares a standing backlog and jumps to live. ~0.5 s at 60 fps — long enough that
|
||||
/// a burst/clump (which drains in a few frames) never reaches it.
|
||||
pub(crate) const STANDING_FRAMES: u32 = 30;
|
||||
/// How long the hand-off queue must sit ≥ [`QUEUE_HIGH`] (never dropping to [`QUEUE_LOW`], and
|
||||
/// still ≥ high at the trip) before the pump declares a standing backlog and jumps to live.
|
||||
/// WALL-CLOCK (latency plan T1.4) — the old 30-FRAME count made the accepted backlog scale with
|
||||
/// fps (500 ms @60 but 125 ms @240) and stretched further under the frame-driven host's slower
|
||||
/// static-scene repeat cadence. 250 ms sits comfortably above a Wi-Fi clump's drain time (a
|
||||
/// clump empties in a few frames, ≤ ~100 ms) at any rate.
|
||||
pub(crate) const STANDING_TIME: Duration = Duration::from_millis(250);
|
||||
|
||||
/// Memory backstop on the pre-decode hand-off queue. The standing-queue detector jumps to live long
|
||||
/// before this (typically ≤ QUEUE_HIGH + STANDING_FRAMES deep), and a jump already requested a
|
||||
/// before this (typically ≤ QUEUE_HIGH + a [`STANDING_TIME`] run deep), and a jump already requested a
|
||||
/// keyframe, so on the rare path that outruns it (a wedged consumer during the flush cooldown) dropping
|
||||
/// the OLDEST queued AU is safe — the pending IDR re-anchors decode regardless. Purely bounds memory.
|
||||
const FRAME_QUEUE_HARD_CAP: usize = 90;
|
||||
@@ -31,14 +34,15 @@ const FRAME_QUEUE_HARD_CAP: usize = 90;
|
||||
/// AUs and requests a keyframe) instead of playing that far behind forever. Deliberately generous — an
|
||||
/// interactive stream is unusable well before 400 ms, but the bound must sit safely above the skew
|
||||
/// handshake's own error (≈ RTT/2) plus normal delivery jitter so a healthy stream can never trip it.
|
||||
/// This is the CLOCK-BASED detector; the clock-free [`QUEUE_HIGH`]/[`STANDING_FRAMES`] detector covers
|
||||
/// This is the CLOCK-BASED detector; the clock-free [`QUEUE_HIGH`]/[`STANDING_TIME`] detector covers
|
||||
/// same-clock and no-handshake sessions (where `clock_offset_ns == 0` disarms this one).
|
||||
pub(crate) const FLUSH_LATENCY: Duration = Duration::from_millis(400);
|
||||
|
||||
/// How many CONSECUTIVE over-bound frames arm the clock-based jump (~0.5 s at 60 fps). A genuine
|
||||
/// standing queue puts EVERY frame over the bound; a one-off burst (an IDR, a Wi-Fi scan blip) clears
|
||||
/// within a frame or two and never reaches the count.
|
||||
pub(crate) const FLUSH_AFTER_FRAMES: u32 = 30;
|
||||
/// How long frames must run CONTINUOUSLY over the [`FLUSH_LATENCY`] bound before the clock-based
|
||||
/// jump fires. WALL-CLOCK (latency plan T1.4, was a 30-frame count — same fps-scaling problem as
|
||||
/// the standing-queue detector). A genuine standing queue puts EVERY frame over the bound; a
|
||||
/// one-off burst (an IDR, a Wi-Fi scan blip) clears within a frame or two and resets the run.
|
||||
pub(crate) const FLUSH_AFTER: Duration = Duration::from_millis(250);
|
||||
|
||||
/// Minimum spacing between jump-to-live events, so a bottleneck that instantly rebuilds the queue (a
|
||||
/// link/consumer that can't sustain the bitrate at all) degrades into a periodic skip + a logged
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! The client worker: QUIC handshake + control/input/datagram tasks + the blocking data-plane pump.
|
||||
|
||||
use super::frame_channel::{
|
||||
CLOCK_RESYNC_INTERVAL, FLUSH_AFTER_FRAMES, FLUSH_COOLDOWN, FLUSH_LATENCY,
|
||||
NOOP_CLOCK_FLUSHES_TO_DISARM, NOOP_FLUSH_DATAGRAMS, QUEUE_HIGH, QUEUE_LOW, STANDING_FRAMES,
|
||||
CLOCK_RESYNC_INTERVAL, FLUSH_AFTER, FLUSH_COOLDOWN, FLUSH_LATENCY,
|
||||
NOOP_CLOCK_FLUSHES_TO_DISARM, NOOP_FLUSH_DATAGRAMS, QUEUE_HIGH, QUEUE_LOW, STANDING_TIME,
|
||||
};
|
||||
use super::worker::reject_from_close;
|
||||
use super::*;
|
||||
@@ -711,12 +711,13 @@ pub(super) async fn run_pump(args: WorkerArgs) {
|
||||
let mut capacity_probe_deadline: Option<Instant> = None;
|
||||
let (mut owd_sum_ns, mut owd_frames) = (0i128, 0u32);
|
||||
let mut flush_in_window = false;
|
||||
// Jump-to-live state (see the guard in the loop below): the clock-based over-bound run
|
||||
// (`stale_frames`, armed only when the skew handshake succeeded so the clocks are comparable),
|
||||
// the clock-free non-draining-queue run (`standing_frames`), and the last-jump instant for the
|
||||
// shared cooldown.
|
||||
let mut stale_frames: u32 = 0;
|
||||
let mut standing_frames: u32 = 0;
|
||||
// Jump-to-live state (see the guard in the loop below): when the clock-based over-bound
|
||||
// run began (`stale_since`, armed only when the skew handshake succeeded so the clocks
|
||||
// are comparable), when the clock-free non-draining-queue run began (`standing_since`),
|
||||
// and the last-jump instant for the shared cooldown. Wall-clock runs (T1.4), not frame
|
||||
// counts — the detectors' sensitivity must not scale with fps or repeat cadence.
|
||||
let mut stale_since: Option<Instant> = None;
|
||||
let mut standing_since: Option<Instant> = None;
|
||||
let mut last_flush: Option<Instant> = None;
|
||||
// Clock-detector health: consecutive clock-triggered flushes that found no local backlog
|
||||
// (see NOOP_FLUSH_DATAGRAMS). Reaching NOOP_CLOCK_FLUSHES_TO_DISARM turns the clock-based
|
||||
@@ -737,7 +738,7 @@ pub(super) async fn run_pump(args: WorkerArgs) {
|
||||
let gen = pump_clock_gen.load(Ordering::Relaxed);
|
||||
if gen != seen_clock_gen {
|
||||
seen_clock_gen = gen;
|
||||
stale_frames = 0;
|
||||
stale_since = None;
|
||||
noop_clock_flushes = 0;
|
||||
if !clock_detector_armed {
|
||||
clock_detector_armed = true;
|
||||
@@ -956,18 +957,18 @@ pub(super) async fn run_pump(args: WorkerArgs) {
|
||||
// FLUSH_COOLDOWN, both suspended during a speed test (the probe MEASURES a saturated
|
||||
// queue; flushing would corrupt its counters):
|
||||
// * clock-based — completed frames sit > FLUSH_LATENCY behind the skew-corrected
|
||||
// capture clock for FLUSH_AFTER_FRAMES straight. Needs the skew handshake, and
|
||||
// capture clock continuously for FLUSH_AFTER. Needs the skew handshake, and
|
||||
// also catches kernel/reassembler backlog the hand-off queue hasn't reached yet.
|
||||
// * clock-free — the pre-decode hand-off queue stopped draining: its depth stayed
|
||||
// ≥ QUEUE_HIGH (never falling to QUEUE_LOW) for STANDING_FRAMES straight. Works
|
||||
// with no handshake / a same-clock session (where the clock path is disarmed),
|
||||
// and is the direct signal that the embedder can't keep up. A transient Wi-Fi
|
||||
// clump drains in a few frames and never reaches the count.
|
||||
// ≥ QUEUE_HIGH (never falling to QUEUE_LOW, still high at the trip) for
|
||||
// STANDING_TIME. Works with no handshake / a same-clock session (where the
|
||||
// clock path is disarmed), and is the direct signal that the embedder can't
|
||||
// keep up. A transient Wi-Fi clump drains within ~100 ms and never trips it.
|
||||
if probe_active {
|
||||
// Keep both detectors disarmed across a speed test so its (deliberately)
|
||||
// saturated queue doesn't leave a primed count that fires the moment it ends.
|
||||
stale_frames = 0;
|
||||
standing_frames = 0;
|
||||
// saturated queue doesn't leave a primed run that fires the moment it ends.
|
||||
stale_since = None;
|
||||
standing_since = None;
|
||||
} else {
|
||||
let lat_ns = if clock_offset_ns != 0 {
|
||||
now_realtime_ns() + clock_offset_ns as i128 - frame.pts_ns as i128
|
||||
@@ -985,23 +986,27 @@ pub(super) async fn run_pump(args: WorkerArgs) {
|
||||
&& clock_offset_ns != 0
|
||||
&& lat_ns > FLUSH_LATENCY.as_nanos() as i128
|
||||
{
|
||||
stale_frames += 1;
|
||||
stale_since.get_or_insert_with(Instant::now);
|
||||
} else {
|
||||
stale_frames = 0;
|
||||
stale_since = None;
|
||||
}
|
||||
let depth = frames.depth();
|
||||
if depth >= QUEUE_HIGH {
|
||||
standing_frames += 1;
|
||||
standing_since.get_or_insert_with(Instant::now);
|
||||
} else if depth <= QUEUE_LOW {
|
||||
standing_frames = 0;
|
||||
standing_since = None;
|
||||
}
|
||||
let clock_behind = stale_frames >= FLUSH_AFTER_FRAMES;
|
||||
let queue_behind = standing_frames >= STANDING_FRAMES;
|
||||
// The queue trip additionally requires the depth to still be high NOW, so
|
||||
// a run that started ≥ high but is hovering in the hysteresis band (a
|
||||
// clump mid-drain) never fires on elapsed time alone.
|
||||
let clock_behind = stale_since.is_some_and(|t| t.elapsed() >= FLUSH_AFTER);
|
||||
let queue_behind = depth >= QUEUE_HIGH
|
||||
&& standing_since.is_some_and(|t| t.elapsed() >= STANDING_TIME);
|
||||
if (clock_behind || queue_behind)
|
||||
&& last_flush.is_none_or(|t| t.elapsed() >= FLUSH_COOLDOWN)
|
||||
{
|
||||
stale_frames = 0;
|
||||
standing_frames = 0;
|
||||
stale_since = None;
|
||||
standing_since = None;
|
||||
last_flush = Some(Instant::now());
|
||||
flush_in_window = true; // strongest "link can't hold the rate" signal
|
||||
let flushed = session.flush_backlog().unwrap_or(0);
|
||||
|
||||
Reference in New Issue
Block a user