Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@ repos:
rev: v1.7.12
hooks:
- id: actionlint
# ADR-2199: actionlint deadlocks when a `run:` script is larger than the pipe
# it writes to shellcheck before starting it, which a user holding many
# pipes makes common. The wrapper runs it under a deadline and fails loudly.
entry: python3 scripts/ci/run_actionlint.py

# Composite actions under .github/actions/ are not workflows: actionlint rejects
# them ("jobs section is missing"). The GitHub action schema is checked here, the
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ lint-go:
lint-actions:
$(call require-tool,actionlint,go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.12)
@echo "--- actionlint (.github/workflows) ---"
@actionlint
@python3 scripts/ci/run_actionlint.py
@echo "--- composite actions (.github/actions): structure + shellcheck ---"
@python3 scripts/ci/check_composite_actions.py

Expand Down
6 changes: 6 additions & 0 deletions changelog.d/added/rust-motion-twin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- **The integer `motion` extractor has a Rust twin, `motion_rust`, that returns the C
extractor's scores bit for bit.**
With `-Denable_rust_features=true`, `VMAF_FEATURE_IMPL=rust` (or
`--feature motion_rust`) computes `motion_sad_score`, `motion2` and `motion3`,
including the five-frame window and the moving average, in Rust; the default
stays the C extractor. See [Motion](docs/metrics/motion.md#rust-implementation).
6 changes: 6 additions & 0 deletions changelog.d/changed/actionlint-bounded-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- **The `actionlint` hook and `make lint-actions` cannot hang
([ADR-2199](docs/adr/2199-actionlint-bounded-run.md)).** They run actionlint
through `scripts/ci/run_actionlint.py`, which gives it 90 seconds
(`ACTIONLINT_TIMEOUT_S`) and fails with exit 124 and the cause named instead
of hanging a commit or a push when a `run:` script is larger than the pipe
actionlint writes it to (a user over `fs.pipe-user-pages-soft`).
3 changes: 1 addition & 2 deletions changelog.d/fixed/api-generator-format-test-pin.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@
the 23.1.2 the repository pins, so the test failed on every hosted leg. It
now uses only the pinned major (`VMAFX_CLANG_FORMAT` names one explicitly)
and skips, naming the version it found, otherwise; the Tooling Tests job
installs the pinned release. `test_gpu_picture_pool_uaf` no longer writes
its deliberately huge allocation on hosts that overcommit memory.
installs the pinned release.
126 changes: 126 additions & 0 deletions core/src/rust/feature/motion/src/extractor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright 2016-2026 Netflix, Inc.
// Copyright 2026 Lusoris
// SPDX-License-Identifier: BSD-2-Clause-Patent
//
// Ported from `init`, `extract` and `flush` of
// core/src/feature/integer_motion.c.

//! The `motion` extractor state and its `Extractor` implementation.

use vmafx_fex::{
Error, Extractor, Flush, Frame, Geometry, Host, Picture, Plane, PlaneView, Sample,
try_filled_vec,
};

use crate::options::MotionOptions;
use crate::sad::{self, MIN_DIM};
use crate::window::{self, Window, c_min};

const SAD_SCORE: &core::ffi::CStr = window::SAD;
const MOTION_SCORE: &core::ffi::CStr = c"VMAF_integer_feature_motion_score";

/// State of one context: options, geometry and the vertical-pass row.
pub struct Motion {
opts: MotionOptions,
w: u32,
h: u32,
bpc: u32,
y_row: Vec<i32>,
}

impl Motion {
fn window(&self) -> Window {
Window {
blend_factor: self.opts.blend_factor,
blend_offset: self.opts.blend_offset,
max_val: self.opts.max_val,
five_frame_window: self.opts.five_frame_window,
moving_average: self.opts.moving_average,
}
}

/// SAD of the luma planes of `prev` and `cur` (`s->pipeline`).
fn sad(&mut self, prev: &Picture<'_>, cur: &Picture<'_>) -> Result<u64, Error> {
let (w, h) = (self.w as usize, self.h as usize);
match (prev.plane(0)?, cur.plane(0)?) {
(Plane::U8(p), Plane::U8(c)) => {
check_dims(&p, &c, w, h)?;
Ok(sad::sad_8(&p, &c, &mut self.y_row))
}
(Plane::U16(p), Plane::U16(c)) => {
check_dims(&p, &c, w, h)?;
Ok(sad::sad_16(&p, &c, self.bpc, &mut self.y_row))
}
_ => Err(Error::InvalidArgument(c"motion: plane types differ")),
}
}

/// The SAD score of frame `index` (`extract()` up to the append).
fn score(&mut self, frame: &Frame<'_>) -> Result<f64, Error> {
let min_idx = self.window().min_idx();
if self.opts.force_zero || frame.index < min_idx {
return Ok(0.0);
}
let prev = if self.opts.five_frame_window {
frame.prev_prev_ref.as_ref()
} else {
frame.prev_ref.as_ref()
};
let prev = prev.ok_or(Error::InvalidArgument(c"motion: no earlier reference"))?;
let sad = self.sad(prev, &frame.reference)?;
let pixels = f64::from(self.w.wrapping_mul(self.h));
let score = (sad as f64) / 256.0 / pixels * self.opts.fps_weight;
Ok(c_min(score, self.opts.max_val))
}
}

/// Planes must match the init geometry, or the loops would read out of bounds.
fn check_dims<T: Sample>(
p: &PlaneView<'_, T>,
c: &PlaneView<'_, T>,
w: usize,
h: usize,
) -> Result<(), Error> {
let ok = p.width() == w && p.height() == h && c.width() == w && c.height() == h;
if ok {
Ok(())
} else {
Err(Error::InvalidArgument(
c"motion: plane size differs from init",
))
}
}

impl Extractor for Motion {
type Options = MotionOptions;

fn init(opts: &MotionOptions, geom: &Geometry) -> Result<Self, Error> {
if geom.h < MIN_DIM || geom.w < MIN_DIM {
return Err(Error::InvalidArgument(
c"motion: frame below the 5-tap filter minimum 3x3",
));
}
let y_row = try_filled_vec(geom.w as usize, 0_i32)?;
Ok(Self {
opts: *opts,
w: geom.w,
h: geom.h,
bpc: geom.bpc,
y_row,
})
}

fn extract(&mut self, frame: &Frame<'_>, host: &mut Host<'_>) -> Result<(), Error> {
let score = self.score(frame)?;
host.emit(SAD_SCORE, frame.index, score)?;
if self.opts.debug {
host.emit(MOTION_SCORE, frame.index, score)?;
}
Ok(())
}

fn flush(&mut self, host: &mut Host<'_>) -> Result<Flush, Error> {
window::flush(host, &self.window())?;
Ok(Flush::Done)
}
}
29 changes: 21 additions & 8 deletions core/src/rust/feature/motion/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
// Copyright 2016-2026 Netflix, Inc.
// Copyright 2026 Lusoris
// SPDX-License-Identifier: EUPL-1.2
// SPDX-License-Identifier: BSD-2-Clause-Patent
//
//! Rust twin of the C `motion` feature extractor (RC4 lane M, ADR-1713).
//! Rust twin of the integer `motion` extractor
//! (`core/src/feature/integer_motion.c`), ported statement by statement: SAD of
//! the blurred frame difference, motion2 and motion3 from the flush, the
//! five-frame window and the moving average. Scores equal the C extractor's
//! bit for bit (`scripts/ci/rust_twin_diff.py --feature motion`).
//!
//! Skeleton created by the framework PR: `TWINS` is empty until the lane
//! lands the twin, so the archive links and the C extractor keeps running.
//! Copy the shape of `vmafx-fex-psnr`.
//! RC4 lane M of the Rust extractor framework (ADR-1713).
//!
//! The C option table, its defaults and its ranges stay in C: this crate only
//! reads the parsed values.

#![forbid(unsafe_code)]

use vmafx_fex::VmafxRsTwin;
mod extractor;
mod options;
pub mod sad;
pub mod window;

pub use extractor::Motion;
pub use options::MotionOptions;
use vmafx_fex::{VmafxRsTwin, twin};

/// The twins this crate registers (none yet).
pub const TWINS: &[VmafxRsTwin] = &[];
/// The twins this crate registers.
pub const TWINS: &[VmafxRsTwin] = &[twin::<Motion>(c"motion", c"motion_rust")];
37 changes: 37 additions & 0 deletions core/src/rust/feature/motion/src/options.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2026 Lusoris
// SPDX-License-Identifier: EUPL-1.2
//
// Option names of core/src/feature/integer_motion.c (names only: defaults,
// aliases and ranges stay in the C option parser).

//! Options of the motion extractor.

use vmafx_fex::{Error, FromOptions, OptionValues};

/// Every option of the C table, read back by name.
#[derive(Clone, Copy, Debug)]
pub struct MotionOptions {
pub force_zero: bool,
pub blend_factor: f64,
pub blend_offset: f64,
pub fps_weight: f64,
pub max_val: f64,
pub five_frame_window: bool,
pub moving_average: bool,
pub debug: bool,
}

impl FromOptions for MotionOptions {
fn from_options(opts: &OptionValues<'_>) -> Result<Self, Error> {
Ok(Self {
force_zero: opts.bool(c"motion_force_zero")?,
blend_factor: opts.f64(c"motion_blend_factor")?,
blend_offset: opts.f64(c"motion_blend_offset")?,
fps_weight: opts.f64(c"motion_fps_weight")?,
max_val: opts.f64(c"motion_max_val")?,
five_frame_window: opts.bool(c"motion_five_frame_window")?,
moving_average: opts.bool(c"motion_moving_average")?,
debug: opts.bool(c"debug")?,
})
}
}
Loading
Loading