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
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1554,6 +1554,19 @@ make `core/AGENTS.md` a generated index over `AGENTS.d/` topic pages ([ADR-1454]
negligible (~15–20s) overhead, and is enforced in the required checks aggregator.


- **The `Cppcheck` check passes again.** The first complete hosted run on
master since 2026-09-30 reported three findings (cppcheck 2.19.0,
`--check-level=exhaustive`): `identicalInnerCondition` in
`core/src/dict.cpp` (`if (*dict) return *dict;`) and
`returnDanglingLifetime` twice in
`core/test/test_video_input_odd_dims.c`, where a frame reader called
through a function pointer was taken for an aggregate that keeps the
address of a local. A later cleanup of `core/src/feature/adm.c` added
eleven `invalidPointerCast` reports: the band planes were carved from a
`char *` cursor with a direct `(float *)` cast. The cursor now has the
sample type. No behaviour changes, and nothing is suppressed.


- **Brought CPU lane back to clang-tidy baseline.** Resolved 13 clang-tidy
regressions introduced by merges into `core/src/picture_pool.cpp`,
`core/src/read_json_model.cpp`, `core/test/test_psnr_hvs_score.c`,
Expand Down Expand Up @@ -2338,6 +2351,22 @@ make `core/AGENTS.md` a generated index over `AGENTS.d/` topic pages ([ADR-1454]
[HIP backend](docs/backends/hip/overview.md#vif_hip-returns-the-cpus-scores-bit-for-bit-2026-10-01)).


- **Integer `adm` in Barten mode (`adm_csf_mode=1`) no longer wraps on
high-contrast pictures (ADR-1472).** The contrast-masking reduction squares
each weighted wavelet coefficient and narrows the square to 32 bits; with
the weight limit of 2^30 that ADR-1325 chose for scales 1 to 3 the square,
and on some pictures the weighted coefficient itself, could leave 32 bits.
`vmaf --feature adm=adm_csf_mode=1` failed every frame of the 10 px
checkerboard (`aim_num=-nan`) and returned `integer_adm2` 0.587 instead of
0.784 on the 1 px checkerboard without any message. The weight limits now
follow from the largest coefficient the wavelet can produce at each scale
(`adm_csf_fixed_limit()`), so no picture can wrap. Barten-mode scores that
were already right move by at most 7.7e-7 on the Netflix pair; Watson97 and
the two blend modes are bit-identical. The GPU twins take the weights from
the same header and follow without a kernel change (CUDA, HIP and SYCL
measured: every `adm` output equals the CPU's).


- **The Windows MSVC builds compile `blur_array.c` again.** A lint cleanup on
2026-10-02 had replaced `NULL` with the C23 keyword `nullptr` in that C file;
MSVC's C mode does not know the keyword.
Expand Down
11 changes: 11 additions & 0 deletions changelog.d/fixed/cppcheck-master-findings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
- **The `Cppcheck` check passes again.** The first complete hosted run on
master since 2026-09-30 reported three findings (cppcheck 2.19.0,
`--check-level=exhaustive`): `identicalInnerCondition` in
`core/src/dict.cpp` (`if (*dict) return *dict;`) and
`returnDanglingLifetime` twice in
`core/test/test_video_input_odd_dims.c`, where a frame reader called
through a function pointer was taken for an aggregate that keeps the
address of a local. A later cleanup of `core/src/feature/adm.c` added
eleven `invalidPointerCast` reports: the band planes were carved from a
`char *` cursor with a direct `(float *)` cast. The cursor now has the
sample type. No behaviour changes, and nothing is suppressed.
14 changes: 14 additions & 0 deletions changelog.d/fixed/integer-adm-cm-weight-budget.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
- **Integer `adm` in Barten mode (`adm_csf_mode=1`) no longer wraps on
high-contrast pictures (ADR-1472).** The contrast-masking reduction squares
each weighted wavelet coefficient and narrows the square to 32 bits; with
the weight limit of 2^30 that ADR-1325 chose for scales 1 to 3 the square,
and on some pictures the weighted coefficient itself, could leave 32 bits.
`vmaf --feature adm=adm_csf_mode=1` failed every frame of the 10 px
checkerboard (`aim_num=-nan`) and returned `integer_adm2` 0.587 instead of
0.784 on the 1 px checkerboard without any message. The weight limits now
follow from the largest coefficient the wavelet can produce at each scale
(`adm_csf_fixed_limit()`), so no picture can wrap. Barten-mode scores that
were already right move by at most 7.7e-7 on the Netflix pair; Watson97 and
the two blend modes are bit-identical. The GPU twins take the weights from
the same header and follow without a kernel change (CUDA, HIP and SYCL
measured: every `adm` output equals the CPU's).
5 changes: 4 additions & 1 deletion core/src/dict.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ namespace
*/
[[nodiscard]] std::expected<VmafDictionary *, int> dict_ensure_allocated(VmafDictionary **dict)
{
if (*dict)
// Compared, not tested as a truth value: cppcheck 2.19 reads
// `if (*dict) return *dict;` as one condition repeated inside itself
// (identicalInnerCondition).
if (*dict != nullptr)
return *dict;

auto *d = static_cast<VmafDictionary *>(std::malloc(sizeof(VmafDictionary)));
Expand Down
20 changes: 16 additions & 4 deletions core/src/feature/AGENTS.d/adm-barten-weights.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
paths:
- core/src/feature/adm_csf_fixed_point.h
- core/src/feature/integer_adm.c
invariant: Integer ADM Barten weights use one power-of-two exponent per scale.
invariant: One power-of-two exponent per scale; weight limits come from adm_csf_fixed_limit(), never from storage.
---
<!-- markdownlint-disable MD013 MD032 MD060 -->
# Integer ADM Barten Weights Exponent Contract
Expand All @@ -15,9 +15,19 @@ when rebasing or changing any integer-ADM twin:

- choose one non-negative power-of-two exponent `k` for all three bands of a
DWT scale; independent band shifts change the metric;
- keep normalized scale-0 weights strictly below 2^16 and scale-1..3 weights
strictly below 2^30, retaining two headroom bits for signed CSF/CM and cube
arithmetic;
- keep each normalized weight strictly below `adm_csf_fixed_limit(scale, band)`
(ADR-1472): 46603.4 for scale-0 h / v, 2^16 for scale-0 d, 279958309,
539893111, 546406567 at scales 1..3. Limit = contrast-masking excess budget
(`(v * v + round) >> 29|30` must fit int32: 2^30 - 1 / 1518500249) divided
by largest wavelet coefficient of the scale (`ADM_DWT_BAND_MAX_SCALE0..3`,
half the absolute sum of the composite filter). Old limit 2^30 wrapped the
square in Barten mode: NaN numerator (10 px checkerboard), `integer_adm2`
0.587 for 0.784 without a message (1 px). Never raise a limit, never widen
or saturate the square in one implementation only;
- changed DWT taps or shifts, `shift_sq`, `i4_shift_dst` -> band bounds
change: update the constants and `BAND_FORMAT` in
`core/test/test_integer_adm_cm_budget.c` together (the test derives the
bounds from the taps and fails until they agree);
- restore `3k` in the host contrast-masking finalizer because the accumulated
signal is cubed, while the denominator continues to use the original float
CSF factors;
Expand All @@ -32,6 +42,8 @@ when rebasing or changing any integer-ADM twin:
The CPU reference checks before computation; GPU twins check before
normalization or device work.

`test_integer_adm_cm_budget` holds the limits against the taps and scores
adversarial frames against `float_adm`.
`test_adm_csf_representable` pins finite, non-degenerate CPU mode-1 output;
the CUDA, SYCL, HIP, and Metal parity fixtures pin their supported scores at
places=4. Metal integer ADM implements modes 0..3 and therefore must not regain
Expand Down
2 changes: 1 addition & 1 deletion core/src/feature/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ PSNR, SSIM, MS-SSIM, LPIPS, …). Parent: [../../AGENTS.md](../../AGENTS.md).
| Touching | Read first | Invariant |
| --- | --- | --- |
| `adm_tools.c`, `adm_tools.h` | [adm-angle-flag](AGENTS.d/adm-angle-flag.md) | adm_angle_flag has exactly one definition across floating-point and integer paths. |
| `adm_csf_fixed_point.h`, `integer_adm.c` | [adm-barten-weights](AGENTS.d/adm-barten-weights.md) | Integer ADM Barten weights use one power-of-two exponent per scale. |
| `adm_csf_fixed_point.h`, `integer_adm.c` | [adm-barten-weights](AGENTS.d/adm-barten-weights.md) | One power-of-two exponent per scale; weight limits come from adm_csf_fixed_limit(), never from storage. |
| `adm_tools.c`, `adm_tools.h` | [adm-contrast-masking](AGENTS.d/adm-contrast-masking.md) | ADM contrast-masking edge policy is asymmetric across CPU and GPU twins. |
| `integer_adm.c`, `integer_adm.h` | [adm-dwt](AGENTS.d/adm-dwt.md) | Integer ADM DWT mirror table for tiny extents and 16-bit vertical int64 sums. |
| `integer_adm.c`, `adm_tools.c` | [adm-kernels-restructure](AGENTS.d/adm-kernels-restructure.md) | Integer ADM kernels header separation, AIM clipping differences, and adm_min_val clamping. |
Expand Down
74 changes: 39 additions & 35 deletions core/src/feature/adm.c
Original file line number Diff line number Diff line change
Expand Up @@ -81,45 +81,48 @@ static int adm_dwt2_dispatch(const float *src, const adm_dwt_band_t_s *dst, int

#define adm_dwt2 adm_dwt2_dispatch

/* The band planes are carved out of one aligned buffer in steps of
* buf_sz_one bytes, a multiple of MAX_ALIGN: every data_top below is aligned
* for the sample type it is cast to. */
static char *init_dwt_band(adm_dwt_band_t *band, char *data_top, size_t buf_sz_one)
/* The band planes are carved out of one aligned buffer in steps of band_len
* samples: buf_sz_one bytes, a multiple of MAX_ALIGN and so of the sample
* size. The cursor has the sample type, so no plane is reached through a
* cast: cppcheck reports `(float *)` on a `char *` cursor as
* invalidPointerCast, and clang-tidy reports the `(void *)` hop that would
* quiet it as bugprone-casting-through-void. */
static float *init_dwt_band(adm_dwt_band_t *band, float *data_top, size_t band_len)
{
band->band_a = (float *)data_top;
data_top += buf_sz_one;
band->band_h = (float *)data_top;
data_top += buf_sz_one;
band->band_v = (float *)data_top;
data_top += buf_sz_one;
band->band_d = (float *)data_top;
data_top += buf_sz_one;
band->band_a = data_top;
data_top += band_len;
band->band_h = data_top;
data_top += band_len;
band->band_v = data_top;
data_top += band_len;
band->band_d = data_top;
data_top += band_len;
return data_top;
}

UNUSED_FUNCTION
static char *init_dwt_band_d(adm_dwt_band_t_d *band, char *data_top, size_t buf_sz_one)
static double *init_dwt_band_d(adm_dwt_band_t_d *band, double *data_top, size_t band_len)
{
band->band_a = (double *)data_top;
data_top += buf_sz_one;
band->band_h = (double *)data_top;
data_top += buf_sz_one;
band->band_v = (double *)data_top;
data_top += buf_sz_one;
band->band_d = (double *)data_top;
data_top += buf_sz_one;
band->band_a = data_top;
data_top += band_len;
band->band_h = data_top;
data_top += band_len;
band->band_v = data_top;
data_top += band_len;
band->band_d = data_top;
data_top += band_len;
return data_top;
}

static char *init_dwt_band_hvd(adm_dwt_band_t *band, char *data_top, size_t buf_sz_one)
static float *init_dwt_band_hvd(adm_dwt_band_t *band, float *data_top, size_t band_len)
{
band->band_a = NULL;
band->band_h = (float *)data_top;
data_top += buf_sz_one;
band->band_v = (float *)data_top;
data_top += buf_sz_one;
band->band_d = (float *)data_top;
data_top += buf_sz_one;
band->band_h = data_top;
data_top += band_len;
band->band_v = data_top;
data_top += band_len;
band->band_d = data_top;
data_top += band_len;
return data_top;
}

Expand Down Expand Up @@ -197,14 +200,15 @@ static int adm_alloc_bands(AdmFrameBufs *b, int w, int h)
return 1;
}

char *data_top = (char *)b->data_buf;
const size_t band_len = buf_sz_one / sizeof(float);
float *data_top = b->data_buf;

data_top = init_dwt_band(&b->ref_dwt2, data_top, buf_sz_one);
data_top = init_dwt_band(&b->dis_dwt2, data_top, buf_sz_one);
data_top = init_dwt_band_hvd(&b->decouple_r, data_top, buf_sz_one);
data_top = init_dwt_band_hvd(&b->decouple_a, data_top, buf_sz_one);
data_top = init_dwt_band_hvd(&b->csf_a, data_top, buf_sz_one);
(void)init_dwt_band_hvd(&b->csf_f, data_top, buf_sz_one);
data_top = init_dwt_band(&b->ref_dwt2, data_top, band_len);
data_top = init_dwt_band(&b->dis_dwt2, data_top, band_len);
data_top = init_dwt_band_hvd(&b->decouple_r, data_top, band_len);
data_top = init_dwt_band_hvd(&b->decouple_a, data_top, band_len);
data_top = init_dwt_band_hvd(&b->csf_a, data_top, band_len);
(void)init_dwt_band_hvd(&b->csf_f, data_top, band_len);
return 0;
}

Expand Down
94 changes: 79 additions & 15 deletions core/src/feature/adm_csf_fixed_point.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@
* horizontal / vertical bands scaled by 2^21 and the diagonal band
* by 2^23;
* - scales 1..3 (32-bit pipeline): `uint32_t i_rfactor[3]`, all three
* bands scaled by 2^32. The later signed multiply/cube stages require
* two headroom bits, so their usable arithmetic budget is 2^30.
* bands scaled by 2^32.
*
* Storage is not the binding limit. The contrast-masking reduction squares
* the weighted sample and narrows the square to int32, so a weight is in
* budget only when the largest wavelet coefficient of its scale, weighted,
* still has a square that fits (ADR-1472; the bounds are below).
*
* Those budgets were sized for the Watson97 CSF, whose weights sit around
* 1e-2. The fork-added `adm_csf_mode` option (integer_adm.h) also exposes
Expand All @@ -27,7 +31,7 @@
* for all three bands of one scale, preserving their relative CSF weights;
* the contrast-masking reduction restores the removed exponent after its
* cube. Invalid negative / non-finite table results are still rejected.
* See ADR-1191, ADR-1325, and
* See ADR-1191, ADR-1325, ADR-1472, and
* docs/state.md :: T-UPSTREAM-1494-ADM-CSF-MODE-IRFACTOR-OVERFLOW-2026-09-03.
*
* The frame-size bound and the rounding constant of the pipeline's right
Expand All @@ -51,15 +55,77 @@
#define ADM_CSF_SCALE0_D_EXP (23)
#define ADM_CSF_S123_EXP (32)

/* Exclusive upper bounds of the corresponding fixed-point arithmetic. A
* converted weight equal to the bound is outside the budget, so comparisons
* are strict. Scale 0 is storage-limited; scales 1..3 keep two bits below
* uint32_t's ceiling because their signed CSF/CM arithmetic and cube
* accumulation need that headroom. */
#define ADM_CSF_SCALE0_LIMIT (65536.0) /* 2^16, uint16_t i_rfactor */
#define ADM_CSF_S123_LIMIT (1073741824.0) /* 2^30, signed headroom */
/* Storage bound of the scale-0 weights: uint16_t i_rfactor. */
#define ADM_CSF_SCALE0_LIMIT (65536.0)
#define ADM_MIN_VIEWING_GEOMETRY (3240.0) /* 1080p at 3H */

/*
* Arithmetic budget of the contrast-masking cube (ADR-1472).
*
* Per sample the reduction forms the excess `v` of the weighted coefficient
* over its masking threshold, then `v_sq = (int32_t)((v * v + round) >> s)`
* and `v_sq * v` (adm_cm_accum_round(), i4_adm_cm_accum_round(), and their
* SIMD and device twins). The square fits int32 only up to
*
* s = 29 (scale 0, horizontal / vertical): v <= 2^30 - 1
* s = 30 (scale 0 diagonal, scales 1..3): v <= 1518500249
*
* A larger excess wraps: the cube turns negative or small, the score is wrong,
* and a negative accumulator ends as NaN in the p-norm.
*
* The excess is at most the weighted coefficient, plus 28 at scales 1..3
* (the threshold there can be as low as -27, ADR-0155's rounding term, and
* the weighting rounds). The weighted coefficient is `band * weight` at
* scale 0 and `(band * weight) >> 28` at scales 1..3. So the weight budget
* of a scale is the excess budget divided by the largest coefficient the
* wavelet can produce there.
*
* That coefficient is bounded by the filter taps, whatever the picture: a
* detail band is a linear function of the centred pixel p / 2^bpc - 1/2,
* 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 (2^14 at
* scale 0, 2^29, 2^27 and 2^26 at scales 1, 2 and 3):
*
* scale 0: 22929.4 scale 2: 751508000
* scale 1: 1448980000 scale 3: 742509000
*
* (the larger of the horizontal / vertical and the diagonal band; a frame
* built from the sign pattern of the composite filter reaches 99.6 % of each
* at 8 bits). The constants below round those up by about half a percent,
* which covers the pipeline's rounding and the decouple stage's reciprocal
* table. core/test/test_integer_adm_cm_budget.c derives the bounds again
* from the taps and holds them against these constants.
*/
#define ADM_CM_EXCESS_MAX_SQ29 (1073741823.0)
#define ADM_CM_EXCESS_MAX_SQ30 (1518500249.0)
#define ADM_I4_CM_EXCESS_SLACK (28.0)
#define ADM_DWT_BAND_MAX_SCALE0 (23040.0)
#define ADM_DWT_BAND_MAX_SCALE1 (1456000000.0)
#define ADM_DWT_BAND_MAX_SCALE2 (755000000.0)
#define ADM_DWT_BAND_MAX_SCALE3 (746000000.0)
#define ADM_I4_CM_WEIGHT_SHIFT (268435456.0) /* 2^28, i4_shift_dst */

/**
* Exclusive upper bound of the fixed-point CSF weight of `band` (0 and 1:
* horizontal and vertical, 2: diagonal) at `scale`. A converted weight equal
* to the bound is outside the budget, so comparisons are strict.
*
* Scale 0: the horizontal and vertical weights are bound by the cube; the
* diagonal weight by its uint16_t storage (65535 * 23040 is still below the
* s = 30 excess budget). Scales 1..3: bound by the cube; every bound is
* below 2^30, so the uint32_t storage and the signed products hold as well.
*/
static inline double adm_csf_fixed_limit(int scale, int band)
{
if (scale == 0) {
return band == 2 ? ADM_CSF_SCALE0_LIMIT : ADM_CM_EXCESS_MAX_SQ29 / ADM_DWT_BAND_MAX_SCALE0;
}
const double band_max = scale == 1 ?
ADM_DWT_BAND_MAX_SCALE1 :
(scale == 2 ? ADM_DWT_BAND_MAX_SCALE2 : ADM_DWT_BAND_MAX_SCALE3);
return (ADM_CM_EXCESS_MAX_SQ30 - ADM_I4_CM_EXCESS_SLACK) * ADM_I4_CM_WEIGHT_SHIFT / band_max;
}

/**
* Preserve the integer pipeline's minimum angular-frequency contract. The
* reference CPU extractor has always rejected viewing geometries below
Expand Down Expand Up @@ -160,18 +226,16 @@ static inline int adm_csf_fixed_scale(int scale, const float rfactor1[3], double
return geometry_err;
}

double limit;

if (scale == 0) {
adm_csf_scale0_fixed(rfactor1, adm_norm_view_dist, adm_ref_display_height, adm_csf_mode,
fixed);
limit = ADM_CSF_SCALE0_LIMIT;
} else {
for (int band = 0; band < 3; ++band) {
fixed[band] = (double)rfactor1[band] * pow(2, ADM_CSF_S123_EXP);
}
limit = ADM_CSF_S123_LIMIT;
}
const double limit[3] = {adm_csf_fixed_limit(scale, 0), adm_csf_fixed_limit(scale, 1),
adm_csf_fixed_limit(scale, 2)};

for (int band = 0; band < 3; ++band) {
if (!adm_csf_fixed_valid(fixed[band])) {
Expand All @@ -186,7 +250,7 @@ static inline int adm_csf_fixed_scale(int scale, const float rfactor1[3], double
}

*normalization_shift = 0u;
while (fixed[0] >= limit || fixed[1] >= limit || fixed[2] >= limit) {
while (fixed[0] >= limit[0] || fixed[1] >= limit[1] || fixed[2] >= limit[2]) {
fixed[0] *= 0.5;
fixed[1] *= 0.5;
fixed[2] *= 0.5;
Expand Down
Loading
Loading