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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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 cadencerefusals withloss_ppm=0throughout.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_scorepastDEPTH_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:
cadence_degradedtransition now logsbehind_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);behind_score(new sharedAtomicU32), 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 warningsclean (non-vacuous —Checking punktfunk-hostverified),native::streamtests 25/25 incl. a new budget test pinning the field case;cargo fmt --checkclean.