#63
perf(encoder): constant QP with voxl's own rate control
What it does
x264 encodes at a constant QP chosen per frame by voxl, with voxl's own rate control in place of x264's CRF + VBV. CRF and VBV need x264's lowres lookahead, which costs about as much as the encode itself. Everything else is as described in #27:
- Per-frame QP: voxl's own size model and VBV-style buffer pick each frame's QP.
- Oversized frames: they're invalidated and re-encoded, with a skipped stand-in frame sent in their place so frame numbers stay continuous.
- Link capacity: measured from how long frames take to be acknowledged.
- Conversion: persistent conversion threads, and only the changed columns are converted.
- Benchmarking:
bench/perfab.pyruns interleaved A/B comparisons.
Changes on top of #27
- Ported to current
main(28 commits since #27's base):- #41 already switched to UMH motion search; its search range of 64 is kept.
- The
--qpand--paintover-qpflags moved tolib.rs(#29). - The README flag table is updated in the restructured README.
- Everything else applied cleanly, including Windows, macOS,
/wait(#28), capture rebuilds on resize (#49) and auth.
- Oversized paint-overs are dropped, not re-encoded. In #27, a sharpening frame that overshot its budget was re-encoded at QP 51, where x264 skips every block, so it refined nothing at the cost of a second full encode. The size model never learned from these misses, so the same misprediction repeated. Now the frame is dropped, its stand-in goes out (the picture on screen was already correct), and its real cost is learned. Before this fix, on a 3 Mbps stress test: 13 of 16 paint-overs re-encoded to nothing, 63 ms median each.
- Pointless paint-overs are skipped: when the budget only allows a QP no sharper than what's on screen, there's nothing to gain from encoding.
- No re-encode when the link can carry the frame. If the measured link carries an oversized frame within one frame interval, it's sent as is. #27 re-encoded frames at 3× the budget even on fast links, where the second encode only adds latency. On a scene-change-heavy test at the 30 Mbps cap, it double-encoded 33–42 frames per run on a 1.7 Gbps link.
--max-kbpsstill holds the average rate.
Results
bench/perfab.py, interleaved, 2 runs per case, Xvfb 2560x1600, --max-kbps=30000, native decoding client. Run on a 4-vCPU VM, so the numbers are noisier than #27's 16-vCPU runs.
| Case | Build | voxl CPU | FPS (worst second) | Latency p50/p99 (ms) | Mbps |
|---|---|---|---|---|---|
| 1080p animation · LAN | main | 0.87 | 60.1 (58.3) | 18/41 | 5.8 |
| this PR | 0.69 (−21%) | 60.1 (59.5) | 16/26 | 6.5 | |
| 1080p animation · 10 Mbps / 40 ms | main | 1.09 | 58.4 (50.0) | 58/87 | 3.6 |
| this PR | 0.71 (−35%) | 60.0 (58.1) | 50/82 | 6.0 | |
| Full screen · LAN | main | 1.12 | 56.2 (45.7) | 34/64 | 3.5 |
| this PR | 0.82 (−27%) | 59.9 (56.2) | 18/38 | 3.4 | |
| Full screen · 10 Mbps / 40 ms | main | 1.10 | 54.5 (42.4) | 64/94 | 2.6 |
| this PR | 1.01 (−8%) | 58.8 (51.8) | 52/93 | 3.0 |
Scene-change stress test: six large windows and full-screen repaints in 4 s, 30 Mbps cap, one viewer answering pings and acking frames, 2 runs each.
| main | #27 ported (no fixes) | this PR | |
|---|---|---|---|
| Encode-thread time | 6.38–6.56 s | 5.73–5.77 s | 5.09–5.15 s |
| Frames over 33 ms | 31–38 | 34–48 | 23–25 |
| Slowest frame | 75–99 ms | 114–118 ms | 86–91 ms |
| Double encodes | — | 33–42 | 1 |
Testing
- Checks:
cargo fmt --check,cargo clippy --all-targets --locked -- -D warnings,cargo test(22 passed, including #27's Exp-Golomb, emulation-prevention and stand-in tests) andcargo package --lockedpass on Linux.oxfmtpasses on the README. - Bitstream, on a shaped 10 Mbps / 40 ms link with a recording running:
- The link was measured at 9,817 kbps.
- The stream had 66 double encodes and one dropped paint-over.
- The recording decodes cleanly with
ffmpeg -err_detect explode -xerror. - All 68 stand-ins decode pixel-identical to the frame before.
- macOS and Windows are type-checked and tested by this PR's CI, not run locally. The
convertsignatures match on all three platforms (macOS ignores the column range, as in #27).
Notes
- Known trade-off: on a slow link with constant scene changes, an oversized motion frame is still encoded twice, so single frames can take longer than on
main, as in #27's design. In the shaped stress test, more frames exceeded 33 ms on the encode thread than withmain(46 vs 32). The viewer-side benchmark above still comes out ahead on latency and FPS. - Flag change:
--qp(default 20) replaces--crf, and--crf Nis read as QP N. Tembo's config doesn't pass--crf. - Not tested: Safari and Firefox decoding (as in #27); real desktop content beyond the synthetic workloads.
Want tembo to make any changes? Add a comment with
@temboand i'll get back to work!
Requesting review from @DarrenBaldwin07 who has experience with the following files modified in this PR:
README.mdsrc/hub.rssrc/lib.rssrc/video.rssrc/server.rssrc/capture.rssrc/convert.rssrc/encoder.rsbench/README.mdbench/nclient.pysrc/mac/capture.rssrc/win/capture.rs