The climb gate scored encode against the negotiated refresh, so a 60 fps game was pinned at the ABR floor #255

Merged
enricobuehler merged 1 commits from worktree-cadence-degraded-floor-lock into main 2026-08-15 21:57:47 +00:00
Owner

A 2026-08-15 field session (2560×1440@120 negotiated, game delivering 53–74 fps) collapsed to the 5000 kbps ABR floor and was then held there for 23 minutes — 94% of the session — by our own climb gate: six bitrate climb refused — encode is behind cadence refusals with loss_ppm=0 throughout.

Root cause. The behind test scored every frame's post-submit work against the negotiated interval (8.33 ms at 120 Hz), but a frame's real budget runs until the next frame that actually exists arrives: a 60 fps source gives every frame twice that. An encoder keeping up with every real frame could be marked behind on most of them, latch behind_score past DEPTH_DEGRADE, and refuse every climb. The client's controller is blameless — its short-ack cap latch plus doubling re-probe ladder behaved exactly as designed against a host that held the flag for 23 minutes.

Fix. The budget is now the observed source-delivery period: an EMA over real frames' arrival spacing (repeats excluded — a keepalive re-encode says nothing about the game's rate; and after a repeat the next real frame lands a tiny delta, so an all-frames EMA would average back to ≈interval and defeat itself), clamped to [interval, 4×interval]. A source at or above the negotiated rate keeps bit-for-bit today's deadline; a hitchy source cannot disarm the detector. This also stops the same mis-scoring from spuriously escalating pipeline depth / pipelined retrieve on below-rate sources.

Observability — the session above sat at the floor with no trace of why:

  • every cadence_degraded transition now logs behind_score, escalated, budget/interval/observed period — rate-limited to one line per direction per 5 s with suppressed flips counted (the score can oscillate ±1 around the latch threshold at frame rate);
  • the control task's climb-refusal line carries the live behind_score (new shared AtomicU32), so the next field log discriminates "the budget was wrong" from "this encoder genuinely can't keep up" by itself.

Gates. amd64 CI container: cargo clippy --all-targets -p punktfunk-host -- -D warnings clean (non-vacuous — Checking punktfunk-host verified), native::stream tests 25/25 incl. a new budget test pinning the field case; cargo fmt --check clean.

A 2026-08-15 field session (2560×1440@120 negotiated, game delivering 53–74 fps) collapsed to the 5000 kbps ABR floor and was then held there for **23 minutes — 94% of the session** — by our own climb gate: six `bitrate climb refused — encode is behind cadence` refusals with `loss_ppm=0` throughout. **Root cause.** The behind test scored every frame's post-submit work against the negotiated interval (8.33 ms at 120 Hz), but a frame's real budget runs until the next frame that actually exists arrives: a 60 fps source gives every frame twice that. An encoder keeping up with every real frame could be marked behind on most of them, latch `behind_score` past `DEPTH_DEGRADE`, and refuse every climb. The client's controller is blameless — its short-ack cap latch plus doubling re-probe ladder behaved exactly as designed against a host that held the flag for 23 minutes. **Fix.** The budget is now the *observed* source-delivery period: an EMA over real frames' arrival spacing (repeats excluded — a keepalive re-encode says nothing about the game's rate; and after a repeat the next real frame lands a tiny delta, so an all-frames EMA would average back to ≈interval and defeat itself), clamped to `[interval, 4×interval]`. A source at or above the negotiated rate keeps bit-for-bit today's deadline; a hitchy source cannot disarm the detector. This also stops the same mis-scoring from spuriously escalating pipeline depth / pipelined retrieve on below-rate sources. **Observability** — the session above sat at the floor with no trace of why: - every `cadence_degraded` transition now logs `behind_score`, `escalated`, budget/interval/observed period — rate-limited to one line per direction per 5 s with suppressed flips counted (the score can oscillate ±1 around the latch threshold at frame rate); - the control task's climb-refusal line carries the live `behind_score` (new shared `AtomicU32`), so the next field log discriminates "the budget was wrong" from "this encoder genuinely can't keep up" by itself. **Gates.** amd64 CI container: `cargo clippy --all-targets -p punktfunk-host -- -D warnings` clean (non-vacuous — `Checking punktfunk-host` verified), `native::stream` tests 25/25 incl. a new budget test pinning the field case; `cargo fmt --check` clean.
enricobuehler added 1 commit 2026-08-15 17:25:37 +00:00
fix(host): the behind-cadence deadline was the negotiated refresh, so a 60fps game pinned ABR at the floor
ci / bun-nix (pull_request) Successful in 23s
ci / rust-arm64 (pull_request) Successful in 1m26s
android / android (pull_request) Successful in 4m38s
ci / web (pull_request) Successful in 4m43s
ci / docs-site (pull_request) Successful in 5m26s
ci / rust (pull_request) Failing after 11m23s
33d0b77e07
A 2026-08-15 field session (2560x1440@120 negotiated, game delivering
53-74 fps) collapsed to the 5000 kbps ABR floor and was then held there
for 23 minutes - 94% of the session - by our own climb gate: six
'bitrate climb refused - encode is behind cadence' refusals with
loss_ppm=0 throughout. The behind test scored every frame's encode work
against the negotiated interval (8.33 ms at 120 Hz), but a frame's real
budget is the arrival of the next frame that actually exists: a 60 fps
source gives every frame twice that. An encoder keeping up with every
real frame could be marked behind on most of them, latch behind_score
past DEPTH_DEGRADE, and refuse every climb the client asked for.

The budget is now the OBSERVED source-delivery period: an EMA over real
frames' arrival spacing (repeats excluded - a keepalive re-encode says
nothing about the game's rate), clamped to [interval, 4x interval] so a
source at or above the negotiated rate keeps bit-for-bit today's
deadline and a hitchy source cannot disarm the detector. This also
stops the same mis-scoring from spuriously escalating pipeline depth /
pipelined retrieve on below-rate sources.

And the gate becomes observable - the session above sat at the floor
with NO trace of why:
- every cadence_degraded transition logs behind_score, escalated,
  budget/interval/observed-period, rate-limited to one line per 5 s
  with suppressed flips counted (the score can oscillate +-1 around
  the latch threshold at frame rate);
- the control task's climb-refusal line now carries the live
  behind_score (new shared AtomicU32), so a field log can finally
  discriminate 'the budget was wrong' from 'this encoder genuinely
  cannot keep up'.

Gated: clippy --all-targets -D warnings + native::stream tests (25,
incl. a new budget test pinning the field case) in the amd64 CI
container; fmt clean.
enricobuehler merged commit c64a1a6767 into main 2026-08-15 21:57:47 +00:00
enricobuehler deleted branch worktree-cadence-degraded-floor-lock 2026-08-15 21:57:49 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#255