Skip to content

float_motion: scale the extra planes with their own stride - #1667

Open
lusoris wants to merge 1 commit into
Netflix:masterfrom
VMAFx:fix/float-motion-scale1-stride
Open

lusoris wants to merge 1 commit into
Netflix:masterfrom
VMAFx:fix/float-motion-scale1-stride

Conversation

@lusoris

@lusoris lusoris commented Oct 2, 2026 •

Copy link
Copy Markdown

With motion_add_scale1, vmaf_image_sad_c() builds a half-resolution copy of both inputs and adds their difference to the score. It passes ALIGN_CEIL(width * sizeof(float)) / sizeof(float) as the stride of the source instead of the img1_stride / img2_stride it was called with. For luma the two are equal. With motion_add_uv the chroma planes go through the same function from buffers float_motion allocates for the luma width, so the stride computed from the chroma width is wrong and every row of the chroma plane is read from the wrong offset.

What breaks today

motion.c:70 and :75-76 on master 9e48141b:

int float_stride = ALIGN_CEIL(width * sizeof(float));
...
vif_scale_frame_s(vif_scale_bilinear, img1, img1_scaled, width, height, float_stride / sizeof(float), ...);
vif_scale_frame_s(vif_scale_bilinear, img2, img2_scaled, width, height, float_stride / sizeof(float), ...);

float_motion.c calls compute_motion() for plane 1 and 2 with ref_pic->w[1] as the width and s->float_stride (computed from the luma width) as the stride. The function's own contract says the strides are in elements of the buffers passed in; the unscaled sum honours that, the scale-1 branch does not.

Reproducer, GCC 16.2.1 release build, src01 576x324 (4:2:0), motion2 of frames 1 to 3 (identical under --cpumask -1):

vmaf -r src01_hrc00_576x324.yuv -d src01_hrc01_576x324.yuv -w 576 -h 324 -p 420 -b 8 --no_prediction --feature float_motion=motion_add_scale1=true:motion_add_uv=true -q --json -o /dev/stdout
options master this branch
motion_add_uv=true 5.364318, 5.185605, 4.885071 same
motion_add_scale1=true 8.054258, 7.789719, 7.313461 same
both 9.899102, 9.578713, 9.015887 (mean 9.423745) 10.248167, 9.910458, 9.330507 (mean 9.725193)

Fix

The two calls take img1_stride and img2_stride, and the local float_stride goes away.

Test

test_motion_sad (new, built when enable_float is on): vmaf_image_sad_c() on the same 50x21 pixels stored with a stride of 56 elements (the aligned one) and with a stride of 128. The sums must be equal, with and without motion_add_scale1. The scale-1 case fails on master ("the sad must not depend on the stride"); the scale-0 case passes on both.

Validation

x86-64 Linux, GCC 16.2.1, master 9e48141b, branch head fe3707b4; -Denable_float=true -Denable_checkasm=true.

  • Release meson test: 26/26 here, 25/25 on master (the extra test is the new one).
  • -Db_sanitize=address,undefined -Db_lto=false: 23 pass and 3 fail of 26 here, 22 pass and 3 fail of 25 on master; test_predict and test_pic_preallocation (SIGABRT from LeakSanitizer) and checkasm (heap-buffer-overflow in adm_dwt2_16) fail on both and are untouched by this change; test_motion_sad passes.
  • The three Netflix pairs, .engagement/golden_compare.py master binary against this branch: ALL IDENTICAL (default dispatch and cpumask=-1; float_motion is not in the default model, so this checks that nothing else moved).
  • The Python tests that set motion_add_scale1 or motion_add_uv (vmafexec_feature_extractor_test.py) set one at a time; both single-option rows above are unchanged. No Python test sets both. The Python suite itself was not run.

Not covered

  • No independent oracle for the chroma scale-1 value beyond the stride-invariance test; the CLI numbers above are before / after, not a reference.
  • 4:2:2 and 4:4:4 input and high bit depth were not run through the command line with both options.

lusoris added a commit to VMAFx/vmafx that referenced this pull request Oct 2, 2026
…each with its size and the upstream change that ends it (ADR-1479 to ADR-1486)

The reference for code inherited from Netflix/vmaf is Netflix's source;
a difference needs an ADR. The upstream parity audit of 2026-10-02 found
deliberate differences that had none of their own, or whose ADR
(ADR-1033) names neither upstream's behaviour nor the size:

- ADR-1479 ciede on 4:2:2: chroma flags (fork PR #1050); 0.153 on 48 of
  48 frames; Netflix/vmaf#1611.
- ADR-1480 speed_temporal buffers at speed_prescale above 1 (#1643);
  up to 195, upstream segfaults on two fixtures; Netflix/vmaf#1627.
- ADR-1481 a failing extractor fails the run (#871); status only, 78
  probe runs where upstream is silent and 88 where it crashes.
- ADR-1482 integer adm on frames of 17 to 32 pixels (#1473, #1507);
  scale 3 up to 0.23; Netflix/vmaf#1599, #1600.
- ADR-1483 odd-sized chroma planes round up (4f08d32); psnr_cb / cr
  up to 0.684 / 0.826 dB, ciede 0.198.
- ADR-1484 float_ms_ssim magnitude before pow() (#641, ADR-1033 item 2);
  NaN upstream on the 10 px checkerboard; Netflix/vmaf#1665.
- ADR-1485 apsnr of a plane without error (#641, item 1); 114 against
  60 dB; Netflix/vmaf#1666.
- ADR-1486 float_motion scale-1 stride (#641, item 9); up to 25.1;
  Netflix/vmaf#1667.

Each ADR gives upstream's file and line at Netflix 9e48141b, the fork's
lines, the reason found in the fork's pull request, commit or code, and
the measured size from the audit. Documentation only.
vmaf_image_sad_c() with motion_add_scale1 builds the half-resolution copy
of both inputs for the second scale. It passed
ALIGN_CEIL(width * sizeof(float)) / sizeof(float) as the stride of the
source instead of the img1_stride / img2_stride it was called with. For
the luma plane the two are equal. With motion_add_uv the chroma planes are
scored by the same function, from buffers float_motion allocates for the
luma width (float_stride), but their width is half of it, so the stride
computed from the width reads every row of the chroma plane from the wrong
offset.

On src01_hrc00_576x324 against src01_hrc01_576x324, motion2 of frames 1
to 3 with motion_add_scale1=true:motion_add_uv=true is 9.899102, 9.578713,
9.015887 on master and 10.248167, 9.910458, 9.330507 with this change; each
option on its own is unchanged. Pass the strides the function was given.

Add test_motion_sad: the sum of absolute differences of the same pixels
must not depend on the stride they are stored with. The scale1 case fails
on master and passes with this change; the scale0 case passes on both.
@lusoris
lusoris force-pushed the fix/float-motion-scale1-stride branch from fe3707b to 77fe5c0 Compare October 8, 2026 17:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant