Repository navigation
fix(adm): bound the integer ADM weights by the contrast-masking cube so Barten mode cannot wrap (ADR-1472) - #1872
Merged
Merged
Conversation
…eck on master (#1871) * fix(ci): fix the fourteen cppcheck findings that fail the Cppcheck check on master The required Cppcheck job (cppcheck 2.19.0, --check-level=exhaustive) fails on master. The first complete hosted run since 2026-09-30 (513d2a6, run 37011276599) reported three findings; #1859, landed after it, added eleven. The merge train does not run cppcheck. - core/src/dict.cpp, identicalInnerCondition: `if (*dict) return *dict;` in a function that returns std::expected reads to cppcheck as one condition tested twice. Now `if (*dict != nullptr) return *dict;`. - core/test/test_video_input_odd_dims.c, returnDanglingLifetime (2): the two frame readers were called through a pointer to a function type, which cppcheck takes for an initializer list that keeps `&vid`. One function, read_frame(), now selects the reader by a bool and calls it directly. The cases and assertions are unchanged. - core/src/feature/adm.c, invalidPointerCast (11): #1859 dropped the `(void *)` hop of the band-plane carving for clang-tidy's bugprone-casting-through-void, which left a direct `(float *)` cast of a `char *` cursor. The cursor now has the sample type and steps buf_sz_one / sizeof(float) samples, so no cast is left and both tools are quiet. The plane addresses are the same. Verified on master 38e8ec0 plus this change. The job's own steps in the dev container (cppcheck 2.19.0-3, Ubuntu 26.04) check 1488 files and exit 0 with an empty report; on 513d2a6 the same steps reproduce the job's XML report byte for byte. clang-tidy 22.1.8 reports 0 for the three files in the cpu lane's configuration. CPU fast suite 244 of 244, Netflix golden gate 271 passed and 12 skipped. Nothing is suppressed. * docs(rebase): note the typed band-plane cursor of adm.c An upstream change to init_dwt_band() or to the carving in compute_adm() conflicts with the typed cursor; the note says which form to keep and why both casts fail a required check.
…so Barten mode cannot wrap (ADR-1472) (#1872) * fix(adm): bound the integer ADM weights by the contrast-masking cube so Barten mode cannot wrap (ADR-1472) The contrast-masking reduction squares the excess of each weighted wavelet coefficient over its threshold and narrows the square to int32 (upstream's I4_ADM_CM_ACCUM_ROUND). That holds for an excess up to 1518500249. ADR-1325 let a scale 1 to 3 weight reach 2^30, which makes the weighted coefficient up to four times the coefficient, so on a high-contrast picture in Barten mode the square wrapped: - adm=adm_csf_mode=1 on the 10 px checkerboard failed every frame with aim_num=-nan (scale 3, diagonal band: coefficient 460572333, square 2246297208); - on the 1 px checkerboard it returned integer_adm2 0.587102 where float_adm gives 0.783557, with no message; - with adm_csf_scale=1.2 (the open row's reproducer) the same operation wraps at scale 1. adm_csf_fixed_limit() now bounds each weight by the excess budget divided by the largest coefficient the wavelet can produce at the scale: half the absolute sum of the composite filter, 22929 at scale 0 and 1448980000, 751508000, 742509000 at scales 1 to 3. Limits: 46603.4 (scale 0 h and v), 65536 (scale 0 d), 279958309, 539893111, 546406567 (were 65536 and 2^30). No kernel changes: CPU, CUDA, HIP, SYCL and Metal take the weights from adm_csf_fixed_scale(). Scores: the three reproducers score and agree with float_adm to 1e-5. Barten-mode outputs that were right move by at most 7.7e-7 on the Netflix pair (exponents 5, 6, 7 become 7, 7, 8). Watson97 and the two blend modes are bit-identical: 1635 of 1695 recorded adm / float_adm cases identical, the other 60 are the integer adm_csf_mode=1 set. Netflix golden gate 271 passed, 12 skipped on x86 GCC and aarch64 GCC. test_integer_adm_cm_budget derives the bounds from the wavelet taps, holds the limits against them from both sides and scores five adversarial frames against float_adm. With the old limits three of its five tests fail; ten planted changes are each detected. Twins on an RTX 4090, a gfx1036 and an Arc A380: every adm output equals --backend cpu under five option sets on the Netflix pair, both checkerboards and six adversarial frames. Upstream Netflix/vmaf cea2b4d83 has the option and the same reduction and no weight normalisation: integer_adm2 0.0027 for 0.9654 on the Netflix pair. * docs: regenerate the indexes and the citation map after rebasing
lusoris
force-pushed
the
fix/integer-adm-aim-wrap
branch
from
October 2, 2026 16:52
37c3872 to
b34732a
Compare
This was referenced Oct 2, 2026
Merged
This branch was successfully deployed
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
Integer
admwithadm_csf_mode=1(Barten) wrapped a 32-bit square in its contrast-masking reduction on high-contrast pictures. It failed with a NaN numerator on the 10 px checkerboard and returned a wronginteger_adm2on the 1 px checkerboard without any message. The weight limits now follow from the arithmetic, so no picture can wrap. ClosesT-ADM-AIM-BARTEN-SCALE-TERM-WRAP-2026-10-01(ADR-1472).Cause
Per sample the reduction forms the excess
vof the weighted wavelet coefficient over its masking threshold, then(upstream's
I4_ADM_CM_ACCUM_ROUND, the fork'si4_adm_cm_accum_round()and every SIMD and device twin). The square fits int32 only up tov = 1518500249. ADR-1325 let a scale 1 to 3 weight reach 2^30, which makes the weighted coefficient up to four times the coefficient. The smallest reproducer, from an instrumented build:The cube of that sample is negative, the band's accumulator follows, and
powf()of a negative base is NaN. A wrap does not always end in NaN: on the 1 px checkerboard one scale-3 square wraps, the accumulator stays positive, and master returns a wrong score silently. The open row's reproducer (adm_csf_scale=1.2) is the same operation at scale 1, so this is that row's defect.Fix
adm_csf_fixed_limit(scale, band)incore/src/feature/adm_csf_fixed_point.hbounds each fixed-point weight by the excess budget divided by the largest coefficient the wavelet can produce at that scale. That coefficient follows from the filter taps: a detail band is linear in the centred pixel, which lies in [-1/2, 1/2), so its magnitude is at most half the absolute sum of the composite filter in the band's fixed-point format.Everything else of ADR-1325 stands (one exponent per scale, restored as
3kafter the cube). No kernel, SIMD or device file changes: CPU, CUDA, HIP, SYCL and Metal calladm_csf_fixed_scale(). The alternatives (widening the square, a per-frame exponent, saturating) are in the ADR; widening cannot be made complete in 64 bits because under a weight of 2^30 the weighted coefficient itself leaves int32.Bits
adm=debug=true:<options>at--precision max, masterade338374against this branch:float_admadm_csf_mode=1aim_num=-naninteger_aim0.999874302adm_csf_mode=1integer_adm20.587102 / 0.663832 / 0.563360, no messageadm_csf_mode=1:adm_csf_scale=1.2integer_adm20.782465adm_csf_mode=1integer_adm22.1e-7,integer_adm34.1e-7,integer_aim7.7e-7adm_csf_mode=2,adm_csf_mode=3adm/float_admcases of standards batch B1 (21 option sets, 20 fixtures, scalar / AVX2 / AVX-512): 1635 of 1695 identical to master; the 60 that differ are the integeradm_csf_mode=1set (20 fixtures, three dispatch levels).integer_aim0.8645 for 1.0037 on the sixth. This PR agrees withfloat_admto 8.7e-6 whereverfloat_admdoes not clip.Test
core/test/test_integer_adm_cm_budget.c(new, suitefast): derives the band bounds from the wavelet taps and holds the header's constants against them (not below, not more than 1 % above); a weight just under its limit keeps the worst square in int32 and one 2 % over does not;adm_csf_fixed_scale()brings a sweep over seven decades and three band ratios under the limits with the smallest exponent; five adversarial frames score in Barten mode and agree withfloat_admto 1e-4 (measured 2.4e-6 or better).With the old limits three of the five tests fail (the other two do not depend on the limits). Ten planted changes (a band bound too low, doubled, halved; the weight shift; the scale-0 budget; a band left out of the normalisation loop; an exponent stepping by two) are each detected.
Golden, twins, cost
Netflix golden gate: 271 passed, 12 skipped on x86-64 GCC and on aarch64 GCC under qemu, on this head.
x86
--suite=fast: 245 of 245.Twins, every one of the 18
adm=debug=trueoutputs against--backend cpu, under the default options,adm_csf_mode=1,adm_csf_mode=1:adm_csf_scale=1.2,adm_csf_mode=1:adm_csf_scale=1.4:adm_csf_diag_scale=0.3andadm_csf_mode=2, on the Netflix pair (48 frames), both checkerboards (3 frames each) and six adversarial frames:adm_cudaGate cells
admandfloat_adm, cpu against cuda: 0 on the Netflix pair (48 frames) and on BBB 3840x2160 (200 frames). Metal takes the weights from the same function and is not measured here.Cost: none. BBB 3840x2160, 20 frames, one thread, median of five at a load average of 45: default 29.99 ms per frame before, 29.13 after;
adm_csf_mode=164.47 before, 65.06 after.Upstream
Netflix/vmaf
cea2b4d83(2026-10-01),libvmaf/src/feature/integer_adm.c: the option exists (line 179) and the reduction is the same (I4_ADM_CM_ACCUM_ROUND, line 698), but upstream converts the weights with(uint16_t)/(uint32_t)casts and no normalisation, so Barten weights (1.21 at scale 0 to 26.98 at scale 3) wrap in the conversion itself. Measured with upstream's binary,adm=adm_csf_mode=1:integer_adm20.002706 againstfloat_adm0.965404 on the Netflix pair; NaN on the 1 px checkerboard. Upstream's default Watson97 weights are inside the budget derived here. For upstream to follow it needs the fork's shared-exponent normalisation (ADR-1325) with these limits.Type
feat— new featurefix— bug fixperf— performance improvementrefactor— no behavior changedocs— documentation onlytest— test-onlybuild/ci— tooling / infraport— cherry-pick from upstream Netflix/vmafsycl/cuda/simd— backend-specificChecklist
make format && make lintis green locally. (clang-format and the commit hooks. clang-tidy 22.1.8: the new test,integer_adm.cand the header measure 0 on the cpu and cuda lanes. The host's clang-tidy became 23.1.1 during this work (system upgrade at 18:05), so the hip, sycl and arm64 lanes could not be measured against their 22.1.8 baselines; under 23.1.1 the new test shows onlymodernize-redundant-void-argonf(void), which that version reports for every C test in the tree.)python3 scripts/ci/run_meson_test.py -- -C build. (--suite=faston a CPU build: 245 of 245.)/cross-backend-diffand the worst ULP is ≤ 2. (No SIMD or GPU file is touched; the twins equal the CPU bit for bit, see above.).c/.cpp/.cu/.h/.hpp, it has the appropriate license header (seeCONTRIBUTING.md). (test_integer_adm_cm_budget.c: EUPL-1.2.)!orBREAKING CHANGE:and the migration path is documented below. (Not breaking: Barten-mode scores that were right move by at most 7.7e-7.)docs/adr/_index_fragments/<NNNN-slug>.md. (1472-integer-adm-cm-weight-budget.md.)Bug-status hygiene (ADR-0165)
docs/state.mdupdated in this PR.T-ADM-AIM-BARTEN-SCALE-TERM-WRAP-2026-10-01moved to "Recently closed" with the cause, the limits and the measurements.Netflix golden-data gate (ADR-0024)
assertAlmostEqual(...)score in the Netflix golden Python tests.Deep-dive deliverables (ADR-0108)
docs/research/1472-integer-adm-cm-weight-budget.md.AGENTS.mdinvariant note —core/src/feature/AGENTS.d/adm-barten-weights.md(the limit bullet now namesadm_csf_fixed_limit()and what must change together) andcore/src/feature/cuda/AGENTS.d/adm.md(what a twin must not do).changelog.d/fixed/integer-adm-cm-weight-budget.md.docs/rebase-notes.md, "Integer ADM weight limits follow from the contrast-masking cube".Reproducer
Known follow-ups
measured_sourcesdo not list the new test; I did not write baselines from this host after its clang-tidy changed version.integer_aimis not clipped at 1 wherefloat_admclips (1.0037 against 1 on an adversarial frame);test_integer_adm_aim_unclippedholds that behaviour. Not changed here.