Skip to content

feat(rust): add the RC4 Rust extractor framework (ADR-1713) - #2086

Merged
lusoris merged 2 commits into
masterfrom
rc4/rust-extractor-framework
Oct 7, 2026
Merged

lusoris merged 2 commits into
masterfrom
rc4/rust-extractor-framework

Conversation

@lusoris

@lusoris lusoris commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This is the shared part of RC4 (#1723, task 1): the framework that the speed_chroma, motion, adm, cambi and prediction lanes build on. Each Rust extractor becomes a twin of a C extractor. It is registered as <name>_rust next to the C extractor, inherits its options, feature names and flags, and is selected at run time with VMAF_FEATURE_IMPL=rust. The C extractors stay the default and the fallback, and libvmaf.h and every exported symbol are unchanged. The reference twin psnr_rust returns the C scores bit for bit on every fixture (results below).

Lands bottom-up per Q-083: v1.0.0-rc.3 is tagged, so RC4 lands; this is the first PR of the Rust stack, and the twins #2085, #2090, #2096, #2097, #2099 follow once it is on master. ADR-1713 is accepted as written (ledger Q-087, 2026-10-07; status, deciders, index row and References updated in this PR).

Rebase onto master (03584029c, after #2173 landed; the merge train stopped on its core/src/libvmaf.c include block, resolved keep-both: vmafx/engine.h from #2173 next to the Rust shim header; 200+ commits since the draft): conflicts resolved per hunk (core/src/meson.build: master's SpEED sources and zimg_dependency kept next to rust_core_dep; rust-ci.yml: master's --workspace comment merged into the two-workspace clippy step; docs/state.md by the row resolver; generated ADR / citation files regenerated). Three gates master gained meanwhile needed changes here, each its own commit:

  • Package licence check (ADR-1699): every crate of core/src/rust ships the root README.md (BSD-2-Clause-Patent in REUSE.toml) and EUPL-1.2 sources, and the psnr twin ports Netflix statements, so the workspace declares license = "EUPL-1.2 AND BSD-2-Clause-Patent", as vmafx-tad already does.

  • clang-tidy coverage (ADR-1762): rust_twins.cpp, test_rust_abi_layout.c and test_rust_twin_registry.c build only with -Denable_rust_features=true, which no lane can configure while the dev image has no Rust; they get the exception tad_rust.c and test_tad_rust.c already have (same reason, expiry 2026-12-31).

  • Stale-text contract (fix(docs): make help, header, option, CI and gate texts say what the code does #1976): its TAD check spelled the pilot's old Meson list and registry entry; it now checks the framework's shape (source listed once, inside the Rust block; the shim registers it; the static registry does not). Moving the source out of the Rust block fails it.

  • Tester image contract (tools/rc1-tester/tests/test_dockerfile_script_imports.py): the vmaf, SYCL, CUDA and HIP build stages of docker/Dockerfile.tester copy scripts/ci/rust_twin_diff.py, which the rust Meson suite names.

  • clang-tidy (cpu lane): enum class FeatureImpl gets a std::uint8_t underlying type (performance-enum-size).

core/test/test_tad_rust.c printed -Wdiscarded-qualifiers in a Rust build (master's file, compiled only with Rust): fixed, so the Rust leg is warning-free (0 warnings with warnings as errors, Q-061).

What changes:

  • Rust workspace core/src/rust/Cargo.toml, separate from the root workspace of the bindings, so its lockfile holds no external crate and the build runs offline from an empty CARGO_HOME (cargo resolves the whole workspace even for -p, and vmafx-sys needs bindgen). vmafx-fex holds the ABI, the Extractor trait, option, picture and host views, and the libm wrappers. vmafx-fex-psnr is the reference twin. The lane crates (speed, motion, adm, cambi, predict) start empty. vmafx-core-rs is the one Rust archive libvmaf links. The TAD pilot becomes an rlib of it and drops its unused build-time cbindgen dependency, so the cargo build runs --offline --locked.
  • C side: core/src/rust/shim/rust_twins.cpp copies each C descriptor and drives Rust with the parsed options, plane views, previous references and the C collector. feature_extractor.cpp walks the static list, then the Rust extractors the shim installs at vmaf_init(). Plain feature-name lookups skip Rust twins. vmaf_feature_extractor_impl_select() applies VMAF_FEATURE_IMPL on every registration path.
  • ABI header: core/src/rust/include/vmafx_rs.h is generated by cbindgen (scripts/dev/rust-abi-header.sh) and committed. test_rust_abi_layout compares every size and offset.
  • Harness: scripts/ci/rust_twin_diff.py runs one binary with c and with rust and requires equal doubles on every metric of every frame, plus a JSON receipt naming the twin. The cells come from the option sets of the vmaf_v1.0.16 models, and each runs at --threads 0,1,4. Both sides refusing an input with the same exit status counts as parity (REFUSED).
  • Threaded flush (lane request M-1): libvmaf initialises a Rust twin's shared context before flush_non_temporal_cpu_extractors() flushes it (init_shared_rust_twin()). C flushes need no init state, which is why this never surfaced.
  • CI: the Rust workflow runs fmt and clippy on every workspace crate (today: vmafx-sys only), checks the header, builds libvmaf with the Rust extractors, and runs the new rust Meson suite and the harness.
  • Two defects fixed on the way, both in docs/state.md. First, TAD was never registered in a Rust build. Second, a Rust build exported the Rust standard library from libvmaf.so; the archive is now linked with --exclude-libs.

Lane requests applied on this branch: S-1 (host log, log-only close), P-1 (predictor header, env resolver), M-1 (threaded flush), A-1 (provenance family), A-2 (refusal parity), A-3 (one guide row per twin) and C-1 (fmt). The shared contract is docs/development/rust-extractor-framework.md.

Type

  • feat — new feature
  • fix — bug fix
  • perf — performance improvement
  • refactor — no behavior change
  • docs — documentation only
  • test — test-only
  • build / ci — tooling / infra
  • port — cherry-pick from upstream Netflix/vmaf
  • sycl / cuda / simd — backend-specific

Checklist

  • Commits follow Conventional Commits (the commit-msg hook enforces this).
  • make format && make lint is green locally. Commit and pre-push hooks pass: clang-format, black, ruff, mypy, shfmt, semgrep, assertion density and HISS audit. cargo fmt --all --check and cargo clippy --workspace --all-targets -- -D warnings are clean.
  • Unit tests pass on the rebased head: python3 scripts/ci/run_meson_test.py -- -C build-rs --suite=fast --suite=rust → 380 OK including the three rust tests, 0 fail (-Denable_rust_features=true -Db_lto=false, -j4). cargo test --manifest-path core/src/rust/Cargo.toml --workspace passes; cargo fmt --check and cargo clippy --workspace --all-targets -D warnings are clean on both workspaces; both lock files load with cargo metadata --locked --offline.
  • If I touched any SIMD/GPU code path, I ran /cross-backend-diff and the worst ULP is ≤ 2. No SIMD or GPU path is touched.
  • If I touched a feature extractor with SIMD/GPU twins, I either updated every twin or listed the gap under "Known follow-ups" below. No C extractor is changed; psnr_rust only reads integer_psnr.c.
  • If I added a new .c / .cpp / .cu / .h / .hpp, it has the appropriate license header (see CONTRIBUTING.md). Fork-authored files are EUPL-1.2. The psnr twin is BSD-2-Clause-Patent because it ports Netflix code statement by statement (ADR-1250).
  • If this is a breaking change, the commit message uses ! or BREAKING CHANGE: and the migration path is documented below. Not breaking.
  • If this PR adds an ADR, the ADR row lives in docs/adr/_index_fragments/<NNNN-slug>.md and the slug is appended to docs/adr/_index_fragments/_order.txt — do not edit docs/adr/README.md directly (regenerated by scripts/docs/concat-adr-index.sh; see ADR-0221).

Bug-status hygiene (ADR-0165)

  • docs/state.md: T-RUST-TAD-NEVER-REGISTERED-2026-10-05 and T-RUST-STD-SYMBOLS-EXPORTED-2026-10-05 are opened and closed here. T-RUST-DEV-CONTAINER-TOOLCHAIN-2026-10-05 is opened (RC4).

Netflix golden-data gate (ADR-0024)

  • I did not modify any assertAlmostEqual(...) score in the Netflix golden Python tests.
  • If I believe a golden value must change, I have explained why below AND pinged @lusoris for a CODEOWNERS exception. No value changes; the default build is unchanged.

Cross-backend numerical results

psnr_rust against psnr on the same binary at --precision max. Every value is equal as an IEEE double, and the receipt names psnr_rust:

fixture     frames  identical  max abs diff
netflix     48      48/48      0
checker1    3       3/3        0
checker10   3       3/3        0
sparks10    5       5/5        0      (10-bit)
bbb4k       200     200/200    0      (3840x2160)

Re-run on the rebased head: rust_twin_diff.py --all-twins --fixtures netflix,checker1,checker10,sparks10,bbb4k --threads 0,1,4 → 15 of 15 cells EQUAL (bbb4k all 200 frames). The model cells (--models, --threads 0) are equal on netflix, checker1 and checker10 (24 of 24). On sparks10 they are equal except the two *_3d0h_2160 models, which both sides refuse (SpEED prescale 0.5 leaves 480x270 too small). No model feature has a twin on this branch yet, so these cells test the selection and fallback path, not a twin.

Planted-defect check: a 1-ulp change in the twin (* (1.0 + f64::EPSILON)) makes the harness exit 1 and print the differing frames in ulp. A hand edit of the generated header makes rust-abi-header.sh --check exit 1.

Performance (if perf or feat)

No performance claim. Benchmarks belong to RC8 (ADR-1490). The default build runs no Rust code.

Deep-dive deliverables (ADR-0108)

  • Research digest — Research-2151.
  • Decision matrix — in ADR-1713 ## Alternatives considered (twins vs replacing entry points, cbindgen at build time vs committed vs hand-written, panic strategy, selection surface).
  • AGENTS.md invariant note — core/src/AGENTS.d/rust-extractor-framework.md (new page, index regenerated) and core/src/feature/rust/AGENTS.md.
  • Reproducer / smoke-test command — pasted below under "Reproducer".
  • CHANGELOG fragment — changelog.d/added/rust-extractor-framework.md, changelog.d/fixed/rust-build-tad-registration-and-symbols.md.
  • Rebase note — "RC4 Rust extractor framework (ADR-1713, 2026-10-05)" in docs/rebase-notes.md.

Reproducer

meson setup build-rs core -Denable_rust_features=true -Denable_cuda=false -Denable_sycl=false -Denable_hip=false
ninja -C build-rs -j4
python3 scripts/ci/run_meson_test.py -- -C build-rs --suite rust
python3 scripts/ci/rust_twin_diff.py --vmaf build-rs/tools/vmaf --feature psnr \
  --fixtures netflix,checker1,checker10,sparks10,bbb4k
nm -D --defined-only build-rs/src/libvmaf.so | grep -c '_RN'   # 0

Local gate on the rebased head

  • Build -Denable_rust_features=true -Db_lto=false, -j4, warnings as errors: 0 warnings. Default build (Rust off): 0 warnings, --suite=fast 380 OK, 0 fail.
  • run_meson_test.py --suite=fast --suite=rust: 384 OK (the three rust tests included), 0 fail.
  • Golden gate (make test-netflix-golden GOLDEN_NINJA_JOBS=4, Rust off as on every default build): 280 passed, 3 skipped.
  • Harness: 15 of 15 psnr cells EQUAL (five fixtures, --threads 0,1,4, bbb4k 200 frames).
  • cargo fmt --check, cargo clippy --workspace --all-targets -D warnings, cargo test --workspace on both workspaces: clean; cargo metadata --locked --offline loads both lock files.
  • tidy: cpu core/src/feature/feature_extractor.cpp, core/src/libvmaf.c 0 findings (dev container, clang-tidy 22.1.8). rust_twins.cpp and the two Rust tests: no lane can build them yet (exceptions above). GPU lanes not measured: the changed lines of the two shared files are outside every backend block.
  • scripts/dev/preflight.sh --stage msvcism: pass. pre-commit run --files over every changed file: pass. Train gates (deliverables, state-md touch and rows, silent revert, praetorctl audit): pass.

Known follow-ups

  • The lanes S, M, A, C and P land the four twins and the predictor on top of this branch.
  • Task 7 of RC4 — Rust metric, zero-copy import, the VMAFx API and the cloud-native platform #1723 adds the CLI flag --feature_impl, decides the default of enable_rust_features and records throughput against the C path.
  • T-RUST-DEV-CONTAINER-TOOLCHAIN-2026-10-05: a pinned Rust toolchain in dev/Containerfile, needed before published artifacts can carry the Rust path.
  • macOS (ld64 has no --exclude-libs) and Windows linking (the archive's native libraries) of the Rust archive.
  • The shim TU and the two Rust tests are compiled only in a Rust build. No clang-tidy lane compiles them until the container has Rust (tidy-coverage exceptions, expiry 2026-12-31).
  • scripts/dev/rust-abi-header.sh --check skipped locally (no cbindgen on this host; the script says so); test_rust_abi_layout passed. The Rust workflow runs the check.

@lusoris lusoris added the rc4 RC4: the vmaf_v1.0.16_3d0h path in Rust; lands after the v1.0.0-rc.3 tag label Oct 5, 2026
@github-actions github-actions Bot added the type:feature New feature or request label Oct 5, 2026
@lusoris
lusoris force-pushed the rc4/rust-extractor-framework branch from e99e0f4 to 5962a1a Compare October 7, 2026 14:26
@lusoris
lusoris marked this pull request as ready for review October 7, 2026 14:26
@lusoris
lusoris force-pushed the rc4/rust-extractor-framework branch from 5962a1a to e8886a9 Compare October 7, 2026 15:19
lusoris and others added 2 commits October 7, 2026 17:34
…y tester leg before a cut (ADR-2198) (#2409)

* ci: build the Windows SYCL zip where its inputs change and check every tester leg before a cut (ADR-2198)

The x64 SYCL tester zip was red on master for two days with nothing required noticing. A pull request now builds it when an input it reads changes, in the light tier through own_input_lanes, and the cut pull request runs scripts/release/check-candidate-legs.py.

* docs: regenerate the indexes and the citation map after rebasing
* feat(rust): add the RC4 Rust extractor framework crates and the psnr reference twin

The workspace gains vmafx-fex (the ABI of the Rust twins, the Extractor
trait, option, picture and host views, libm wrappers), the reference twin
vmafx-fex-psnr, empty crates for the speed_chroma, motion, adm, cambi and
prediction lanes, and vmafx-core-rs, the one Rust archive libvmaf links.
The TAD pilot becomes an rlib of that archive and drops its unused
build-time cbindgen dependency, so the Rust build needs no network. The C
header of the ABI is generated by scripts/dev/rust-abi-header.sh and
committed. ADR-1713.

* feat(rust): register the Rust twins through a C shim and select them with VMAF_FEATURE_IMPL

core/src/rust/shim/rust_twins.cpp turns each Rust twin into a
VmafFeatureExtractor named <c name>_rust that copies the C extractor's
descriptor (options, priv size, provided features, flags) and drives the
Rust entry points with the parsed options, plane views, the previous
references and the C feature collector. feature_extractor.cpp walks the
static list and then the Rust extractors the shim installs at vmaf_init(),
skips Rust twins in plain feature-name lookups, audits them like device
twins and offers vmaf_feature_extractor_impl_select(), which libvmaf
applies on every registration path when VMAF_FEATURE_IMPL=rust.

Meson builds one archive (vmafx-core-rs) with an offline cargo build and
a depfile, links it into libvmaf only, and keeps its symbols out of the
dynamic symbol table with --exclude-libs. The TAD pilot is now registered
in Rust builds; before, feature_extractor.cpp never saw HAVE_RUST_TAD.

scripts/ci/rust_twin_diff.py runs both implementations of one binary and
requires equal doubles on every metric and a receipt naming the twin; the
psnr reference twin is equal on the Netflix pair, both checkerboard pairs,
the 10-bit sparks pair and 200 frames of 4K. ADR-1713.

* feat(rust): give the Rust twins a host log and a log-only close

Lane request S-1: VmafxRsHost gains a `log` callback (appended, so no
offset moves) and the twin's close entry point receives a host whose
collector callbacks fail but whose log works, so a twin can report what
its C extractor logs at close. Extractor::close() runs before the state
drops; Host::log_fmt formats into a stack buffer without allocating.

Lane request P-1: the header script also generates
core/src/rust/include/vmafx_rs_predict.h once the predictor's cbindgen
configuration exists, and vmaf_feature_impl_rust_requested() is the one
reader of VMAF_FEATURE_IMPL for every Rust path.

The build wrapper keeps a plain subprocess call: the safe_subprocess
helper resolves symlinks and a rustup cargo is a symlink to rustup. The
shim asserts its preconditions (Power-of-10 rule 5) and propagates the
dictionary free result instead of discarding it.

* docs(rust): document the Rust extractor framework and gate it in the Rust workflow

docs/development/rust-extractor-framework.md explains how to build and
select the Rust path, how a twin is registered and written, the arithmetic
rules for bit identity, the differential harness and its fixtures, and the
tests. VMAF_FEATURE_IMPL joins the environment variable reference; the
build-flag, Rust guide and TAD pages follow the new build. A core/src
AGENTS.d page records the invariants of the framework, the TAD crate's
AGENTS.md its new shape.

The Rust workflow runs fmt and clippy on every workspace crate, checks the
cbindgen header, builds libvmaf with the Rust extractors, runs the rust
Meson suite and the harness on the Netflix and checkerboard pairs; the
impact selector covers the new paths and the Meson runner inventory lists
the workflow. docs/state.md records the two defects fixed on the way and
the missing container toolchain.

* fix(rust): format the host text buffer and record the psnr twin's provenance

Lane request C-1: cargo fmt --all --check failed on the StackText literal
in vmafx-fex's host.rs. Lane request A-1: the reference twin ports
integer_psnr.c statement by statement, so relicense_provenance.toml gives
it the netflix-integer-psnr family (documented-port instead of
provenance-unreviewed); each RC4 lane adds the block of its own crate.

* build(rust): give the libvmaf-linked crates a workspace without external crates

An offline cargo build from an empty CARGO_HOME failed in the root
workspace ("failed to select a version" for bindgen, required by
vmafx-sys): cargo resolves the whole workspace even for one package. The
framework crates, the twins, the predictor, vmafx-core-rs and the TAD
crate now form core/src/rust/Cargo.toml, whose lockfile holds only those
nine crates; the root workspace keeps the bindings and excludes both
directories. Meson builds against the new manifest, and the Rust workflow
runs fmt, clippy and tests on both workspaces and builds vmafx-core-rs
offline from an empty CARGO_HOME, which fails as soon as a crate there
gains an external dependency. The TAD rebuild step went with TAD's
build.rs.

* fix(rust): initialise a Rust twin's shared context before a threaded flush

Lane request M-1: with --threads N, flush_non_temporal_cpu_extractors()
calls flush() on the shared context of a non-temporal extractor, which is
never initialised (only the per-thread copies are). The C extractors'
flush needs no init state; a Rust twin's flush found no instance and the
run failed with "problem flushing context". init_shared_rust_twin()
initialises that context with the run's picture parameters first. Lane M
measured motion_rust with the five-frame window equal to C at --threads 1
and 4 with the change, failing without it.

Lane request A-2: the harness runs every cell once per --threads value
(default 0,1), and a cell where both sides refuse the input with the same
exit status is REFUSED and passes (speed_chroma at prescale 0.5 refuses
480x270 in C), while one side refusing alone stays an error.

* test(rust): run every twin cell serially and with thread pools of 1 and 4

The flush of a non-temporal extractor runs on the shared context only with
a thread pool, so the harness default becomes --threads 0,1,4 and the rust
Meson suite's harness test runs all three. motion and motion_v2 are the only
non-temporal C extractors with a flush; motion_rust (lane M) is the twin
that exercises init_shared_rust_twin() and failed at --threads 1 and 4
without it.

* docs(rust): give each RC4 twin its own row in the framework guide

Lane request A-3: one row per twin, so the PR that lands a twin edits only
its own row.

* fix(rust): hold the framework to the licence and tidy-coverage gates master gained

Two gates landed on master after the framework branched:

- The package licence check (ADR-1699) wants each crate's `license` to be
  the AND of the licences of the files it ships. Every crate of the
  libvmaf-linked workspace ships the root README.md (BSD-2-Clause-Patent in
  REUSE.toml) next to its EUPL-1.2 sources, and the psnr twin carries
  statements ported from integer_psnr.c, so the workspace declares
  "EUPL-1.2 AND BSD-2-Clause-Patent", as vmafx-tad already does.
- The clang-tidy coverage rule wants every tracked translation unit read by
  a lane or named in an exception. The shim and its two tests build only
  with -Denable_rust_features=true, and the dev image every lane runs in
  has no Rust toolchain yet, so they get the entry tad_rust.c and
  test_tad_rust.c already have, with the same expiry, until a pinned
  toolchain is in dev/Containerfile and a lane reads them.

* docs(adr): accept ADR-1713 for the RC4 Rust extractor framework (Q-087)

The maintainer accepted the framework decision as written (ledger Q-087,
2026-10-07): Rust twins registered next to the C extractors as
`<name>_rust`, selected with VMAF_FEATURE_IMPL, one Rust archive, the C ABI
unchanged. The status line, the deciders and the index row say so, and the
References cite the ledger entry.

* fix(test): return the TAD test's failure message without discarding const

expect_score() took its message as `const char *` and returned it as the
`char *` the test runner expects, which GCC reports as
-Wdiscarded-qualifiers. The file builds only with
-Denable_rust_features=true, so no warning-checked leg compiled it before
the Rust framework gate. The message is a string literal like every other
message the runner returns; it is now passed as `char *`.

* test(build): follow the TAD pilot into the Rust framework in the stale-text contract

The contract that landed on master (#1976, defect 18) checks that a build
without enable_rust_features neither compiles tad_rust.c nor registers
TAD, and it spelled that through the pilot's own Meson list
(rust_tad_direct_sources) and its #if HAVE_RUST_TAD registry entry. The
framework (ADR-1713) replaced both: tad_rust.c is a direct libvmaf source
inside the Rust block next to the shim, and the shim registers the pilot.
The test now reads that shape: the source is listed once, inside the Rust
block, the shim assigns vmaf_fex_tad, and the static registry does not name
it. Moving the source out of the Rust block fails the test.

* fix(rust): give the VMAF_FEATURE_IMPL selector enum a one-byte underlying type

clang-tidy 22.1.8 (cpu lane, dev container) reported performance-enum-size on enum class FeatureImpl in core/src/feature/feature_extractor.cpp: three enumerators fit std::uint8_t. The lane now reads 0 findings in feature_extractor.cpp and libvmaf.c.

* build(docker): copy the Rust twin harness into the tester image's build stages

Master's tester image contract (tools/rc1-tester/tests/
test_dockerfile_script_imports.py) requires every Meson build stage to copy
each file outside core/ that the Meson tree names; the framework's rust
suite names scripts/ci/rust_twin_diff.py, so the vmaf, SYCL, CUDA and HIP
build stages copy it. The test failed without the four lines.

* docs(adr): regenerate the ADR index pages after the rebase onto master

* docs(adr): regenerate the generated files after the rebase onto master

* docs: regenerate the indexes and the citation map after rebasing
@lusoris
lusoris force-pushed the rc4/rust-extractor-framework branch from e8886a9 to ccf7ccf Compare October 7, 2026 16:43
@lusoris
lusoris merged commit ccf7ccf into master Oct 7, 2026
23 of 37 checks passed
@lusoris
lusoris deleted the rc4/rust-extractor-framework branch October 7, 2026 16:44
lusoris added a commit that referenced this pull request Oct 8, 2026
…after the library split

WP6 (ADR-2094) removed libvmaf.get_static_lib(): libvmaf is the compat library, and white-box tests link the engine through vmaf_test_link. test_rust_twin_registry (framework #2086, on master) still asked for the static libvmaf, so meson setup failed with -Denable_rust_features=true. The citation map loses the compat_libvmaf_gen.c sites the split removed and gains the sites of the restack.

Signed-off-by: Lusoris <lusoris@proton.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rc4 RC4: the vmaf_v1.0.16_3d0h path in Rust; lands after the v1.0.0-rc.3 tag type:feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant