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:
@@ -32,6 +32,22 @@ pub trait Capturer: Send {
|
||||
self.next_frame().map(Some)
|
||||
}
|
||||
|
||||
/// Whether this backend can block until a frame ARRIVES ([`wait_arrival`]
|
||||
/// (Self::wait_arrival)) — the frame-driven encode trigger (latency plan T1.1). `false`
|
||||
/// (the default) keeps the encode loop on its legacy fixed-cadence tick for this backend.
|
||||
fn supports_arrival_wait(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Block until a FRESH frame is available via [`try_latest`](Self::try_latest) or
|
||||
/// `deadline` passes — the encode loop's frame-driven wait (latency plan T1.1): waking on
|
||||
/// the compositor's publish instead of sampling at a free-running tick deletes the
|
||||
/// sample-and-hold (~half a frame interval on average). Must NOT consume the frame (the
|
||||
/// loop's `try_latest` call does); backends buffer internally where the arrival channel
|
||||
/// can't be peeked. Only called when [`supports_arrival_wait`](Self::supports_arrival_wait)
|
||||
/// is `true`; errors surface at the following `try_latest`.
|
||||
fn wait_arrival(&mut self, _deadline: std::time::Instant) {}
|
||||
|
||||
/// Gate expensive per-frame work so the capturer can be kept alive (reused) between
|
||||
/// streams without burning CPU. The portal capturer skips the de-pad copy while inactive;
|
||||
/// the default is a no-op (synthetic sources are produced on demand). Set `true` for the
|
||||
|
||||
@@ -35,6 +35,10 @@ use std::time::Duration;
|
||||
/// and no second session to conflict with).
|
||||
pub struct PortalCapturer {
|
||||
frames: Receiver<CapturedFrame>,
|
||||
/// A frame [`wait_arrival`](Capturer::wait_arrival) received while blocking (the channel
|
||||
/// can't be peeked, so the wait must consume) — always the FIRST candidate the next
|
||||
/// `try_latest`/`next_frame` considers, before draining anything newer off the channel.
|
||||
pending: Option<CapturedFrame>,
|
||||
active: Arc<AtomicBool>,
|
||||
/// Set true once the PipeWire stream agrees a video format. Read in [`next_frame`]'s timeout
|
||||
/// branch to tell "format never negotiated" (modifier/format mismatch) apart from "negotiated
|
||||
@@ -166,6 +170,7 @@ impl PwHandles {
|
||||
fn into_capturer(self, node_id: u32, keepalive: Option<Box<dyn Send>>) -> PortalCapturer {
|
||||
PortalCapturer {
|
||||
frames: self.frames,
|
||||
pending: None,
|
||||
active: self.active,
|
||||
negotiated: self.negotiated,
|
||||
streaming: self.streaming,
|
||||
@@ -266,6 +271,9 @@ impl Capturer for PortalCapturer {
|
||||
self.node_id
|
||||
));
|
||||
}
|
||||
if let Some(f) = self.pending.take() {
|
||||
return Ok(f); // a wait_arrival stash outranks the channel (it's older)
|
||||
}
|
||||
let slice = Duration::from_millis(500)
|
||||
.min(deadline.saturating_duration_since(std::time::Instant::now()));
|
||||
match self.frames.recv_timeout(slice) {
|
||||
@@ -276,6 +284,27 @@ impl Capturer for PortalCapturer {
|
||||
}
|
||||
}
|
||||
|
||||
fn supports_arrival_wait(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn wait_arrival(&mut self, deadline: std::time::Instant) {
|
||||
// Frame-driven trigger (latency plan T1.1): block on the PipeWire channel until the
|
||||
// compositor delivers a frame or the deadline passes. The channel can't be peeked, so
|
||||
// the received frame is stashed in `pending` — `try_latest` starts from it and still
|
||||
// drains anything newer. A broken/ended stream just returns; the following
|
||||
// `try_latest` surfaces the error through its existing paths.
|
||||
if self.pending.is_some() || self.broken.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let Some(left) = deadline.checked_duration_since(std::time::Instant::now()) else {
|
||||
return;
|
||||
};
|
||||
if let Ok(f) = self.frames.recv_timeout(left) {
|
||||
self.pending = Some(f);
|
||||
}
|
||||
}
|
||||
|
||||
fn try_latest(&mut self) -> Result<Option<CapturedFrame>> {
|
||||
if self.broken.load(Ordering::Relaxed) {
|
||||
return Err(anyhow!(
|
||||
@@ -285,8 +314,9 @@ impl Capturer for PortalCapturer {
|
||||
));
|
||||
}
|
||||
// Drain to the newest queued frame without blocking; `None` means the compositor
|
||||
// hasn't produced a new frame since last call (static/idle desktop).
|
||||
let mut latest = None;
|
||||
// hasn't produced a new frame since last call (static/idle desktop). A frame
|
||||
// `wait_arrival` stashed is the oldest candidate — anything on the channel is newer.
|
||||
let mut latest = self.pending.take();
|
||||
loop {
|
||||
match self.frames.try_recv() {
|
||||
Ok(frame) => latest = Some(frame),
|
||||
|
||||
@@ -1625,6 +1625,32 @@ impl Capturer for IddPushCapturer {
|
||||
self.try_consume()
|
||||
}
|
||||
|
||||
fn supports_arrival_wait(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn wait_arrival(&mut self, deadline: Instant) {
|
||||
// Frame-driven trigger (latency plan T1.1): wake the encode loop the moment the driver
|
||||
// publishes a frame we haven't consumed. The shared-header token is the truth (state,
|
||||
// not edge — the auto-reset event may have been consumed by an earlier wait); the event
|
||||
// is just the wakeup, waited in ≤16 ms slices exactly like `next_frame`'s bringup wait.
|
||||
loop {
|
||||
let tok = frame::FrameToken::unpack(self.latest());
|
||||
if tok.generation == self.generation && u64::from(tok.seq) != self.last_seq {
|
||||
return; // a fresh publish exists — `try_latest` will consume it
|
||||
}
|
||||
let Some(left) = deadline.checked_duration_since(Instant::now()) else {
|
||||
return;
|
||||
};
|
||||
let ms = (left.as_millis() as u32).clamp(1, 16);
|
||||
// SAFETY: `self.event` is the live frame-ready `OwnedHandle` this capturer owns; its
|
||||
// raw value (borrowed for the call, so it outlives this synchronous wait) is a valid
|
||||
// auto-reset event handle. `WaitForSingleObject` only reads the handle; the bounded
|
||||
// timeout keeps the wait sliced.
|
||||
let _ = unsafe { WaitForSingleObject(HANDLE(self.event.as_raw_handle()), ms) };
|
||||
}
|
||||
}
|
||||
|
||||
fn hdr_meta(&self) -> Option<punktfunk_core::quic::HdrMeta> {
|
||||
// While the display is HDR we emit BT.2020 PQ (Rgb10a2) → the encoder forces HEVC Main10 + the
|
||||
// PQ VUI; pair that with a mastering-display SEI so any decoder tone-maps from a real grade. The
|
||||
|
||||
Reference in New Issue
Block a user