#63

perf(encoder): constant QP with voxl's own rate control

Openvrmiguelwants to merge intofrom

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.py runs interleaved A/B comparisons.

Changes on top of #27

  1. 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 --qp and --paintover-qp flags moved to lib.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.
  2. 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.
  3. 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.
  4. 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-kbps still 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.

CaseBuildvoxl CPUFPS (worst second)Latency p50/p99 (ms)Mbps
1080p animation · LANmain0.8760.1 (58.3)18/415.8
this PR0.69 (−21%)60.1 (59.5)16/266.5
1080p animation · 10 Mbps / 40 msmain1.0958.4 (50.0)58/873.6
this PR0.71 (−35%)60.0 (58.1)50/826.0
Full screen · LANmain1.1256.2 (45.7)34/643.5
this PR0.82 (−27%)59.9 (56.2)18/383.4
Full screen · 10 Mbps / 40 msmain1.1054.5 (42.4)64/942.6
this PR1.01 (−8%)58.8 (51.8)52/933.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 time6.38–6.56 s5.73–5.77 s5.09–5.15 s
Frames over 33 ms31–3834–4823–25
Slowest frame75–99 ms114–118 ms86–91 ms
Double encodes—33–421

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) and cargo package --locked pass on Linux. oxfmt passes 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 convert signatures 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 with main (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 N is 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 @tembo and i'll get back to work!

    

Activity

3
tembo[bot]4h ago

Requesting review from @DarrenBaldwin07 who has experience with the following files modified in this PR:

  • README.md
  • src/hub.rs
  • src/lib.rs
  • src/video.rs
  • src/server.rs
  • src/capture.rs
  • src/convert.rs
  • src/encoder.rs
  • bench/README.md
  • bench/nclient.py
  • src/mac/capture.rs
  • src/win/capture.rs
tembo[bot]reviewed4h ago