feat(recovery): wire LTR-RFI loss recovery into every client
ci / web (push) Successful in 48s
ci / rust (push) Failing after 48s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 7s
ci / docs-site (push) Successful in 1m0s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 7s
decky / build-publish (push) Successful in 17s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
apple / swift (push) Successful in 4m51s
ci / bench (push) Successful in 6m13s
docker / deploy-docs (push) Successful in 20s
windows-host / package (push) Failing after 8m22s
flatpak / build-publish (push) Failing after 8m4s
arch / build-publish (push) Successful in 11m53s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m45s
android / android (push) Successful in 12m53s
deb / build-publish (push) Successful in 13m4s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m16s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m29s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m42s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m2s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 5m29s
release / apple (push) Successful in 25m48s
apple / screenshots (push) Successful in 19m31s

Centralize the client-side loss-range detector in punktfunk-core so every
embedder shares one implementation instead of re-deriving the wrapping
frame-index arithmetic:

- NativeClient::note_frame_index(frame_index) folds each received AU (in
  receive order) through RfiRecovery::observe, firing a throttled RFI request
  for the exact lost span [first_missing, frame_index-1] on a forward gap. A
  host that can RFI (AMD LTR / NVENC) re-references a known-good frame instead
  of paying a 20-40x IDR spike; the frames_dropped-driven keyframe path stays
  the backstop for when the recovery frame itself is lost.
- Export request_rfi + note_frame_index over the C ABI (Apple client).
- Call it from the Android (hw+sw pumps), Apple (StreamPump + Stage2Pipeline
  via PunktfunkConnection.noteFrameIndex), and Windows in-process pumps.
  Linux/Deck inherit it through pf-client-core's session pump.
- Split the decision into a pure RfiRecovery::observe(frame_index, now) and add
  8 unit tests: arming, contiguous runs, exact lost-range, single-frame drop,
  the 100ms throttle (burst-suppress then re-open), reorder stragglers, and
  u32 wraparound (contiguous + gap-range).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 19:11:01 +02:00
parent e55ff1bb28
commit 7cea893db5
8 changed files with 325 additions and 0 deletions
+37
View File
@@ -1597,6 +1597,43 @@ PunktfunkStatus punktfunk_connection_request_mode(const PunktfunkConnection *c,
PunktfunkStatus punktfunk_connection_request_keyframe(const PunktfunkConnection *c);
#endif
#if defined(PUNKTFUNK_FEATURE_QUIC)
// Ask the host to recover from loss by **reference-frame invalidation** rather than a full IDR:
// report the range `[first_frame, last_frame]` of access units the client can no longer trust
// (the first missing `frame_index` through the newest received). An RFI-capable host (AMD LTR /
// NVENC) re-references a known-good picture before `first_frame` and emits a clean P-frame tagged
// `USER_FLAG_RECOVERY_ANCHOR` — no 20-40x IDR spike; a host that can't RFI forces an IDR instead
// (same effect as [`punktfunk_connection_request_keyframe`]). Non-blocking, fire-and-forget; the
// recovered frame is the only ack, so THROTTLE it exactly like the keyframe request. Prefer this
// over the keyframe request on loss so AMD/RFI hosts avoid the spike; keep the keyframe request as
// the backstop for when the recovery frame itself is lost.
//
// # Safety
// `c` is a valid connection handle.
PunktfunkStatus punktfunk_connection_request_rfi(const PunktfunkConnection *c,
uint32_t first_frame,
uint32_t last_frame);
#endif
#if defined(PUNKTFUNK_FEATURE_QUIC)
// Feed each received frame's `frame_index` (the [`PunktfunkFrame::frame_index`] field, in receive
// order) so the client recovers from loss with a cheap reference-frame invalidation instead of a
// full IDR. On a forward gap (a `frame_index` jump = the intervening frames were lost and the
// following AUs reference a picture that never arrived) this fires a THROTTLED
// [`punktfunk_connection_request_rfi`] for the lost range; an RFI-capable host (AMD LTR / NVENC)
// then recovers with a clean P-frame instead of a 20-40x IDR spike. Call it for every received
// frame — it is cheap and idempotent, and the [`punktfunk_connection_frames_dropped`]-driven
// keyframe request stays the backstop. Writes whether a forward gap was detected this call to
// `gap_out` (nullable — a client with a post-loss display freeze can use it to re-arm; most
// clients pass NULL and ignore it).
//
// # Safety
// `c` is a valid connection handle; `gap_out` is writable or NULL.
PunktfunkStatus punktfunk_connection_note_frame_index(const PunktfunkConnection *c,
uint32_t frame_index,
bool *gap_out);
#endif
#if defined(PUNKTFUNK_FEATURE_QUIC)
// Cumulative access units the host→client reassembler dropped as unrecoverable (FEC couldn't
// rebuild them). A video loop polls this and calls [`punktfunk_connection_request_keyframe`]