Repository navigation
fix(cambi): keep the c-values walks inside short frames - #1642
Merged
Merged
Conversation
lusoris
force-pushed
the
fix/cambi-short-frame-oob
branch
from
September 30, 2026 20:25
0dc4716 to
06c9a43
Compare
This was referenced Sep 30, 2026
lusoris
force-pushed
the
fix/cambi-short-frame-oob
branch
from
September 30, 2026 23:28
06c9a43 to
a25d390
Compare
17 of 26 tasks
lusoris
force-pushed
the
fix/cambi-short-frame-oob
branch
from
October 1, 2026 00:12
a25d390 to
ee81dbc
Compare
lusoris
added a commit
that referenced
this pull request
Oct 1, 2026
Align SYCL feature twins with CPU reference arithmetic: - float_ssim_sycl / integer_ssim_sycl: implement CPU reference arithmetic matching CUDA twin (#1637). float_ssim evaluates per-pixel l*c*s with exact fp32 pairs (Ff) and work-group fixed-point sums reduced in double on the host, preserving ADR-1370 fp32 frame-mean rounding. integer_ssim groups terms as ((w*a)*b)/den without identical-window shortcuts. Both achieve exact match on flat identical 64x64 frames (72.247199 dB). - integer_psnr_sycl: mark with VMAF_FEATURE_EXTRACTOR_TEMPORAL flag so --subsample scaling operates consistently. - integer_motion_v2_sycl: apply motion_fps_weight and motion_max_val cap in collect() and derive motion2_v2 / motion3_v2 in flush(), including 1-frame inputs. - Drop duplicate CPU CAMBI edits belonging to #1642. - Re-record .standards-baseline.json with pinned praetor engine.
16 of 19 tasks
lusoris
force-pushed
the
fix/cambi-short-frame-oob
branch
from
October 1, 2026 01:43
ee81dbc to
f44c313
Compare
At the coarsest of CAMBI's five scales a wide, short input can have no more rows than pad_size, half the window: 1920x128 leaves 8 rows against pad_size 11, and with the default window every height up to 176 at 1920 wide (352 at 3840) is affected. The c-values walk ran its first pass and its top edge for pad_size rows and started its bottom edge at height - pad_size whatever the height, so it read image and mask rows past both ends of the frame and wrote them into c_values (Netflix/vmaf#1628). Under ASan, master fails at 1920x2, 1920x16, 1920x64, 1920x128, 1920x160 and 3840x128 at every dispatch level; a release build aborts or segfaults on 1920x64, 1920x160, 2560x224, 3840x256 and 1921x129 4:4:4. Port the upstream fix (Netflix/vmaf#1629) to calculate_c_values() in cambi.c and to the upstream-mirror calculate_c_values_avx2(), and give cambi_calculate_c_values_frame(), the walk the dispatched AVX2 scan, AVX-512 and NEON drivers share, the same three bounds: first pass i < MIN(pad_size, height) top edge i < MIN(pad_size + 1, height) bottom edge i = MAX(height - pad_size, 0) The same walks had the column version of the bug: the scalar and AVX2-mirror first column loops ran to pad_size whatever the width, so on a scale with fewer than pad_size columns (default window: up to 80 wide at 1080 high, 160 at 1920 high) they counted columns past the frame. decimate() works in place, so those columns hold the previous scale's pixels, inside the stride, where no sanitizer sees the read. The shared SIMD walk never visited them, so --cpumask 63 and the default dispatch disagreed, and so did the CUDA, HIP and Metal twins, which run the scalar walk on the host. All eight loops now run to MIN(pad_size, width). This column bound goes beyond upstream Netflix/vmaf#1629, which bounds only the rows. Upstream has the same column loops. Scores are identical to master at --precision max, at the AVX-512, AVX2 and C dispatch levels, for every size the old loops handled: the Netflix pairs, both checkerboard pairs, BBB 3840x2160, and ramp and noise inputs at 1920x176, 2560x240, 3840x352, 96x1080 and 1920x1080. Short frames that master completed on the C path while reading outside the frame change (3840x128 horizontal ramp 19.544347 -> 19.512269). Scores change for frames narrower than pad_size at some scale (measured: 64x1920 vertical ramp master 14.975700714938673 vs branch 14.964394451743877 on the C path; the SIMD paths already agreed, giving 14.964394451743877 on both; another vertical ramp variant moves from 16.141046 to 16.131541; cambi_cuda on the RTX 4090 and cambi_hip on gfx1036 give the new CPU score bit for bit). ADR-1393 records clipping over rejecting such frames in init(); Research-2132 has the measurements. test_calculate_c_values_short_frame and test_calculate_c_values_narrow_frame run every driver the host has on 1 to 10 rows and 1 to pad + 2 columns against a from-scratch clipped-window histogram and a sentinel margin. On master sources the short test fails for 1 to 4 rows on all five drivers without a sanitizer; with the column loops unbounded the narrow test fails on the scalar walk, and with only the AVX2 mirror unbounded on that walk. test_cambi_stage_simd also sweeps 1-row and pad-row frames. test_cambi's AVX2 parity check never ran on master: #1479 changed its gate to vmaf_get_cpu_flags_x86(), but #1483 merged first, and when #1479 was rebased onto it the branch kept its comment and took #1483's helper with the old vmaf_get_cpu_flags() gate. It now reads CPUID. docs/state.md closes T-CAMBI-SHORT-FRAME-OOB-2026-09-30, T-CAMBI-NARROW-FRAME-COLUMNS-2026-09-30 and T-CAMBI-AVX2-PARITY-GATE-LOST-IN-REBASE-2026-09-30, and opens T-CLI-PRE-REGISTRATION-OPTS-DICT-LEAK-2026-09-30 (fix in #1647). calculate_c_values_scan_avx2() carries a cited cppcheck suppression: its non-const picture comes from upstream's VmafCalcCValues callback type. core/test/meson.build raises test_gpu_public_header_docs timeout to 120s for Windows MinGW64.
lusoris
force-pushed
the
fix/cambi-short-frame-oob
branch
from
October 1, 2026 06:40
f44c313 to
8605979
Compare
This was referenced Oct 1, 2026
lusoris
added a commit
that referenced
this pull request
Oct 1, 2026
#1642 clipped the CPU c-values walk to short and narrow frames and described the CUDA, HIP and Metal twins as running that walk on the host. With cambi_hip device-resident that no longer holds for HIP (nor for CUDA since ADR-1379): only the Metal twin calls vmaf_cambi_calculate_c_values(). The feature notes, the metric pages and the changelog entry say so, and the state row records that cambi_hip equals the CPU on the gfx1036 on narrow frames as well.
lusoris
added a commit
that referenced
this pull request
Oct 1, 2026
Align SYCL feature twins with CPU reference arithmetic: - float_ssim_sycl / integer_ssim_sycl: implement CPU reference arithmetic matching CUDA twin (#1637). float_ssim evaluates per-pixel l*c*s with exact fp32 pairs (Ff) and work-group fixed-point sums reduced in double on the host, preserving ADR-1370 fp32 frame-mean rounding. integer_ssim groups terms as ((w*a)*b)/den without identical-window shortcuts. Both achieve exact match on flat identical 64x64 frames (72.247199 dB). - integer_psnr_sycl: mark with VMAF_FEATURE_EXTRACTOR_TEMPORAL flag so --subsample scaling operates consistently. - integer_motion_v2_sycl: apply motion_fps_weight and motion_max_val cap in collect() and derive motion2_v2 / motion3_v2 in flush(), including 1-frame inputs. - Drop duplicate CPU CAMBI edits belonging to #1642. - Re-record .standards-baseline.json with pinned praetor engine.
lusoris
added a commit
that referenced
this pull request
Oct 1, 2026
* fix(sycl): RC3 parity follow-ups Align SYCL feature twins with CPU reference arithmetic: - float_ssim_sycl / integer_ssim_sycl: implement CPU reference arithmetic matching CUDA twin (#1637). float_ssim evaluates per-pixel l*c*s with exact fp32 pairs (Ff) and work-group fixed-point sums reduced in double on the host, preserving ADR-1370 fp32 frame-mean rounding. integer_ssim groups terms as ((w*a)*b)/den without identical-window shortcuts. Both achieve exact match on flat identical 64x64 frames (72.247199 dB). - integer_psnr_sycl: mark with VMAF_FEATURE_EXTRACTOR_TEMPORAL flag so --subsample scaling operates consistently. - integer_motion_v2_sycl: apply motion_fps_weight and motion_max_val cap in collect() and derive motion2_v2 / motion3_v2 in flush(), including 1-frame inputs. - Drop duplicate CPU CAMBI edits belonging to #1642. - Re-record .standards-baseline.json with pinned praetor engine. * docs(sycl): document RC3 SYCL follow-ups Document the three resolved RC3 SYCL parity gaps (SSIM identical/flat-frame arithmetic, PSNR temporal subsampling, motion_v2 FPS weighting and one-frame flush) in the SYCL backend overview and record rebase notes. * docs: regenerate the indexes and the citation map after rebasing
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
CPU
cambiread and wrote outside its buffers on wide, short frames (Netflix/vmaf#1628). At the coarsest of the five scales, such a frame has no more rows than half the window (pad_sizerows): 1920x128 leaves 8 rows againstpad_size11, and with the default window every height up to 176 at 1920 wide (352 at 3840) is affected. The c-values walk ran its first pass and its top edge forpad_sizerows, and started its bottom edge atheight - pad_size, whatever the height. On master, ASan reports a heap-buffer-overflow or SEGV at 1920x2, 1920x16, 1920x64, 1920x128, 1920x160 and 3840x128, at every dispatch level. A release build aborts or segfaults on 1920x64, 1920x160, 2560x224, 3840x256 and 1921x129 4:4:4.This ports the upstream fix (Netflix/vmaf#1629) and applies it to all three copies of the walk in the fork:
calculate_c_values()incore/src/feature/cambi.c(the scalar path, and the host c-values pass of the CUDA, HIP and Metal twins throughvmaf_cambi_calculate_c_values())calculate_c_values_avx2()inx86/cambi_avx2.c(the upstream mirror; built and tested, not dispatched)cambi_calculate_c_values_frame()incambi_c_values_frame.h, which the dispatched AVX2 scan, AVX-512 and NEON drivers share (fork-local)The PR also fixes the column version of the bug: the scalar and AVX2-mirror first column loops ran to
pad_sizewhatever the width, so on a scale with fewer thanpad_sizecolumns (default window: up to 80 wide at 1080 high, 160 at 1920 high) they counted columns past the frame.decimate()works in place, so those columns hold the previous scale's pixels, inside the stride, where no sanitizer sees the read. The shared SIMD walk never visited them, so--cpumask 63and the default dispatch disagreed, and so did the CUDA, HIP and Metal twins, which run the scalar walk on the host. All eight loops now run toMIN(pad_size, width). Upstream has the same column loops.Scores change for frames narrower than
pad_sizeat some scale (measured: 64x1920 vertical ramp master 14.975700714938673 vs branch 14.964394451743877 on the C path; the SIMD paths already agreed, giving 14.964394451743877 on both; another vertical ramp variant moves from 16.14104577967309 to 16.131541053198784). This column bound goes beyond upstream Netflix/vmaf#1629, which bounds only the rows.Three more changes in the same test file:
test_calculate_c_values_short_frameis the fork's version of upstream's sentinel test, extended. It runs every driver the host has (scalar, AVX2 mirror, AVX2 scan, AVX-512, NEON) on views of 1 to 10 rows into a taller picture. Rows outside the frame must keep a sentinel, and every in-frame c-value must equal a from-scratch window histogram.test_cambi_stage_simdalso sweeps 1-row andpad-row frames now.test_calculate_c_values_narrow_frameis the column twin: checks every driver on 1- topad + 2-column views whose stride holds banded content against a from-scratch histogram of the clipped window.test_cambi's AVX2 parity check never ran on master. perf(cambi): dispatch AVX-512 and NEON for every CAMBI stage, and scan c-values columns on AVX2 #1479 changed its gate tovmaf_get_cpu_flags_x86()on its branch. fix: clear the -Denable_asm=false and aarch64 build warnings, and lint vif_neon.c to zero #1483 merged first, and when perf(cambi): dispatch AVX-512 and NEON for every CAMBI stage, and scan c-values columns on AVX2 #1479 was rebased onto it, it kept its comment but took fix: clear the -Denable_asm=false and aarch64 build warnings, and lint vif_neon.c to zero #1483's helper with the oldvmaf_get_cpu_flags()gate. The gate now reads CPUID.Type
fix— bug fixport— cherry-pick from upstream Netflix/vmafsycl/cuda/simd— backend-specificChecklist
make format && make lintis green for the touched files. The pre-commit and pre-push hook sets pass over the whole branch diff (pre-commit run --from-ref origin/master --to-ref HEAD, both stages). clang-tidy 22 throughscripts/ci/tidy-ratchet.py --lane cpu --onlyoncambi.c,x86/cambi_avx2.candtest/test_cambi.c: 0 diagnostics in those files. cppcheck 2.22 withmake lint's arguments (--enable=all --check-level=exhaustive, the generated POSIX model, the public-entrypoint library and.cppcheck-suppressions.txt) on the same three TUs: no findings. The one pre-existing finding in a touched file,constParameterPointeroncalculate_c_values_scan_avx2(), carries a cited inline suppression: its non-const picture is fixed by upstream'sVmafCalcCValuescallback type (Research-2132). Whole-treemake lintstill reports pre-existing findings in untouched files, such ascambi_avx512.c.test_cambi(31),test_cambi_simd,test_cambi_stage_simd,test_cambi_spatial_mask_simdandtest_cambi_dispatch_invariancepass in a gcc 16 release build and in a clang 22 ASan+UBSan build with no sanitizer report.test_cambiandtest_cambi_stage_simdalso pass on an aarch64 cross build underqemu-aarch64-static, which exercises the NEON driver./cross-backend-diffand the worst ULP is ≤ 2. On the branch, the AVX-512, AVX2 and C dispatch levels give bit-identicalcambiandcambi=full_ref=truescores on every input in the table below, including the short and narrow frames (on master they split on narrow frames or crash on short ones).cambi_cudaon the RTX 4090 andcambi_hipon the gfx1036 give the CPU's default-dispatch score bit for bit on four narrow sizes. The SYCL twin, which computes c-values on the device and already clamps each window to the frame, was not run: see Known follow-ups.Bug-status hygiene (ADR-0165)
docs/state.mdupdated:T-CAMBI-SHORT-FRAME-OOB-2026-09-30,T-CAMBI-NARROW-FRAME-COLUMNS-2026-09-30andT-CAMBI-AVX2-PARITY-GATE-LOST-IN-REBASE-2026-09-30are closed;T-CLI-PRE-REGISTRATION-OPTS-DICT-LEAK-2026-09-30, found while testing CLI error exits, is opened as RC2 stabilisation. fix(cli): release option dictionaries on every early exit #1647 fixes it and moves the row to Recently closed; whichever of the two PRs merges second keeps only the closed row.Netflix golden-data gate (ADR-0024)
assertAlmostEqual(...)score in the Netflix golden Python tests. The five golden files andpython/test/cambi_test.pypass against this branch in thescripts/ci/setup-golden-build.shprofile build (core/build-golden).Cross-backend numerical results
zeus, master10f27efe2against this branch, clang 22 release builds,--precision max, 3 frames of 8-bit 4:2:0 unless noted, at--cpumask 0(AVX-512),48(AVX2) and63(C), forcambiandcambi=full_ref=true.src01_hrc00/01_576x324; checkerboard 1-px; ramps at 1920x1080; all four inputs at 96x1080pad_sizerows; levels 0 and 63)Short frames with fewer than
pad_sizerows that master completed on the C path move, for example a 3840x128 horizontal ramp16 + 60 * x / w(distorted frame one level brighter) from 19.544346510264372 to 19.512268984993305. Narrow frames move on the C path only, to the SIMD score, for example a 64x1920 vertical ramp16 + 200 * y / h(distorted +1) from 16.14104577967309 to 16.131541053198784.cambi_cuda(RTX 4090) andcambi_hip(gfx1036) gave exactly the old C-path value before and exactly the new one after, at 64x1920, 128x1920, 32x1080 and 216x3840. Research-2132 lists every value, the input generators and the earlier measurements of the row fix (BBB 3840x2160, both checkerboard pairs, 4:4:4 inputs).Scores change for frames narrower than
pad_sizeat some scale (measured: 64x1920 vertical ramp master 14.975700714938673 vs branch 14.964394451743877 on the C path; the SIMD paths already agreed, giving 14.964394451743877 on both; another vertical ramp variant moves from 16.14104577967309 to 16.131541053198784). This column bound goes beyond upstream Netflix/vmaf#1629, which bounds only the rows.7680x216 still fails cleanly with
cambi: window_size 85 too large for reciprocal LUT.Deep-dive deliverables (ADR-0108)
## Alternatives considered: clip in every walk (chosen), reject ininit()(the alternative in cambi: out-of-bounds read and write on wide, short frames (e.g. 1920x128), C and AVX2 paths Netflix/vmaf#1628), port only upstream's row bounds, make the SIMD walks read the extra columns too.AGENTS.mdinvariant note —core/src/feature/AGENTS.md: the three walks keep both bounds, and an upstream sync keeps them.changelog.d/fixed/cambi-short-frame-oob.md;CHANGELOG.mdis regenerated withscripts/release/concat-changelog-fragments.sh --write.docs/rebase-notes.md, entryfix/cambi-short-frame-oob, including what to keep if upstream rejects such frames ininit()instead.Reproducer
meson setup build-asan core -Db_sanitize=address,undefined -Db_lundef=false \ -Denable_cuda=false -Denable_sycl=false ninja -C build-asan tools/vmaf test/test_cambi ./build-asan/test/test_cambi # short_frame and narrow_frame: pass head -c $((3 * 1920 * 128 * 3 / 2)) /dev/urandom > ref.yuv head -c $((3 * 1920 * 128 * 3 / 2)) /dev/urandom > dis.yuv for mask in 0 48 63; do ./build-asan/tools/vmaf -r ref.yuv -d dis.yuv -w 1920 -h 128 -p 420 -b 8 \ --frame_cnt 3 --no_prediction --feature cambi --cpumask $mask \ --json -o /dev/null done # master: ASan SEGV; branch: exit 0Known follow-ups
cvals_prime()andcvals_column()ininteger_cambi_sycl.cppclamp every window to rows[0, height - 1]and columns[0, width - 1], so by code reading the twin already computes the clipped window. It was not run: the Arc A380 on this host runs the xe kernel driver this boot, and under it SYCL kernels that use scratch memory (IGCprivate_sizeorspill_sizeabove 0) return wrong values. Switching to i915 needs root and a reboot.wip/handoff-hip-rc3-deviceneed the same short- and narrow-frame check before they replace the host walk.T-CLI-PRE-REGISTRATION-OPTS-DICT-LEAK-2026-09-30is fixed in fix(cli): release option dictionaries on every early exit #1647.