fix(venc): stop requesting an IDR on every bitrate write - #112
fix(venc): stop requesting an IDR on every bitrate write#112vertexodessa wants to merge 1 commit into
Conversation
apply_bitrate() on Star6E and Maruko requested an IDR after each rate change "so the decoder resyncs against the new rate-control state". The decoder needs no resync: the rate controller absorbs a mid-GOP rate change, and the frame-shm throttle clamp has relied on exactly that -- re-programming the encoder as often as every 200 ms through the want_idr=0 path -- since it shipped. The forced IDR was pure cost, and on the bench link the dominant latency-spike source. An IDR is the largest frame the encoder can emit (~42 KB at 10 Mbps, measured 2026-08-22), so every adaptive-ladder or congestion-control bitrate write that cleared the 100 ms IDR gate turned rate control into an IDR train. The E0 capture's long-standing "IDR burst every ~19 pictures" is two gate windows at 90 fps. apply_bitrate_ex(kbps, want_idr) collapses into apply_bitrate(kbps); the throttle path calls the same function. A controller that wants a resync point calls request_idr() / /api/v1/idr explicitly. CV610 never IDR'd on bitrate and /api/v1/dual/set keeps its own behaviour. Contract 0.18.2 -> 0.18.3 (behavioral), recorded in HTTP_API_CONTRACT.md and HISTORY.md 0.65.3. Verified: make test 2479/0; star6e and maruko cross-builds clean. Running on the bench drone since 2026-08-22 08:56 (waybeam 0.66.0 local build) alongside maxIpProp=2; quiet-link G2G p50 62-72 ms, no IDR trains on ladder writes.
PR Summary by QodoStop forcing IDR frames on Star6E/Maruko bitrate writes
AI Description
Diagram
High-Level Assessment
Files changed (6)
|
Code Review by Qodo🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)
Great, no issues found!Qodo reviewed your code and found no material issues that require reviewTip of the day💡 Did you know, you can commit Qodo's fix in one click with committable suggestions (GitHub & GitLab) |
snokvist
left a comment
There was a problem hiding this comment.
The bitrate-control code change is correct in scope, but this needs factual/documentation corrections before merge.
Required corrections:
/api/v1/idrdoes not exist. The registered ch0 endpoint is/request/idr;/api/v1/dual/idris ch1-specific and/api/v1/idr/statsis read-only. Please remove/api/v1/idrfromHISTORY.md, the contract change log, and the PR text (or add and test an alias, though that is not needed for this fix). This was also confirmed on the exact PR binary:GET /api/v1/idrreturned 404 andGET /request/idrsucceeded.- The contract's
/api/v1/dual/settable says a ch1 bitrate write issues an IDR, butdual_apply_bitrate()only updatesMI_VENC_ChnAttr; it does not request one. Since this PR explicitly describes dual behavior, please correct that row to say no implicit IDR and point to/api/v1/dual/idrfor an explicit request. - The
/api/v1/idr/statsexample still reportsmin_spacing_us: 250000, whileIDR_RATE_LIMIT_MIN_SPACING_USis100000. Please correct this directly related contract drift. - Please narrow the measurement claims.
~42 KB at 10 Mbpsis one measured frame in the stated bench setup, not “the largest frame the encoder can produce.” Likewise, the latency run used a local0.66.0build alongsidemaxIpProp=2, so it does not isolate this patch as the dominant cause or prove that it explains the E0 anomaly. It is fair to say the code previously converted sufficiently spaced bitrate writes into forced IDRs, that this patch removes those requests, and that the mixed bench run was consistent with the expected improvement.
Independent exact-head device check performed here:
- Commit:
11dcabbf2abcda5987f3b48502f6eeb80768d3b9 - Binary SHA-256:
88f60e8b160c21e768e466ce2e9166e81f6ee8a7788fadb6a49902fc200841d6 - Device: Star6E SSC338Q, IMX335,
1920x1080@60, H.265 CBR,frame-shm:// - Ten volatile
/api/v1/live/set?video0.bitrate=...writes, spaced 150 ms apart: all returned 200; ch0 IDR counters stayed exactlyhonored=8, dropped=9;framesSentadvanced1804 -> 1911; transport drops stayed at 12. - Control: a later
/request/idradvancedhonored=8 -> 9. - No visual/decoder capture was performed. Maruko was unreachable, so it remains compile-tested only here. CV610 is unchanged by the patch.
The existing make test result is valid for the covered suite, but the host test binary does not link star6e_controls.c or maruko_controls.c, so 2479/0 does not itself test the removed IDR calls. The device counter test above directly covers Star6E; please describe the unit result accordingly.
Related IDR audit (not a blocker to keeping this PR bitrate-focused):
video0.qpDeltaandvideo0.maxIBytes/maxPBytesstill force IDRs on both Star6E and Maruko after changing RC parameters. They are the same decoder-neutral class as bitrate/QP bounds and should be removed in a focused follow-up unless a separate device requirement is demonstrated.- Keep IDRs for live FPS rebinds, output re-enable/new destination, reference-chain-breaking output drops, recording start, explicit IDR endpoints, and the opt-in scene detector; those have stream-recovery or explicit-policy reasons.
- Star6E output disable currently calls
apply_fps(idle), which requests an IDR after output has already been disabled; output enable then callsapply_fps(restored)and requests another IDR. That nested behavior deserves a separate targeted fix so disable emits none and enable emits exactly one reliable recovery IDR.
After the factual corrections, I consider the bitrate code merge-ready. It applies cleanly to the current fork's Star6E/Maruko control files; version/history/contract integration will require manual conflict resolution because the fork is already at 0.67.0.
|
Additional code-audit correction: the output-toggle issue exists on both backends and is more severe on Maruko. At this PR head,
Star6E has the same nested disable/enable structure, but its shared gate normally coalesces the duplicate; that also means a mandatory output-enable/new-destination recovery IDR can theoretically be swallowed by an unrelated request inside the 100 ms window. This is pre-existing and need not expand the bitrate patch, but it should become a focused follow-up. The clean policy split is:
Until that follow-up lands, please also avoid/repair contract language claiming that all listed Maruko IDR sources go through the shared limiter and appear in its stats. |
|
Heads-up: #114 (parity sync v0.65.2 → v0.67.1, 54 commits) is open and will conflict with this one on bookkeeping. This PR claims The code itself doesn't overlap much — #114 leaves Sorry for the churn — this bundle had been accumulating on the fork for a while. Happy to help with the rebase if useful. |
|
#114 is merged, so this now shows as conflicting. Overlap is four files:
Worth flagging: the sync added a 1 s holdoff on the ring-full recovery IDR with honored-aware anchor rollback ( |
TL;DRThis PR removes the explicit Setup
ObservationAt the receiver, real IDRs (HEVC NAL 19/20) arrive at 6.5/sec, spaced ~200 ms, each 46–70 KB. This rate is independent of GOP (tested 2 s and 4 s), resilience preset (racing / patrol / off), scene detection (off), intra-refresh (off), and receiver-side IDR requests (≈0). So it is neither GOP, nor intra, nor request-driven.
Mechanism
I suspect the "E0 capture's IDR burst every ~19 pictures ... two gate windows at 90 fps" noted in the PR description is this same effect: 19 pictures @ 90 fps ≈ 211 ms ≈ one 200 ms throttle window, i.e. one Confirming testToggling
The keyframes collapse to exactly the GOP, confirming the throttle's bitrate write is the sole extra source. (The large latency drop is a side effect: the forced 55–70 KB IDRs were themselves backing up the ring, which drove the throttle to clamp harder — a self-reinforcing loop.) Why the PR's bench didn't catch itThe verification was on a quiet link ("no IDR trains on ladder writes"). On a quiet link the ring doesn't back up, the throttle stays at 1000, and Suggested follow-upChange the CBR bitrate without a full How the loop works (intuitive)It's a control loop whose actuator has a self-defeating side effect:
The sting: the throttle exists to relieve ring pressure, but its actuator (bitrate-via- Happy to run more measurements on the rig if useful. |
|
Thanks for the follow-up analysis — you were right, and I was wrong in my earlier review. I took this to a Star6E bench (SSC338Q, IMX335 1920x1080@60, H.265 CBR, GDR via the
Two further data points that shape the fix:
Your deadband suggestion was the right call, and it's in. I've opened #116, which rebuilds this on the current tip (your commit cherry-picked, authorship intact) and adds: the same removal for On your ~200 ms observation: note the ring-full recovery IDR was un-paced on the This PR is superseded by #116, but the substantive commit there is yours. Happy to have you review it — particularly the deadband constant, since you have a rig that actually drives the throttle into oscillation and I had to induce ring pressure by stopping the consumer outright. |
Summary
apply_bitrate()on Star6E and Maruko requested an IDR after every rate change "so the decoder resyncs against the new rate-control state". The decoder needs no resync: the rate controller absorbs a mid-GOP rate change, and the frame-shm throttle clamp has relied on exactly that since it shipped, re-programming the encoder as often as every 200 ms through thewant_idr=0path.The forced IDR was pure cost, and on the bench link the dominant latency-spike source. An IDR is the largest frame the encoder can emit (~42 KB at 10 Mbps, measured 2026-08-22), so every adaptive-ladder or congestion-control bitrate write that cleared the 100 ms IDR gate turned rate control into an IDR train. The E0 capture's long-standing "IDR burst every ~19 pictures" is two gate windows at 90 fps.
Behaviour
video0.bitratewrites via/setand/live/setno longer request an IDR on Star6E and Maruko.apply_bitrate_ex(kbps, want_idr)collapses intoapply_bitrate(kbps); the throttle path calls the same function./api/v1/idr(or/request/idr) explicitly, as before./api/v1/dual/setkeeps its owndual_apply_bitrate()behaviour.HTTP_API_CONTRACT.md;VERSION0.65.2 -> 0.65.3,HISTORY.mdentry.Verification
make test: 2479 passed, 0 failedmake build SOC_BUILD=star6eandSOC_BUILD=maruko: cleanmaxIpProp=2: quiet-link glass-to-glass p50 62-72 ms, no IDR trains on ladder writes.Independent of #110 and #111 (all three bump
VERSION/HISTORY.md/contract from the same base); whichever lands later needs a trivial rebase of those three files.