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

Re-submitting this here as a formal PR.


>From 5d44f43ea2007780b922edabc487f2f39844eed1 Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 1 Jun 2026 17:34:30 +0200
Subject: [PATCH 1/7] swscale: add missing validation for newly added enums

Gives slightly better error messages for invalid values.

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

diff --git a/libswscale/swscale.c b/libswscale/swscale.c
index 25bd8fc293..ca7eddbdef 100644
--- a/libswscale/swscale.c
+++ b/libswscale/swscale.c
@@ -1441,6 +1441,9 @@ static int validate_params(SwsContext *ctx)
     VALIDATE(threads,       0, SWS_MAX_THREADS);
     VALIDATE(dither,        0, SWS_DITHER_NB - 1)
     VALIDATE(alpha_blend,   0, SWS_ALPHA_BLEND_NB - 1)
+    VALIDATE(intent,        0, SWS_INTENT_NB - 1);
+    VALIDATE(scaler,        0, SWS_SCALE_NB - 1)
+    VALIDATE(scaler_sub,    0, SWS_SCALE_NB - 1)
     return 0;
 }
 
-- 
2.52.0


>From d188ff3cf1b8e2acea84aa753ac37dea0cbdab43 Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 1 Jun 2026 17:17:35 +0200
Subject: [PATCH 2/7] swscale: add new SwsContext.backends option

This allows constraining the set of available backends. This serves as a
better replacement for the "unstable" flag, which is a bit ambiguous. Allows
users to, for example, opt into the memcpy or x86 backend, while excluding
e.g. the upcoming JIT backends.

Signed-off-by: Niklas Haas <[email protected]>
---
 doc/APIchanges                |  3 +++
 doc/scaler.texi               | 41 +++++++++++++++++++++++++++++++++++
 libswscale/aarch64/ops.c      |  1 +
 libswscale/graph.c            | 12 ++++++++--
 libswscale/ops_backend.c      |  1 +
 libswscale/ops_dispatch.c     |  5 ++++-
 libswscale/ops_dispatch.h     |  1 +
 libswscale/ops_memcpy.c       |  1 +
 libswscale/options.c          | 13 +++++++++++
 libswscale/swscale.h          | 39 +++++++++++++++++++++++++++++++--
 libswscale/swscale_internal.h |  2 ++
 libswscale/utils.c            | 12 ++++++++++
 libswscale/version.h          |  2 +-
 libswscale/vulkan/ops.c       |  2 ++
 libswscale/x86/ops.c          |  1 +
 15 files changed, 130 insertions(+), 6 deletions(-)

diff --git a/doc/APIchanges b/doc/APIchanges
index 4695a38e8a..325d50e6b8 100644
--- a/doc/APIchanges
+++ b/doc/APIchanges
@@ -2,6 +2,9 @@ The last version increases of all libraries were on 2025-03-28
 
 API changes, most recent first:
 
+2026-06-xx - xxxxxxxxxx - lsws 9.8.100 - swscale.h
+  Add enum SwsBackend, SwsBackendFlags and SwsContext.backends
+
 2026-05-xx - xxxxxxxxxx - lavf 62.19.100 - avformat.h
   Add AVStreamGroupLayeredVideo
   Add AVStreamGroup.params.layered_video
diff --git a/doc/scaler.texi b/doc/scaler.texi
index aecc3ad248..a66c8217f8 100644
--- a/doc/scaler.texi
+++ b/doc/scaler.texi
@@ -191,6 +191,47 @@ No blending
 
 @end table
 
+@item sws_backends
+Set the allowed swscale backends. This is a flags option, so multiple backends
+may be combined.
+
+@table @samp
+@item auto
+Automatic selection. Equal to either @samp{stable} or @samp{all} depending on
+whether or not the @samp{unstable} flag is set. This is the default value.
+
+@item stable
+All stable backends.
+
+@item unstable
+All unstable backends.
+
+@item all
+All available backends.
+
+@item legacy
+Legacy swscale code.
+
+@item c
+Template-based reference code.
+
+@item memcpy
+Fast path using libc @code{memcpy}.
+
+@item x86
+x86 SIMD kernels.
+
+@item aarch64
+AArch64 NEON kernels.
+
+@item spirv
+Vulkan SPIR-V backend.
+
+@item glsl
+Vulkan GLSL backend.
+
+@end table
+
 @end table
 
 @c man end SCALER OPTIONS
diff --git a/libswscale/aarch64/ops.c b/libswscale/aarch64/ops.c
index 753aef51ae..4598a8db6b 100644
--- a/libswscale/aarch64/ops.c
+++ b/libswscale/aarch64/ops.c
@@ -254,6 +254,7 @@ error:
 /*********************************************************************/
 const SwsOpBackend backend_aarch64 = {
     .name      = "aarch64",
+    .flags     = SWS_BACKEND_AARCH64,
     .compile   = aarch64_compile,
     .hw_format = AV_PIX_FMT_NONE,
 };
diff --git a/libswscale/graph.c b/libswscale/graph.c
index 2ee1bd84a2..37c11f9dd5 100644
--- a/libswscale/graph.c
+++ b/libswscale/graph.c
@@ -548,6 +548,9 @@ static int add_legacy_sws_pass(SwsGraph *graph, const 
SwsFormat *src,
 {
     int ret, warned = 0;
     SwsContext *const ctx = graph->ctx;
+    const SwsBackend backend = ff_sws_enabled_backends(ctx);
+    if (!(backend & SWS_BACKEND_LEGACY))
+        return AVERROR(ENOTSUP);
     if (src->hw_format != AV_PIX_FMT_NONE || dst->hw_format != AV_PIX_FMT_NONE)
         return AVERROR(ENOTSUP);
 
@@ -626,8 +629,12 @@ static int add_convert_pass(SwsGraph *graph, const 
SwsFormat *src,
     SwsContext *ctx = graph->ctx;
     int ret = AVERROR(ENOTSUP);
 
-    /* Mark the entire new ops infrastructure as experimental for now */
-    if (!(ctx->flags & SWS_UNSTABLE))
+    /* Preemptively skip the ops list generation if the backend was
+     * constrained to the legacy implementation only. This would
+     * normally also fail in ff_sws_compile_pass() with the same
+     * error, but this way saves a bit of unnecessary overhead */
+    const SwsBackend backends = ff_sws_enabled_backends(ctx);
+    if (backends == SWS_BACKEND_LEGACY)
         goto fail;
 
     SwsOpList *ops;
@@ -908,6 +915,7 @@ static int opts_equal(const SwsContext *c1, const 
SwsContext *c2)
            c1->intent        == c2->intent        &&
            c1->scaler        == c2->scaler        &&
            c1->scaler_sub    == c2->scaler_sub    &&
+           c1->backends      == c2->backends      &&
            !memcmp(c1->scaler_params, c2->scaler_params, 
sizeof(c1->scaler_params));
 
 }
diff --git a/libswscale/ops_backend.c b/libswscale/ops_backend.c
index 07919539e5..254814ee37 100644
--- a/libswscale/ops_backend.c
+++ b/libswscale/ops_backend.c
@@ -111,6 +111,7 @@ static int compile(SwsContext *ctx, SwsOpList *ops, 
SwsCompiledOp *out)
 
 const SwsOpBackend backend_c = {
     .name       = "c",
+    .flags      = SWS_BACKEND_C,
     .compile    = compile,
     .hw_format  = AV_PIX_FMT_NONE,
 };
diff --git a/libswscale/ops_dispatch.c b/libswscale/ops_dispatch.c
index 8b04ef1c96..f73f801a20 100644
--- a/libswscale/ops_dispatch.c
+++ b/libswscale/ops_dispatch.c
@@ -28,6 +28,7 @@
 #include "ops.h"
 #include "ops_internal.h"
 #include "ops_dispatch.h"
+#include "swscale_internal.h"
 
 typedef struct SwsOpPass {
     SwsCompiledOp comp;
@@ -96,10 +97,12 @@ int ff_sws_ops_compile(SwsContext *ctx, const SwsOpBackend 
*backend,
     if (backend)
         return compile_backend(ctx, backend, ops, out);
 
+    const SwsBackend enabled = ff_sws_enabled_backends(ctx);
     for (int n = 0; ff_sws_op_backends[n]; n++) {
         const SwsOpBackend *backend = ff_sws_op_backends[n];
         if (ops->src.hw_format != backend->hw_format ||
-            ops->dst.hw_format != backend->hw_format)
+            ops->dst.hw_format != backend->hw_format ||
+            !(enabled & backend->flags))
             continue;
         if (compile_backend(ctx, backend, ops, out) < 0)
             continue;
diff --git a/libswscale/ops_dispatch.h b/libswscale/ops_dispatch.h
index be771da9a9..1678cc4bf2 100644
--- a/libswscale/ops_dispatch.h
+++ b/libswscale/ops_dispatch.h
@@ -129,6 +129,7 @@ void ff_sws_compiled_op_unref(SwsCompiledOp *comp);
 
 typedef struct SwsOpBackend {
     const char *name; /* Descriptive name for this backend */
+    SwsBackend flags; /* Set of SWS_BACKEND_* */
 
     /**
      * Compile an operation list to an implementation chain. May modify `ops`
diff --git a/libswscale/ops_memcpy.c b/libswscale/ops_memcpy.c
index 7fcd230d7b..769de96747 100644
--- a/libswscale/ops_memcpy.c
+++ b/libswscale/ops_memcpy.c
@@ -143,6 +143,7 @@ static int compile(SwsContext *ctx, SwsOpList *ops, 
SwsCompiledOp *out)
 
 const SwsOpBackend backend_murder = {
     .name       = "memcpy",
+    .flags      = SWS_BACKEND_MEMCPY,
     .compile    = compile,
     .hw_format  = AV_PIX_FMT_NONE,
 };
diff --git a/libswscale/options.c b/libswscale/options.c
index 8109f1d23a..960c6b91dc 100644
--- a/libswscale/options.c
+++ b/libswscale/options.c
@@ -106,6 +106,19 @@ static const AVOption swscale_options[] = {
         { "saturation",            "saturation mapping",             0, 
AV_OPT_TYPE_CONST,  { .i64 = SWS_INTENT_SATURATION            }, .flags = VE, 
.unit = "intent" },
         { "absolute_colorimetric", "absolute colorimetric clipping", 0, 
AV_OPT_TYPE_CONST,  { .i64 = SWS_INTENT_ABSOLUTE_COLORIMETRIC }, .flags = VE, 
.unit = "intent" },
 
+    { "sws_backends",    "set allowed swscale backends",  OFFSET(backends),  
AV_OPT_TYPE_FLAGS,  { .i64  = 0                    }, .flags = VE, .unit = 
"sws_backend", .max = UINT_MAX },
+        { "auto",        "automatic selection",           0,                 
AV_OPT_TYPE_CONST,  { .i64  = 0                    }, .flags = VE, .unit = 
"sws_backend" },
+        { "stable",      "All stable backends",           0,                 
AV_OPT_TYPE_CONST,  { .i64  = SWS_BACKEND_STABLE   }, .flags = VE, .unit = 
"sws_backend" },
+        { "unstable",    "All unstable backends",         0,                 
AV_OPT_TYPE_CONST,  { .i64  = SWS_BACKEND_UNSTABLE }, .flags = VE, .unit = 
"sws_backend" },
+        { "all",         "All available backends",        0,                 
AV_OPT_TYPE_CONST,  { .i64  = SWS_BACKEND_ALL      }, .flags = VE, .unit = 
"sws_backend" },
+        { "legacy",      "legacy swscale code",           0,                 
AV_OPT_TYPE_CONST,  { .i64  = SWS_BACKEND_LEGACY   }, .flags = VE, .unit = 
"sws_backend" },
+        { "c",           "template-based reference code", 0,                 
AV_OPT_TYPE_CONST,  { .i64  = SWS_BACKEND_C        }, .flags = VE, .unit = 
"sws_backend" },
+        { "memcpy",      "fast path using libc memcpy",   0,                 
AV_OPT_TYPE_CONST,  { .i64  = SWS_BACKEND_MEMCPY   }, .flags = VE, .unit = 
"sws_backend" },
+        { "x86",         "x86 SIMD kernels",              0,                 
AV_OPT_TYPE_CONST,  { .i64  = SWS_BACKEND_X86      }, .flags = VE, .unit = 
"sws_backend" },
+        { "aarch64",     "AArch64 NEON kernels",          0,                 
AV_OPT_TYPE_CONST,  { .i64  = SWS_BACKEND_AARCH64  }, .flags = VE, .unit = 
"sws_backend" },
+        { "spirv",       "Vulkan SPIR-V backend",         0,                 
AV_OPT_TYPE_CONST,  { .i64  = SWS_BACKEND_SPIRV    }, .flags = VE, .unit = 
"sws_backend" },
+        { "glsl",        "Vulkan GLSL backend",           0,                 
AV_OPT_TYPE_CONST,  { .i64  = SWS_BACKEND_GLSL     }, .flags = VE, .unit = 
"sws_backend" },
+
     { NULL }
 };
 
diff --git a/libswscale/swscale.h b/libswscale/swscale.h
index fa0769aa23..8954f1a7d8 100644
--- a/libswscale/swscale.h
+++ b/libswscale/swscale.h
@@ -107,6 +107,29 @@ typedef enum SwsScaler {
     SWS_SCALE_MAX_ENUM = 0x7FFFFFFF, ///< force size to 32 bits, not a valid 
filter type
 } SwsScaler;
 
+typedef enum SwsBackend {
+    /* Stable backends */
+    SWS_BACKEND_LEGACY      = (1 << 0), ///< Legacy bespoke format-specific 
code
+    SWS_BACKEND_STABLE      = SWS_BACKEND_LEGACY,
+
+    /* Unstable backends (auto-selected only if SWS_UNSTABLE is enabled) */
+    SWS_BACKEND_C           = (1 << 1), ///< Template-based C reference 
implementation
+    SWS_BACKEND_MEMCPY      = (1 << 2), ///< Fast path using libc memcpy() / 
memset()
+    SWS_BACKEND_X86         = (1 << 3), ///< Chained x86 SIMD kernels
+    SWS_BACKEND_AARCH64     = (1 << 4), ///< Chained AArch64 NEON kernels
+    SWS_BACKEND_SPIRV       = (1 << 5), ///< Vulkan SPIR-V backend
+    SWS_BACKEND_GLSL        = (1 << 6), ///< Vulkan GLSL backend
+    SWS_BACKEND_UNSTABLE    = SWS_BACKEND_C |
+                              SWS_BACKEND_MEMCPY |
+                              SWS_BACKEND_X86 |
+                              SWS_BACKEND_AARCH64 |
+                              SWS_BACKEND_SPIRV |
+                              SWS_BACKEND_GLSL,
+
+    SWS_BACKEND_ALL = SWS_BACKEND_STABLE | SWS_BACKEND_UNSTABLE,
+    SWS_BACKEND_MAX_ENUM = 0x7FFFFFFF, ///< force size to 32 bits, not a valid 
backend
+} SwsBackend;
+
 typedef enum SwsFlags {
     /**
      * Return an error on underspecified conversions. Without this flag,
@@ -280,6 +303,16 @@ typedef struct SwsContext {
      */
     SwsScaler scaler_sub;
 
+    /**
+     * Bitmask of SWS_BACKEND_*. If non-zero, this will restrict the available
+     * backends to the specified set. If left as zero, a default set of
+     * backends will be selected automatically (based on SWS_UNSTABLE).
+     *
+     * Note: This is only relevant for the new API (sws_scale_frame()). The
+     * stateful legacy API always implies SWS_BACKEND_LEGACY.
+     */
+    SwsBackend backends;
+
     /* Remember to add new fields to graph.c:opts_equal() */
 } SwsContext;
 
@@ -299,7 +332,8 @@ void sws_free_context(SwsContext **ctx);
  ***************************/
 
 /**
- * Test if a given (software) pixel format is supported.
+ * Test if a given (software) pixel format is supported by any backend,
+ * excluding unstable backends.
  *
  * @param output  If 0, test if compatible with the source/input frame;
  *                otherwise, with the destination/output frame.
@@ -310,7 +344,8 @@ void sws_free_context(SwsContext **ctx);
 int sws_test_format(enum AVPixelFormat format, int output);
 
 /**
- * Test if a given hardware pixel format is supported.
+ * Test if a given hardware pixel format is supported by any backend,
+ * excluding unstable backends.
  *
  * @param format  The hardware format to check, or AV_PIX_FMT_NONE.
  *
diff --git a/libswscale/swscale_internal.h b/libswscale/swscale_internal.h
index f50b769f1f..9a822cb8e4 100644
--- a/libswscale/swscale_internal.h
+++ b/libswscale/swscale_internal.h
@@ -81,6 +81,8 @@ static inline SwsInternal *sws_internal(const SwsContext *sws)
     return (SwsInternal *) sws;
 }
 
+SwsBackend ff_sws_enabled_backends(const SwsContext *ctx);
+
 typedef struct Range {
     unsigned int start;
     unsigned int len;
diff --git a/libswscale/utils.c b/libswscale/utils.c
index 41c1cc5bb6..6ecba2c0b3 100644
--- a/libswscale/utils.c
+++ b/libswscale/utils.c
@@ -68,6 +68,18 @@
 #include "vulkan/ops.h"
 #endif
 
+SwsBackend ff_sws_enabled_backends(const SwsContext *ctx)
+{
+    if (ctx->backends)
+        return ctx->backends;
+
+    SwsBackend fallback = SWS_BACKEND_STABLE;
+    if (ctx->flags & SWS_UNSTABLE)
+        fallback |= SWS_BACKEND_UNSTABLE;
+
+    return fallback;
+}
+
 /**
  * Allocate and return an SwsContext without performing initialization.
  */
diff --git a/libswscale/version.h b/libswscale/version.h
index 4c6af261e6..c0610fec1e 100644
--- a/libswscale/version.h
+++ b/libswscale/version.h
@@ -28,7 +28,7 @@
 
 #include "version_major.h"
 
-#define LIBSWSCALE_VERSION_MINOR   7
+#define LIBSWSCALE_VERSION_MINOR   8
 #define LIBSWSCALE_VERSION_MICRO 100
 
 #define LIBSWSCALE_VERSION_INT  AV_VERSION_INT(LIBSWSCALE_VERSION_MAJOR, \
diff --git a/libswscale/vulkan/ops.c b/libswscale/vulkan/ops.c
index c944e120c5..5f289baf12 100644
--- a/libswscale/vulkan/ops.c
+++ b/libswscale/vulkan/ops.c
@@ -1689,6 +1689,7 @@ static int compile_spirv(SwsContext *sws, SwsOpList *ops, 
SwsCompiledOp *out)
 
 const SwsOpBackend backend_spirv = {
     .name      = "spirv",
+    .flags     = SWS_BACKEND_SPIRV,
     .compile   = compile_spirv,
     .hw_format = AV_PIX_FMT_VULKAN,
 };
@@ -1702,6 +1703,7 @@ static int compile_glsl(SwsContext *sws, SwsOpList *ops, 
SwsCompiledOp *out)
 
 const SwsOpBackend backend_glsl = {
     .name      = "glsl",
+    .flags     = SWS_BACKEND_GLSL,
     .compile   = compile_glsl,
     .hw_format = AV_PIX_FMT_VULKAN,
 };
diff --git a/libswscale/x86/ops.c b/libswscale/x86/ops.c
index 20369652cc..c8dde4d5d4 100644
--- a/libswscale/x86/ops.c
+++ b/libswscale/x86/ops.c
@@ -1054,6 +1054,7 @@ static int compile(SwsContext *ctx, SwsOpList *ops, 
SwsCompiledOp *out)
 
 const SwsOpBackend backend_x86 = {
     .name       = "x86",
+    .flags      = SWS_BACKEND_X86,
     .compile    = compile,
     .hw_format  = AV_PIX_FMT_NONE,
 };
-- 
2.52.0


>From 25b95fc5c542954e27a64a0db692e1e0e1f960cf Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 1 Jun 2026 17:32:03 +0200
Subject: [PATCH 3/7] swscale/tests/swscale: add -backends option

Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/tests/swscale.c | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/libswscale/tests/swscale.c b/libswscale/tests/swscale.c
index a6a034c420..cd83f3ccad 100644
--- a/libswscale/tests/swscale.c
+++ b/libswscale/tests/swscale.c
@@ -60,6 +60,7 @@ struct options {
     int unscaled;
     int legacy;
     int pretty;
+    int backends;
 };
 
 struct mode {
@@ -244,6 +245,7 @@ static int scale_new(AVFrame *dst, const AVFrame *src,
     sws_src_dst->flags  = mode->flags;
     sws_src_dst->dither = mode->dither;
     sws_src_dst->threads = opts->threads;
+    sws_src_dst->backends = opts->backends;
 
     int ret = sws_frame_setup(sws_src_dst, dst, src);
     if (ret < 0) {
@@ -898,6 +900,8 @@ static int parse_options(int argc, char **argv, struct 
options *opts, FILE **fp)
                     "       Test with a specific combination of flags\n"
                     "   -dither <mode>\n"
                     "       Test with a specific dither mode\n"
+                    "   -backend <backends>\n"
+                    "       Restrict to the given set of allowed swscale 
backends\n"
                     "   -unscaled <1 or 0>\n"
                     "       If 1, test only conversions that do not involve 
scaling\n"
                     "   -legacy <1 or 0>\n"
@@ -966,6 +970,15 @@ static int parse_options(int argc, char **argv, struct 
options *opts, FILE **fp)
                 fprintf(stderr, "invalid flags %s\n", argv[i + 1]);
                 return -1;
             }
+        } else if (!strcmp(argv[i], "-backends")) {
+            SwsContext *dummy = sws_alloc_context();
+            const AVOption *backends_opt = av_opt_find(dummy, "sws_backends", 
NULL, 0, 0);
+            int ret = av_opt_eval_flags(dummy, backends_opt, argv[i + 1], 
&opts->backends);
+            sws_free_context(&dummy);
+            if (ret < 0) {
+                fprintf(stderr, "invalid backends %s\n", argv[i + 1]);
+                return -1;
+            }
         } else if (!strcmp(argv[i], "-dither")) {
             opts->dither = atoi(argv[i + 1]);
         } else if (!strcmp(argv[i], "-unscaled")) {
-- 
2.52.0


>From 7804d07934219cf73d02a896eab97cf97fc630df Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Mon, 1 Jun 2026 18:04:29 +0200
Subject: [PATCH 4/7] swscale/format: generalize ff_test_fmt() to take
 SwsBackend

This allows us to test support in either the legacy code, or the ops-based
code, or both.

Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/format.c  | 45 +++++++++++++++++++++++++++++++++++++++-----
 libswscale/format.h  | 13 ++++++++++++-
 libswscale/swscale.c |  5 +++--
 3 files changed, 55 insertions(+), 8 deletions(-)

diff --git a/libswscale/format.c b/libswscale/format.c
index 7782a0233d..22e75e71dc 100644
--- a/libswscale/format.c
+++ b/libswscale/format.c
@@ -554,11 +554,27 @@ bool ff_infer_colors(SwsColor *src, SwsColor *dst)
     return incomplete;
 }
 
-int sws_test_format(enum AVPixelFormat format, int output)
+/* Variant of sws_test_format() for the ops-based code */
+static int test_format_ops(enum AVPixelFormat format, int output);
+static int test_format_legacy(enum AVPixelFormat format, int output)
 {
     return output ? sws_isSupportedOutput(format) : 
sws_isSupportedInput(format);
 }
 
+int ff_sws_test_pixfmt_backend(const SwsBackend backends,
+                               enum AVPixelFormat format, int output)
+{
+    /* The only non-ops backend is the legacy backend */
+    const SwsBackend backends_ops = SWS_BACKEND_ALL ^ SWS_BACKEND_LEGACY;
+    return (backends & SWS_BACKEND_LEGACY) && test_format_legacy(format, 
output) ||
+           (backends & backends_ops)       && test_format_ops(format, output);
+}
+
+int sws_test_format(enum AVPixelFormat format, int output)
+{
+    return ff_sws_test_pixfmt_backend(SWS_BACKEND_STABLE, format, output);
+}
+
 int sws_test_hw_format(enum AVPixelFormat format)
 {
     switch (format) {
@@ -611,10 +627,10 @@ static int test_loc(enum AVChromaLocation loc)
     return (unsigned)loc < AVCHROMA_LOC_NB;
 }
 
-int ff_test_fmt(const SwsFormat *fmt, int output)
+int ff_test_fmt(const SwsBackend backends, const SwsFormat *fmt, int output)
 {
     return fmt->width > 0 && fmt->height > 0            &&
-           sws_test_format    (fmt->format,     output) &&
+           ff_sws_test_pixfmt_backend(backends, fmt->format, output) &&
            sws_test_colorspace(fmt->csp,        output) &&
            sws_test_primaries (fmt->color.prim, output) &&
            sws_test_transfer  (fmt->color.trc,  output) &&
@@ -627,7 +643,7 @@ int sws_test_frame(const AVFrame *frame, int output)
 {
     for (int field = 0; field < 2; field++) {
         const SwsFormat fmt = ff_fmt_from_frame(frame, field);
-        if (!ff_test_fmt(&fmt, output))
+        if (!ff_test_fmt(SWS_BACKEND_STABLE, &fmt, output))
             return 0;
         if (!fmt.interlaced)
             break;
@@ -907,6 +923,18 @@ static int fmt_analyze(enum AVPixelFormat fmt, 
SwsReadWriteOp *rw_op,
     return 0;
 }
 
+static int test_format_ops(enum AVPixelFormat format, int output)
+{
+    SwsReadWriteOp rw;
+    SwsSwizzleOp swizzle;
+    SwsPackOp pack;
+    SwsShiftOp shift;
+    SwsPixelType pixel_type, raw_type;
+    int ret = fmt_analyze(format, &rw, &pack, &swizzle, &shift,
+                          &pixel_type, &raw_type);
+    return ret == 0;
+}
+
 static SwsSwizzleOp swizzle_inv(SwsSwizzleOp swiz) {
     /* Input[x] =: Output[swizzle.x] */
     unsigned out[4];
@@ -1670,4 +1698,11 @@ fail:
     return ret;
 }
 
-#endif /* CONFIG_UNSTABLE */
+#else /* !CONFIG_UNSTABLE */
+
+static int test_format_ops(enum AVPixelFormat format, int output)
+{
+    return 0;
+}
+
+#endif
diff --git a/libswscale/format.h b/libswscale/format.h
index 844ea353b3..67f25d7006 100644
--- a/libswscale/format.h
+++ b/libswscale/format.h
@@ -149,7 +149,18 @@ static inline int ff_fmt_align(enum AVPixelFormat fmt)
     }
 }
 
-int ff_test_fmt(const SwsFormat *fmt, int output);
+/* Internal helper to test a format for either the legacy backend or the
+ * ops-based backends, depending on `backends` (must be nonzero). */
+int ff_sws_test_pixfmt_backend(const SwsBackend backends,
+                               enum AVPixelFormat format, int output);
+
+/**
+ * Statically test if a given format is supported by the given set of
+ * backends. This is a heuristic, which may have false positives if a
+ * specific backend does not actually implement all operations that would be
+ * required to support the format.
+ */
+int ff_test_fmt(SwsBackend backends, const SwsFormat *fmt, int output);
 
 /* Returns true if the formats are incomplete, false otherwise */
 bool ff_infer_colors(SwsColor *src, SwsColor *dst);
diff --git a/libswscale/swscale.c b/libswscale/swscale.c
index ca7eddbdef..558e332eb2 100644
--- a/libswscale/swscale.c
+++ b/libswscale/swscale.c
@@ -1488,6 +1488,7 @@ int sws_frame_setup(SwsContext *ctx, const AVFrame *dst, 
const AVFrame *src)
     }
 
     int dst_width = dst->width;
+    const SwsBackend backends = ff_sws_enabled_backends(ctx);
     for (int field = 0; field < 2; field++) {
         SwsFormat src_fmt = ff_fmt_from_frame(src, field);
         SwsFormat dst_fmt = ff_fmt_from_frame(dst, field);
@@ -1499,8 +1500,8 @@ int sws_frame_setup(SwsContext *ctx, const AVFrame *dst, 
const AVFrame *src)
             goto fail;
         }
 
-        src_ok = ff_test_fmt(&src_fmt, 0);
-        dst_ok = ff_test_fmt(&dst_fmt, 1);
+        src_ok = ff_test_fmt(backends, &src_fmt, 0);
+        dst_ok = ff_test_fmt(backends, &dst_fmt, 1);
         if ((!src_ok || !dst_ok) && !ff_props_equal(&src_fmt, &dst_fmt)) {
             err_msg = src_ok ? "Unsupported output" : "Unsupported input";
             ret = AVERROR(ENOTSUP);
-- 
2.52.0


>From 89baec50d60cbff9b1d03e2bddbd08bc5b0d8c5c Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Wed, 3 Jun 2026 21:10:30 +0200
Subject: [PATCH 5/7] swscale/graph: add metadata about backends in use

Not currently publicly visible, but useful inside the test framework
nonetheless.

Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/graph.c        |  2 ++
 libswscale/graph.h        |  2 ++
 libswscale/ops_dispatch.c | 11 ++++++++---
 libswscale/ops_dispatch.h |  3 +++
 4 files changed, 15 insertions(+), 3 deletions(-)

diff --git a/libswscale/graph.c b/libswscale/graph.c
index 37c11f9dd5..a5547de44d 100644
--- a/libswscale/graph.c
+++ b/libswscale/graph.c
@@ -487,6 +487,7 @@ static int init_legacy_subpass(SwsGraph *graph, SwsContext 
*sws,
                                 setup_legacy_swscale, sws, 
free_legacy_swscale, &pass);
     if (ret < 0)
         return ret;
+    pass->backend = SWS_BACKEND_LEGACY;
 
     /**
      * For slice threading, we need to create sub contexts, similar to how
@@ -853,6 +854,7 @@ int ff_sws_graph_init(SwsGraph *graph, SwsContext *ctx, 
const SwsFormat *dst,
 
     /* Resolve output buffers for all intermediate passes */
     for (int i = 0; i < graph->num_passes; i++) {
+        graph->backend |= graph->passes[i]->backend;
         ret = pass_alloc_output(graph->passes[i]->input);
         if (ret < 0)
             goto error;
diff --git a/libswscale/graph.h b/libswscale/graph.h
index 8098aaed06..adf4b19675 100644
--- a/libswscale/graph.h
+++ b/libswscale/graph.h
@@ -81,6 +81,7 @@ struct SwsPass {
      * are always equal to (or smaller than, for the last slice) `slice_h`.
      */
     SwsPassFunc run;
+    SwsBackend backend; /* backend this pass is using, or 0 */
     enum AVPixelFormat format; /* new pixel format */
     int width, height; /* new output size */
     int slice_h;       /* filter granularity */
@@ -124,6 +125,7 @@ typedef struct SwsGraph {
     int num_threads; /* resolved at init() time */
     bool incomplete; /* set during init() if formats had to be inferred */
     bool noop;       /* set during init() if the graph is a no-op */
+    SwsBackend backend; /* backends this graph is using, set during init() */
 
     AVBufferRef *hw_frames_ref;
 
diff --git a/libswscale/ops_dispatch.c b/libswscale/ops_dispatch.c
index f73f801a20..927cb75509 100644
--- a/libswscale/ops_dispatch.c
+++ b/libswscale/ops_dispatch.c
@@ -77,6 +77,7 @@ static int compile_backend(SwsContext *ctx, const 
SwsOpBackend *backend,
         goto fail;
     }
 
+    compiled.backend = backend;
     *out = compiled;
 
     av_log(ctx, AV_LOG_VERBOSE, "Compiled using backend '%s': "
@@ -501,9 +502,12 @@ static int compile(SwsGraph *graph, const SwsOpBackend 
*backend,
     if (p->comp.opaque) {
         SwsCompiledOp c = *comp;
         av_free(p);
-        return ff_sws_graph_add_pass(graph, dst->format, dst->width, 
dst->height,
-                                     input, c.slice_align, c.func_opaque,
-                                     NULL, c.priv, c.free, output);
+        ret = ff_sws_graph_add_pass(graph, dst->format, dst->width, 
dst->height,
+                                    input, c.slice_align, c.func_opaque,
+                                    NULL, c.priv, c.free, output);
+        if (ret >= 0)
+            (*output)->backend = comp->backend->flags;
+        return ret;
     }
 
     const SwsOp *read  = ff_sws_op_list_input(ops);
@@ -585,6 +589,7 @@ static int compile(SwsGraph *graph, const SwsOpBackend 
*backend,
     if (ret < 0)
         return ret;
 
+    (*output)->backend = comp->backend->flags;
     align_pass(input,   comp->block_size, comp->over_read,  p->pixel_bits_in);
     align_pass(*output, comp->block_size, comp->over_write, p->pixel_bits_out);
     return 0;
diff --git a/libswscale/ops_dispatch.h b/libswscale/ops_dispatch.h
index 1678cc4bf2..a2aa00a813 100644
--- a/libswscale/ops_dispatch.h
+++ b/libswscale/ops_dispatch.h
@@ -111,6 +111,9 @@ typedef struct SwsCompiledOp {
      */
     bool opaque;
 
+    /* Set by ff_sws_ops_compile(), informative */
+    const struct SwsOpBackend *backend;
+
     /* Execution parameters for all functions */
     int slice_align; /* slice height alignment */
     int cpu_flags;   /* active set of CPU flags (informative) */
-- 
2.52.0


>From 3ded0a52dedbebdbf0df0fc6c4dc4def24a1c4c5 Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Wed, 3 Jun 2026 21:29:23 +0200
Subject: [PATCH 6/7] swscale/graph: move legacy fallback out of
 add_convert_pass()

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

diff --git a/libswscale/graph.c b/libswscale/graph.c
index a5547de44d..acd0ce615e 100644
--- a/libswscale/graph.c
+++ b/libswscale/graph.c
@@ -622,13 +622,12 @@ static int add_legacy_sws_pass(SwsGraph *graph, const 
SwsFormat *src,
  * Format conversion and scaling *
  *********************************/
 
-#if CONFIG_UNSTABLE
-static int add_convert_pass(SwsGraph *graph, const SwsFormat *src,
-                            const SwsFormat *dst, SwsPass *input,
-                            SwsPass **output)
+static int add_ops_convert_pass(SwsGraph *graph, const SwsFormat *src,
+                                const SwsFormat *dst, SwsPass *input,
+                                SwsPass **output)
 {
+#if CONFIG_UNSTABLE
     SwsContext *ctx = graph->ctx;
-    int ret = AVERROR(ENOTSUP);
 
     /* Preemptively skip the ops list generation if the backend was
      * constrained to the legacy implementation only. This would
@@ -636,12 +635,12 @@ static int add_convert_pass(SwsGraph *graph, const 
SwsFormat *src,
      * error, but this way saves a bit of unnecessary overhead */
     const SwsBackend backends = ff_sws_enabled_backends(ctx);
     if (backends == SWS_BACKEND_LEGACY)
-        goto fail;
+        return AVERROR(ENOTSUP);
 
     SwsOpList *ops;
-    ret = ff_sws_op_list_generate(ctx, src, dst, &ops, &graph->incomplete);
+    int ret = ff_sws_op_list_generate(ctx, src, dst, &ops, &graph->incomplete);
     if (ret < 0)
-        goto fail;
+        return ret;
 
     av_log(ctx, AV_LOG_VERBOSE, "Conversion pass for %s -> %s:\n",
            av_get_pix_fmt_name(src->format), av_get_pix_fmt_name(dst->format));
@@ -649,22 +648,24 @@ static int add_convert_pass(SwsGraph *graph, const 
SwsFormat *src,
     av_log(ctx, AV_LOG_DEBUG, "Unoptimized operation list:\n");
     ff_sws_op_list_print(ctx, AV_LOG_DEBUG, AV_LOG_TRACE, ops);
 
-    ret = ff_sws_compile_pass(graph, NULL, &ops, SWS_OP_FLAG_OPTIMIZE, input, 
output);
-    if (ret < 0)
-        goto fail;
-
-    ret = 0;
-    /* fall through */
-
-fail:
-    if (ret == AVERROR(ENOTSUP))
-        return add_legacy_sws_pass(graph, src, dst, input, output);
-    return ret;
-}
+    return ff_sws_compile_pass(graph, NULL, &ops, SWS_OP_FLAG_OPTIMIZE, input, 
output);
 #else
-#define add_convert_pass add_legacy_sws_pass
+    return AVERROR(ENOTSUP);
 #endif
+}
 
+static int add_convert_pass(SwsGraph *graph, const SwsFormat *src,
+                            const SwsFormat *dst, SwsPass *input,
+                            SwsPass **output)
+{
+    int ret;
+
+    ret = add_ops_convert_pass(graph, src, dst, input, output);
+    if (ret == AVERROR(ENOTSUP))
+        ret = add_legacy_sws_pass(graph, src, dst, input, output);
+
+    return 0;
+}
 
 /**************************
  * Gamut and tone mapping *
-- 
2.52.0


>From 9c7f584f0924016406e09ab2d2cea70e378be526 Mon Sep 17 00:00:00 2001
From: Niklas Haas <[email protected]>
Date: Wed, 3 Jun 2026 21:31:21 +0200
Subject: [PATCH 7/7] swscale/graph: only prefer unstable backends with
 SWS_UNSTABLE

If the user passes `-backends all` but without `-flags unstable`, then the
default/legacy backend will be picked unless it doesn't support a given
pixel format.

This allows gradually opting into the new code to handle more pixel formats
than what the legacy backend currently supports, without disturbing the
predictable output/behavior.

Signed-off-by: Niklas Haas <[email protected]>
---
 libswscale/graph.c   | 13 +++++++++++--
 libswscale/swscale.h |  6 +++---
 2 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/libswscale/graph.c b/libswscale/graph.c
index acd0ce615e..1c6a34dee1 100644
--- a/libswscale/graph.c
+++ b/libswscale/graph.c
@@ -658,11 +658,20 @@ static int add_convert_pass(SwsGraph *graph, const 
SwsFormat *src,
                             const SwsFormat *dst, SwsPass *input,
                             SwsPass **output)
 {
+    SwsContext *ctx = graph->ctx;
     int ret;
 
-    ret = add_ops_convert_pass(graph, src, dst, input, output);
-    if (ret == AVERROR(ENOTSUP))
+    if (ctx->flags & SWS_UNSTABLE) {
+        /* Prefer unstable ops backend over legacy backend */
+        ret = add_ops_convert_pass(graph, src, dst, input, output);
+        if (ret == AVERROR(ENOTSUP))
+            ret = add_legacy_sws_pass(graph, src, dst, input, output);
+    } else {
+        /* Prefer legacy backend for stability reasons */
         ret = add_legacy_sws_pass(graph, src, dst, input, output);
+        if (ret == AVERROR(ENOTSUP))
+            ret = add_ops_convert_pass(graph, src, dst, input, output);
+    }
 
     return 0;
 }
diff --git a/libswscale/swscale.h b/libswscale/swscale.h
index 8954f1a7d8..9b53ebbdff 100644
--- a/libswscale/swscale.h
+++ b/libswscale/swscale.h
@@ -180,9 +180,9 @@ typedef enum SwsFlags {
     SWS_BITEXACT       = 1 << 19,
 
     /**
-     * Allow using experimental new code paths. This may be faster, slower,
-     * or produce different output, with semantics subject to change at any
-     * point in time. For testing and debugging purposes only.
+     * Allow/prefer using experimental new code paths. This may be faster,
+     * slower, or produce different output, with semantics subject to change
+     * at any point in time. For testing and debugging purposes only.
      */
     SWS_UNSTABLE = 1 << 20,
 
-- 
2.52.0

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

Reply via email to