fix(abr): eleven defects from a sweep of the Automatic-bitrate path #28
Merged
enricobuehler
merged 5 commits from 2026-08-03 17:33:31 +00:00
fix/abr-sweep into main
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1ae8b4d4ca |
fix(client/abr): let the ceiling follow a host-initiated re-target
apple / swift (pull_request) Successful in 1m15s
apple / screenshots (pull_request) Skipped
android / android (pull_request) Successful in 4m8s
ci / web (pull_request) Successful in 2m1s
ci / rust-arm64 (pull_request) Successful in 2m25s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m14s
ci / docs-site (pull_request) Successful in 1m14s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m44s
ci / rust (pull_request) Successful in 9m9s
Interaction between two fixes in this series. The host now tells the client when a rebuild re-resolves an Automatic rate, and that rate can legitimately sit ABOVE the client's climb ceiling — the ceiling is the negotiated start rate until the capacity probe raises it, while the host's re-resolve answers "what do these pixels actually need" (a 1080p session mirroring a 4K panel resolves ~3× higher). Left alone, the client would learn the new rate, notice it was above a stale ceiling, and step the host straight back down off the rate it had just chosen for itself. So an ack raises the ceiling to meet it. `set_ceiling` only ever raises and still clamps to PUNKTFUNK_ABR_MAX_MBPS, which is the one limit that should bind here. No effect on ordinary acks: a climb is never requested above the effective ceiling to begin with. |
||
|
|
33ecd8e1a5 |
fix(client/abr): a granted climb disproves the learned cap
Completing the cap-escape fix. Backing the re-probe clock off to 12 s got the client asking again quickly, but each ask only LIFTED the cap by +12.5 % — so even a host that had fully recovered still granted the session its real ceiling one small step at a time, ~4 minutes from the 20 Mbps default to a 300 Mbps link. The crawl was never the point; re-learning was. A request granted IN FULL at or above the cap is the host's own word that the limit is gone. Drop the cap outright at that point instead of nudging it. A standing limit is unaffected — it answers the same re-probe with another short ack, which re-latches it and doubles its clock, exactly as before. Adds the end-to-end regression the sweep was really about: a session pinned at 20 Mbps by a transient cadence refusal, under a probe-measured 300 Mbps ceiling, now reaches 150 Mbps in 22 windows (~16 s) where it used to need ~17 minutes. |
||
|
|
48565c4e9e |
fix(host/abr): stop pinning Automatic sessions, and tell the client when the rate moves
Two host-side halves of the same sweep. **The cadence latch.** `cadence_degraded` — which makes the control task refuse bitrate CLIMBS — was latched true for as long as the session was escalated (adaptive capture depth or pipelined retrieve), independently of whether encode was still missing deadlines. The client cannot tell that refusal apart from an encoder's real ceiling: both arrive as a short `BitrateChanged`, and two identical ones latch a cap. Escalation needs ~20 net behind-frames, which a startup hitch supplies while the ABR is still in slow start at the 20 Mbps default — so one transient pinned the whole session there, long after the escalation had bought back the headroom it was for, and escaping cost +12.5 % per 60 s. An escalated session is still judged strictly (ANY net behind-frame keeps it flagged, where an unescalated one gets the full bucket), but being escalated no longer flags it by itself: escalating exists so cadence CAN be held, and once it is, refusing climbs refuses the thing that worked. The rule moves into `encode_behind_cadence` so it is stateable and testable. **The silent re-target.** `adopt_built_bitrate` publishes the rate a rebuilt pipeline actually opened at — `build_pipeline` re-resolves an Automatic rate whenever the source delivers a size the session did not negotiate, the mirrored-panel case — and the encoder's own clamp can land below what the control task already acked. Neither reached the client, whose controller keeps its own copy of that number as its climb base. A 1080p client mirroring a 4K panel therefore believed 20 Mbps while the host encoded 60, and its first climb computed from the stale base asked for 40: a re-target DOWNWARD, paying an encoder rebuild to get there. Both paths now push the applied rate to the control task, which sends `BitrateChanged` — the existing 9-byte message, which already means precisely this and which clients already handle arriving unprompted. No wire-format change, no capability negotiation, old clients unaffected. 2 host tests added. |
||
|
|
e9a7373c76 |
fix(client/abr): measure delivered throughput in media bytes, not wire bytes
The controller's two throughput-driven gates both compare "what the pipeline carried" against the ENCODER's target: the utilization gate asks whether a clean window actually tested that target (a calm menu proves nothing), and the never-decaying proven mark bounds how far every later climb may step. Both were fed `bytes_received`, which counts every accepted datagram — headers, FEC parity, probe filler, audio. So the figure rose with the redundancy the host adds in ANSWER to loss: at 25 % FEC the gate passed with the encoder emitting ~55 % of target, and the proven mark inherited the same inflation permanently. The signal was weakest exactly on the lossy links it exists for. Count data-shard payload separately at the reassembler's routing decision — the same place, and for the same reason, the probe counters are already stamped — and feed the ABR that. First time both gates are dimensionally honest: a media rate compared against a media target. |
||
|
|
f7a8c2013d |
fix(client/abr): the controller stops learning the wrong lessons from one window
Six defects found by a sweep of the Automatic-bitrate path, all of them the same shape: a single window, or a single refusal, taught the controller something it then treated as permanent. - Rolling baselines (OWD, client decode, host encode) armed off ONE sample. The baseline is a rolling minimum, so one window IS the floor — and `on_ack` deliberately clears the encode baseline after every decrease we ourselves asked for, re-opening that hole each time. A calm re-seed window followed by ordinary motion read as 4 ms of "congestion", backed off, cleared again, and ratcheted toward the floor on a link that was never the problem. All three now need BASELINE_MIN_WINDOWS of evidence before they may fire, via one shared `score_baseline` (the three copies had already drifted apart). - A mode switch rebased only the encode baseline. Decode and OWD are just as mode-scoped: 4K120 decodes slower and puts bigger frames on the wire than 1080p60, so the old floor was one the new mode cleared on its first window — ~30 s of every window scoring bad, i.e. a backoff every other window. A switch UP in mode cratered the rate instead of raising it. `proven_kbps` goes with them; throughput the old mode's decoder digested is not evidence about this one. - `proven_kbps` — never decayed, and permanent authority over how far every later climb may step — was raised by any window without a decode rise, including ones scored SEVERE. The windows that overstate delivered throughput are exactly the damaged ones: a stall's backlog draining at once, a flush's queue, the FEC surge answering a loss burst. Now only clean windows raise it. - A learned cap escaped at +12.5 % per ~60 s. The host cannot distinguish a durable encoder ceiling from a climb refused while it is transiently behind cadence, and the latter routinely latches during slow start at the 20 Mbps default — from which crossing the gap to a probe-measured ceiling took upwards of twenty minutes. Re-probe after 12 s instead, doubling the interval each time the lift is immediately re-learned: a transient is out in one interval, a real ceiling settles into a slow poll. - The decode cap latched AT the rate that choked, authorizing a climb straight back into the failure, and a bare jump-to-live flush could teach a "decoder knee" from what was a network event. It now latches just under the choke rate (inside the ±1/8 band the evidence already required) and only credits a flush where the decode signal is absent and cannot speak for itself. - PUNKTFUNK_ABR_MAX_MBPS bound only probe-learned ceilings, not the negotiated start rate — so the one knob an Automatic session gives the operator did nothing when the session already started above it. It now binds at construction, and a session sitting above its ceiling steps down to it (no congestion signal will ever find that: the link is fine, the cap is policy). Also: a SetBitrate dropped by a full control queue counted toward MAX_UNACKED, so three of them retired the controller for the session while logging that an "older host" was at fault. The pump now tells the controller what happened. Wire format and ABI untouched. 34 abr tests green (3 new). |