Repository navigation
feat(api): implement the VMAFx core API on the engine (RC4 WP2, ADR-1852) - #2199
Merged
Merged
Conversation
This was referenced Oct 5, 2026
lusoris
force-pushed
the
rc4/api-wp2-core
branch
from
October 5, 2026 23:14
008fdd7 to
b190030
Compare
This was referenced Oct 6, 2026
Draft
lusoris
force-pushed
the
rc4/api-wp1-generator
branch
from
October 7, 2026 15:02
cf1ea99 to
62d15ca
Compare
12 of 18 tasks
lusoris
force-pushed
the
rc4/api-wp1-generator
branch
from
October 7, 2026 15:29
62d15ca to
18b04cf
Compare
lusoris
force-pushed
the
rc4/api-wp2-core
branch
from
October 7, 2026 15:48
b190030 to
0f56b5a
Compare
lusoris
marked this pull request as ready for review
October 7, 2026 15:48
42 tasks
#2426) * ci: turn warnings into errors on the Windows MSVC legs that print none scripts/ci/werror-args.sh gains an msvc mode that prints -Dwerror=true: Meson makes that /WX on every cl.exe compile and -WX on every link.exe link, and core/src/meson.build adds --Werror all-warnings to the nvcc fatbins. Windows MSVC+CUDA, Windows ARM64 MSVC and Windows MSVC+CUDA (full) call it from a bash step, because their configure steps run under cmd. The icx-cl leg (Windows MSVC+SYCL) is not at zero yet and stays listed in docs/development/ci.md. test_werror_args.py fails an MSVC leg that is neither gated nor listed. * docs(ci): record the MSVC gate's proof runs and the icx-cl leg's count The planted C4305 failed all three gated legs as C2220, the planted linker directive the x64 and ARM64 links as LNK1218, and the gate commit passed them with no warning in the logs. Windows MSVC+SYCL printed 4,361 warnings on the gate commit; the state row and docs/development/ci.md list what is left.
…852) (#2199) * feat(api): implement the VMAFx core API on the engine (RC4 WP2, ADR-1852) A program can now score videos through vmafx/*.h alone. The definition (core/api/vmafx.toml, ABI 0.1.1 per ADR-1897) gains contexts with a log callback and options, option sets, extractor / model / model-set registration, imported scores, feature resolution (ADR-1359), frame retention, refcounted models and model sets with the SHA-256 of the bytes as loaded, the CPU device, host frames allocated or borrowed without a copy, submission and flush, and synchronous per-frame and pooled scores for features, models and model sets. Errors also name the kind of subject and the function; an input struct below its introduction size is the new VMAFX_E_ABI. The implementation in core/src/vmafx/ calls the engine through vmaf_engine_* entry points: the bodies of 14 more libvmaf functions are renamed in core/src/libvmaf.c and the libvmaf names forward to them until the compat layer generates them as shims. A context with a log callback gets the engine messages of its calls through a per-thread sink in core/src/log.cpp and never changes the process log level. A frame reference is one count of the engine picture's counter, so one frame is scored by several contexts without a copy (ADR-1880). ADR-1906 records these rules. Scores equal the libvmaf calls bit for bit: test_vmafx_bitexact compares every feature and model score, per frame and pooled with every method, of the golden pair, both checkerboards and the 10-bit sparks pair for vmaf_v1.0.16_3d0h and vmaf_v0.6.1. New tests cover every function's success, named failures and NULL error path, struct size negotiation, logging, lifetimes under ASan and UBSan, and SHA-256; each was shown failing on a planted defect. Two generator fixes for WP1 (requests/WP1-2): Python records keep to_c() when they gain pointer fields, and the generator tests no longer assume the definition's ABI version or function set. Found on the way: a model set scored per frame and then pooled over that frame fails in libvmaf too (T-MODEL-SET-SCORE-NOT-IDEMPOTENT-2026-10-05). * refactor(api): bring the VMAFx core API sources and tests to zero clang-tidy findings The cpu lane measured 33 findings in the files of the core API commit (dev container, clang-tidy 22.1.8). The VMAFx log level and pixel format reach the engine through explicit mappings instead of enum casts; pointer offsets in the SHA-256 code are computed in size_t; the model file reader cannot reach malloc(0) on any analysed path; the held-reference array is cast from realloc explicitly. The tests compare doubles through their integer bits instead of memcmp, take the model and fixture directories from Meson at build time instead of getenv, release their clip buffers on every path, and test functions over the branch threshold are split into smaller ones. A source contract test (test_gpu_float_ssim_auto_scale_contract) now reads the vmaf_engine_* bodies the libvmaf entry points forward to. vmafx_device_create() and the two registration functions assert the state their checks established, so every fork-added function of 20 lines or more carries an assert (assertion-density gate). * feat(api): route every message of a VMAFx context to its log callback (ADR-1906) The maintainer chose full routing for RC4 (ADR-1906, now Accepted): a context with a log callback receives every message the library raises for it, worker threads included, and nothing of it reaches the process log. Each job the engine runs on a worker thread now captures the log sink of the call that submitted it and installs it while it runs (ThreadDataBatch.log_sink in core/src/libvmaf.c, vmaf_log_thread_sink() in core/src/log.cpp); core/src/thread_pool.c is unchanged. The error prints of the float ADM, SSIM, MS-SSIM, motion and VIF code went to stdout and now go through vmaf_log(). A model belongs to no context: VmafxModelConfig gains log_level, log_callback and log_user, and a load routes its messages there. VmafxLogCallback moves to vmafx/types.h and documents that it runs on library threads, possibly several at once. The engine's init no longer sets the process log level; vmafx_context_create() sets it for a context without a callback only, so a context with a callback never writes it. A ThreadSanitizer run of the new test found that the level itself is a plain global every init writes while vmaf_log() reads it on all threads; that is a master defect and its fix is master PR #2207 (T-LOG-LEVEL-GLOBAL-DATA-RACE-2026-10-06), which this branch gets on its rebase. test_vmafx_log_routing covers a message raised on a worker thread with n_threads = 4, two contexts on two threads in a deterministic interleaving and concurrently with worker threads, the process log of a context without a callback, and a model load. Planted defects (no job sink, a process-wide sink, a model load without its sink, a plain context that leaves the process level) each fail it; with #2207's atomic level applied TSan reports nothing in 8 runs. A source contract (test_engine_log_routing_contract.py) refuses a direct stdout / stderr write in core/src outside a declared exception table. * ci(tidy): measure the translation units of the VMAFx core API (WP2) in the cpu lane The clang-tidy coverage rule on master requires every tracked translation unit to be read by a lane. The new and touched units of this pull request were measured in the dev container (scripts/dev/tidy-lane.sh --write --only ... cpu, clang-tidy 22.1.8): 0 findings, 0 uncited NOLINT; they join the cpu lane's measured sources. * fix(api): emit no bare `return None` in the generated Python binding Master's hooks now lint bindings/python/ with the pinned ruff, whose RET501 rewrites a method that returns `None` as its only value. The generated binding of WP2 had three such methods, so a commit that stages bindings/python/vmafx/_api.py came out of the hooks changed and the generated-file check would then fail. The generator leaves the `return` out when a method returns `None`; the binding is otherwise unchanged. * test(api): count one direct write in metal/common.mm after master's Metal cleanup The routing contract holds an exact count per file. Master's Metal host cleanup (#2222) left one device-listing write in core/src/metal/common.mm where the contract counted two; the count follows, so a new write still fails the test. * chore(api): regenerate the generated files after the rebase onto master * docs: regenerate the indexes and the citation map after rebasing
lusoris
force-pushed
the
rc4/api-wp2-core
branch
from
October 7, 2026 17:12
0f56b5a to
5af6f00
Compare
This was referenced Oct 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
RC4 work package 2: the VMAFx core API, implemented on the engine. A program can now score videos through
vmafx/*.halone, and every score equals thelibvmaf.hcall's bit for bit. WP1 #2187 is on master; this PR's diff against master is its own commits (core API, tidy, full log routing, the rebase fix-ups). It implements design sections 2.2 to 2.6 and the synchronous part of 2.8 of ADR-1852 / Research-2158 in one PR: context, options, errors, models, frames, submission and scores (no stacked series needed).What the definition gains (
core/api/vmafx.toml, ABI0.1.0->0.1.1, additions only, all in nodeVMAFX_0.1per ADR-1897):VmafxContextConfiggainslog_callback/log_user(appended; a 0.1.0-sized struct still works). A context with a callback receives every message the library raises for it at or below its level, on the calling thread (a per-thread sink incore/src/log.cpparound every engine call) and on its worker threads (every job captures the submitting call's sink,ThreadDataBatch.log_sinkincore/src/libvmaf.c;thread_pool.cunchanged), and nothing of it reaches the process log; it never changes the process level. The float ADM / SSIM / MS-SSIM / motion / VIF error prints went to stdout and now go throughvmaf_log(). A model load routes to the callback of itsVmafxModelConfig(newlog_level/log_callback/log_user). Without a callback a context logs to stderr and sets the level asvmaf_init()does.vmafx_context_set_option(perceptual_weight,perceptual_weight_strength).VmafxOptions(vmafx_options_set/free, the libvmaf feature dictionary under its new name),vmafx_context_use_feature,vmafx_context_use_model,vmafx_context_use_model_set,vmafx_context_import_score,vmafx_context_extractor_count,vmafx_feature_resolve(twin lookup of ADR-1359; on a context without a device backend the answer is the CPU extractor),vmafx_context_frame_retention(1, or 2 with an n-2 reader, ADR-1478).vmafx_model_load/load_file/override_feature/ref/unref/name/feature_count/feature_name/hash/builtin_next/default_version, and model sets (vmafx_model_set_load/load_file/override_feature/ref/unref/lead/size/hash). The hash is the SHA-256 of the bytes as loaded (built-in ==sha256sum model/<file>.json). A context holds a reference to every model and set it uses (ADR-1755); an override is refused withVMAFX_E_BUSYonce the model is shared.VmafxDeviceskeleton (CPU only; WP3 fills the backends),vmafx_frame_create_host,vmafx_frame_wrap_host(borrowed planes, release callback, no copy),vmafx_frame_planes,vmafx_frame_ref/unref. A frame reference is one count of the engine picture's counter.vmafx_submit(consumes both references on every path; refuses non-increasing indices and geometry changes namingreference/distorted),vmafx_flush,vmafx_score_frame,vmafx_score_frame_model_set,vmafx_score_pooled,vmafx_feature_score_pooled,vmafx_score_pooled_model_set.VMAFX_PENDINGwhere libvmaf returns-EAGAIN, without an error.vmafx_error_subject_kind(VmafxSubjectKind),vmafx_error_function; every failure names its subject; with aNULLerror pointer the message reaches the log atERRORwhatever the level. New statusVMAFX_E_ABI(-11): an input struct below its introduction size.core/src/libvmaf.cbecomevmaf_engine_*(core/src/vmafx/engine.h); the libvmaf names forward to them until WP6 generates them as shims. Return values and behaviour of everylibvmaf.hfunction are unchanged.--abi-check --against-ref origin/rc4/api-generation-prototype:definition is an append-only successor of origin/rc4/api-generation-prototype (95 additions); same againstorigin/rc4/api-wp1-generator. No break of a prototype entry, so no new minor.Generator changes (WP1-owned, needed here, filed as request WP1-2): Python records keep
to_c()when they gain pointer fields (elseContextConfigwith its callback broke the binding), and the generator tests no longer assume the live definition's ABI version or function set. Request WP1-1 asks the generator to emit the struct introduction sizes the C code keeps by hand (core/src/vmafx/internal.h,VMAFX_MIN_*).Landing (Q-083)
Lands bottom-up per Q-083:
v1.0.0-rc.3is tagged, so RC4 lands through the merge train, one API PR at a time. Its base #2187 is on master; this PR was rebased onto master18b04cf8b(the base's commits dropped), retargeted tomaster, and #2213 (WP3 common) follows once it lands. The API docs keep marking the VMAFx API as a preview (docs/api/vmafx/index.md, ABI 0.x) until the rc.4 cut. ADR-1906 is Accepted.ABI check against master:
python3 scripts/codegen/vmafx-api.py --abi-check --against-ref origin/master->definition is an append-only successor of origin/master (95 additions). Additions only, nodeVMAFX_0.1,abi_version0.1.1 (patch bump over master's).Rebase: conflicts only in generated files (
docs/adr/titles.md, ADR by-tag pages,CHANGELOG.md, citations), taken from master and regenerated once at the tip (vmafx-api.py --write,make docs-fragments-write,check-source-adr-citations.py --write);docs/state.mdbyscripts/dev/resolve-state-md-conflict.py.Local gate on the rebased stack (CPU,
-Db_lto=false,-j4, warnings as errors): build 0 warnings;run_meson_test.py --suite=fast390 OK, 0 fail, 1 skipped (test_vmafx_api_abi_append_onlybefore master had a definition); codegen tests 61 passed; affected suites: tooling 2210 passed, 0 failed;make test-netflix-golden GOLDEN_NINJA_JOBS=4280 passed, 3 skipped;preflight.sh --stage msvcismpass; train gates (deliverables, state-md touch and rows, silent revert,praetorctl audit) pass.Type
feat— new featureChecklist
make format && make lintis green locally — the commit hooks pass on both commits (clang-format, markdownlint, semgrep, source ADR citations, generated-index freshness, FFmpeg patch stack, HISS audit).python3 scripts/ci/run_meson_test.py -- -C build --suite=fast --num-processes 4→ 360 OK, 0 fail, 1 skipped (test_vmafx_api_abi_append_only: the merge base withorigin/masterhas no definition yet) on a CPU build (-Db_lto=false)./cross-backend-diffand the worst ULP is ≤ 2. — not applicable: no SIMD/GPU code touched..c/.cpp/.cu/.h/.hpp, it has the appropriate license header (EUPL-1.2, fork-authored).!orBREAKING CHANGE:and the migration path is documented below. — not a breaking change: additions only;VmafxContextConfiggrew at its end and a 0.1.0-sized struct is accepted.docs/adr/_index_fragments/<NNNN-slug>.mdand the slug is appended todocs/adr/_index_fragments/_order.txt— ADR-1906:docs/adr/_index_fragments/1906-vmafx-core-api-semantics.md, slug appended, indexes regenerated.tidy: cpu
core/src/vmafx/{context,device,error,frame_host,model,options,register,score,sha256,sized,submit,status_gen,compat_libvmaf_gen}.c,core/src/libvmaf.c,core/src/log.cpp,core/src/model.c,core/src/picture.c,core/src/feature/{adm,ssim,motion,ms_ssim,vif}.c,core/test/test_vmafx_{context,model,frame,score,bitexact,lifetime,sha256,log_routing,api_slice,abi_layout}.c(and through themcore/src/log.h,core/src/vmafx/*.h,core/include/vmafx/*.h,core/test/vmafx_test_util.h) 0 findings, 0 uncited NOLINT (dev container, clang-tidy 22.1.8,scripts/dev/tidy-lane.sh --only ... cpu, measured per commit). The lane reports 7 findings incore/include/libvmaf/picture.hthat this branch inherits from its base and master fixed in #2186; they go away on the rebase onto master. GPU lanes not measured: no GPU translation unit touched.scripts/dev/preflight.sh --stage msvcism: pass.praetorctl audit -base origin/rc4/api-wp1-generator: governance gates passed; HISS debt ratchet 6 -> 6, no finding in a touched file.Bug-status hygiene (ADR-0165)
docs/state.mdupdated — no state delta: the two defects WP2 found are master defects already closed on master with their own rows: T-MODEL-SET-SCORE-NOT-IDEMPOTENT-2026-10-05 (fix(model): read a model collection's stored score instead of predicting a frame twice #2206; the rebase dropped this branch's open row and the separate-session workaround intest_vmafx_score) and T-LOG-LEVEL-GLOBAL-DATA-RACE-2026-10-06 (fix(log): make the process log level atomic so contexts on several threads do not race #2207). This branch keeps only the VMAFx part of the latter: the engine's init no longer sets the level,vmafx_context_create()sets it for a context without a callback only.Netflix golden-data gate (ADR-0024)
assertAlmostEqual(...)score in the Netflix golden Python tests.Golden gate on this branch (
make test-netflix-goldenwithGOLDEN_NINJA_JOBS=4,core/build-goldenbuilt with gcc; the pytest step ran with the repository venv because the worktree venv the target bootstraps carries only meson and ninja): 280 passed, 3 skipped onb19003049; earlier heads too.Cross-backend numerical results
Not applicable: no extractor or kernel changed. VMAFx against libvmaf on the same build (
core/test/test_vmafx_bitexact.c): 8 cases (Netflix 576x324 pair 48 frames, 1080p checkerboard 1 px and 10 px 3 frames, 10-bit sparks 480x270 5 frames, each withvmaf_v1.0.16_3d0handvmaf_v0.6.1), 2912 values compared bit for bit (every feature in the collector at every frame, every feature pooled with all 8 methods, the model score per frame and pooled with all 8 methods): 0 differ.Performance (if
perforfeat)No engine path changed; the VMAFx layer adds argument checks and one thread-local pointer swap per engine call. Borrowed host frames remove the copy into
vmaf_picture_allocpictures thatlibvmaf.hcallers make.Deep-dive deliverables (ADR-0108)
docs/adr/1906-vmafx-core-api-semantics.md## Alternatives considered(logging scope, struct sizes, frame reference counting, model immutability).AGENTS.mdinvariant note —core/src/AGENTS.d/vmafx-api.md(index regenerated): core API files, failure and logging rules, size table, frame and model references, themodel.c/dict.cppbodies WP6 must rename, the engine defects found.changelog.d/added/api-core-context-model-frame-score.md.docs/rebase-notes.md, "VMAFx core API: engine entry points, per-thread log sink, shared picture helpers".User documentation:
docs/api/vmafx/index.mdrewritten for the core API (a complete scoring program, contexts and logging, registration, models, frames, scores, the rules every call follows); the generated reference pages cover every function, struct and constant;docs/development/api-generation.mdlists the new tests.Reproducer
python3 scripts/codegen/vmafx-api.py --check python3 scripts/codegen/vmafx-api.py --abi-check --against-ref origin/rc4/api-generation-prototype meson setup build core -Denable_cuda=false -Denable_sycl=false -Denable_hip=false -Db_lto=false ninja -C build -j4 python3 scripts/ci/run_meson_test.py -- -C build test_vmafx_context test_vmafx_model \ test_vmafx_frame test_vmafx_score test_vmafx_bitexact test_vmafx_lifetime \ test_vmafx_sha256 test_vmafx_api_slice test_vmafx_python_binding test_vmafx_abi_layout # lifetimes under ASan / UBSan meson setup build-asan core -Db_sanitize=address,undefined -Db_lundef=false -Db_lto=false ninja -C build-asan -j4 test/test_vmafx_lifetime test/test_vmafx_frame test/test_vmafx_bitexact python3 scripts/ci/run_meson_test.py -- -C build-asan test_vmafx_lifetime test_vmafx_frame test_vmafx_bitexacttest_vmafx_bitexactneeds the fixtures inpython/test/resource/yuv(skipped with 77 without them).Tests
test_vmafx_context(static link)test_vmafx_modelsha256sum, every refusal (unknown version, set as model, pkl, missing file, directory, bad flags, short config), override only while unshared, references, built-in list, model setstest_vmafx_frametest_vmafx_scoreVMAFX_PENDINGwithout an error, refusalstest_vmafx_bitexact(static link)test_vmafx_lifetime(Linux static,--wrap=vmaf_thread_pool_destroy)test_vmafx_sha256atest_vmafx_python_bindingtest_vmafx_log_routing(Linux, static link)n_threads= 4 reach the callback and nothing reaches stdout / stderr; two contexts on two threads in a deterministic interleaving (A logs again while B's sink is active) and concurrently with worker threads never get each other's lines; a context without a callback logs to stderr at its level; a model load routes to its config's callbacktest_engine_log_routing_contract.pycore/srcoutside a declared exception table (exact counts; scanner self-tests plant each write form)ASan + UBSan (
-Db_sanitize=address,undefined): all nine VMAFx test programs clean. TSan (-Db_sanitize=thread): with #2207's atomic level applied,test_vmafx_log_routing0 reports in 8 runs,test_vmafx_contextandtest_vmafx_lifetimeclean; without it the concurrent case reports the master race (3 of 3 runs) until this branch rebases onto #2207.Gates shown failing on planted defects (measured on this branch)
test_vmafx_contexttest_engine_messages_reach_callbackfailstest_vmafx_contexttest_callback_context_keeps_process_levelfailsstruct_sizetest_vmafx_contexttest_config_older_structcrashes (garbage callback pointer)test_vmafx_modeltest_hash_equals_file_digestfailstest_vmafx_modeltest_override_featurefailstest_vmafx_frametest_submit_and_flushfails (release count)test_vmafx_frametest_submit_and_flushfails (release count)test_vmafx_score,test_vmafx_bitexactVMAFX_PENDINGreported as an errortest_vmafx_scoretest_pending_is_not_an_errorfailstest_vmafx_bitexacttest_vmafx_sha256test_vmafx_lifetime(ASan)test_vmafx_lifetime(ASan)test_vmafx_log_routingtest_worker_messages_reach_callbackfailstest_vmafx_log_routingtest_two_contexts_interleavedfailstest_vmafx_log_routingtest_model_load_messages_routedfailstest_vmafx_log_routingtest_plain_context_logs_to_process_logfailsprintfplanted infeature/motion.ctest_engine_log_routing_contract.pytest_vmafx_log_routingKnown follow-ups
test_vmafx_score.test_engine_log_routing_contract.py, each with reason and expiry): CUDA / SYCL / Metal device prints (WP3 lanes; no VMAFx context attaches a device before WP3), the libsvm model-text parser error at model load (needs the libsvm link change, WP6),vmaf_write_output()(WP5vmafx_report_write),vifdiff()(no extractor calls it).libvmaf.hcalls on a bridged handle keep the process log (compat).adm_norm_view_distbelow 3 (-EINVALat the first submit, ADR-1191 / ADR-1325 table range); device-targeted scoring for close viewing (RC5) will need it. The VMAFx error names the frame index, not the extractor, because the engine does not report which extractor failed (WP5's producer record can).vmafx_context_attach_device, imports, fences and admission extendcore/src/vmafx/device.c/ frames; the frame reference rule (ADR-1906 item 4) is the base.core/src/model.c/core/src/dict.cppbodies the core API calls first (core/src/AGENTS.d/vmafx-api.md).-Wextra -Werrormissing-field-initializerson*_INIT(WP1 note): not hit, the WP2 tests are C.