PR #25567 opened by Niklas Haas (haasn)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25567
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25567.patch

This branch has a number of improvements to the self-test:
* Instead of just checking the overall MSE, we also evaluate the maximum error 
against the maximum permissible per-pixel error; derived as a heuristic from 
the bit depth of the dst format
* The reference is now gbrapf32 instead of yuva444p, meaning we can detect 
errors at floating point precision instead of always rounding back to 8-bit; 
allows detecting errors in high-bit depth conversions
* Instead of testing directly against the source, we test against an 
already-reduced subset of the source, limited to what the formats can actually 
contain (e.g. stripping alpha/color information), and quantized to the actual 
source format

This enhanced self-test actually fails a large portion of the legacy unscaled 
converters. The new ops code passes all.


>From 2fceef734790b10a8d85f1e70397adab353d6d39 Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 26 Oct 2026 13:58:17 +0100
Subject: [PATCH 01/17] swscale/tests/swscale: factor out print_ssim()

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/tests/swscale.c | 18 ++++++++++++------
 1 file changed, 12 insertions(+), 6 deletions(-)

diff --git a/libswscale/tests/swscale.c b/libswscale/tests/swscale.c
index 23b41dc7298..6ddfadee1ed 100644
--- a/libswscale/tests/swscale.c
+++ b/libswscale/tests/swscale.c
@@ -526,6 +526,11 @@ static void print_test_params(AVBPrint *bp,
                mode->scaler_sub);
 }
 
+static void print_ssim(AVBPrint *bp, const float ssim[4])
+{
+    av_bprintf(bp, "SSIM={Y=%f U=%f V=%f A=%f}", ssim[0], ssim[1], ssim[2], 
ssim[3]);
+}
+
 static void print_results(const AVFrame *ref, const AVFrame *src, const 
AVFrame *dst,
                           int dst_w, int dst_h,
                           const struct mode *mode, const struct options *opts,
@@ -540,9 +545,9 @@ static void print_results(const AVFrame *ref, const AVFrame 
*src, const AVFrame
         print_test_params(bp, src, dst, mode, opts);
 
         if (!opts->bench || !ref_r) {
-            av_bprintf(bp, ", SSIM={Y=%f U=%f V=%f A=%f} loss=%e",
-                       r->ssim[0], r->ssim[1], r->ssim[2], r->ssim[3],
-                       r->loss);
+            av_bprintf(bp, ", ");
+            print_ssim(bp, r->ssim);
+            av_bprintf(bp, " loss=%e", r->loss);
             if (ref_r)
                 av_bprintf(bp, " (ref=%e)", ref_r->loss);
         }
@@ -609,9 +614,10 @@ static void print_results(const AVFrame *ref, const 
AVFrame *src, const AVFrame
             print_test_params(bp, src, dst, mode, opts);
             av_bprintf(bp, "\n");
         }
-        av_bprintf(bp, "  loss %e is %s by %e, ref loss %e SSIM={Y=%f U=%f 
V=%f A=%f}\n",
-                   r->loss, worse_str, r->loss - ref_r->loss, ref_r->loss,
-                   ref_r->ssim[0], ref_r->ssim[1], ref_r->ssim[2], 
ref_r->ssim[3]);
+        av_bprintf(bp, "  loss %e is %s by %e, ref loss %e ",
+                   r->loss, worse_str, r->loss - ref_r->loss, ref_r->loss);
+        print_ssim(bp, ref_r->ssim);
+        av_bprintf(bp, "\n");
         if (av_bprint_is_complete(bp))
             av_log(NULL, level, "%s", bp->str);
     }
-- 
2.54.0


>From 0420cbc81efdbaedf5197f87205885df38232b5b Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 26 Oct 2026 13:58:50 +0100
Subject: [PATCH 02/17] swscale/tests/swscale: compute result badness in
 run_test()

Decide whether a result is worse in one place, and pass the outcome to
print_results().

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/tests/swscale.c | 46 ++++++++++++++++++--------------------
 1 file changed, 22 insertions(+), 24 deletions(-)

diff --git a/libswscale/tests/swscale.c b/libswscale/tests/swscale.c
index 6ddfadee1ed..00a7250d6d1 100644
--- a/libswscale/tests/swscale.c
+++ b/libswscale/tests/swscale.c
@@ -259,6 +259,13 @@ static float get_loss(const float ssim[4])
     return 1.0 - sum;
 }
 
+/* Returns 0 if as good as ref, 1 if slightly worse, 2 if clearly worse */
+static int compare_loss(float loss, float ref)
+{
+    return loss - ref > 1e-2 ? 2 :
+           loss - ref > 1e-4 ? 1 : 0;
+}
+
 static void unref_buffers(AVFrame *frame)
 {
     for (int i = 0; i < FF_ARRAY_ELEMS(frame->buf); i++) {
@@ -531,12 +538,12 @@ static void print_ssim(AVBPrint *bp, const float ssim[4])
     av_bprintf(bp, "SSIM={Y=%f U=%f V=%f A=%f}", ssim[0], ssim[1], ssim[2], 
ssim[3]);
 }
 
-static void print_results(const AVFrame *ref, const AVFrame *src, const 
AVFrame *dst,
-                          int dst_w, int dst_h,
+static void print_results(const AVFrame *src, const AVFrame *dst,
                           const struct mode *mode, const struct options *opts,
                           const struct test_results *r,
                           const struct test_results *ref_r,
-                          float expected_loss)
+                          float expected_loss,
+                          int selftest_badness, int ref_badness)
 {
     AVBPrint bprint, *bp = &bprint;
     av_bprint_init(bp, 0, AV_BPRINT_SIZE_AUTOMATIC);
@@ -581,8 +588,8 @@ static void print_results(const AVFrame *ref, const AVFrame 
*src, const AVFrame
         }
     }
 
-    if (r->loss - expected_loss > 1e-4 && dst_w >= ref->width && dst_h >= 
ref->height) {
-        const int bad = r->loss - expected_loss > 1e-2;
+    if (selftest_badness) {
+        const int bad = selftest_badness > 1;
         const int level = bad ? AV_LOG_ERROR : AV_LOG_WARNING;
         const char *worse_str = bad ? "WORSE" : "worse";
         av_bprint_clear(bp);
@@ -596,7 +603,7 @@ static void print_results(const AVFrame *ref, const AVFrame 
*src, const AVFrame
             av_log(NULL, level, "%s", bp->str);
     }
 
-    if (ref_r && r->loss - ref_r->loss > 1e-4) {
+    if (ref_badness) {
         /**
          * The new scaling code does not (yet) perform error diffusion for
          * low bit depth output, which impacts the SSIM score slightly for
@@ -606,7 +613,7 @@ static void print_results(const AVFrame *ref, const AVFrame 
*src, const AVFrame
          * implemented in the new ops code.
          */
         const int dst_bits = av_pix_fmt_desc_get(dst->format)->comp[0].depth;
-        const int bad = r->loss - ref_r->loss > 1e-2 && dst_bits > 1;
+        const int bad = ref_badness > 1 && dst_bits > 1;
         const int level = bad ? AV_LOG_ERROR : AV_LOG_WARNING;
         const char *worse_str = bad ? "WORSE" : "worse";
         av_bprint_clear(bp);
@@ -646,6 +653,7 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
     AVFrame *src = runner->src;
     AVFrame *dst = NULL, *out = NULL;
     const int comps = fmt_comps(t->src_fmt) & fmt_comps(t->dst_fmt);
+    int selftest_badness = 0, ref_badness = 0;
     int ret;
 
     /* Estimate the expected amount of loss from bit depth reduction */
@@ -719,24 +727,14 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
     }
 
     r.loss = get_loss(r.ssim);
-    if ((opts->api != IMPL_LEGACY) && r.loss - expected_loss > 1e-2 && 
t->dst_w >= ref->width && t->dst_h >= ref->height) {
-        ret = -1;
-        goto bad_loss;
-    }
+    if (t->dst_w >= ref->width && t->dst_h >= ref->height)
+        selftest_badness = compare_loss(r.loss, expected_loss);
+    if (ref_r)
+        ref_badness = compare_loss(r.loss, ref_r->loss);
 
-    if (ref_r && r.loss - ref_r->loss > 1e-2) {
-        ret = -1;
-        goto bad_loss;
-    }
-
-    ret = 0; /* fall through */
-
-bad_loss:
-    print_results(ref, src, dst,
-                  t->dst_w, t->dst_h,
-                  &t->mode, opts,
-                  &r, ref_r,
-                  expected_loss);
+    ret = (opts->api != IMPL_LEGACY && selftest_badness > 1) || ref_badness > 
1 ? -1 : 0;
+    print_results(src, dst, &t->mode, opts, &r, ref_r,
+                  expected_loss, selftest_badness, ref_badness);
 
  error:
     av_frame_free(&dst);
-- 
2.54.0


>From f636eab200a49b7ded33cf11b875a3216355f713 Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 26 Oct 2026 13:59:14 +0100
Subject: [PATCH 03/17] swscale/tests/swscale: expect chroma SSIM to follow the
 luma estimate

Chroma is also affected by quantization; expecting a perfect score only
works because of its low weight in the loss.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/tests/swscale.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/libswscale/tests/swscale.c b/libswscale/tests/swscale.c
index 00a7250d6d1..33a7bcae209 100644
--- a/libswscale/tests/swscale.c
+++ b/libswscale/tests/swscale.c
@@ -664,7 +664,7 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
     const float out_var = estimate_quantization_noise(ref->format);
     const float total_var = src_var + dst_var + out_var;
     const float ssim_luma = (2 * ref_var + c1) / (2 * ref_var + total_var + 
c1);
-    const float ssim_expected[4] = { ssim_luma, 1, 1, 1 }; /* for simplicity */
+    const float ssim_expected[4] = { ssim_luma, ssim_luma, ssim_luma, 1 }; /* 
for simplicity */
     const float expected_loss = get_loss(ssim_expected);
 
     struct test_results r = { 0 };
-- 
2.54.0


>From a1486fd0644c96c8a706dda20e4f9d5b785c727c Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 26 Oct 2026 19:08:37 +0100
Subject: [PATCH 04/17] swscale/tests/swscale: compare per-plane SSIM instead
 of a combined loss

Check each plane against the expected or reference SSIM, instead of a
weighted loss.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/tests/swscale.c | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

diff --git a/libswscale/tests/swscale.c b/libswscale/tests/swscale.c
index 33a7bcae209..13c7bbb73cd 100644
--- a/libswscale/tests/swscale.c
+++ b/libswscale/tests/swscale.c
@@ -260,10 +260,16 @@ static float get_loss(const float ssim[4])
 }
 
 /* Returns 0 if as good as ref, 1 if slightly worse, 2 if clearly worse */
-static int compare_loss(float loss, float ref)
+static int compare_ssim(const float ssim[4], const float ref[4])
 {
-    return loss - ref > 1e-2 ? 2 :
-           loss - ref > 1e-4 ? 1 : 0;
+    int res = 0;
+    for (int p = 0; p < 4; p++) {
+        if (ref[p] - ssim[p] > 1e-2)
+            return 2;
+        if (ref[p] - ssim[p] > 1e-4)
+            res = 1;
+    }
+    return res;
 }
 
 static void unref_buffers(AVFrame *frame)
@@ -728,9 +734,9 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
 
     r.loss = get_loss(r.ssim);
     if (t->dst_w >= ref->width && t->dst_h >= ref->height)
-        selftest_badness = compare_loss(r.loss, expected_loss);
+        selftest_badness = compare_ssim(r.ssim, ssim_expected);
     if (ref_r)
-        ref_badness = compare_loss(r.loss, ref_r->loss);
+        ref_badness = compare_ssim(r.ssim, ref_r->ssim);
 
     ret = (opts->api != IMPL_LEGACY && selftest_badness > 1) || ref_badness > 
1 ? -1 : 0;
     print_results(src, dst, &t->mode, opts, &r, ref_r,
-- 
2.54.0


>From 0f2cdea08106d5b452bff52071823f7381217c83 Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 26 Oct 2026 19:21:31 +0100
Subject: [PATCH 05/17] swscale/tests/swscale: store SSIM scores as double

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/tests/swscale.c | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/libswscale/tests/swscale.c b/libswscale/tests/swscale.c
index 13c7bbb73cd..685cd4fdd89 100644
--- a/libswscale/tests/swscale.c
+++ b/libswscale/tests/swscale.c
@@ -87,7 +87,7 @@ struct mode {
 };
 
 struct test_results {
-    float ssim[4];
+    double ssim[4];
     float loss;
     int64_t time;
     int iters;
@@ -199,7 +199,7 @@ static int fmt_comps(enum AVPixelFormat fmt)
     return comps;
 }
 
-static void get_ssim(float ssim[4], const AVFrame *out, const AVFrame *ref, 
int comps)
+static void get_ssim(double ssim[4], const AVFrame *out, const AVFrame *ref, 
int comps)
 {
     av_assert1(out->format == AV_PIX_FMT_YUVA444P);
     av_assert1(ref->format == out->format);
@@ -247,7 +247,7 @@ static void get_ssim(float ssim[4], const AVFrame *out, 
const AVFrame *ref, int
     }
 }
 
-static float get_loss(const float ssim[4])
+static float get_loss(const double ssim[4])
 {
     const float weights[3] = { 0.8, 0.1, 0.1 }; /* tuned for Y'CbCr */
 
@@ -260,7 +260,7 @@ static float get_loss(const float ssim[4])
 }
 
 /* Returns 0 if as good as ref, 1 if slightly worse, 2 if clearly worse */
-static int compare_ssim(const float ssim[4], const float ref[4])
+static int compare_ssim(const double ssim[4], const double ref[4])
 {
     int res = 0;
     for (int p = 0; p < 4; p++) {
@@ -539,7 +539,7 @@ static void print_test_params(AVBPrint *bp,
                mode->scaler_sub);
 }
 
-static void print_ssim(AVBPrint *bp, const float ssim[4])
+static void print_ssim(AVBPrint *bp, const double ssim[4])
 {
     av_bprintf(bp, "SSIM={Y=%f U=%f V=%f A=%f}", ssim[0], ssim[1], ssim[2], 
ssim[3]);
 }
@@ -670,7 +670,7 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
     const float out_var = estimate_quantization_noise(ref->format);
     const float total_var = src_var + dst_var + out_var;
     const float ssim_luma = (2 * ref_var + c1) / (2 * ref_var + total_var + 
c1);
-    const float ssim_expected[4] = { ssim_luma, ssim_luma, ssim_luma, 1 }; /* 
for simplicity */
+    const double ssim_expected[4] = { ssim_luma, ssim_luma, ssim_luma, 1 }; /* 
for simplicity */
     const float expected_loss = get_loss(ssim_expected);
 
     struct test_results r = { 0 };
@@ -894,7 +894,7 @@ static int run_file_tests(const struct options *opts)
 
         ret = sscanf(buf,
                      "%20s %dx%d -> %20s %dx%d, flags=0x%x dither=%u 
scaler=%u/%u, "
-                     "SSIM={Y=%f U=%f V=%f A=%f} loss=%e%n",
+                     "SSIM={Y=%lf U=%lf V=%lf A=%lf} loss=%e%n",
                      src_fmt_str, &sw, &sh, dst_fmt_str, &dw, &dh,
                      &mode.flags, &mode.dither,
                      &mode.scaler, &mode.scaler_sub,
-- 
2.54.0


>From 7bc4b0608ec55b3e5e6095d89e9d04ad8d62c0bd Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 26 Oct 2026 19:24:49 +0100
Subject: [PATCH 06/17] swscale/tests/swscale: use SwsCompMask for fmt_comps()

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/tests/swscale.c | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/libswscale/tests/swscale.c b/libswscale/tests/swscale.c
index 685cd4fdd89..9ec2621562a 100644
--- a/libswscale/tests/swscale.c
+++ b/libswscale/tests/swscale.c
@@ -47,6 +47,7 @@
 
 #include "libswscale/swscale.h"
 #include "libswscale/format.h"
+#include "libswscale/uops.h"
 
 #define IMPL_NEW    0
 #define IMPL_LEGACY 1
@@ -190,16 +191,17 @@ static float estimate_quantization_noise(enum 
AVPixelFormat fmt)
     }
 }
 
-static int fmt_comps(enum AVPixelFormat fmt)
+static SwsCompMask fmt_comps(enum AVPixelFormat fmt)
 {
     const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(fmt);
-    int comps = desc->nb_components >= 3 ? 0x7 : 0x1;
+    SwsCompMask comps = desc->nb_components >= 3 ? SWS_COMP_ELEMS(3) : 
SWS_COMP(0);
     if (desc->flags & AV_PIX_FMT_FLAG_ALPHA)
-        comps |= 0x8;
+        comps |= SWS_COMP(3);
     return comps;
 }
 
-static void get_ssim(double ssim[4], const AVFrame *out, const AVFrame *ref, 
int comps)
+static void get_ssim(double ssim[4], const AVFrame *out, const AVFrame *ref,
+                     SwsCompMask comps)
 {
     av_assert1(out->format == AV_PIX_FMT_YUVA444P);
     av_assert1(ref->format == out->format);
@@ -213,7 +215,7 @@ static void get_ssim(double ssim[4], const AVFrame *out, 
const AVFrame *ref, int
 
         const int is_chroma = p == 1 || p == 2;
         const uint8_t def = is_chroma ? 128 : 0xFF;
-        const int has_ref = comps & (1 << p);
+        const int has_ref = SWS_COMP_TEST(comps, p);
         double sum = 0;
         int count = 0;
 
@@ -658,7 +660,7 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
     const struct test_results *ref_r = t->has_ref ? &t->ref_r : NULL;
     AVFrame *src = runner->src;
     AVFrame *dst = NULL, *out = NULL;
-    const int comps = fmt_comps(t->src_fmt) & fmt_comps(t->dst_fmt);
+    const SwsCompMask comps = fmt_comps(t->src_fmt) & fmt_comps(t->dst_fmt);
     int selftest_badness = 0, ref_badness = 0;
     int ret;
 
-- 
2.54.0


>From 47d77825ebecbc94a3cd53c82879766cd69a25d9 Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 26 Oct 2026 12:24:32 +0100
Subject: [PATCH 07/17] swscale/tests/swscale: init the reference frame before
 the runners

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/tests/swscale.c | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/libswscale/tests/swscale.c b/libswscale/tests/swscale.c
index 9ec2621562a..a9d07c51c31 100644
--- a/libswscale/tests/swscale.c
+++ b/libswscale/tests/swscale.c
@@ -1349,18 +1349,6 @@ int main(int argc, char **argv)
     ff_sfc64_init(&state.prng_state, 0, 0, 0, 12);
     signal(SIGINT, exit_handler);
 
-    state.runners = av_calloc(opts.runners, sizeof(*state.runners));
-    if (!state.runners) {
-        ret = AVERROR(ENOMEM);
-        goto error;
-    }
-
-    for (int i = 0; i < opts.runners; i++) {
-        ret = runner_init(&state.runners[i]);
-        if (ret < 0)
-            goto error;
-    }
-
     state.ref = av_frame_alloc();
     if (!state.ref) {
         ret = AVERROR(ENOMEM);
@@ -1374,6 +1362,18 @@ int main(int argc, char **argv)
     if (ret < 0)
         goto error;
 
+    state.runners = av_calloc(opts.runners, sizeof(*state.runners));
+    if (!state.runners) {
+        ret = AVERROR(ENOMEM);
+        goto error;
+    }
+
+    for (int i = 0; i < opts.runners; i++) {
+        ret = runner_init(&state.runners[i]);
+        if (ret < 0)
+            goto error;
+    }
+
     if (opts.runners > 1) {
         ret = av_thread_message_queue_alloc(&state.queue, opts.runners,
                                             sizeof(struct test_case));
-- 
2.54.0


>From f9a84a9a9e6b49bdb922da5da66c26b21aea27aa Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 26 Oct 2026 12:06:16 +0100
Subject: [PATCH 08/17] swscale/tests/swscale: compare against the decoded
 source

Compare the output against the source decoded back to the reference
format, reduced to the components that both formats have. This avoids
penalizing the conversion due to information that was not there to
begin with, or is necessarily lost due to the restricted colorspace of
the destination format.

While this does risk missing an error in the auxiliary conversion
itself, that should hopefully be mitigated by the fact that an upcoming
commit makes us always choose a floating-point RGBA format, which is a
straightforward conversion from most formats. Any error here would also
likely round-trip.

The reduced frames are cached per runner until the source changes. Note
that we can re-use the same SwsContext as the ref->src, since a change
in the source format already invalidates these cached frames.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/tests/swscale.c | 79 +++++++++++++++++++++++++++++++++++++-
 1 file changed, 77 insertions(+), 2 deletions(-)

diff --git a/libswscale/tests/swscale.c b/libswscale/tests/swscale.c
index a9d07c51c31..fe94ba0e71a 100644
--- a/libswscale/tests/swscale.c
+++ b/libswscale/tests/swscale.c
@@ -100,6 +100,8 @@ struct runner {
     SwsContext *sws_src_dst;
     SwsContext *sws_dst_out;
     AVFrame *src;
+    /* src decoded back to the reference format, see get_src_ref() */
+    AVFrame *src_ref[4];
 };
 
 struct test_case {
@@ -652,7 +654,61 @@ static int init_frame(AVFrame **pframe, const AVFrame *ref,
     return 0;
 }
 
-/* Runs a series of ref -> src -> dst -> out, and compares out vs ref */
+static int comps_index(SwsCompMask comps)
+{
+    switch (comps) {
+    case SWS_COMP_MASK(1, 0, 0, 0): return 0;
+    case SWS_COMP_MASK(1, 0, 0, 1): return 1;
+    case SWS_COMP_MASK(1, 1, 1, 0): return 2;
+    case SWS_COMP_MASK(1, 1, 1, 1): return 3;
+    }
+
+    av_unreachable("invalid component mask");
+    return -1;
+}
+
+/* Reduce a frame's contents to a representable subset */
+static int reduce_frame(AVFrame *out, const AVFrame *in, SwsCompMask comps,
+                        SwsContext *sws)
+{
+    enum AVPixelFormat format;
+    const bool has_alpha = SWS_COMP_TEST(comps, 3);
+    if (SWS_COMP_TEST(comps, 1)) {
+        format = has_alpha ? AV_PIX_FMT_GBRAPF32 : AV_PIX_FMT_GBRPF32;
+    } else { /* grayscale */
+        format = has_alpha ? AV_PIX_FMT_YAF32 : AV_PIX_FMT_GRAYF32;
+    }
+
+    AVFrame *tmp = NULL;
+    int ret = init_frame(&tmp, in, in->width, in->height, format);
+    if (ret >= 0)
+        ret = checked_sws_scale_frame(sws, tmp, in);
+    if (ret >= 0)
+        ret = checked_sws_scale_frame(sws, out, tmp);
+    av_frame_free(&tmp);
+    return ret;
+}
+
+static int get_src_ref(struct runner *runner, SwsCompMask comps,
+                       const AVFrame **out)
+{
+    const int idx = comps_index(comps);
+    AVFrame *const frame = runner->src_ref[idx];
+
+    if (!frame->buf[0]) {
+        int ret = reduce_frame(frame, runner->src, comps, runner->sws_ref_src);
+        if (ret < 0)
+            return ret;
+    }
+
+    *out = frame;
+    return 0;
+}
+
+/**
+ * Runs a series of ref -> src -> dst -> out, and compares out against src
+ * decoded back to the reference format.
+ */
 static int run_test(struct runner *runner, const struct test_case *t,
                     const struct options *opts)
 {
@@ -660,6 +716,7 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
     const struct test_results *ref_r = t->has_ref ? &t->ref_r : NULL;
     AVFrame *src = runner->src;
     AVFrame *dst = NULL, *out = NULL;
+    const AVFrame *src_ref;
     const SwsCompMask comps = fmt_comps(t->src_fmt) & fmt_comps(t->dst_fmt);
     int selftest_badness = 0, ref_badness = 0;
     int ret;
@@ -696,6 +753,9 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
             goto error;
         }
         runner->src = src;
+
+        for (int i = 0; i < FF_ARRAY_ELEMS(runner->src_ref); i++)
+            unref_buffers(runner->src_ref[i]);
     }
 
     ret = init_frame(&dst, ref, t->dst_w, t->dst_h, t->dst_fmt);
@@ -719,7 +779,11 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
     if (ret < 0)
         goto error;
 
-    get_ssim(r.ssim, out, ref, comps);
+    ret = get_src_ref(runner, comps, &src_ref);
+    if (ret < 0)
+        goto error;
+
+    get_ssim(r.ssim, out, src_ref, comps);
 
     if (opts->api == IMPL_LEGACY) {
         /* Legacy swscale does not perform bit accurate upconversions of low
@@ -962,6 +1026,8 @@ error:
 
 static int runner_init(struct runner *runner)
 {
+    const AVFrame *ref = state.ref;
+
     runner->sws_ref_src = sws_alloc_context();
     runner->sws_src_dst = sws_alloc_context();
     runner->sws_dst_out = sws_alloc_context();
@@ -971,6 +1037,13 @@ static int runner_init(struct runner *runner)
     runner->sws_dst_out->flags = AUX_FLAGS;
     runner->sws_ref_src->backends = AUX_BACKENDS;
     runner->sws_dst_out->backends = AUX_BACKENDS;
+
+    for (int i = 0; i < FF_ARRAY_ELEMS(runner->src_ref); i++) {
+        int ret = init_frame(&runner->src_ref[i], ref, ref->width, ref->height,
+                             ref->format);
+        if (ret < 0)
+            return ret;
+    }
     return 0;
 }
 
@@ -980,6 +1053,8 @@ static void runner_free(struct runner *runner)
     sws_free_context(&runner->sws_src_dst);
     sws_free_context(&runner->sws_dst_out);
     av_frame_free(&runner->src);
+    for (int i = 0; i < FF_ARRAY_ELEMS(runner->src_ref); i++)
+        av_frame_free(&runner->src_ref[i]);
 }
 
 static int run_tests(const struct options *opts)
-- 
2.54.0


>From fba9a89abcba1e0d9aa28707e92e0eab828259fe Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 26 Oct 2026 19:35:34 +0100
Subject: [PATCH 09/17] swscale/tests/swscale: drop the legacy SSIM clamp

The reference now includes the source quantization noise, so legacy
swscale no longer gains an advantage from removing it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/tests/swscale.c | 13 -------------
 1 file changed, 13 deletions(-)

diff --git a/libswscale/tests/swscale.c b/libswscale/tests/swscale.c
index fe94ba0e71a..b7c703caab3 100644
--- a/libswscale/tests/swscale.c
+++ b/libswscale/tests/swscale.c
@@ -785,19 +785,6 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
 
     get_ssim(r.ssim, out, src_ref, comps);
 
-    if (opts->api == IMPL_LEGACY) {
-        /* Legacy swscale does not perform bit accurate upconversions of low
-         * bit depth RGB. This artificially improves the SSIM score because the
-         * resulting error deletes some of the input dither noise. This gives
-         * it an unfair advantage when compared against a bit exact reference.
-         * Work around this by ensuring that the resulting SSIM score is not
-         * higher than it theoretically "should" be. */
-        if (src_var > dst_var) {
-            const float src_loss = (2 * ref_var + c1) / (2 * ref_var + src_var 
+ c1);
-            r.ssim[0] = FFMIN(r.ssim[0], src_loss);
-        }
-    }
-
     r.loss = get_loss(r.ssim);
     if (t->dst_w >= ref->width && t->dst_h >= ref->height)
         selftest_badness = compare_ssim(r.ssim, ssim_expected);
-- 
2.54.0


>From 44c418c677a14e2cd9d174d28b359e38e3eebaf5 Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 26 Oct 2026 19:37:17 +0100
Subject: [PATCH 10/17] swscale/tests/swscale: reduce the reference by the dst
 components only

The decoded source already lacks the components missing from the source
format.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/tests/swscale.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/libswscale/tests/swscale.c b/libswscale/tests/swscale.c
index b7c703caab3..dc2ed4e469a 100644
--- a/libswscale/tests/swscale.c
+++ b/libswscale/tests/swscale.c
@@ -717,7 +717,7 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
     AVFrame *src = runner->src;
     AVFrame *dst = NULL, *out = NULL;
     const AVFrame *src_ref;
-    const SwsCompMask comps = fmt_comps(t->src_fmt) & fmt_comps(t->dst_fmt);
+    const SwsCompMask comps = fmt_comps(t->dst_fmt);
     int selftest_badness = 0, ref_badness = 0;
     int ret;
 
-- 
2.54.0


>From fdb24205ec66fcc34de8a5ceaf03f29ec655320a Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 26 Oct 2026 12:42:45 +0100
Subject: [PATCH 11/17] swscale/tests/swscale: drop plane skipping from
 get_ssim()

The reference already holds neutral values for the planes the
conversion cannot represent.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/tests/swscale.c | 11 +++--------
 1 file changed, 3 insertions(+), 8 deletions(-)

diff --git a/libswscale/tests/swscale.c b/libswscale/tests/swscale.c
index dc2ed4e469a..e26967b29e3 100644
--- a/libswscale/tests/swscale.c
+++ b/libswscale/tests/swscale.c
@@ -202,8 +202,7 @@ static SwsCompMask fmt_comps(enum AVPixelFormat fmt)
     return comps;
 }
 
-static void get_ssim(double ssim[4], const AVFrame *out, const AVFrame *ref,
-                     SwsCompMask comps)
+static void get_ssim(double ssim[4], const AVFrame *out, const AVFrame *ref)
 {
     av_assert1(out->format == AV_PIX_FMT_YUVA444P);
     av_assert1(ref->format == out->format);
@@ -214,10 +213,6 @@ static void get_ssim(double ssim[4], const AVFrame *out, 
const AVFrame *ref,
         const int stride_b = ref->linesize[p];
         const int w = out->width;
         const int h = out->height;
-
-        const int is_chroma = p == 1 || p == 2;
-        const uint8_t def = is_chroma ? 128 : 0xFF;
-        const int has_ref = SWS_COMP_TEST(comps, p);
         double sum = 0;
         int count = 0;
 
@@ -231,7 +226,7 @@ static void get_ssim(double ssim[4], const AVFrame *out, 
const AVFrame *ref,
                 for (int yy = 0; yy < 4; yy++) {
                     for (int xx = 0; xx < 4; xx++) {
                         int a = out->data[p][(y + yy) * stride_a + x + xx];
-                        int b = has_ref ? ref->data[p][(y + yy) * stride_b + x 
+ xx] : def;
+                        int b = ref->data[p][(y + yy) * stride_b + x + xx];
                         s1  += a;
                         s2  += b;
                         ss  += a * a + b * b;
@@ -783,7 +778,7 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
     if (ret < 0)
         goto error;
 
-    get_ssim(r.ssim, out, src_ref, comps);
+    get_ssim(r.ssim, out, src_ref);
 
     r.loss = get_loss(r.ssim);
     if (t->dst_w >= ref->width && t->dst_h >= ref->height)
-- 
2.54.0


>From c612d4f121b18e1343d37fb2544372b252b7e5a5 Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 26 Oct 2026 19:39:50 +0100
Subject: [PATCH 12/17] swscale/tests/swscale: use GBRAPF32 as the reference
 format

Instead of measuring the SSIM in YCbCr and using the BT.601 matrix
to compute the expected loss for RGB formats, we compute the SSIM
in RGB and use the BT.601 matrix to compute the loss for YUV formats.

Using GBRAPF32 here has several other advantages:

1. The conversions are simple and less likely to be wrong
2. The format has high precision, so the only quantization noise is
   the one from the src/dst formats; skipping the extra quantization
   stage from dst -> ref
3. As a consequence, the SSIM scores can be much more sensitive to the
   actual error, even for high-bit depth formats where the error might
   be less than a single 8-bit codepoint of the previous yuva444p ref.

The performance impact of doing this is minimal; especially after the
prior commit which uses various subsets of GBRAPF32 for reduce_format().

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/tests/swscale.c | 63 +++++++++++++++++++++-----------------
 1 file changed, 35 insertions(+), 28 deletions(-)

diff --git a/libswscale/tests/swscale.c b/libswscale/tests/swscale.c
index e26967b29e3..bdc381596b8 100644
--- a/libswscale/tests/swscale.c
+++ b/libswscale/tests/swscale.c
@@ -170,26 +170,26 @@ static void exit_handler(int sig)
     exit(sig);
 }
 
-/* Estimate luma variance assuming uniform dither noise distribution */
-static float estimate_quantization_noise(enum AVPixelFormat fmt)
+/**
+ * Estimate the variance of R, G or B plane p, assuming uniform dither noise
+ * distribution. For non-RGB formats, this uses the luma depth.
+ */
+static float estimate_quantization_noise(enum AVPixelFormat fmt, int p)
 {
     const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(fmt);
+    const int plane = desc->flags & AV_PIX_FMT_FLAG_RGB ? p : 0;
+    const int depth = desc->comp[plane].depth;
     float variance = 1.0 / 12;
-    if (desc->comp[0].depth < 8) {
+    if (depth < 8) {
         /* Extra headroom for very low bit depth output */
-        variance *= (8 - desc->comp[0].depth);
+        variance *= (8 - depth);
     }
 
     if (desc->flags & AV_PIX_FMT_FLAG_FLOAT) {
         return 0.0;
-    } else if (desc->flags & AV_PIX_FMT_FLAG_RGB) {
-        const float r = 0.299 / (1 << desc->comp[0].depth);
-        const float g = 0.587 / (1 << desc->comp[1].depth);
-        const float b = 0.114 / (1 << desc->comp[2].depth);
-        return (r * r + g * g + b * b) * variance;
     } else {
-        const float y = 1.0 / (1 << desc->comp[0].depth);
-        return y * y * variance;
+        const float q = 1.0 / (1 << depth);
+        return q * q * variance;
     }
 }
 
@@ -204,11 +204,13 @@ static SwsCompMask fmt_comps(enum AVPixelFormat fmt)
 
 static void get_ssim(double ssim[4], const AVFrame *out, const AVFrame *ref)
 {
-    av_assert1(out->format == AV_PIX_FMT_YUVA444P);
+    av_assert1(out->format == AV_PIX_FMT_GBRAPF32);
     av_assert1(ref->format == out->format);
     av_assert1(ref->width == out->width && ref->height == out->height);
 
     for (int p = 0; p < 4; p++) {
+        const uint8_t *data_a = out->data[p];
+        const uint8_t *data_b = ref->data[p];
         const int stride_a = out->linesize[p];
         const int stride_b = ref->linesize[p];
         const int w = out->width;
@@ -219,14 +221,16 @@ static void get_ssim(double ssim[4], const AVFrame *out, 
const AVFrame *ref)
         /* 4x4 SSIM */
         for (int y = 0; y < (h & ~3); y += 4) {
             for (int x = 0; x < (w & ~3); x += 4) {
-                const float c1 = .01 * .01 * 255 * 255 * 64;
-                const float c2 = .03 * .03 * 255 * 255 * 64 * 63;
-                int s1 = 0, s2 = 0, ss = 0, s12 = 0, var, covar;
+                const double c1 = .01 * .01 * 64;
+                const double c2 = .03 * .03 * 64 * 63;
+                double s1 = 0, s2 = 0, ss = 0, s12 = 0, var, covar;
 
                 for (int yy = 0; yy < 4; yy++) {
+                    const float *row_a = (const float *) (data_a + (y + yy) * 
stride_a);
+                    const float *row_b = (const float *) (data_b + (y + yy) * 
stride_b);
                     for (int xx = 0; xx < 4; xx++) {
-                        int a = out->data[p][(y + yy) * stride_a + x + xx];
-                        int b = ref->data[p][(y + yy) * stride_b + x + xx];
+                        double a = row_a[x + xx];
+                        double b = row_b[x + xx];
                         s1  += a;
                         s2  += b;
                         ss  += a * a + b * b;
@@ -242,13 +246,14 @@ static void get_ssim(double ssim[4], const AVFrame *out, 
const AVFrame *ref)
             }
         }
 
-        ssim[p] = count ? sum / count : 0.0;
+        const int indices[4] = { 1, 2, 0, 3 }; /* GBRA */
+        ssim[indices[p]] = count ? sum / count : 0.0;
     }
 }
 
 static float get_loss(const double ssim[4])
 {
-    const float weights[3] = { 0.8, 0.1, 0.1 }; /* tuned for Y'CbCr */
+    const float weights[3] = { 0.299, 0.587, 0.114 }; /* BT.601 luma */
 
     float sum = 0;
     for (int i = 0; i < 3; i++)
@@ -540,7 +545,7 @@ static void print_test_params(AVBPrint *bp,
 
 static void print_ssim(AVBPrint *bp, const double ssim[4])
 {
-    av_bprintf(bp, "SSIM={Y=%f U=%f V=%f A=%f}", ssim[0], ssim[1], ssim[2], 
ssim[3]);
+    av_bprintf(bp, "SSIM={R=%f G=%f B=%f A=%f}", ssim[0], ssim[1], ssim[2], 
ssim[3]);
 }
 
 static void print_results(const AVFrame *src, const AVFrame *dst,
@@ -719,12 +724,14 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
     /* Estimate the expected amount of loss from bit depth reduction */
     const float c1 = 0.01 * 0.01; /* stabilization constant */
     const float ref_var = 1.0 / 12.0; /* uniformly distributed signal */
-    const float src_var = estimate_quantization_noise(t->src_fmt);
-    const float dst_var = estimate_quantization_noise(t->dst_fmt);
-    const float out_var = estimate_quantization_noise(ref->format);
-    const float total_var = src_var + dst_var + out_var;
-    const float ssim_luma = (2 * ref_var + c1) / (2 * ref_var + total_var + 
c1);
-    const double ssim_expected[4] = { ssim_luma, ssim_luma, ssim_luma, 1 }; /* 
for simplicity */
+    double ssim_expected[4];
+    ssim_expected[3] = 1.0; /* for simplicity */
+    for (int p = 0; p < 3; p++) {
+        const float src_var = estimate_quantization_noise(t->src_fmt, p);
+        const float dst_var = estimate_quantization_noise(t->dst_fmt, p);
+        const float total_var = src_var + dst_var;
+        ssim_expected[p] = (2 * ref_var + c1) / (2 * ref_var + total_var + c1);
+    }
     const float expected_loss = get_loss(ssim_expected);
 
     struct test_results r = { 0 };
@@ -942,7 +949,7 @@ static int run_file_tests(const struct options *opts)
 
         ret = sscanf(buf,
                      "%20s %dx%d -> %20s %dx%d, flags=0x%x dither=%u 
scaler=%u/%u, "
-                     "SSIM={Y=%lf U=%lf V=%lf A=%lf} loss=%e%n",
+                     "SSIM={R=%lf G=%lf B=%lf A=%lf} loss=%e%n",
                      src_fmt_str, &sw, &sh, dst_fmt_str, &dw, &dh,
                      &mode.flags, &mode.dither,
                      &mode.scaler, &mode.scaler_sub,
@@ -1413,7 +1420,7 @@ int main(int argc, char **argv)
     }
     state.ref->width  = opts.w;
     state.ref->height = opts.h;
-    state.ref->format = AV_PIX_FMT_YUVA444P;
+    state.ref->format = AV_PIX_FMT_GBRAPF32;
 
     ret = init_ref(state.ref, &opts);
     if (ret < 0)
-- 
2.54.0


>From 42d612de18c97246dbe9df7ef564066ceb123311 Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 26 Oct 2026 19:55:22 +0100
Subject: [PATCH 13/17] swscale/tests/swscale: switch from SSIM to MSE

A number of factors motivate this change:

1. The old SSIM was incorrectly implemented; I copied values from x264
   which were tuned for a different block size (64 instead of 16).
2. MSE is simpler to reason about, and has nice additive properties
   which allows us to directly estimate the quantization noise of
   multiple error sources, rather than the previous hack that relied
   on a uniform signal source.
3. SSIM is blind to low bit depth quantization errors at the tolerances
   we were using; MSE can be computed more accurately against a much
   tighter estimate.
4. MSE is more faithfully composable when comparing multiple planes.
5. MSE is simpler and faster to compute.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/tests/swscale.c | 136 ++++++++++++++++++-------------------
 1 file changed, 66 insertions(+), 70 deletions(-)

diff --git a/libswscale/tests/swscale.c b/libswscale/tests/swscale.c
index bdc381596b8..37bb06d6b0b 100644
--- a/libswscale/tests/swscale.c
+++ b/libswscale/tests/swscale.c
@@ -88,8 +88,8 @@ struct mode {
 };
 
 struct test_results {
-    double ssim[4];
-    float loss;
+    double mse[4];
+    double loss; /* PSNR of the weighted MSE, in dB */
     int64_t time;
     int iters;
 };
@@ -202,75 +202,69 @@ static SwsCompMask fmt_comps(enum AVPixelFormat fmt)
     return comps;
 }
 
-static void get_ssim(double ssim[4], const AVFrame *out, const AVFrame *ref)
+static void get_mse(double mse[4], const AVFrame *out, const AVFrame *ref)
 {
     av_assert1(out->format == AV_PIX_FMT_GBRAPF32);
     av_assert1(ref->format == out->format);
     av_assert1(ref->width == out->width && ref->height == out->height);
+    const int w = out->width;
+    const int h = out->height;
 
     for (int p = 0; p < 4; p++) {
         const uint8_t *data_a = out->data[p];
         const uint8_t *data_b = ref->data[p];
         const int stride_a = out->linesize[p];
         const int stride_b = ref->linesize[p];
-        const int w = out->width;
-        const int h = out->height;
         double sum = 0;
-        int count = 0;
 
-        /* 4x4 SSIM */
-        for (int y = 0; y < (h & ~3); y += 4) {
-            for (int x = 0; x < (w & ~3); x += 4) {
-                const double c1 = .01 * .01 * 64;
-                const double c2 = .03 * .03 * 64 * 63;
-                double s1 = 0, s2 = 0, ss = 0, s12 = 0, var, covar;
-
-                for (int yy = 0; yy < 4; yy++) {
-                    const float *row_a = (const float *) (data_a + (y + yy) * 
stride_a);
-                    const float *row_b = (const float *) (data_b + (y + yy) * 
stride_b);
-                    for (int xx = 0; xx < 4; xx++) {
-                        double a = row_a[x + xx];
-                        double b = row_b[x + xx];
-                        s1  += a;
-                        s2  += b;
-                        ss  += a * a + b * b;
-                        s12 += a * b;
-                    }
-                }
-
-                var = ss * 64 - s1 * s1 - s2 * s2;
-                covar = s12 * 64 - s1 * s2;
-                sum += (2 * s1 * s2 + c1) * (2 * covar + c2) /
-                       ((s1 * s1 + s2 * s2 + c1) * (var + c2));
-                count++;
+        for (int y = 0; y < h; y++) {
+            const float *row_a = (const float *) data_a;
+            const float *row_b = (const float *) data_b;
+            for (int x = 0; x < w; x++) {
+                const float d = row_a[x] - row_b[x];
+                sum += d * d;
             }
+
+            data_a += stride_a;
+            data_b += stride_b;
         }
 
         const int indices[4] = { 1, 2, 0, 3 }; /* GBRA */
-        ssim[indices[p]] = count ? sum / count : 0.0;
+        mse[indices[p]] = sum / (w * h);
     }
 }
 
-static float get_loss(const double ssim[4])
+/**
+ * Rounded down to the 6 decimals it is printed with, so that the MSE parsed
+ * back from a reference file is never lower than the actual one.
+ */
+static double get_psnr(double mse)
 {
-    const float weights[3] = { 0.299, 0.587, 0.114 }; /* BT.601 luma */
-
-    float sum = 0;
-    for (int i = 0; i < 3; i++)
-        sum += weights[i] * ssim[i];
-    sum *= ssim[3]; /* ensure alpha errors get caught */
-
-    return 1.0 - sum;
+    return floor(-10.0 * log10(mse) * 1e6) / 1e6;
 }
 
-/* Returns 0 if as good as ref, 1 if slightly worse, 2 if clearly worse */
-static int compare_ssim(const double ssim[4], const double ref[4])
+static double get_loss(const double mse[4])
+{
+    const double weights[4] = { 0.299, 0.587, 0.114, 1.0 };
+
+    double sum = 0.0;
+    for (int i = 0; i < 4; i++)
+        sum += weights[i] * mse[i];
+
+    return get_psnr(sum);
+}
+
+/**
+ * Returns 0 if as good as ref, 1 if slightly worse, 2 if clearly worse. The
+ * absolute margins absorb the error from resampling.
+ */
+static int compare_mse(const double mse[4], const double ref[4])
 {
     int res = 0;
     for (int p = 0; p < 4; p++) {
-        if (ref[p] - ssim[p] > 1e-2)
+        if (mse[p] > 4 * ref[p] + 1e-4)
             return 2;
-        if (ref[p] - ssim[p] > 1e-4)
+        if (mse[p] > 2 * ref[p] + 5e-5)
             res = 1;
     }
     return res;
@@ -543,16 +537,17 @@ static void print_test_params(AVBPrint *bp,
                mode->scaler_sub);
 }
 
-static void print_ssim(AVBPrint *bp, const double ssim[4])
+static void print_psnr(AVBPrint *bp, const double mse[4])
 {
-    av_bprintf(bp, "SSIM={R=%f G=%f B=%f A=%f}", ssim[0], ssim[1], ssim[2], 
ssim[3]);
+    av_bprintf(bp, "PSNR={R=%f G=%f B=%f A=%f}",
+               get_psnr(mse[0]), get_psnr(mse[1]), get_psnr(mse[2]), 
get_psnr(mse[3]));
 }
 
 static void print_results(const AVFrame *src, const AVFrame *dst,
                           const struct mode *mode, const struct options *opts,
                           const struct test_results *r,
                           const struct test_results *ref_r,
-                          float expected_loss,
+                          double expected_loss,
                           int selftest_badness, int ref_badness)
 {
     AVBPrint bprint, *bp = &bprint;
@@ -563,10 +558,10 @@ static void print_results(const AVFrame *src, const 
AVFrame *dst,
 
         if (!opts->bench || !ref_r) {
             av_bprintf(bp, ", ");
-            print_ssim(bp, r->ssim);
-            av_bprintf(bp, " loss=%e", r->loss);
+            print_psnr(bp, r->mse);
+            av_bprintf(bp, " loss=%f", r->loss);
             if (ref_r)
-                av_bprintf(bp, " (ref=%e)", ref_r->loss);
+                av_bprintf(bp, " (ref=%f)", ref_r->loss);
         }
 
         if (opts->bench) {
@@ -607,8 +602,8 @@ static void print_results(const AVFrame *src, const AVFrame 
*dst,
             print_test_params(bp, src, dst, mode, opts);
             av_bprintf(bp, "\n");
         }
-        av_bprintf(bp, "  loss %e is %s by %e, expected loss %e\n",
-                   r->loss, worse_str, r->loss - expected_loss, expected_loss);
+        av_bprintf(bp, "  loss %f is %s by %f, expected loss %f\n",
+                   r->loss, worse_str, expected_loss - r->loss, expected_loss);
         if (av_bprint_is_complete(bp))
             av_log(NULL, level, "%s", bp->str);
     }
@@ -616,7 +611,7 @@ static void print_results(const AVFrame *src, const AVFrame 
*dst,
     if (ref_badness) {
         /**
          * The new scaling code does not (yet) perform error diffusion for
-         * low bit depth output, which impacts the SSIM score slightly for
+         * low bit depth output, which impacts the MSE slightly for
          * very low bit-depth formats (e.g. monow, monob). Since this is an
          * expected result, drop the badness from an error to a warning for
          * such cases. This can be removed again once error diffusion is
@@ -631,9 +626,9 @@ static void print_results(const AVFrame *src, const AVFrame 
*dst,
             print_test_params(bp, src, dst, mode, opts);
             av_bprintf(bp, "\n");
         }
-        av_bprintf(bp, "  loss %e is %s by %e, ref loss %e ",
-                   r->loss, worse_str, r->loss - ref_r->loss, ref_r->loss);
-        print_ssim(bp, ref_r->ssim);
+        av_bprintf(bp, "  loss %f is %s by %f, ref loss %f ",
+                   r->loss, worse_str, ref_r->loss - r->loss, ref_r->loss);
+        print_psnr(bp, ref_r->mse);
         av_bprintf(bp, "\n");
         if (av_bprint_is_complete(bp))
             av_log(NULL, level, "%s", bp->str);
@@ -722,17 +717,14 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
     int ret;
 
     /* Estimate the expected amount of loss from bit depth reduction */
-    const float c1 = 0.01 * 0.01; /* stabilization constant */
-    const float ref_var = 1.0 / 12.0; /* uniformly distributed signal */
-    double ssim_expected[4];
-    ssim_expected[3] = 1.0; /* for simplicity */
+    double mse_expected[4];
+    mse_expected[3] = 0.0; /* for simplicity */
     for (int p = 0; p < 3; p++) {
         const float src_var = estimate_quantization_noise(t->src_fmt, p);
         const float dst_var = estimate_quantization_noise(t->dst_fmt, p);
-        const float total_var = src_var + dst_var;
-        ssim_expected[p] = (2 * ref_var + c1) / (2 * ref_var + total_var + c1);
+        mse_expected[p] = src_var + dst_var;
     }
-    const float expected_loss = get_loss(ssim_expected);
+    const double expected_loss = get_loss(mse_expected);
 
     struct test_results r = { 0 };
 
@@ -785,13 +777,13 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
     if (ret < 0)
         goto error;
 
-    get_ssim(r.ssim, out, src_ref);
+    get_mse(r.mse, out, src_ref);
 
-    r.loss = get_loss(r.ssim);
+    r.loss = get_loss(r.mse);
     if (t->dst_w >= ref->width && t->dst_h >= ref->height)
-        selftest_badness = compare_ssim(r.ssim, ssim_expected);
+        selftest_badness = compare_mse(r.mse, mse_expected);
     if (ref_r)
-        ref_badness = compare_ssim(r.ssim, ref_r->ssim);
+        ref_badness = compare_mse(r.mse, ref_r->mse);
 
     ret = (opts->api != IMPL_LEGACY && selftest_badness > 1) || ref_badness > 
1 ? -1 : 0;
     print_results(src, dst, &t->mode, opts, &r, ref_r,
@@ -945,15 +937,16 @@ static int run_file_tests(const struct options *opts)
         int sw, sh, dw, dh;
         struct test_results r = { 0 };
         struct mode mode;
+        double psnr[4];
         int n = 0;
 
         ret = sscanf(buf,
                      "%20s %dx%d -> %20s %dx%d, flags=0x%x dither=%u 
scaler=%u/%u, "
-                     "SSIM={R=%lf G=%lf B=%lf A=%lf} loss=%e%n",
+                     "PSNR={R=%lf G=%lf B=%lf A=%lf} loss=%lf%n",
                      src_fmt_str, &sw, &sh, dst_fmt_str, &dw, &dh,
                      &mode.flags, &mode.dither,
                      &mode.scaler, &mode.scaler_sub,
-                     &r.ssim[0], &r.ssim[1], &r.ssim[2], &r.ssim[3],
+                     &psnr[0], &psnr[1], &psnr[2], &psnr[3],
                      &r.loss, &n);
         if (ret != 15) {
             av_log(NULL, AV_LOG_FATAL,
@@ -972,6 +965,9 @@ static int run_file_tests(const struct options *opts)
             }
         }
 
+        for (int p = 0; p < 4; p++)
+            r.mse[p] = pow(10.0, -psnr[p] / 10.0);
+
         src_fmt = av_get_pix_fmt(src_fmt_str);
         dst_fmt = av_get_pix_fmt(dst_fmt_str);
         if (src_fmt == AV_PIX_FMT_NONE || dst_fmt == AV_PIX_FMT_NONE) {
-- 
2.54.0


>From bdd1935b4b2580c973381f3b8deee9e03cde7030 Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 26 Oct 2026 13:32:46 +0100
Subject: [PATCH 14/17] swscale/tests/swscale: move fmt_is_subsampled() up

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/tests/swscale.c | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/libswscale/tests/swscale.c b/libswscale/tests/swscale.c
index 37bb06d6b0b..5dde1122373 100644
--- a/libswscale/tests/swscale.c
+++ b/libswscale/tests/swscale.c
@@ -193,6 +193,12 @@ static float estimate_quantization_noise(enum 
AVPixelFormat fmt, int p)
     }
 }
 
+static inline int fmt_is_subsampled(enum AVPixelFormat fmt)
+{
+    return av_pix_fmt_desc_get(fmt)->log2_chroma_w != 0 ||
+           av_pix_fmt_desc_get(fmt)->log2_chroma_h != 0;
+}
+
 static SwsCompMask fmt_comps(enum AVPixelFormat fmt)
 {
     const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(fmt);
@@ -795,12 +801,6 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
     return ret;
 }
 
-static inline int fmt_is_subsampled(enum AVPixelFormat fmt)
-{
-    return av_pix_fmt_desc_get(fmt)->log2_chroma_w != 0 ||
-           av_pix_fmt_desc_get(fmt)->log2_chroma_h != 0;
-}
-
 static inline int fmt_is_supported_by_hw(enum AVPixelFormat fmt)
 {
     /* Semi-planar formats are only supported by the legacy path, which
-- 
2.54.0


>From 05feee80104ad0c7a11a9593d7843b3445293051 Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Wed, 28 Oct 2026 18:01:22 +0100
Subject: [PATCH 15/17] swscale/tests/swscale: check each sample against one
 code step

Rounding, truncation and dithering all pick one of the two codes
surrounding a value, so require every sample to be within one code step
of the decoded source. Resampled conversions and legacy swscale are not
checked.

For non-RGB formats, the step is a conservative bound derived from the
luma depth: an R, G or B sample of a limited range YUV format combines
the error of one luma and up to two chroma codes, which stays below
four codes of the luma depth.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/tests/swscale.c | 86 ++++++++++++++++++++++++++++++++++----
 1 file changed, 79 insertions(+), 7 deletions(-)

diff --git a/libswscale/tests/swscale.c b/libswscale/tests/swscale.c
index 5dde1122373..f5a8cddb94b 100644
--- a/libswscale/tests/swscale.c
+++ b/libswscale/tests/swscale.c
@@ -193,6 +193,47 @@ static float estimate_quantization_noise(enum 
AVPixelFormat fmt, int p)
     }
 }
 
+/**
+ * The output is compared against the source decoded back to the reference
+ * format, reduced to what the destination format can represent. Rounding,
+ * truncation and dithering all pick one of the two codes surrounding a value,
+ * so every sample must be within one code step of it.
+ *
+ * For non-RGB formats, a step is estimated from the luma depth, with headroom
+ * for limited range and, for YUV, for the errors of all components adding up
+ * in each of R, G and B. For floating point formats, a step is the precision
+ * near 1.0.
+ */
+#define TOLERANCE_RELATIVE 1e-3 /* relative to one step */
+#define TOLERANCE_ABSOLUTE 1e-6 /* single precision error */
+
+/* Size of one code of component `c` of `desc` */
+static double get_code_size(const AVPixFmtDescriptor *desc, int c)
+{
+    const int depth = desc->comp[c].depth;
+    if (desc->flags & AV_PIX_FMT_FLAG_FLOAT)
+        return ldexp(1.0, depth > 16 ? -23 : -10); /* precision near 1.0 */
+    return 1.0 / ((1 << depth) - 1);
+}
+
+/* Upper bound on one code step of each R, G, B, A plane of `fmt` */
+static void get_steps(double step[4], enum AVPixelFormat fmt)
+{
+    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(fmt);
+    const int rgb   = desc->flags & (AV_PIX_FMT_FLAG_RGB | 
AV_PIX_FMT_FLAG_PAL);
+    const int fp    = desc->flags & AV_PIX_FMT_FLAG_FLOAT;
+    const int alpha = !!(desc->flags & AV_PIX_FMT_FLAG_ALPHA);
+    const int gray  = desc->nb_components - alpha < 3;
+    const int headroom = rgb || fp ? 1 : gray ? 2 : 4;
+
+    /* Only RGB formats have a separate depth for each of R, G and B */
+    const int per_plane = rgb && !gray;
+    for (int p = 0; p < 3; p++)
+        step[p] = headroom * get_code_size(desc, per_plane ? p : 0);
+
+    step[3] = alpha ? get_code_size(desc, desc->nb_components - 1) : 0.0;
+}
+
 static inline int fmt_is_subsampled(enum AVPixelFormat fmt)
 {
     return av_pix_fmt_desc_get(fmt)->log2_chroma_w != 0 ||
@@ -208,7 +249,8 @@ static SwsCompMask fmt_comps(enum AVPixelFormat fmt)
     return comps;
 }
 
-static void get_mse(double mse[4], const AVFrame *out, const AVFrame *ref)
+static void get_mse(double mse[4], float max_err[4],
+                    const AVFrame *out, const AVFrame *ref)
 {
     av_assert1(out->format == AV_PIX_FMT_GBRAPF32);
     av_assert1(ref->format == out->format);
@@ -222,13 +264,15 @@ static void get_mse(double mse[4], const AVFrame *out, 
const AVFrame *ref)
         const int stride_a = out->linesize[p];
         const int stride_b = ref->linesize[p];
         double sum = 0;
+        float max = 0;
 
         for (int y = 0; y < h; y++) {
             const float *row_a = (const float *) data_a;
             const float *row_b = (const float *) data_b;
             for (int x = 0; x < w; x++) {
-                const float d = row_a[x] - row_b[x];
+                const float d = fabsf(row_a[x] - row_b[x]);
                 sum += d * d;
+                max = FFMAX(max, d);
             }
 
             data_a += stride_a;
@@ -237,6 +281,7 @@ static void get_mse(double mse[4], const AVFrame *out, 
const AVFrame *ref)
 
         const int indices[4] = { 1, 2, 0, 3 }; /* GBRA */
         mse[indices[p]] = sum / (w * h);
+        max_err[indices[p]] = max;
     }
 }
 
@@ -554,7 +599,8 @@ static void print_results(const AVFrame *src, const AVFrame 
*dst,
                           const struct test_results *r,
                           const struct test_results *ref_r,
                           double expected_loss,
-                          int selftest_badness, int ref_badness)
+                          int selftest_badness, const double *bad_steps,
+                          int ref_badness)
 {
     AVBPrint bprint, *bp = &bprint;
     av_bprint_init(bp, 0, AV_BPRINT_SIZE_AUTOMATIC);
@@ -614,6 +660,16 @@ static void print_results(const AVFrame *src, const 
AVFrame *dst,
             av_log(NULL, level, "%s", bp->str);
     }
 
+    if (bad_steps) {
+        av_bprint_clear(bp);
+        print_test_params(bp, src, dst, mode, opts);
+        av_bprintf(bp, "\n  largest error relative to one step "
+                   "{R=%.2f G=%.2f B=%.2f A=%.2f} is WORSE than allowed\n",
+                   bad_steps[0], bad_steps[1], bad_steps[2], bad_steps[3]);
+        if (av_bprint_is_complete(bp))
+            av_log(NULL, AV_LOG_ERROR, "%s", bp->str);
+    }
+
     if (ref_badness) {
         /**
          * The new scaling code does not (yet) perform error diffusion for
@@ -719,7 +775,9 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
     AVFrame *dst = NULL, *out = NULL;
     const AVFrame *src_ref;
     const SwsCompMask comps = fmt_comps(t->dst_fmt);
-    int selftest_badness = 0, ref_badness = 0;
+    double step[4], err[4];
+    float max_err[4];
+    int bad = 0, selftest_badness = 0, ref_badness = 0;
     int ret;
 
     /* Estimate the expected amount of loss from bit depth reduction */
@@ -732,6 +790,10 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
     }
     const double expected_loss = get_loss(mse_expected);
 
+    /* Resampling is not modelled */
+    const int resampled = t->dst_w != ref->width || t->dst_h != ref->height ||
+                          fmt_is_subsampled(t->src_fmt) || 
fmt_is_subsampled(t->dst_fmt);
+
     struct test_results r = { 0 };
 
     if (!src || src->format != t->src_fmt) {
@@ -783,7 +845,17 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
     if (ret < 0)
         goto error;
 
-    get_mse(r.mse, out, src_ref);
+    get_mse(r.mse, max_err, out, src_ref);
+
+    get_steps(step, t->dst_fmt);
+    for (int p = 0; p < 4; p++) {
+        err[p] = max_err[p] / (step[p] * (1.0 + TOLERANCE_RELATIVE) +
+                               TOLERANCE_ABSOLUTE);
+        bad |= err[p] > 1.0;
+    }
+    /* The bound does not hold after resampling, and legacy swscale is known
+     * to fail it */
+    bad &= opts->api != IMPL_LEGACY && !resampled;
 
     r.loss = get_loss(r.mse);
     if (t->dst_w >= ref->width && t->dst_h >= ref->height)
@@ -791,9 +863,9 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
     if (ref_r)
         ref_badness = compare_mse(r.mse, ref_r->mse);
 
-    ret = (opts->api != IMPL_LEGACY && selftest_badness > 1) || ref_badness > 
1 ? -1 : 0;
+    ret = (opts->api != IMPL_LEGACY && selftest_badness > 1) || bad || 
ref_badness > 1 ? -1 : 0;
     print_results(src, dst, &t->mode, opts, &r, ref_r,
-                  expected_loss, selftest_badness, ref_badness);
+                  expected_loss, selftest_badness, bad ? err : NULL, 
ref_badness);
 
  error:
     av_frame_free(&dst);
-- 
2.54.0


>From cb13e880f5e6257480c3e9cd18ebe270839a20d3 Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 26 Oct 2026 20:08:43 +0100
Subject: [PATCH 16/17] swscale/tests/swscale: allow for clipping of
 out-of-range values

Values the destination format cannot reach are clipped to the nearest
code, so the error may additionally be as large as the excess.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/tests/swscale.c | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/libswscale/tests/swscale.c b/libswscale/tests/swscale.c
index f5a8cddb94b..f08174866e7 100644
--- a/libswscale/tests/swscale.c
+++ b/libswscale/tests/swscale.c
@@ -249,6 +249,11 @@ static SwsCompMask fmt_comps(enum AVPixelFormat fmt)
     return comps;
 }
 
+/**
+ * Values of `ref` outside [0, 1] are clipped to the nearest code, so the error
+ * of a sample may additionally be as large as that excess; `max_err` excludes
+ * it.
+ */
 static void get_mse(double mse[4], float max_err[4],
                     const AVFrame *out, const AVFrame *ref)
 {
@@ -271,8 +276,9 @@ static void get_mse(double mse[4], float max_err[4],
             const float *row_b = (const float *) data_b;
             for (int x = 0; x < w; x++) {
                 const float d = fabsf(row_a[x] - row_b[x]);
+                const float excess = FFMAX3(row_b[x] - 1.0f, -row_b[x], 0.0f);
                 sum += d * d;
-                max = FFMAX(max, d);
+                max = FFMAX(max, d - excess);
             }
 
             data_a += stride_a;
-- 
2.54.0


>From afecc9a8f416f15495eb17dc438350897fa40d2e Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Wed, 28 Oct 2026 18:01:33 +0100
Subject: [PATCH 17/17] swscale/tests/swscale: only use the noise estimate for
 resampled conversions

The per-sample check supersedes it for all other conversions.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/tests/swscale.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/libswscale/tests/swscale.c b/libswscale/tests/swscale.c
index f08174866e7..32b0cc5c9ed 100644
--- a/libswscale/tests/swscale.c
+++ b/libswscale/tests/swscale.c
@@ -864,7 +864,7 @@ static int run_test(struct runner *runner, const struct 
test_case *t,
     bad &= opts->api != IMPL_LEGACY && !resampled;
 
     r.loss = get_loss(r.mse);
-    if (t->dst_w >= ref->width && t->dst_h >= ref->height)
+    if (resampled && t->dst_w >= ref->width && t->dst_h >= ref->height)
         selftest_badness = compare_mse(r.mse, mse_expected);
     if (ref_r)
         ref_badness = compare_mse(r.mse, ref_r->mse);
-- 
2.54.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to