ADR-0337: motion_v2 inherits motion v1's public option surface (duplicate registration)¶
- Status: Accepted
- Date: 2026-05-09
- Deciders: Lusoris, Claude (Opus 4.7)
- Tags: upstream-port, motion, feature-extractor, cli, public-api, fork-local
Context¶
Netflix/vmaf upstream landed four motion_v2 commits between 2026-05-07 and 2026-05-08:
| SHA | Subject | Touches |
|---|---|---|
856d3835 | fix mirroring behaviour | scalar + AVX2 + AVX-512 |
c17dd898 | add motion_max_val | integer_motion_v2.c |
a2b59b77 | add motion_five_frame_window | integer_motion_v2.c, feature_extractor.h, libvmaf.c, tools/vmaf.c |
4e469601 | port remaining options | integer_motion_v2.c |
Together they extend motion_v2's public option surface to match the existing motion v1 surface (motion_force_zero, motion_blend_factor, motion_blend_offset, motion_fps_weight, motion_max_val, motion_five_frame_window, motion_moving_average) and add a third per-frame feature (VMAF_integer_feature_motion3_v2_score).
PR #460 (the fork-side cluster port, still draft) and PR #453 (an earlier, narrower attempt) both deferred a2b59b77 + 4e469601 with the rationale that the option surface "would duplicate against motion v1, which already exposes the same knobs via ADR-0158." The deferral note in each PR called for an architectural ADR before either commit lands, because once the duplication ships it is behaviourally observable: a single VMAF model can name motion_v2 and motion in the same feature list, and a downstream model parser must decide whether the same option string (e.g. motion_blend_factor=0.5) targets one extractor, the other, or both.
A second concern is the motion_five_frame_window=true mode in a2b59b77. Upstream wires it through a new fex->prev_prev_ref field on VmafFeatureExtractor, plus matching picture-pool sizing in vmaf_read_pictures (n‑threads × 2
- 2). The fork's
read_pictures_*decomposition (ADR-0152 monotonic-index gate) and existingdnn-block additions toVmafContextdiverge from upstream's layout; porting theprev_prev_refplumbing surfaces a four-conflict-region merge incore/src/libvmaf.cplus one incore/tools/vmaf.c. The 3-frame default mode (motion_five_frame_window=false) does not need that plumbing — it touches onlyprev_ref, which the fork already provides.
NASA/JPL Power-of-10 rule 6 (declared scope) and CERT C "fail loud, fail early" both argue for accepting the 3-frame option surface and rejecting motion_five_frame_window=true with -ENOTSUP at init() until the picture-pool refactor lands as its own PR. This mirrors the precedent set in ADR-0219 §Decision for the GPU motion3 extractors.
Decision¶
We will register the full motion v1 option surface a second time on motion_v2, as separate VmafOption[] entries owned by motion_v2's MotionV2State. v1 and v2 remain independent extractors with independent option tables; the duplication is deliberate and documented. motion_v2's option help strings and aliases match upstream 4e469601 byte-for-byte so future /sync-upstream runs find no behavioural delta.
motion_five_frame_window=true is rejected at init() with -ENOTSUP and a log message pointing at this ADR. The 3-frame default mode (the only mode any shipped VMAF model uses) is fully supported and bit-exact against the CPU motion v1 emission for motion3_v2_score (modulo the motion_v2-specific pipelined SAD that already meets places=4 against v1 in the existing snapshot gate).
Picture-pool plumbing (prev_prev_ref field, n_threads * 2 + 2 sizing, vmaf_read_pictures 2-frame ring) is deferred to a follow-up PR. That PR will flip the -ENOTSUP guard to a prev_prev_ref lookup and add a Netflix-golden five-frame fixture once the picture-pool refactor is reviewed in isolation.
Alternatives considered¶
| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
| A1 — Duplicate option surfaces (chosen) | v1 and v2 stay structurally independent; no cross-extractor coupling; matches upstream byte-for-byte; docs duplicate per-extractor (which every backend variant already does); bit-exactness against Netflix golden trivially preserved (v1 untouched) | Option-table source duplicates (~80 LOC of VmafOption[] rows + 7 struct fields); two motion_blend_factor help strings live in two .c files | Best ratio of correctness to risk; duplication cost is purely textual and the fork already accepts it for motion vs motion_cuda vs motion_sycl vs motion_vulkan |
| A2 — Shared option-parser table | Zero duplication; single help-string source | Couples v1 and v2 internals; introduces a new shared header (motion_options.h) that has to thread offsetof() calls across two struct definitions; option parsing is already centralised by the framework's VmafOption[] plumbing — only the table content duplicates, which is the cheap part | Over-engineering; the duplication objection isn't load-bearing once the framework's existing parser is examined |
| A3 — v2 deprecates v1 | Cleanest long-term; eliminates the legacy 3-frame blur ring buffer | Breaks the user-visible CLI (--feature motion resolves to v1 today); breaks every shipped VMAF model that names motion; triggers a CLAUDE.md §1 / ADR-0024 golden-data discussion (the goldens are CPU-only and pin against v1's exact output); separate ADR scope | Out of budget for this PR; would need its own ADR + a --legacy-motion migration path; defer indefinitely |
| A4 — Defer everything until upstream coverage gate | Zero risk | Fork falls behind upstream on a publicly-discoverable CLI surface; future model files referencing motion_v2=motion_blend_factor=… would error with "unknown option"; PR #460 has been draft for 1 day and #453 longer | Pure procrastination |
Consequences¶
- Positive:
motion_v2accepts the same option strings as v1, so downstream VMAF models or/sync-upstreamruns that pull upstream model files do not error on unknown options.- The four-commit upstream cluster lands as a coherent set; future
/sync-upstreamaudits will not flag any of856d3835,c17dd898,a2b59b77, or4e469601as pending ports. motion3_v2_scoreis now emitted, closing a coverage gap against motion v1'smotion3_score.-
The
-ENOTSUPguard onmotion_five_frame_window=truemirrors ADR-0219 §Decision for the GPU twins — everymotion_v2*consumer (CPU, CUDA, SYCL, HIP, Vulkan) now reports the same error for the same input, which is easier to reason about than per-backend silent fallbacks. -
Negative:
- ~80 lines of
VmafOption[]duplicate betweeninteger_motion.candinteger_motion_v2.c. Touching the help string for one extractor requires touching it for the other; ADR-0141 (touched-file lint-clean) catches drift on the next edit. -
motion_five_frame_window=truereturns-ENOTSUPwhere the user might expect a fall-back to 3-frame mode. This is deliberate (CERT C fail-loud) and matches ADR-0219's GPU precedent. -
Neutral / follow-ups:
- Picture-pool refactor (
prev_prev_ref+n_threads * 2 + 2sizing) tracked as a separate follow-up. The follow-up PR flips the-ENOTSUPguard and adds a five-frame-window fixture once the picture-pool refactor passes review in isolation. - GPU twins (CUDA, SYCL, HIP, Vulkan) of
motion_v2do not need the option surface in this PR. The 3-frame post-process is host-side scalar (per ADR-0219); whether GPU twins gain the same options will be decided when each twin needs to emitmotion3_v2_score. Until then GPU twins keep their existing options table. - Netflix-golden gate (CPU, places=4, ADR-0024) is unaffected: motion v1 is untouched and the goldens pin v1's output. The
motion_v2snapshot undertestdata/scores_cpu_motion_v2_*.jsonis not regenerated by this PR — theMIN(score, motion_max_val)clip and motion3 emission are additive at the default option values, and existing snapshots were taken against scores well below the 10000.0 default cap.
References¶
- Upstream commits:
856d3835,c17dd898,a2b59b77,4e469601. - Fork PRs preceding this one: PR #453 (narrow attempt; mirror fix citation backfill; deferred a2b59b77 + 4e469601), PR #460 (cluster port; deferred via PR-body §1 "Public-API surface change in
4e469601was NOT ported"). Both surfaced the architectural question this ADR resolves. - Sister ADRs:
- ADR-0158 — motion v1 option surface (the duplicated source of truth).
- ADR-0219 — GPU motion3
-ENOTSUPprecedent formotion_five_frame_window=true. - ADR-0145 — fork-local NEON
motion_v2twin (mirror fix propagates to it). - ADR-0152 — fork-local
read_picturesdecomposition that drives the picture-pool deferral. - ADR-0024 — Netflix golden gate.
- Source:
req— agent brief 2026-05-09 (PR #460 / #453 follow-up): "ADR is needed before the option surface duplicates between v1 and v2."