ADR-1565: A libx265 two-pass cell at a CRF is pass 1 at the CRF, then ABR at pass 1's bitrate¶
- Status: Accepted
- Date: 2026-10-04
- Deciders: maintainer, agent
- Tags: tools, vmaf-tune, python, go, codec, fork-local
Context¶
vmaf-tune corpus --two-pass with libx265 could not encode. The adapter kept -crf in both passes and x265 refuses that on the second: "Constant rate-factor is incompatible with 2pass without vbv-maxrate in the previous pass", exit 183 (state row T-VMAFTUNE-X265-TWO-PASS-CRF-2026-10-04; test_real_x265_two_pass_smoke showed it under VMAF_TUNE_INTEGRATION=1). libx264 drops -crf in pass mode instead, so its two-pass cells ignore the CRF they are labelled with. What a two-pass cell at a CRF should mean is a design question: a bitrate target, a VBV cap, or something else.
Checked with ffmpeg n9.0.2 and x265 on a 2 s 320x180 clip: pass 1 at -crf 28 with -x265-params pass=1:stats=... encodes and writes a real bitstream; pass 2 with -b:v <kbps>k -x265-params pass=2:stats=... encodes; pass 2 with -crf 28 exits 183.
Decision¶
A libx265 two-pass cell at a CRF runs pass 1 at that CRF, writing its bitstream to a file; ffprobe reports the container bit rate of that file; pass 2 is an ABR encode (-b:v <kbps>k, no -crf) at that bitrate, reading pass 1's stats. The cell records that its rate control is ABR at that bitrate: the result's request lists -b:v <kbps>k in extra_params, which the corpus row's extra_params column carries, and the crf column stays the pass-1 CRF. If ffprobe reports no bit rate the cell fails (exit 1, [pass 1 bitrate unavailable]) and pass 2 does not run; there is no fallback to a guessed bitrate or to single pass.
The behaviour belongs to the adapter flag two_pass_abr_at_pass1_bitrate (Python) / TwoPassABRAtPass1Bitrate (Go), set for libx265 only; the adapter version becomes 2 so cached results never alias the old behaviour. Python vmaftune.encode._encode_abr_two_pass and Go corpus.runABRTwoPassEncode are the two implementations; the argv swap is _with_abr_rate_control / withABRRateControl.
Alternatives considered¶
| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
| Pass 1 at the CRF, pass 2 ABR at pass 1's bitrate (chosen) | Keeps the CRF meaning (the bitrate it produces); uses only options x265 accepts; measured on a real encode | The cell is no longer a pure CRF cell; one ffprobe call per cell | Chosen by the maintainer (popup 2026-10-04) |
Pass 1 with vbv-maxrate, pass 2 keeps -crf | Keeps -crf in both passes | Needs a VBV bitrate to choose, which is the open question again; crf plus VBV changes the rate control | A second free parameter with no principled value |
Drop -crf and take the bitrate from the user | Matches libx264's behaviour | The cell ignores its CRF label | The CRF is the corpus axis |
| Fall back to single pass for libx265 | No new code | --two-pass silently does nothing for x265 | Silent fallback |
| New corpus columns (rate control, ABR bitrate) | Explicit | A corpus schema bump (v4) and a Go row mirror for one adapter | Disproportionate; extra_params already lists the argv the encode used |
Consequences¶
- Positive:
corpus --two-passwithlibx265encodes; the row says how. - Negative: a libx265 two-pass row's bitrate and VMAF belong to an ABR encode, not to a CRF encode; trainers that read
crfas the quality knob should filter onextra_paramscontaining-b:v. ffprobe becomes a dependency of a libx265 two-pass run, resolved next to the ffmpeg binary. - Neutral / follow-ups: libx264's two-pass cells still drop
-crf(T-VMAFTUNE-TWOPASS-CRF-INVALID-2026-08-30); aligning them is a separate decision.