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

These should hopefully be uncontroversial.


>From 2e65ff23c675e8b01e56185e32ab1355a514189a Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Wed, 28 Oct 2026 19:37:29 +0100
Subject: [PATCH 1/8] swscale/ops_dispatch: always split off unresolved filters

Instead of only doing so after compilation of the whole op list failed with
AVERROR(ENOTSUP). No backend currently implements standalone filter ops, so
this does not change the result; but it avoids splitting op lists that are
unsupported for other reasons, e.g. by some CPUs only. This can be revisited
once a backend actually implements standalone filters.

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

diff --git a/libswscale/ops_dispatch.c b/libswscale/ops_dispatch.c
index 21e5535f14b..bfc776c43ac 100644
--- a/libswscale/ops_dispatch.c
+++ b/libswscale/ops_dispatch.c
@@ -743,11 +743,7 @@ static int compile_subpass(const CompileArgs *args, 
SwsOpList **pops,
         }
     }
 
-    ret = compile_single(args, ops, link, input, output);
-    if (ret != AVERROR(ENOTSUP))
-        goto fail; /* either success or a hard error */
-
-    /* Find any unresolved filter */
+    /* Split off any unresolved filters, since no backend implements these */
     for (int idx = 1; idx < ops->num_ops - 1; idx++) {
         const SwsOp *op = &ops->ops[idx];
         if (op->op == SWS_OP_FILTER_H || op->op == SWS_OP_FILTER_V) {
@@ -766,11 +762,11 @@ static int compile_subpass(const CompileArgs *args, 
SwsOpList **pops,
         }
     }
 
-    /* If we didn't find any more operations to eliminate, then this ops list
-     * is simply unsupported by any of the available backends */
-    av_log(ctx, AV_LOG_WARNING, "No backend found for operations:\n");
-    ff_sws_op_list_print(ctx, AV_LOG_WARNING, AV_LOG_TRACE, ops);
-    ret = AVERROR(ENOTSUP);
+    ret = compile_single(args, ops, link, input, output);
+    if (ret == AVERROR(ENOTSUP)) {
+        av_log(ctx, AV_LOG_WARNING, "No backend found for operations:\n");
+        ff_sws_op_list_print(ctx, AV_LOG_WARNING, AV_LOG_TRACE, ops);
+    }
 
 fail:
     ff_sws_op_list_free(&ops);
-- 
2.54.0


>From 658d01210e43a1209e4e0840deeffcae60a0036c Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Thu, 29 Oct 2026 15:14:48 +0100
Subject: [PATCH 2/8] swscale/uops: move expand32() from x86 to header

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/uops.h    | 11 +++++++++++
 libswscale/x86/ops.c | 14 +-------------
 2 files changed, 12 insertions(+), 13 deletions(-)

diff --git a/libswscale/uops.h b/libswscale/uops.h
index 4d25107927b..2a48746fc65 100644
--- a/libswscale/uops.h
+++ b/libswscale/uops.h
@@ -101,6 +101,17 @@ static inline SwsPixel ff_sws_pixel_from_q64(SwsPixelType 
type, AVRational64 val
     return (SwsPixel) {0};
 }
 
+/* Expand pixel value to 32-bits by repeating as necessary */
+static inline SwsPixel ff_sws_pixel_expand32(SwsPixelType type, SwsPixel val)
+{
+    switch (type) {
+    case SWS_PIXEL_U8:  val.u32 = val.u8  * 0x01010101u; break;
+    case SWS_PIXEL_U16: val.u32 = val.u16 * 0x00010001u; break;
+    default: break;
+    }
+    return val;
+}
+
 static inline bool ff_sws_pixel_is_1s(SwsPixelType type, SwsPixel val)
 {
     switch (ff_sws_pixel_type_size(type)) {
diff --git a/libswscale/x86/ops.c b/libswscale/x86/ops.c
index 9cc5ba7a0aa..f1349a9ec93 100644
--- a/libswscale/x86/ops.c
+++ b/libswscale/x86/ops.c
@@ -622,22 +622,10 @@ do {
     return 0;
 }
 
-/* Expand pixel value to 32-bits by repeating as necessary */
-static uint32_t expand32(const SwsPixelType type, const SwsPixel value)
-{
-    switch (type) {
-    case SWS_PIXEL_U8:  return value.u8  * 0x01010101u;
-    case SWS_PIXEL_U16: return value.u16 * 0x00010001u;
-    case SWS_PIXEL_U32: return value.u32;
-    case SWS_PIXEL_F32: return value.u32; /* reinterpret */
-    default: return 0;
-    }
-}
-
 static void normalize_clear(SwsUOp *uop)
 {
     for (int i = 0; i < 4; i++)
-        uop->data.vec4[i].u32 = expand32(uop->type, uop->data.vec4[i]);
+        uop->data.vec4[i] = ff_sws_pixel_expand32(uop->type, 
uop->data.vec4[i]);
 }
 
 static int compile_uops_x86(SwsContext *ctx, const SwsUOpList *uops, 
SwsCompiledOp *out)
-- 
2.54.0


>From 3fe56043df09071075a0a83cd6f9e091c4226fac Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Wed, 28 Oct 2026 18:16:55 +0100
Subject: [PATCH 3/8] swscale/uops: always expand clear values to 32 bits

This is backwards compatible, as reading the original pixel type from the
expanded value gives the same result regardless of endianness.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/uops.c       |  2 +-
 libswscale/uops.h       |  4 ++++
 tests/checkasm/sw_ops.c | 11 ++++++++++-
 3 files changed, 15 insertions(+), 2 deletions(-)

diff --git a/libswscale/uops.c b/libswscale/uops.c
index 54d5581107f..4f19ee33f00 100644
--- a/libswscale/uops.c
+++ b/libswscale/uops.c
@@ -603,7 +603,7 @@ static int translate_op(SwsContext *ctx, SwsUOpList *uops, 
SwsUOpFlags flags,
             const SwsPixelType type = op->clear.type[i];
             const SwsPixel px = ff_sws_pixel_from_q64(type, v);
             uop.type = pixel_type_to_int(flags, type);
-            uop.data.vec4[i] = px;
+            uop.data.vec4[i] = ff_sws_pixel_expand32(type, px);
             if (v.num == 0)
                 uop.par.clear.zero |= SWS_COMP(i);
             else if (ff_sws_pixel_is_1s(type, px))
diff --git a/libswscale/uops.h b/libswscale/uops.h
index 2a48746fc65..f89ee5ef52c 100644
--- a/libswscale/uops.h
+++ b/libswscale/uops.h
@@ -255,6 +255,10 @@ typedef struct SwsPackUOp {
     uint8_t pattern[4]; /* bit depth pattern, from MSB to LSB */
 } SwsPackUOp;
 
+/**
+ * The clear values (SwsUOp.data.vec4) are always expanded to repeating 32-bit
+ * patterns, so they remain valid if the uop is executed as a smaller type.
+ */
 typedef struct SwsClearUOp {
     SwsCompMask one;  /* mask of coefficients equal to all 1s */
     SwsCompMask zero; /* mask of coefficients equal to all 0s */
diff --git a/tests/checkasm/sw_ops.c b/tests/checkasm/sw_ops.c
index 0d17d8bd7df..01d473564dc 100644
--- a/tests/checkasm/sw_ops.c
+++ b/tests/checkasm/sw_ops.c
@@ -488,6 +488,15 @@ static void check_vec4(const char *name, SwsUOp *uop)
     check_simple(name, uop);
 }
 
+static void check_clear(const char *name, SwsUOp *uop)
+{
+    /* Clear values are always expanded to 32 bits, see SwsClearUOp */
+    for (int i = 0; i < 4; i++)
+        uop->data.vec4[i] = ff_sws_pixel_expand32(uop->type, rndpx(uop->type));
+
+    check_simple(name, uop);
+}
+
 #define MK_RANGES(R) ((const unsigned[]) { R, R, R, R })
 
 static void check_read(const char *name, SwsUOp *uop)
@@ -715,7 +724,7 @@ void checkasm_check_sw_ops(void)
     CHECK_FOR(PACK,             check_pack);
     CHECK_FOR(LSHIFT,           check_simple);
     CHECK_FOR(RSHIFT,           check_simple);
-    CHECK_FOR(CLEAR,            check_vec4);
+    CHECK_FOR(CLEAR,            check_clear);
     CHECK_FOR(LINEAR,           check_linear);
     CHECK_FOR(DITHER,           check_dither);
     CHECK_FOR(LUT_3D,           check_lut_3d);
-- 
2.54.0


>From ea507fbad76a5f328be76c09640d9b43ef06c00b Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Wed, 28 Oct 2026 19:41:20 +0100
Subject: [PATCH 4/8] swscale/x86/ops: don't expand clear values

They are already expanded to 32 bits, as documented for SwsClearUOp.

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

diff --git a/libswscale/x86/ops.c b/libswscale/x86/ops.c
index f1349a9ec93..e477c3e917e 100644
--- a/libswscale/x86/ops.c
+++ b/libswscale/x86/ops.c
@@ -622,12 +622,6 @@ do {
     return 0;
 }
 
-static void normalize_clear(SwsUOp *uop)
-{
-    for (int i = 0; i < 4; i++)
-        uop->data.vec4[i] = ff_sws_pixel_expand32(uop->type, 
uop->data.vec4[i]);
-}
-
 static int compile_uops_x86(SwsContext *ctx, const SwsUOpList *uops, 
SwsCompiledOp *out)
 {
     int ret, mmsize = get_mmsize();
@@ -662,8 +656,6 @@ static int compile_uops_x86(SwsContext *ctx, const 
SwsUOpList *uops, SwsCompiled
         int op_block_size = out->block_size;
 
         if (uop_is_type_invariant(uop->uop)) {
-            if (uop->uop == SWS_UOP_CLEAR)
-                normalize_clear(uop);
             op_block_size *= ff_sws_pixel_type_size(uop->type);
             uop->type = SWS_PIXEL_U8;
         }
-- 
2.54.0


>From f88fac8d981fa56fda67ba154a5113350acdf6bc Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Thu, 29 Oct 2026 16:49:48 +0100
Subject: [PATCH 5/8] swscale/x86/ops_int: fix pointer increment in
 WRITE_PLANAR

The output pointer was advanced by the number of pixels instead of the
number of bytes, unlike READ_PLANAR. This made no difference so far, as
planar writes are only ever instantiated for SWS_PIXEL_U8.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/x86/ops_int.asm | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/libswscale/x86/ops_int.asm b/libswscale/x86/ops_int.asm
index c5d38b69ff3..20b34dc7394 100644
--- a/libswscale/x86/ops_int.asm
+++ b/libswscale/x86/ops_int.asm
@@ -200,10 +200,10 @@ IF Y,   movu [out1q + mmsize], my2
 IF Z,   movu [out2q + mmsize], mz2
 IF W,   movu [out3q + mmsize], mw2
 %endif
-IF X,   add out0q, BLOCK_WIDTH
-IF Y,   add out1q, BLOCK_WIDTH
-IF Z,   add out2q, BLOCK_WIDTH
-IF W,   add out3q, BLOCK_WIDTH
+IF X,   add out0q, BLOCK_SIZE
+IF Y,   add out1q, BLOCK_SIZE
+IF Z,   add out2q, BLOCK_SIZE
+IF W,   add out3q, BLOCK_SIZE
         RET
 %endmacro
 
-- 
2.54.0


>From 3b2d243f8ea47426c0549c7ca28632211f950f74 Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Wed, 4 Nov 2026 22:14:52 +0100
Subject: [PATCH 6/8] tests/checkasm/sw_ops: test SWS_UOP_ADD with
 per-component constants

SWS_UOP_ADD reads data.vec4, but check_scalar() only randomized
data.scalar, i.e. the first component.

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

diff --git a/tests/checkasm/sw_ops.c b/tests/checkasm/sw_ops.c
index 01d473564dc..298618029f5 100644
--- a/tests/checkasm/sw_ops.c
+++ b/tests/checkasm/sw_ops.c
@@ -474,12 +474,6 @@ static void check_simple(const char *name, const SwsUOp 
*uop)
     check_range(name, uop, NULL);
 }
 
-static void check_scalar(const char *name, SwsUOp *uop)
-{
-    uop->data.scalar = rndpx(uop->type);
-    check_simple(name, uop);
-}
-
 static void check_vec4(const char *name, SwsUOp *uop)
 {
     for (int i = 0; i < 4; i++)
@@ -717,7 +711,7 @@ void checkasm_check_sw_ops(void)
     CHECK_FOR(TO_U32,           check_cast);
     CHECK_FOR(TO_F32,           check_cast);
     CHECK_FOR(SCALE,            check_scale);
-    CHECK_FOR(ADD,              check_scalar);
+    CHECK_FOR(ADD,              check_vec4);
     CHECK_FOR(MIN,              check_vec4);
     CHECK_FOR(MAX,              check_vec4);
     CHECK_FOR(UNPACK,           check_unpack);
-- 
2.54.0


>From 011dd623d57eaf759afc49ecec2b1d7595598d57 Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Tue, 1 Sep 2026 18:42:09 +0200
Subject: [PATCH 7/8] swscale/filters: constrain SWS_FILTER_BICUBIC parameter
 range

The filter is not really useful outside of these parameters, and clamping
the domain lets us reason about the expected overshoot.

In particular, B < 0 is completely broken, as is B >= 3 (already identified
in the code). C is more relaxed, but outside [-1.5, 2.0] it would also
overshoot by too much (relative to the upcoming change). There's a final
constraint of 2B + C <= 3.

Limiting both parameters to [0, 1] solves all problems at once and is the
standard practice.

Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/filters.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/libswscale/filters.c b/libswscale/filters.c
index fb54d2bbd8b..3073ed8cdc5 100644
--- a/libswscale/filters.c
+++ b/libswscale/filters.c
@@ -148,7 +148,8 @@ static bool validate_params(const SwsFilterFunction *fun, 
SwsScaler scaler)
     case SWS_SCALE_LANCZOS:
         return fun->params[0] >= 1.0 && fun->params[0] <= RADIUS_MAX; /* 
radius */
     case SWS_SCALE_BICUBIC:
-        return fun->params[0] < 3.0; /* B param (division by zero) */
+        return fun->params[0] >= 0.0 && fun->params[0] <= 1.0 /* B */ &&
+               fun->params[1] >= 0.0 && fun->params[1] <= 1.0 /* C */;
     default:
         return true;
     }
-- 
2.54.0


>From e58175e3ced923d89f81b903c6262f71799683d8 Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Tue, 1 Sep 2026 21:29:41 +0200
Subject: [PATCH 8/8] swscale/filters: properly handle filter tails

When the filter overlaps an image boundary, we need to add all the
weights past the edge to the appropriate edge weight.

Apart from generating correct filtering behavior near boundaries, this
also fixes an edge case where the `wsum < 0` guard could fire, as well
as some examples of catastrophic filter overshoot.

Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/filters.c | 34 ++++++++++++++++++++++------------
 1 file changed, 22 insertions(+), 12 deletions(-)

diff --git a/libswscale/filters.c b/libswscale/filters.c
index 3073ed8cdc5..811c5729ecd 100644
--- a/libswscale/filters.c
+++ b/libswscale/filters.c
@@ -82,13 +82,11 @@ static void compute_row(SwsFilterWeights *f, const 
SwsFilterFunction *fun,
         return;
     }
 
-    /* First pixel that is actually within the filter envelope */
-    const double start_pos = src_pos - radius;
-    int64_t start_idx = ceil(start_pos);
-    start_idx = FFMAX(start_idx, 0); /* edge clamping */
-    start_idx = FFMIN(start_idx, f->src_size - f->filter_size);
-    const double offset = start_idx - src_pos;
-    *pos = start_idx;
+    /* First and last pixels that are actually within the filter envelope */
+    const int start   = ceil (src_pos - radius);
+    const int stop    = floor(src_pos + radius);
+    const int clamped = av_clip(start, 0, f->src_size - f->filter_size);
+    *pos = clamped;
 
     /**
      * Generate raw filter weights with maximum precision. Sum the positive
@@ -96,14 +94,26 @@ static void compute_row(SwsFilterWeights *f, const 
SwsFilterFunction *fun,
      * summation order should already give the best precision because abs(w)
      * is monotonically decreasing
      */
-    const double base = stretch_inv * offset;
+    const double base = stretch_inv * (clamped - src_pos);
     double wsum_pos = 0.0, wsum_neg = 0.0;
     for (int i = 0; i < f->filter_size; i++) {
-        tmp[i] = scaler_sample(fun, base + stretch_inv * i);
-        if (tmp[i] >= 0)
-            wsum_pos += tmp[i];
+        /* Fold extra edge weights into the first/last weight in case the
+         * filter got truncated; loop over all needed samples  */
+        int i0 = i, i1 = i;
+        if (i == 0)
+            i0 = start - clamped;
+        if (i == f->filter_size - 1)
+            i1 = stop - clamped;
+
+        double w = 0.0;
+        for (int k = i0; k <= i1; k++)
+            w += scaler_sample(fun, base + stretch_inv * k);
+
+        if (w >= 0)
+            wsum_pos += w;
         else
-            wsum_neg += tmp[i];
+            wsum_neg += w;
+        tmp[i] = w;
     }
 
     const double wsum = wsum_pos + wsum_neg;
-- 
2.54.0

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

Reply via email to