PR #25675 opened by Kacper Michajłow (kasper93)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25675
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25675.patch


From f218bddb194067eea286e783d1974e76cbb0776d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Mon, 2 Nov 2026 09:00:38 +0100
Subject: [PATCH 1/8] swscale: add SwsLut1D
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

A table of one variable with linear interpolation between its entries,
sampled either at the integers of an input scaled to the table, or
evenly within each octave of a linear input from 2^-40 up to 2^4, so
that transfer functions keep the precision of 16-bit codes.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libswscale/Makefile |  1 +
 libswscale/lut1d.c  | 59 +++++++++++++++++++++++++++++++++++++++++++++
 libswscale/lut1d.h  | 54 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 114 insertions(+)
 create mode 100644 libswscale/lut1d.c
 create mode 100644 libswscale/lut1d.h

diff --git a/libswscale/Makefile b/libswscale/Makefile
index 4bd5ff551be..3b6f2b0c653 100644
--- a/libswscale/Makefile
+++ b/libswscale/Makefile
@@ -29,6 +29,7 @@ OBJS = alphablend.o                                     \
        vscale.o                                         \
 
 OBJS +=                                                 \
+       lut1d.o                                          \
        ops.o                                            \
        ops_chain.o                                      \
        ops_dispatch.o                                   \
diff --git a/libswscale/lut1d.c b/libswscale/lut1d.c
new file mode 100644
index 00000000000..4c2fca822d9
--- /dev/null
+++ b/libswscale/lut1d.c
@@ -0,0 +1,59 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <math.h>
+
+#include "libavutil/avassert.h"
+#include "libavutil/common.h"
+#include "libavutil/refstruct.h"
+
+#include "lut1d.h"
+
+static double log_entry(int i)
+{
+    const int mask = (1 << LUT1D_LOG_BITS) - 1;
+    return ldexp(1.0 + (i & mask) / (double) (1 << LUT1D_LOG_BITS),
+                 LUT1D_LOG_MIN + (i >> LUT1D_LOG_BITS));
+}
+
+SwsLut1D *ff_sws_lut1d_generate(SwsLut1DType type, int size,
+                                double (*fn)(double x, const void *opaque),
+                                const void *opaque)
+{
+    av_assert0(type != SWS_LUT1D_LOG || size == LUT1D_LOG_SIZE);
+
+    SwsLut1D *lut = av_refstruct_alloc_ext(sizeof(*lut) + (size + 1) * 
sizeof(float),
+                                           0, NULL, NULL);
+    if (!lut)
+        return NULL;
+
+    lut->type = type;
+    lut->size = size;
+    for (int i = 0; i < size; i++)
+        lut->table[i] = fn(type == SWS_LUT1D_LOG ? log_entry(i) : i, opaque);
+    lut->table[size] = lut->table[size - 1];
+
+    lut->zero = type == SWS_LUT1D_LOG ? fn(0.0, opaque) : lut->table[0];
+    lut->min = lut->max = lut->zero;
+    for (int i = 0; i < size; i++) {
+        lut->min = FFMIN(lut->min, lut->table[i]);
+        lut->max = FFMAX(lut->max, lut->table[i]);
+    }
+
+    return lut;
+}
diff --git a/libswscale/lut1d.h b/libswscale/lut1d.h
new file mode 100644
index 00000000000..f7f5e23de29
--- /dev/null
+++ b/libswscale/lut1d.h
@@ -0,0 +1,54 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef SWSCALE_LUT1D_H
+#define SWSCALE_LUT1D_H
+
+enum {
+    /* SWS_LUT1D_LOG layout: 2^LUT1D_LOG_BITS entries per octave, from
+     * 2^LUT1D_LOG_MIN up to 2^LUT1D_LOG_MAX */
+    LUT1D_LOG_BITS = 8,
+    LUT1D_LOG_MIN  = -40,
+    LUT1D_LOG_MAX  = 4,
+    LUT1D_LOG_SIZE = ((LUT1D_LOG_MAX - LUT1D_LOG_MIN) << LUT1D_LOG_BITS) + 1,
+};
+
+typedef enum SwsLut1DType {
+    SWS_LUT1D_UNIFORM, /* entry i is the value at i, input in [0, size - 1] */
+    SWS_LUT1D_LOG,     /* entries per octave of the input, see LUT1D_LOG_BITS,
+                        * below the first one interpolating from `zero` */
+} SwsLut1DType;
+
+/* A function of one variable sampled into a table, linearly interpolated */
+typedef struct SwsLut1D {
+    SwsLut1DType type;
+    int size;           /* number of entries */
+    float zero;         /* value at 0 (SWS_LUT1D_LOG) */
+    float min, max;     /* bounds of the table values */
+    float table[];      /* size + 1 entries, the last one padding */
+} SwsLut1D;
+
+/**
+ * Allocates a refstruct of `size` entries (LUT1D_LOG_SIZE for SWS_LUT1D_LOG)
+ * filled with fn() at each entry. Returns NULL on allocation failure.
+ */
+SwsLut1D *ff_sws_lut1d_generate(SwsLut1DType type, int size,
+                                double (*fn)(double x, const void *opaque),
+                                const void *opaque);
+
+#endif /* SWSCALE_LUT1D_H */
-- 
2.54.0


From b2e69d366982b41af8787e0397dbfa9ddd7b6ad0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Mon, 2 Nov 2026 09:00:39 +0100
Subject: [PATCH 2/8] swscale/ops: add SWS_OP_LUT_1D
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Applies a SwsLut1D to each color component and passes alpha through.
The optimizer treats it like the 3D LUT. Nothing generates it yet.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libswscale/ops.c           | 33 ++++++++++++++++++++++++++++++++-
 libswscale/ops.h           |  7 +++++++
 libswscale/ops_optimizer.c | 11 +++++++++++
 3 files changed, 50 insertions(+), 1 deletion(-)

diff --git a/libswscale/ops.c b/libswscale/ops.c
index 07dd1aba482..247ab6c1005 100644
--- a/libswscale/ops.c
+++ b/libswscale/ops.c
@@ -18,6 +18,8 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  */
 
+#include <math.h>
+
 #include "libavutil/attributes.h"
 #include "libavutil/avassert.h"
 #include "libavutil/avstring.h"
@@ -89,6 +91,7 @@ const char *ff_sws_op_type_name(SwsOpType op)
     case SWS_OP_FILTER_H:    return "SWS_OP_FILTER_H";
     case SWS_OP_FILTER_V:    return "SWS_OP_FILTER_V";
     case SWS_OP_LUT_3D:      return "SWS_OP_LUT_3D";
+    case SWS_OP_LUT_1D:      return "SWS_OP_LUT_1D";
     case SWS_OP_INVALID:     return "SWS_OP_INVALID";
     case SWS_OP_TYPE_NB: break;
     }
@@ -303,7 +306,8 @@ void ff_sws_apply_op_q(const SwsOp *op, AVRational64 x[4])
          * conceptually modify individual components */
         return;
     case SWS_OP_LUT_3D:
-        /* 3D LUTs are treated as a black box, so set those values to NaN */
+    case SWS_OP_LUT_1D:
+        /* LUTs are treated as a black box, so set those values to NaN */
         for (int i = 0; i < 3; i++)
             x[i] = (AVRational64) {0};
         return;
@@ -361,6 +365,7 @@ static void update_output_pads(SwsOpList *ops, SwsOp *op)
     case SWS_OP_SWAP_BYTES:
     case SWS_OP_UNPACK:
     case SWS_OP_LUT_3D:
+    case SWS_OP_LUT_1D:
         break; /* special cases, handled below */
     default: {
         AVRational64 min[4], max[4];
@@ -595,6 +600,16 @@ static void update_output_pads(SwsOpList *ops, SwsOp *op)
         out[3].min = in[3].min;
         out[3].max = in[3].max;
         break;
+    case SWS_OP_LUT_1D:
+        for (int i = 0; i < 3; i++) {
+            FORWARD(i, i, flags & 0);
+            out[i].min = Q(floorf(op->lut1d.lut->min));
+            out[i].max = Q(ceilf(op->lut1d.lut->max));
+        }
+        FORWARD(3, 3, flags);
+        out[3].min = in[3].min;
+        out[3].max = in[3].max;
+        break;
     case SWS_OP_INVALID:
     case SWS_OP_TYPE_NB:
         av_unreachable("Invalid operation type!");
@@ -668,6 +683,10 @@ static void update_input_pads(SwsOpList *ops, SwsOp *op, 
SwsCompMask need_out[4]
             need_in[i] = need_out[0] | need_out[1] | need_out[2];
         need_in[3] = need_out[3];
         break;
+    case SWS_OP_LUT_1D:
+        for (int i = 0; i < 4; i++)
+            need_in[i] = need_out[i];
+        break;
     }
 
     memcpy(need_out, need_in, sizeof(need_in));
@@ -703,6 +722,9 @@ static void op_ref(SwsOp *op)
     case SWS_OP_LUT_3D:
         av_refstruct_ref_c(op->lut3d.lut);
         break;
+    case SWS_OP_LUT_1D:
+        av_refstruct_ref_c(op->lut1d.lut);
+        break;
     }
 }
 
@@ -722,6 +744,9 @@ static void op_unref(SwsOp *op)
     case SWS_OP_LUT_3D:
         av_refstruct_unref(&op->lut3d.lut);
         break;
+    case SWS_OP_LUT_1D:
+        av_refstruct_unref(&op->lut1d.lut);
+        break;
     }
 
     *op = (SwsOp) {0};
@@ -1019,6 +1044,12 @@ void ff_sws_op_desc(AVBPrint *bp, const SwsOp *op)
     case SWS_OP_LUT_3D:
         av_bprintf(bp, "%-20s: %s", name, op->lut3d.dynamic ? "dynamic" : 
"static");
         break;
+    case SWS_OP_LUT_1D:
+        if (op->lut1d.lut->type == SWS_LUT1D_LOG)
+            av_bprintf(bp, "%-20s: log", name);
+        else
+            av_bprintf(bp, "%-20s: %d entries", name, op->lut1d.lut->size);
+        break;
     case SWS_OP_TYPE_NB:
         break;
     }
diff --git a/libswscale/ops.h b/libswscale/ops.h
index 25b137f8b47..7599067cf18 100644
--- a/libswscale/ops.h
+++ b/libswscale/ops.h
@@ -29,6 +29,7 @@
 
 #include "graph.h"
 #include "filters.h"
+#include "lut1d.h"
 #include "lut3d.h"
 #include "rational64.h"
 #include "uops.h"
@@ -65,6 +66,7 @@ typedef enum SwsOpType {
 
     /* Table-based operations. Defined for floating point types only. */
     SWS_OP_LUT_3D,          /* apply a SwsLut3D */
+    SWS_OP_LUT_1D,          /* apply a SwsLut1D to each color component */
 
     SWS_OP_TYPE_NB,
 } SwsOpType;
@@ -226,6 +228,10 @@ typedef struct SwsLut3dOp {
     bool dynamic;
 } SwsLut3dOp;
 
+typedef struct SwsLut1dOp {
+    const SwsLut1D *lut; /* refstruct */
+} SwsLut1dOp;
+
 typedef enum SwsCompFlags {
     SWS_COMP_EXACT   = 1 << 0, /* value is an exact integer */
     SWS_COMP_ZERO    = 1 << 1, /* known to be a constant zero */
@@ -264,6 +270,7 @@ struct SwsOp {
         SwsDitherOp     dither;
         SwsFilterOp     filter;
         SwsLut3dOp      lut3d;
+        SwsLut1dOp      lut1d;
     };
 
     /**
diff --git a/libswscale/ops_optimizer.c b/libswscale/ops_optimizer.c
index 731dffbf027..197703ce4ef 100644
--- a/libswscale/ops_optimizer.c
+++ b/libswscale/ops_optimizer.c
@@ -84,6 +84,7 @@ static bool op_commute_clear(SwsOp *op, SwsOp *next)
     case SWS_OP_UNPACK:
     case SWS_OP_CLEAR:
     case SWS_OP_LUT_3D:
+    case SWS_OP_LUT_1D:
         return false;
     case SWS_OP_TYPE_NB:
         break;
@@ -161,6 +162,7 @@ static bool op_commute_swizzle(SwsOp *op, SwsOp *next)
     case SWS_OP_PACK:
     case SWS_OP_UNPACK:
     case SWS_OP_LUT_3D:
+    case SWS_OP_LUT_1D:
         return false;
     case SWS_OP_TYPE_NB:
         break;
@@ -205,6 +207,7 @@ static bool op_commute_filter(SwsOp *op, SwsOp *prev)
     case SWS_OP_FILTER_H:
     case SWS_OP_FILTER_V:
     case SWS_OP_LUT_3D:
+    case SWS_OP_LUT_1D:
         return false;
     case SWS_OP_TYPE_NB:
         break;
@@ -760,6 +763,14 @@ retry:
                 goto retry;
             }
             break;
+
+        case SWS_OP_LUT_1D:
+            /* Eliminate unnecessary 1DLUT */
+            if (!(needed & SWS_COMP_ELEMS(3))) {
+                ff_sws_op_list_remove_at(ops, n, 1);
+                goto retry;
+            }
+            break;
         }
     }
 
-- 
2.54.0


From 5cb44db1cb07df7525f8005c29e3460b8c91b885 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Thu, 29 Oct 2026 09:32:30 +0100
Subject: [PATCH 3/8] swscale/uops: add SWS_UOP_LUT_1D reference implementation
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Looks up each color component in the table with linear interpolation
between the two nearest entries. A logarithmic table is indexed by the
exponent and the leading mantissa bits of the input, inputs below the
first entry interpolate from the value at zero.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libswscale/uops.c              | 12 +++++++
 libswscale/uops.h              |  8 +++++
 libswscale/uops_backend.c      |  1 +
 libswscale/uops_list.h         |  1 +
 libswscale/uops_macros.h       |  8 +++++
 libswscale/uops_macros_gen.c   |  3 ++
 libswscale/uops_tmpl.c         | 60 ++++++++++++++++++++++++++++++++++
 libswscale/x86/uops_macros.asm | 12 +++++++
 libswscale/x86/uops_macros.h   |  8 +++++
 9 files changed, 113 insertions(+)

diff --git a/libswscale/uops.c b/libswscale/uops.c
index a7150c11307..0659499e248 100644
--- a/libswscale/uops.c
+++ b/libswscale/uops.c
@@ -131,6 +131,9 @@ void ff_sws_uop_name(const SwsUOp *op, char 
buf[SWS_UOP_NAME_MAX])
     case SWS_UOP_LUT_3D:
         av_bprintf(&bp, "_%s", par->lut3d.dynamic ? "dynamic" : "static");
         break;
+    case SWS_UOP_LUT_1D:
+        av_bprintf(&bp, "_%s", par->lut1d.log ? "log" : "uniform");
+        break;
     }
 
     av_assert0(av_bprint_is_complete(&bp));
@@ -150,6 +153,9 @@ static void uop_uninit(SwsUOp *uop)
     case SWS_UOP_LUT_3D:
         av_refstruct_unref(&uop->data.lut3d);
         break;
+    case SWS_UOP_LUT_1D:
+        av_refstruct_unref(&uop->data.lut1d);
+        break;
     }
 
     *uop = (SwsUOp) {0};
@@ -623,6 +629,12 @@ static int translate_op(SwsContext *ctx, SwsUOpList *uops, 
SwsUOpFlags flags,
         uop.par.lut3d.dynamic = op->lut3d.dynamic;
         uop.data.lut3d = av_refstruct_ref_c(op->lut3d.lut);
         break;
+    case SWS_OP_LUT_1D:
+        uop.uop = SWS_UOP_LUT_1D;
+        uop.mask &= SWS_COMP_ELEMS(3);
+        uop.par.lut1d.log = op->lut1d.lut->type == SWS_LUT1D_LOG;
+        uop.data.lut1d = av_refstruct_ref_c(op->lut1d.lut);
+        break;
     default:
         return AVERROR(ENOTSUP);
     }
diff --git a/libswscale/uops.h b/libswscale/uops.h
index 506b3ff6222..5ef4d1ec081 100644
--- a/libswscale/uops.h
+++ b/libswscale/uops.h
@@ -34,6 +34,7 @@
 
 typedef struct SwsContext       SwsContext;
 typedef struct SwsFilterWeights SwsFilterWeights;
+typedef struct SwsLut1D         SwsLut1D;
 typedef struct SwsLut3D         SwsLut3D;
 typedef struct SwsOpList        SwsOpList;
 
@@ -206,6 +207,7 @@ typedef enum SwsUOpType {
     SWS_UOP_LINEAR_FMA,      /* with SWS_UOP_FLAG_FMA */
     SWS_UOP_DITHER,          /* mask = components to dither */
     SWS_UOP_LUT_3D,          /* mask = needed output components */
+    SWS_UOP_LUT_1D,          /* mask = color components to look up */
 
     /* Platform-specific uops would go here */
     SWS_UOP_TYPE_NB,
@@ -272,6 +274,10 @@ typedef struct SwsLut3DUOp {
     int dynamic;
 } SwsLut3DUOp;
 
+typedef struct SwsLut1DUOp {
+    int log; /* SWS_LUT1D_LOG table */
+} SwsLut1DUOp;
+
 /**
  * Computes (1 << size_log2) + MAX(y_offset). The dither matrix attached to
  * the SwsUOp is always pre-padded to this number of lines.
@@ -288,6 +294,7 @@ typedef union SwsUOpParams {
     SwsLinearUOp    lin;
     SwsDitherUOp    dither;
     SwsLut3DUOp     lut3d;
+    SwsLut1DUOp     lut1d;
 } SwsUOpParams;
 
 typedef struct SwsUOp {
@@ -306,6 +313,7 @@ typedef struct SwsUOp {
         SwsPixel mat4x5[4][5];      /* row major */
         SwsShuffleMask shuffle;     /* for SWS_UOP_RW_SHUFFLE */
         const SwsLut3D *lut3d;      /* for SWS_UOP_LUT_3D; refstruct */
+        const SwsLut1D *lut1d;      /* for SWS_UOP_LUT_1D; refstruct */
         void *opaque;               /* reserved for internal use */
     } data;
 } SwsUOp;
diff --git a/libswscale/uops_backend.c b/libswscale/uops_backend.c
index 1fd07fdeeec..b9facb9815c 100644
--- a/libswscale/uops_backend.c
+++ b/libswscale/uops_backend.c
@@ -93,6 +93,7 @@
     SWS_FOR(TYPE, LINEAR,         REF_ENTRY)                \
     SWS_FOR(TYPE, DITHER,         REF_ENTRY)                \
     SWS_FOR(TYPE, LUT_3D,         REF_ENTRY)                \
+    SWS_FOR(TYPE, LUT_1D,         REF_ENTRY)                \
     /* end of macro */
 
 static const SwsUOpTable uop_table = {
diff --git a/libswscale/uops_list.h b/libswscale/uops_list.h
index 31632dfcc0f..a3038addf44 100644
--- a/libswscale/uops_list.h
+++ b/libswscale/uops_list.h
@@ -58,5 +58,6 @@
     ENTRY(SWS_UOP_CLEAR,                "clear")                     \
     ENTRY(SWS_UOP_DITHER,               "dither")                    \
     ENTRY(SWS_UOP_LUT_3D,               "lut_3d")                    \
+    ENTRY(SWS_UOP_LUT_1D,               "lut_1d")                    \
 
 #endif /* SWSCALE_UOPS_LIST_H */
diff --git a/libswscale/uops_macros.h b/libswscale/uops_macros.h
index acad94d2f82..31c1f2af143 100644
--- a/libswscale/uops_macros.h
+++ b/libswscale/uops_macros.h
@@ -303,6 +303,8 @@
 #define SWS_FOR_STRUCT_U8_DITHER(MACRO, ...)
 #define SWS_FOR_U8_LUT_3D(MACRO, ...)
 #define SWS_FOR_STRUCT_U8_LUT_3D(MACRO, ...)
+#define SWS_FOR_U8_LUT_1D(MACRO, ...)
+#define SWS_FOR_STRUCT_U8_LUT_1D(MACRO, ...)
 #define SWS_FOR_U16_READ_PLANAR(MACRO, ...) \
     MACRO(__VA_ARGS__, u16_read_planar_x                       , 
SWS_PIXEL_U16, SWS_UOP_READ_PLANAR     , 0x1) \
     MACRO(__VA_ARGS__, u16_read_planar_xy                      , 
SWS_PIXEL_U16, SWS_UOP_READ_PLANAR     , 0x3) \
@@ -587,6 +589,8 @@
 #define SWS_FOR_STRUCT_U16_DITHER(MACRO, ...)
 #define SWS_FOR_U16_LUT_3D(MACRO, ...)
 #define SWS_FOR_STRUCT_U16_LUT_3D(MACRO, ...)
+#define SWS_FOR_U16_LUT_1D(MACRO, ...)
+#define SWS_FOR_STRUCT_U16_LUT_1D(MACRO, ...)
 #define SWS_FOR_U32_READ_PLANAR(MACRO, ...) \
     MACRO(__VA_ARGS__, u32_read_planar_x                       , 
SWS_PIXEL_U32, SWS_UOP_READ_PLANAR     , 0x1) \
     MACRO(__VA_ARGS__, u32_read_planar_xyz                     , 
SWS_PIXEL_U32, SWS_UOP_READ_PLANAR     , 0x7) \
@@ -815,6 +819,8 @@
 #define SWS_FOR_STRUCT_U32_DITHER(MACRO, ...)
 #define SWS_FOR_U32_LUT_3D(MACRO, ...)
 #define SWS_FOR_STRUCT_U32_LUT_3D(MACRO, ...)
+#define SWS_FOR_U32_LUT_1D(MACRO, ...)
+#define SWS_FOR_STRUCT_U32_LUT_1D(MACRO, ...)
 #define SWS_FOR_F32_READ_PLANAR(MACRO, ...)
 #define SWS_FOR_STRUCT_F32_READ_PLANAR(MACRO, ...)
 #define SWS_FOR_F32_READ_PLANAR_FH(MACRO, ...) \
@@ -1083,5 +1089,7 @@
     MACRO(__VA_ARGS__, f32_lut_3d_xyz_dynamic                  , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LUT_3D          , .mask = 0x7, .par.lut3d.dynamic 
= 1) \
     MACRO(__VA_ARGS__, f32_lut_3d_xyzw_static                  , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LUT_3D          , .mask = 0xf, .par.lut3d.dynamic 
= 0) \
     MACRO(__VA_ARGS__, f32_lut_3d_xyzw_dynamic                 , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LUT_3D          , .mask = 0xf, .par.lut3d.dynamic 
= 1)
+#define SWS_FOR_F32_LUT_1D(MACRO, ...)
+#define SWS_FOR_STRUCT_F32_LUT_1D(MACRO, ...)
 
 #endif /* SWSCALE_UOPS_MACROS_H */
diff --git a/libswscale/uops_macros_gen.c b/libswscale/uops_macros_gen.c
index 2af7e6fd984..15cddbc3a7b 100644
--- a/libswscale/uops_macros_gen.c
+++ b/libswscale/uops_macros_gen.c
@@ -117,6 +117,9 @@ static void print_params(AVBPrint *bp, const SwsUOp *uop, 
bool designated)
     case SWS_UOP_LUT_3D:
         FMT("par.lut3d.dynamic", "%d", par->lut3d.dynamic);
         break;
+    case SWS_UOP_LUT_1D:
+        FMT("par.lut1d.log", "%d", par->lut1d.log);
+        break;
     }
 
 #undef FMT
diff --git a/libswscale/uops_tmpl.c b/libswscale/uops_tmpl.c
index e94bfca417f..fa872c745d3 100644
--- a/libswscale/uops_tmpl.c
+++ b/libswscale/uops_tmpl.c
@@ -19,6 +19,7 @@
  */
 
 #include <libavutil/bswap.h>
+#include <libavutil/intfloat.h>
 
 #include "uops_tmpl.h"
 
@@ -1017,6 +1018,65 @@ DECL_FUNC(lut3d, const SwsCompMask mask, const int 
dynamic)
 SWS_FOR(PX, LUT_3D, DECL_IMPL, lut3d)
 SWS_FOR_STRUCT(PX, LUT_3D, DECL_ENTRY, .setup = fn(setup_lut3d) )
 
+DECL_SETUP(setup_lut1d, params, out)
+{
+    const SwsLut1D *lut = params->uop->data.lut1d;
+    out->priv.ptr = (void *) av_refstruct_ref_c(lut);
+    out->free = ff_op_priv_unref;
+    return 0;
+}
+
+#if IS_FLOAT
+#define LUT1D_FRAC_BITS (FLT_MANT_DIG - 1 - LUT1D_LOG_BITS)
+#define LUT1D_EXP_BIAS  (FLT_MAX_EXP - 1)
+
+av_always_inline static
+pixel_t fn(lut1d_lookup)(const SwsLut1D *restrict lut1d, const int log, 
pixel_t v)
+{
+    int i;
+    pixel_t f;
+
+    if (log) {
+        if (!(v > 0))
+            return lut1d->zero;
+
+        /* The exponent and the leading mantissa bits are the table index */
+        const uint32_t bits = av_float2int(v);
+        i = (int) (bits >> LUT1D_FRAC_BITS) -
+            ((LUT1D_EXP_BIAS + LUT1D_LOG_MIN) << LUT1D_LOG_BITS);
+        if (i < 0) {
+            f = v * (pixel_t) (1LL << -LUT1D_LOG_MIN);
+            return lerp(lut1d->zero, lut1d->table[0], f);
+        }
+
+        i = FFMIN(i, lut1d->size - 1);
+        f = (bits & ((1 << LUT1D_FRAC_BITS) - 1)) * (1.0f / (1 << 
LUT1D_FRAC_BITS));
+    } else {
+        i = (int) v;
+        f = v - i;
+    }
+
+    return lerp(lut1d->table[i], lut1d->table[i + 1], f);
+}
+
+DECL_FUNC(lut1d, const SwsCompMask mask, const int log)
+{
+    const SwsLut1D *restrict lut1d = impl->priv.ptr;
+
+    SWS_LOOP
+    for (int i = 0; i < SWS_BLOCK_SIZE; i++) {
+        if (X) x[i] = fn(lut1d_lookup)(lut1d, log, x[i]);
+        if (Y) y[i] = fn(lut1d_lookup)(lut1d, log, y[i]);
+        if (Z) z[i] = fn(lut1d_lookup)(lut1d, log, z[i]);
+    }
+
+    CONTINUE(x, y, z, w);
+}
+#endif /* IS_FLOAT */
+
+SWS_FOR(PX, LUT_1D, DECL_IMPL, lut1d)
+SWS_FOR_STRUCT(PX, LUT_1D, DECL_ENTRY, .setup = fn(setup_lut1d) )
+
 #undef PIXEL_MAX
 #undef PIXEL_SWAP
 #undef pixel_t
diff --git a/libswscale/x86/uops_macros.asm b/libswscale/x86/uops_macros.asm
index 09bc564ba3e..7f8aabf66dd 100644
--- a/libswscale/x86/uops_macros.asm
+++ b/libswscale/x86/uops_macros.asm
@@ -246,6 +246,9 @@
 %macro DECL_U8_LUT_3D 1
 %endmacro
 
+%macro DECL_U8_LUT_1D 1
+%endmacro
+
 %macro DECL_U16_READ_PLANAR 1
     DECL_OP %1, u16_read_planar_x                       , SWS_PIXEL_U16, 
SWS_UOP_READ_PLANAR     , 0x1
     DECL_OP %1, u16_read_planar_xy                      , SWS_PIXEL_U16, 
SWS_UOP_READ_PLANAR     , 0x3
@@ -464,6 +467,9 @@
 %macro DECL_U16_LUT_3D 1
 %endmacro
 
+%macro DECL_U16_LUT_1D 1
+%endmacro
+
 %macro DECL_U32_READ_PLANAR 1
     DECL_OP %1, u32_read_planar_x                       , SWS_PIXEL_U32, 
SWS_UOP_READ_PLANAR     , 0x1
     DECL_OP %1, u32_read_planar_xyz                     , SWS_PIXEL_U32, 
SWS_UOP_READ_PLANAR     , 0x7
@@ -650,6 +656,9 @@
 %macro DECL_U32_LUT_3D 1
 %endmacro
 
+%macro DECL_U32_LUT_1D 1
+%endmacro
+
 %macro DECL_F32_READ_PLANAR 1
 %endmacro
 
@@ -887,3 +896,6 @@
     DECL_OP %1, f32_lut_3d_xyzw_static                  , SWS_PIXEL_F32, 
SWS_UOP_LUT_3D          , 0xf, 0
     DECL_OP %1, f32_lut_3d_xyzw_dynamic                 , SWS_PIXEL_F32, 
SWS_UOP_LUT_3D          , 0xf, 1
 %endmacro
+
+%macro DECL_F32_LUT_1D 1
+%endmacro
diff --git a/libswscale/x86/uops_macros.h b/libswscale/x86/uops_macros.h
index 66b3a6afe27..47572d10f5f 100644
--- a/libswscale/x86/uops_macros.h
+++ b/libswscale/x86/uops_macros.h
@@ -351,6 +351,8 @@
 #define SWS_FOR_STRUCT_U8_DITHER(MACRO, ...)
 #define SWS_FOR_U8_LUT_3D(MACRO, ...)
 #define SWS_FOR_STRUCT_U8_LUT_3D(MACRO, ...)
+#define SWS_FOR_U8_LUT_1D(MACRO, ...)
+#define SWS_FOR_STRUCT_U8_LUT_1D(MACRO, ...)
 #define SWS_FOR_U16_READ_PLANAR(MACRO, ...) \
     MACRO(__VA_ARGS__, u16_read_planar_x                       , 
SWS_PIXEL_U16, SWS_UOP_READ_PLANAR     , 0x1) \
     MACRO(__VA_ARGS__, u16_read_planar_xy                      , 
SWS_PIXEL_U16, SWS_UOP_READ_PLANAR     , 0x3) \
@@ -643,6 +645,8 @@
 #define SWS_FOR_STRUCT_U16_DITHER(MACRO, ...)
 #define SWS_FOR_U16_LUT_3D(MACRO, ...)
 #define SWS_FOR_STRUCT_U16_LUT_3D(MACRO, ...)
+#define SWS_FOR_U16_LUT_1D(MACRO, ...)
+#define SWS_FOR_STRUCT_U16_LUT_1D(MACRO, ...)
 #define SWS_FOR_U32_READ_PLANAR(MACRO, ...) \
     MACRO(__VA_ARGS__, u32_read_planar_x                       , 
SWS_PIXEL_U32, SWS_UOP_READ_PLANAR     , 0x1) \
     MACRO(__VA_ARGS__, u32_read_planar_xyz                     , 
SWS_PIXEL_U32, SWS_UOP_READ_PLANAR     , 0x7) \
@@ -871,6 +875,8 @@
 #define SWS_FOR_STRUCT_U32_DITHER(MACRO, ...)
 #define SWS_FOR_U32_LUT_3D(MACRO, ...)
 #define SWS_FOR_STRUCT_U32_LUT_3D(MACRO, ...)
+#define SWS_FOR_U32_LUT_1D(MACRO, ...)
+#define SWS_FOR_STRUCT_U32_LUT_1D(MACRO, ...)
 #define SWS_FOR_F32_READ_PLANAR(MACRO, ...)
 #define SWS_FOR_STRUCT_F32_READ_PLANAR(MACRO, ...)
 #define SWS_FOR_F32_READ_PLANAR_FH(MACRO, ...) \
@@ -1203,5 +1209,7 @@
     MACRO(__VA_ARGS__, f32_lut_3d_xyz_dynamic                  , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LUT_3D          , .mask = 0x7, .par.lut3d.dynamic 
= 1) \
     MACRO(__VA_ARGS__, f32_lut_3d_xyzw_static                  , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LUT_3D          , .mask = 0xf, .par.lut3d.dynamic 
= 0) \
     MACRO(__VA_ARGS__, f32_lut_3d_xyzw_dynamic                 , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LUT_3D          , .mask = 0xf, .par.lut3d.dynamic 
= 1)
+#define SWS_FOR_F32_LUT_1D(MACRO, ...)
+#define SWS_FOR_STRUCT_F32_LUT_1D(MACRO, ...)
 
 #endif /* SWSCALE_X86_UOPS_MACROS_H */
-- 
2.54.0


From 1f3f4cd3246081ed232abc8243fd3e8a18b86259 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Sun, 1 Nov 2026 15:55:10 +0100
Subject: [PATCH 4/8] swscale/ops_chain: run a linear uop without a kernel for
 its pattern as general one
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The backends have kernels for the patterns of zero and one coefficients
that the enumeration of the untagged conversions produces, and refuse a
linear uop with another pattern, like the matrix of primaries whose red
has no Z, which drops a tagged conversion to the legacy backend. Run
such a uop on the kernel of the general pattern instead, which
multiplies the zeros and ones out.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libswscale/ops_chain.c | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/libswscale/ops_chain.c b/libswscale/ops_chain.c
index 42f26035a7b..71c37c86728 100644
--- a/libswscale/ops_chain.c
+++ b/libswscale/ops_chain.c
@@ -57,6 +57,22 @@ int ff_sws_op_chain_append(SwsOpChain *chain, SwsFuncPtr 
func,
     return 0;
 }
 
+static bool uop_generalize(SwsUOp *uop)
+{
+    const uint32_t keep = SWS_MASK_ROW(3) | SWS_MASK_COL(3);
+    SwsLinearUOp *lin = &uop->par.lin;
+
+    if (uop->uop != SWS_UOP_LINEAR && uop->uop != SWS_UOP_LINEAR_FMA)
+        return false;
+    if (!((lin->zero | lin->one | lin->exact) & ~keep))
+        return false;
+
+    lin->zero  &= keep;
+    lin->one   &= keep;
+    lin->exact &= keep;
+    return true;
+}
+
 int ff_sws_uop_lookup(SwsContext *ctx, const SwsUOpTable *const tables[],
                       int num_tables, const SwsUOp *uop, const int block_size,
                       SwsOpChain *chain)
@@ -96,8 +112,15 @@ int ff_sws_uop_lookup(SwsContext *ctx, const SwsUOpTable 
*const tables[],
     }
 
     if (!match) {
+        SwsUOp general = *uop;
         char name[64];
         ff_sws_uop_name(uop, name);
+        if (uop_generalize(&general)) {
+            av_log(ctx, AV_LOG_DEBUG, "No implementation found for: %s, "
+                   "using the general kernel\n", name);
+            return ff_sws_uop_lookup(ctx, tables, num_tables, &general,
+                                     block_size, chain);
+        }
         av_log(ctx, AV_LOG_DEBUG, "No implementation found for: %s\n", name);
         return AVERROR(ENOTSUP);
     }
-- 
2.54.0


From 96c19e252178aa5492e94be7378dee62b098ab66 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Mon, 2 Nov 2026 09:01:23 +0100
Subject: [PATCH 5/8] swscale/cms: decide if exact color map is possible or
 mapping is needed
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

A color map that only changes the encoding of the colors and their
transfer, with the primaries within the destination gamut and the
luminance within the destination range, is one that tables of the
transfers and a matrix in linear light convert exactly. Compressing the
gamut or the luminance, adapting the white point, compensating the
black point and HLG, whose OOTF mixes the components, need the 3DLUT.
Add ff_sws_color_map_exact() for that distinction.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libswscale/cms.c | 55 +++++++++++++++++++++++++++++++++++++-----------
 libswscale/cms.h | 10 +++++++++
 2 files changed, 53 insertions(+), 12 deletions(-)

diff --git a/libswscale/cms.c b/libswscale/cms.c
index 6d283ea1d3e..b5b4b66e709 100644
--- a/libswscale/cms.c
+++ b/libswscale/cms.c
@@ -31,6 +31,28 @@
 #include "libswscale/swscale.h"
 #include "format.h"
 
+/* Whether the intent has nothing to map: the gamut and the luminance of the
+ * source fit in the destination*/
+static bool map_within(const SwsColorMap *map)
+{
+    switch (map->intent) {
+    case SWS_INTENT_ABSOLUTE_COLORIMETRIC:
+    case SWS_INTENT_RELATIVE_COLORIMETRIC:
+        /* These intents only distinguish between clipping and not clipping,
+         * which only matters if the gamut or luminance exceeds the target */
+        return ff_prim_superset(&map->dst.gamut, &map->src.gamut) &&
+               av_cmp_q(map->src.max_luma, map->dst.max_luma) <= 0;
+    case SWS_INTENT_PERCEPTUAL:
+    case SWS_INTENT_SATURATION:
+        /* These intents distort even colors that are inside the gamut */
+        return ff_prim_equal(&map->dst.gamut, &map->src.gamut) &&
+               !av_cmp_q(map->src.max_luma, map->dst.max_luma);
+    default:
+        av_assert0(!"Invalid gamut mapping intent?");
+        return true;
+    }
+}
+
 bool ff_sws_color_map_noop(const SwsColorMap *map)
 {
     /* If the encoding space is different, we must go through a conversion */
@@ -41,19 +63,28 @@ bool ff_sws_color_map_noop(const SwsColorMap *map)
     if (av_cmp_q(map->src.min_luma, map->dst.min_luma))
         return false;
 
-    switch (map->intent) {
-    case SWS_INTENT_ABSOLUTE_COLORIMETRIC:
-    case SWS_INTENT_RELATIVE_COLORIMETRIC:
-        return ff_prim_superset(&map->dst.gamut, &map->src.gamut) &&
-               av_cmp_q(map->src.max_luma, map->dst.max_luma) <= 0;
-    case SWS_INTENT_PERCEPTUAL:
-    case SWS_INTENT_SATURATION:
-        return ff_prim_equal(&map->dst.gamut, &map->src.gamut) &&
-               !av_cmp_q(map->src.max_luma, map->dst.max_luma);
-    default:
-        av_assert0(!"Invalid gamut mapping intent?");
-        return true;
+    return map_within(map);
+}
+
+bool ff_sws_color_map_exact(const SwsColorMap *map)
+{
+    /* Chromatic adaptation is left to the 3DLUT */
+    if (map->src.prim != map->dst.prim) {
+        const AVColorPrimariesDesc *src = 
av_csp_primaries_desc_from_id(map->src.prim);
+        const AVColorPrimariesDesc *dst = 
av_csp_primaries_desc_from_id(map->dst.prim);
+        if (!src || !dst || av_cmp_q(src->wp.x, dst->wp.x) || 
av_cmp_q(src->wp.y, dst->wp.y))
+            return false;
     }
+
+    /* The OOTF of HLG mixes the components */
+    if (map->src.trc == AVCOL_TRC_ARIB_STD_B67 || map->dst.trc == 
AVCOL_TRC_ARIB_STD_B67)
+        return false;
+
+    /* Black point compensation changes the PQ intensity, left to the 3DLUT */
+    if (av_cmp_q(map->src.min_luma, map->dst.min_luma))
+        return false;
+
+    return map_within(map);
 }
 
 /* Approximation of gamut hull at a given intensity level */
diff --git a/libswscale/cms.h b/libswscale/cms.h
index 5323a27260b..874c85b1531 100644
--- a/libswscale/cms.h
+++ b/libswscale/cms.h
@@ -69,6 +69,16 @@ typedef struct SwsColorMap {
  */
 bool ff_sws_color_map_noop(const SwsColorMap *map);
 
+/**
+ * Returns true if the color map only changes the encoding of the colors and
+ * their transfer, with the primaries within the destination gamut and the
+ * luminance within the destination range, which tables of the transfers and
+ * a matrix in linear light convert exactly. Compressing the gamut or the
+ * luminance, adapting the white point, compensating the black point and HLG,
+ * whose OOTF mixes the components, need the 3DLUT.
+ */
+bool ff_sws_color_map_exact(const SwsColorMap *map);
+
 /**
  * Generates a single end-to-end color mapping 3DLUT embedding a static tone
  * mapping curve.
-- 
2.54.0


From c5660d03d4bbee6524ed9f259bf37072fa78aec7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Mon, 2 Nov 2026 09:01:24 +0100
Subject: [PATCH 6/8] swscale/format: convert colors exactly when no 3DLUT is
 given
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

A color map that changes only the encoding of the colors, the transfer,
the luminance range and the primaries within the destination gamut, has
nothing for the 3DLUT to do but add the error of its grid. Convert such
maps with a table of the source transfer, the matrix of the primaries in
linear light and a table of the inverse destination transfer. SDR
transfers map relatively, their white and black to those of the other
side, PQ absolutely, as its codes are light, with the luminance scaled
between the ranges of the two sides. Integer RGB codes index a table of
their size exactly, other input and the output use a table spaced
evenly within each octave.

The graph still generates the 3DLUT for every map that is not a no-op
and passes it, so nothing reaches the exact conversion yet.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libswscale/format.c | 256 +++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 255 insertions(+), 1 deletion(-)

diff --git a/libswscale/format.c b/libswscale/format.c
index fbe2d813f9c..538c548851c 100644
--- a/libswscale/format.c
+++ b/libswscale/format.c
@@ -310,6 +310,8 @@ static void sanitize_fmt(SwsFormat *fmt, const 
AVPixFmtDescriptor *desc)
         fmt->color = (SwsColor) {
             .prim = AVCOL_PRI_BT709, /* swscale currently hard-codes this XYZ 
matrix */
             .trc  = AVCOL_TRC_SMPTE428,
+            .min_luma = { 0, 1 },
+            .max_luma = { 203, 1 },
         };
     } else if (desc->nb_components < 3) {
         /* Grayscale formats */
@@ -1804,15 +1806,267 @@ static int apply_lut3d(SwsContext *ctx, SwsPixelType 
type, SwsOpList *ops,
     return 0;
 }
 
+typedef struct TrcTable {
+    av_csp_eotf_function eotf;
+    AVRational Lw, Lb;  /* display light at linear 1 and 0 */
+    double range;       /* table input of the top of the signal */
+    bool absolute;      /* codes are light, not relative to Lw and Lb (PQ) */
+} TrcTable;
+
+static int trc_table(const SwsColor *color, bool inverse, double range,
+                     TrcTable *table)
+{
+    *table = (TrcTable) {
+        .eotf  = inverse ? av_csp_itu_eotf_inv(color->trc) : 
av_csp_itu_eotf(color->trc),
+        .Lw    = color->max_luma,
+        .Lb    = color->min_luma,
+        .range = range,
+    };
+
+    /* PQ codes are absolute light up to 10000 cd/m2, the mastering display
+     * only steers the color map */
+    if (color->trc == AVCOL_TRC_SMPTE2084) {
+        table->Lw = av_make_q(10000, 1);
+        table->Lb = av_make_q(0, 1);
+        table->absolute = true;
+    }
+
+    if (!table->eotf)
+        return AVERROR(ENOTSUP);
+    if (av_cmp_q(table->Lw, table->Lb) <= 0)
+        return AVERROR(EINVAL);
+    return 0;
+}
+
+static double trc_linearize(double x, const void *opaque)
+{
+    const TrcTable *t = opaque;
+    const double Lw = av_q2d(t->Lw), Lb = av_q2d(t->Lb);
+    double c[3];
+    /* The logarithmic table extends past the top of the signal */
+    c[0] = c[1] = c[2] = FFMIN(x / t->range, 1.0);
+    t->eotf(Lw, Lb, c);
+    return (c[0] - Lb) / (Lw - Lb);
+}
+
+static double trc_delinearize(double x, const void *opaque)
+{
+    const TrcTable *t = opaque;
+    const double Lw = av_q2d(t->Lw), Lb = av_q2d(t->Lb);
+    double c[3];
+    c[0] = c[1] = c[2] = Lb + (Lw - Lb) * x;
+    t->eotf(Lw, Lb, c);
+    return c[0];
+}
+
+static int append_lut1d(SwsOpList *ops, SwsLut1DType type, int size,
+                        double (*fn)(double x, const void *opaque),
+                        const void *opaque)
+{
+    SwsLut1D *lut = ff_sws_lut1d_generate(type, size, fn, opaque);
+    if (!lut)
+        return AVERROR(ENOMEM);
+
+    return ff_sws_op_list_append(ops, &(SwsOp) {
+        .op        = SWS_OP_LUT_1D,
+        .lut1d.lut = lut,
+    });
+}
+
+/* Size of the table the integer RGB codes of a format index directly, or 0
+ * for a format that uses the logarithmic table: float, or with more codes
+ * than that table has entries */
+static int trc_table_size(const SwsFormat *fmt)
+{
+    const AVPixFmtDescriptor *desc = fmt->desc;
+    if (fmt->csp != AVCOL_SPC_RGB || fmt->range != AVCOL_RANGE_JPEG ||
+        desc->flags & AV_PIX_FMT_FLAG_FLOAT || desc->nb_components < 3)
+        return 0;
+
+    for (int i = 1; i < 3; i++) {
+        if (desc->comp[i].depth != desc->comp[0].depth)
+            return 0;
+    }
+
+    return desc->comp[0].depth < av_log2(LUT1D_LOG_SIZE) ? 1 << 
desc->comp[0].depth : 0;
+}
+
+static void cie_xyz_q64(const AVCIExy *xy, AVRational64 xyz[3])
+{
+    xyz[0] = Q64(xy->x);
+    xyz[1] = Q64(xy->y);
+    xyz[2] = ff_sub_q64(ff_sub_q64(Q(1), xyz[0]), xyz[1]);
+}
+
+static void mat3_invert_q64(AVRational64 m[3][3])
+{
+    AVRational64 adj[3][3], det = Q(0);
+
+    for (int i = 0; i < 3; i++) {
+        const int i1 = (i + 1) % 3, i2 = (i + 2) % 3;
+        for (int j = 0; j < 3; j++) {
+            const int j1 = (j + 1) % 3, j2 = (j + 2) % 3;
+            adj[j][i] = ff_sub_q64(ff_mul_q64(m[i1][j1], m[i2][j2]),
+                                   ff_mul_q64(m[i1][j2], m[i2][j1]));
+        }
+    }
+
+    for (int j = 0; j < 3; j++)
+        det = ff_add_q64(det, ff_mul_q64(m[0][j], adj[j][0]));
+
+    for (int i = 0; i < 3; i++) {
+        for (int j = 0; j < 3; j++)
+            m[i][j] = ff_div_q64(adj[i][j], det);
+    }
+}
+
+/**
+ * Matrix of the RGB of the primaries to CIE XYZ, or its inverse, with the
+ * white at Y = 1. Exact in rationals, unlike ff_sws_rgb2xyz().
+ */
+static void rgb2xyz_q64(const AVColorPrimariesDesc *desc, bool inverse,
+                        AVRational64 m[3][3])
+{
+    const AVCIExy *prim[3] = { &desc->prim.r, &desc->prim.g, &desc->prim.b };
+    AVRational64 p[3][3], inv[3][3], white[3], s[3];
+
+    /* Chromaticities, not XYZ at Y = 1, as the primaries of XYZ have y = 0 */
+    for (int j = 0; j < 3; j++) {
+        AVRational64 xyz[3];
+        cie_xyz_q64(prim[j], xyz);
+        for (int i = 0; i < 3; i++)
+            p[i][j] = inv[i][j] = xyz[i];
+    }
+
+    mat3_invert_q64(inv);
+    cie_xyz_q64(&desc->wp, white);
+    white[0] = ff_div_q64(white[0], white[1]);
+    white[2] = ff_div_q64(white[2], white[1]);
+    white[1] = Q(1);
+    for (int i = 0; i < 3; i++) {
+        s[i] = Q(0);
+        for (int j = 0; j < 3; j++)
+            s[i] = ff_add_q64(s[i], ff_mul_q64(inv[i][j], white[j]));
+    }
+
+    for (int i = 0; i < 3; i++) {
+        for (int j = 0; j < 3; j++)
+            m[i][j] = inverse ? ff_div_q64(inv[i][j], s[i]) : 
ff_mul_q64(p[i][j], s[j]);
+    }
+}
+
+/**
+ * Matrix from the linear light of the source to that of the destination,
+ * through CIE XYZ. The light of an SDR transfer is relative to its range, so
+ * its white and black map to those of the other side. PQ codes are light, so
+ * between PQ and an SDR transfer the luminance is scaled and offset between
+ * the two ranges.
+ */
+static SwsLinearOp color_matrix(SwsPixelType type, const SwsFormat *src,
+                                const SwsFormat *dst, const TrcTable *in,
+                                const TrcTable *out)
+{
+    AVRational64 scale = Q(1), offset = Q(0);
+    AVRational64 to_xyz[3][3], to_rgb[3][3];
+
+    if (in->absolute || out->absolute) {
+        const AVRational64 in_range  = ff_sub_q64(Q64(in->Lw), Q64(in->Lb));
+        const AVRational64 out_range = ff_sub_q64(Q64(out->Lw), Q64(out->Lb));
+        scale  = ff_div_q64(in_range, out_range);
+        offset = ff_div_q64(ff_sub_q64(Q64(in->Lb), Q64(out->Lb)), out_range);
+    }
+
+    SwsLinearOp c = LINEAR_OP_DATA(type,
+        { Q(0), Q(0), Q(0), Q(0), offset },
+        { Q(0), Q(0), Q(0), Q(0), offset },
+        { Q(0), Q(0), Q(0), Q(0), offset },
+        { Q(0), Q(0), Q(0), Q(1), Q(0)   },
+    );
+
+    rgb2xyz_q64(av_csp_primaries_desc_from_id(src->color.prim), false, to_xyz);
+    rgb2xyz_q64(av_csp_primaries_desc_from_id(dst->color.prim), true,  to_rgb);
+    for (int i = 0; i < 3; i++) {
+        for (int j = 0; j < 3; j++) {
+            for (int k = 0; k < 3; k++)
+                c.m[i][j] = ff_add_q64(c.m[i][j], ff_mul_q64(to_rgb[i][k], 
to_xyz[k][j]));
+            c.m[i][j] = ff_mul_q64(c.m[i][j], scale);
+        }
+    }
+
+    return c;
+}
+
+/**
+ * Appends the exact conversion of the source color to the destination color:
+ * a table of the source transfer, which integer codes index directly when
+ * trc_table_size() allows, the matrix between the primaries in linear light
+ * and a table of the inverse destination transfer.
+ */
+static int convert_colors(SwsPixelType type, SwsOpList *ops,
+                          const SwsFormat *src, const SwsFormat *dst)
+{
+    const int size = trc_table_size(src);
+    TrcTable in, out;
+
+    RET(trc_table(&src->color, false, size ? size - 1 : 1.0, &in));
+    RET(trc_table(&dst->color, true, 1.0, &out));
+
+    if (size) {
+        const AVRational64 codes = Q(size - 1);
+        RET(ff_sws_op_list_append(ops, &(SwsOp) {
+            .op     = SWS_OP_LINEAR,
+            .lin    = LINEAR_OP_DATA(type,
+                { codes,  Q(0),  Q(0), Q(0), Q(0) },
+                {  Q(0), codes,  Q(0), Q(0), Q(0) },
+                {  Q(0),  Q(0), codes, Q(0), Q(0) },
+                {  Q(0),  Q(0),  Q(0), Q(1), Q(0) },
+            ),
+        }));
+
+        RET(ff_sws_op_list_append(ops, &(SwsOp) {
+            .op     = SWS_OP_MAX,
+            .clamp  = {{ Q(0), Q(0), Q(0) }},
+        }));
+
+        RET(ff_sws_op_list_append(ops, &(SwsOp) {
+            .op     = SWS_OP_MIN,
+            .clamp  = {{ codes, codes, codes }},
+        }));
+
+        RET(append_lut1d(ops, SWS_LUT1D_UNIFORM, size, trc_linearize, &in));
+    } else {
+        RET(append_lut1d(ops, SWS_LUT1D_LOG, LUT1D_LOG_SIZE, trc_linearize, 
&in));
+    }
+
+    RET(ff_sws_op_list_append(ops, &(SwsOp) {
+        .op     = SWS_OP_LINEAR,
+        .lin    = color_matrix(type, src, dst, &in, &out),
+    }));
+
+    return append_lut1d(ops, SWS_LUT1D_LOG, LUT1D_LOG_SIZE, trc_delinearize, 
&out);
+}
+
 static int fmt_convert(SwsContext *ctx, const SwsFormat *src, const SwsFormat 
*dst,
                        const SwsLut3D *lut3d, SwsOpList *ops, bool *incomplete)
 {
     const SwsPixelType type = SWS_PIXEL_F32;
+    const SwsColorMap map = {
+        .intent = ctx->intent,
+        .src    = src->color,
+        .dst    = dst->color,
+    };
+
     RET(decode_pixfmt(ops, src));
     RET(decode_colors(ctx, type, ops, src, incomplete));
     RET(add_filters(ctx, type, ops, src, dst));
-    if (lut3d)
+    if (lut3d) {
         RET(apply_lut3d(ctx, type, ops, lut3d));
+    } else if (!ff_sws_color_map_noop(&map)) {
+        /* Gamut and tone mapping need the 3DLUT */
+        if (!ff_sws_color_map_exact(&map))
+            return AVERROR(ENOTSUP);
+        RET(convert_colors(type, ops, src, dst));
+    }
     RET(encode_colors(ctx, type, ops, src, dst, incomplete));
     RET(encode_pixfmt(ops, dst));
     return 0;
-- 
2.54.0


From 5f78265ebec053e5bfd6879fa8ab282200069f2e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Mon, 2 Nov 2026 09:01:24 +0100
Subject: [PATCH 7/8] swscale/ops: enumerate an exact color conversion
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libswscale/op_list_gen_template.c  | 50 +++++++++++++++++++++++-------
 libswscale/tests/sws_ops_aarch64.c |  5 +--
 libswscale/uops_macros.h           | 14 +++++++--
 libswscale/x86/uops_macros.asm     |  6 ++++
 libswscale/x86/uops_macros.h       | 16 ++++++++--
 tests/ref/fate/sws-ops-list        |  2 +-
 6 files changed, 75 insertions(+), 18 deletions(-)

diff --git a/libswscale/op_list_gen_template.c 
b/libswscale/op_list_gen_template.c
index dbcf9f6f9d6..5855b10904f 100644
--- a/libswscale/op_list_gen_template.c
+++ b/libswscale/op_list_gen_template.c
@@ -41,17 +41,24 @@ struct EnumFmtPriv {
     enum AVPixelFormat dst_start, dst_end;
 };
 
-static int enum_ops_fmt(const struct EnumFmtPriv *s,
-                        enum AVPixelFormat src_fmt,
-                        enum AVPixelFormat dst_fmt)
+/**
+ * Colors of the one tagged conversion the enumeration runs. The untagged
+ * formats infer matching colors and convert none, while an exact conversion
+ * of any colors is the same two tables and a matrix, so this one lists the
+ * operations for all of them.
+ */
+static const SwsColor sdr_reference = {
+    .prim = AVCOL_PRI_BT709,  .trc = AVCOL_TRC_BT709,     .min_luma = { 0, 1 
}, .max_luma = {   203, 1 },
+};
+static const SwsColor hdr_reference = {
+    .prim = AVCOL_PRI_BT2020, .trc = AVCOL_TRC_SMPTE2084, .min_luma = { 0, 1 
}, .max_luma = { 10000, 1 },
+};
+
+static int enum_ops_sizes(const struct EnumFmtPriv *s, const SwsFormat *src,
+                          SwsFormat *dst, bool incomplete)
 {
     int ret = 0;
     SwsOpList *ops = NULL;
-    SwsFormat src, dst;
-    ff_fmt_from_pixfmt(src_fmt, &src);
-    ff_fmt_from_pixfmt(dst_fmt, &dst);
-    bool incomplete = ff_infer_colors(&src.color, &dst.color);
-    src.width = src.height = DUMMY_SIZE;
 
     static const int dst_sizes[][2] = {
         { DUMMY_SIZE,     DUMMY_SIZE     },
@@ -61,10 +68,10 @@ static int enum_ops_fmt(const struct EnumFmtPriv *s,
     };
 
     for (int i = 0; i < FF_ARRAY_ELEMS(dst_sizes); i++) {
-        dst.width  = dst_sizes[i][0];
-        dst.height = dst_sizes[i][1];
+        dst->width  = dst_sizes[i][0];
+        dst->height = dst_sizes[i][1];
 
-        ret = ff_sws_op_list_generate(s->ctx, &src, &dst, s->lut3d, &ops, 
&incomplete);
+        ret = ff_sws_op_list_generate(s->ctx, src, dst, s->lut3d, &ops, 
&incomplete);
         if (ret == AVERROR(ENOTSUP))
             return 0; /* silently skip unsupported formats */
         else if (ret < 0)
@@ -86,6 +93,27 @@ fail:
     return ret;
 }
 
+static int enum_ops_fmt(const struct EnumFmtPriv *s,
+                        enum AVPixelFormat src_fmt,
+                        enum AVPixelFormat dst_fmt)
+{
+    SwsFormat src, dst;
+    ff_fmt_from_pixfmt(src_fmt, &src);
+    ff_fmt_from_pixfmt(dst_fmt, &dst);
+    bool incomplete = ff_infer_colors(&src.color, &dst.color);
+    src.width = src.height = DUMMY_SIZE;
+
+    int ret = enum_ops_sizes(s, &src, &dst, incomplete);
+    if (ret < 0 || s->lut3d)
+        return ret;
+
+    src.color = sdr_reference;
+    dst.color = hdr_reference;
+    src.color.gamut = av_csp_primaries_desc_from_id(src.color.prim)->prim;
+    dst.color.gamut = av_csp_primaries_desc_from_id(dst.color.prim)->prim;
+    return enum_ops_sizes(s, &src, &dst, incomplete);
+}
+
 static int enum_fmt_slice(void *priv, int jobnr, int threadnr, int nb_jobs,
                           int nb_threads)
 {
diff --git a/libswscale/tests/sws_ops_aarch64.c 
b/libswscale/tests/sws_ops_aarch64.c
index 3a56d71f593..88544e4e451 100644
--- a/libswscale/tests/sws_ops_aarch64.c
+++ b/libswscale/tests/sws_ops_aarch64.c
@@ -249,8 +249,8 @@ static const SwsOpBackend backend_collect = {
 /*********************************************************************/
 static int register_op(SwsContext *ctx, void *opaque, SwsOpList *ops)
 {
-    /* Skip ops lists which include filtering, since this is still not
-     * supported. */
+    /* Skip ops lists which include filtering or 1D LUTs, since these are
+     * still not supported. */
     for (int i = 0; i < ops->num_ops; i++) {
         const SwsOp *op = &ops->ops[i];
         switch (op->op) {
@@ -261,6 +261,7 @@ static int register_op(SwsContext *ctx, void *opaque, 
SwsOpList *ops)
             break;
         case SWS_OP_FILTER_H:
         case SWS_OP_FILTER_V:
+        case SWS_OP_LUT_1D:
             return 0;
         }
     }
diff --git a/libswscale/uops_macros.h b/libswscale/uops_macros.h
index 31c1f2af143..040bc660e18 100644
--- a/libswscale/uops_macros.h
+++ b/libswscale/uops_macros.h
@@ -972,16 +972,20 @@
 #define SWS_FOR_F32_MAX(MACRO, ...) \
     MACRO(__VA_ARGS__, f32_max_x                               , 
SWS_PIXEL_F32, SWS_UOP_MAX             , 0x1) \
     MACRO(__VA_ARGS__, f32_max_xy                              , 
SWS_PIXEL_F32, SWS_UOP_MAX             , 0x3) \
+    MACRO(__VA_ARGS__, f32_max_yz                              , 
SWS_PIXEL_F32, SWS_UOP_MAX             , 0x6) \
     MACRO(__VA_ARGS__, f32_max_xyz                             , 
SWS_PIXEL_F32, SWS_UOP_MAX             , 0x7) \
     MACRO(__VA_ARGS__, f32_max_w                               , 
SWS_PIXEL_F32, SWS_UOP_MAX             , 0x8) \
     MACRO(__VA_ARGS__, f32_max_xw                              , 
SWS_PIXEL_F32, SWS_UOP_MAX             , 0x9) \
+    MACRO(__VA_ARGS__, f32_max_yzw                             , 
SWS_PIXEL_F32, SWS_UOP_MAX             , 0xe) \
     MACRO(__VA_ARGS__, f32_max_xyzw                            , 
SWS_PIXEL_F32, SWS_UOP_MAX             , 0xf)
 #define SWS_FOR_STRUCT_F32_MAX(MACRO, ...) \
     MACRO(__VA_ARGS__, f32_max_x                               , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_MAX             , .mask = 0x1) \
     MACRO(__VA_ARGS__, f32_max_xy                              , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_MAX             , .mask = 0x3) \
+    MACRO(__VA_ARGS__, f32_max_yz                              , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_MAX             , .mask = 0x6) \
     MACRO(__VA_ARGS__, f32_max_xyz                             , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_MAX             , .mask = 0x7) \
     MACRO(__VA_ARGS__, f32_max_w                               , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_MAX             , .mask = 0x8) \
     MACRO(__VA_ARGS__, f32_max_xw                              , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_MAX             , .mask = 0x9) \
+    MACRO(__VA_ARGS__, f32_max_yzw                             , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_MAX             , .mask = 0xe) \
     MACRO(__VA_ARGS__, f32_max_xyzw                            , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_MAX             , .mask = 0xf)
 #define SWS_FOR_F32_UNPACK(MACRO, ...)
 #define SWS_FOR_STRUCT_F32_UNPACK(MACRO, ...)
@@ -1001,6 +1005,7 @@
     MACRO(__VA_ARGS__, f32_linear_xyz_xxx0x_xxx0x_xxx0x        , 
SWS_PIXEL_F32, SWS_UOP_LINEAR          , 0x7, 0x40000, 0xba108) \
     MACRO(__VA_ARGS__, f32_linear_xyz_x0x0x_xxx0x_xx00x        , 
SWS_PIXEL_F32, SWS_UOP_LINEAR          , 0x7, 0x40000, 0xbb10a) \
     MACRO(__VA_ARGS__, f32_linear_xyz_xxx00_xxx0x_xxx0x        , 
SWS_PIXEL_F32, SWS_UOP_LINEAR          , 0x7, 0x40000, 0xba118) \
+    MACRO(__VA_ARGS__, f32_linear_xyz_xxx00_xxx00_xxx00        , 
SWS_PIXEL_F32, SWS_UOP_LINEAR          , 0x7, 0x40000, 0xbe318) \
     MACRO(__VA_ARGS__, f32_linear_xyz_x000x_0x00x_00x0x        , 
SWS_PIXEL_F32, SWS_UOP_LINEAR          , 0x7, 0x40000, 0xbadae) \
     MACRO(__VA_ARGS__, f32_linear_xyz_x0000_0x000_00x00        , 
SWS_PIXEL_F32, SWS_UOP_LINEAR          , 0x7, 0x40000, 0xbefbe) \
     MACRO(__VA_ARGS__, f32_linear_xyz_10x0x_1xx0x_1x00x        , 
SWS_PIXEL_F32, SWS_UOP_LINEAR          , 0x7, 0x40421, 0xbb10a) \
@@ -1018,6 +1023,7 @@
     MACRO(__VA_ARGS__, f32_linear_xyz_xxx0x_xxx0x_xxx0x        , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR          , .mask = 0x7, .par.lin.one = 
0x40000, .par.lin.zero = 0xba108) \
     MACRO(__VA_ARGS__, f32_linear_xyz_x0x0x_xxx0x_xx00x        , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR          , .mask = 0x7, .par.lin.one = 
0x40000, .par.lin.zero = 0xbb10a) \
     MACRO(__VA_ARGS__, f32_linear_xyz_xxx00_xxx0x_xxx0x        , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR          , .mask = 0x7, .par.lin.one = 
0x40000, .par.lin.zero = 0xba118) \
+    MACRO(__VA_ARGS__, f32_linear_xyz_xxx00_xxx00_xxx00        , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR          , .mask = 0x7, .par.lin.one = 
0x40000, .par.lin.zero = 0xbe318) \
     MACRO(__VA_ARGS__, f32_linear_xyz_x000x_0x00x_00x0x        , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR          , .mask = 0x7, .par.lin.one = 
0x40000, .par.lin.zero = 0xbadae) \
     MACRO(__VA_ARGS__, f32_linear_xyz_x0000_0x000_00x00        , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR          , .mask = 0x7, .par.lin.one = 
0x40000, .par.lin.zero = 0xbefbe) \
     MACRO(__VA_ARGS__, f32_linear_xyz_10x0x_1xx0x_1x00x        , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR          , .mask = 0x7, .par.lin.one = 
0x40421, .par.lin.zero = 0xbb10a) \
@@ -1089,7 +1095,11 @@
     MACRO(__VA_ARGS__, f32_lut_3d_xyz_dynamic                  , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LUT_3D          , .mask = 0x7, .par.lut3d.dynamic 
= 1) \
     MACRO(__VA_ARGS__, f32_lut_3d_xyzw_static                  , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LUT_3D          , .mask = 0xf, .par.lut3d.dynamic 
= 0) \
     MACRO(__VA_ARGS__, f32_lut_3d_xyzw_dynamic                 , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LUT_3D          , .mask = 0xf, .par.lut3d.dynamic 
= 1)
-#define SWS_FOR_F32_LUT_1D(MACRO, ...)
-#define SWS_FOR_STRUCT_F32_LUT_1D(MACRO, ...)
+#define SWS_FOR_F32_LUT_1D(MACRO, ...) \
+    MACRO(__VA_ARGS__, f32_lut_1d_xyz_uniform                  , 
SWS_PIXEL_F32, SWS_UOP_LUT_1D          , 0x7, 0) \
+    MACRO(__VA_ARGS__, f32_lut_1d_xyz_log                      , 
SWS_PIXEL_F32, SWS_UOP_LUT_1D          , 0x7, 1)
+#define SWS_FOR_STRUCT_F32_LUT_1D(MACRO, ...) \
+    MACRO(__VA_ARGS__, f32_lut_1d_xyz_uniform                  , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LUT_1D          , .mask = 0x7, .par.lut1d.log = 
0) \
+    MACRO(__VA_ARGS__, f32_lut_1d_xyz_log                      , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LUT_1D          , .mask = 0x7, .par.lut1d.log = 1)
 
 #endif /* SWSCALE_UOPS_MACROS_H */
diff --git a/libswscale/x86/uops_macros.asm b/libswscale/x86/uops_macros.asm
index 7f8aabf66dd..154e5110f9e 100644
--- a/libswscale/x86/uops_macros.asm
+++ b/libswscale/x86/uops_macros.asm
@@ -792,9 +792,11 @@
 %macro DECL_F32_MAX 1
     DECL_OP %1, f32_max_x                               , SWS_PIXEL_F32, 
SWS_UOP_MAX             , 0x1
     DECL_OP %1, f32_max_xy                              , SWS_PIXEL_F32, 
SWS_UOP_MAX             , 0x3
+    DECL_OP %1, f32_max_yz                              , SWS_PIXEL_F32, 
SWS_UOP_MAX             , 0x6
     DECL_OP %1, f32_max_xyz                             , SWS_PIXEL_F32, 
SWS_UOP_MAX             , 0x7
     DECL_OP %1, f32_max_w                               , SWS_PIXEL_F32, 
SWS_UOP_MAX             , 0x8
     DECL_OP %1, f32_max_xw                              , SWS_PIXEL_F32, 
SWS_UOP_MAX             , 0x9
+    DECL_OP %1, f32_max_yzw                             , SWS_PIXEL_F32, 
SWS_UOP_MAX             , 0xe
     DECL_OP %1, f32_max_xyzw                            , SWS_PIXEL_F32, 
SWS_UOP_MAX             , 0xf
 %endmacro
 
@@ -821,6 +823,7 @@
     DECL_OP %1, f32_linear_xyz_xxx0x_xxx0x_xxx0x        , SWS_PIXEL_F32, 
SWS_UOP_LINEAR          , 0x7, 0x40000, 0xba108
     DECL_OP %1, f32_linear_xyz_x0x0x_xxx0x_xx00x        , SWS_PIXEL_F32, 
SWS_UOP_LINEAR          , 0x7, 0x40000, 0xbb10a
     DECL_OP %1, f32_linear_xyz_xxx00_xxx0x_xxx0x        , SWS_PIXEL_F32, 
SWS_UOP_LINEAR          , 0x7, 0x40000, 0xba118
+    DECL_OP %1, f32_linear_xyz_xxx00_xxx00_xxx00        , SWS_PIXEL_F32, 
SWS_UOP_LINEAR          , 0x7, 0x40000, 0xbe318
     DECL_OP %1, f32_linear_xyz_x000x_0x00x_00x0x        , SWS_PIXEL_F32, 
SWS_UOP_LINEAR          , 0x7, 0x40000, 0xbadae
     DECL_OP %1, f32_linear_xyz_x0000_0x000_00x00        , SWS_PIXEL_F32, 
SWS_UOP_LINEAR          , 0x7, 0x40000, 0xbefbe
     DECL_OP %1, f32_linear_xyz_10x0x_1xx0x_1x00x        , SWS_PIXEL_F32, 
SWS_UOP_LINEAR          , 0x7, 0x40421, 0xbb10a
@@ -849,6 +852,7 @@
     DECL_OP %1, f32_linear_fma_xyz_xxx00_xxx0x_xxx0x    , SWS_PIXEL_F32, 
SWS_UOP_LINEAR_FMA      , 0x7, 0x40000, 0xba118, 0xfa118
     DECL_OP %1, f32_linear_fma_xyz_xXX00_XxX0x_XXX0x    , SWS_PIXEL_F32, 
SWS_UOP_LINEAR_FMA      , 0x7, 0x40000, 0xba118, 0xfbdbe
     DECL_OP %1, f32_linear_fma_xyz_xXX00_XXX0x_XXX0x    , SWS_PIXEL_F32, 
SWS_UOP_LINEAR_FMA      , 0x7, 0x40000, 0xba118, 0xfbdfe
+    DECL_OP %1, f32_linear_fma_xyz_xXX00_xXX00_xXX00    , SWS_PIXEL_F32, 
SWS_UOP_LINEAR_FMA      , 0x7, 0x40000, 0xbe318, 0xffbde
     DECL_OP %1, f32_linear_fma_xyz_X000x_0X00x_00X0x    , SWS_PIXEL_F32, 
SWS_UOP_LINEAR_FMA      , 0x7, 0x40000, 0xbadae, 0xfbdef
     DECL_OP %1, f32_linear_fma_xyz_x0000_0x000_00x00    , SWS_PIXEL_F32, 
SWS_UOP_LINEAR_FMA      , 0x7, 0x40000, 0xbefbe, 0xfefbe
     DECL_OP %1, f32_linear_fma_xyz_10X0x_1XX0x_1X00x    , SWS_PIXEL_F32, 
SWS_UOP_LINEAR_FMA      , 0x7, 0x40421, 0xbb10a, 0xfbdef
@@ -898,4 +902,6 @@
 %endmacro
 
 %macro DECL_F32_LUT_1D 1
+    DECL_OP %1, f32_lut_1d_xyz_uniform                  , SWS_PIXEL_F32, 
SWS_UOP_LUT_1D          , 0x7, 0
+    DECL_OP %1, f32_lut_1d_xyz_log                      , SWS_PIXEL_F32, 
SWS_UOP_LUT_1D          , 0x7, 1
 %endmacro
diff --git a/libswscale/x86/uops_macros.h b/libswscale/x86/uops_macros.h
index 47572d10f5f..09976127c7c 100644
--- a/libswscale/x86/uops_macros.h
+++ b/libswscale/x86/uops_macros.h
@@ -1036,16 +1036,20 @@
 #define SWS_FOR_F32_MAX(MACRO, ...) \
     MACRO(__VA_ARGS__, f32_max_x                               , 
SWS_PIXEL_F32, SWS_UOP_MAX             , 0x1) \
     MACRO(__VA_ARGS__, f32_max_xy                              , 
SWS_PIXEL_F32, SWS_UOP_MAX             , 0x3) \
+    MACRO(__VA_ARGS__, f32_max_yz                              , 
SWS_PIXEL_F32, SWS_UOP_MAX             , 0x6) \
     MACRO(__VA_ARGS__, f32_max_xyz                             , 
SWS_PIXEL_F32, SWS_UOP_MAX             , 0x7) \
     MACRO(__VA_ARGS__, f32_max_w                               , 
SWS_PIXEL_F32, SWS_UOP_MAX             , 0x8) \
     MACRO(__VA_ARGS__, f32_max_xw                              , 
SWS_PIXEL_F32, SWS_UOP_MAX             , 0x9) \
+    MACRO(__VA_ARGS__, f32_max_yzw                             , 
SWS_PIXEL_F32, SWS_UOP_MAX             , 0xe) \
     MACRO(__VA_ARGS__, f32_max_xyzw                            , 
SWS_PIXEL_F32, SWS_UOP_MAX             , 0xf)
 #define SWS_FOR_STRUCT_F32_MAX(MACRO, ...) \
     MACRO(__VA_ARGS__, f32_max_x                               , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_MAX             , .mask = 0x1) \
     MACRO(__VA_ARGS__, f32_max_xy                              , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_MAX             , .mask = 0x3) \
+    MACRO(__VA_ARGS__, f32_max_yz                              , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_MAX             , .mask = 0x6) \
     MACRO(__VA_ARGS__, f32_max_xyz                             , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_MAX             , .mask = 0x7) \
     MACRO(__VA_ARGS__, f32_max_w                               , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_MAX             , .mask = 0x8) \
     MACRO(__VA_ARGS__, f32_max_xw                              , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_MAX             , .mask = 0x9) \
+    MACRO(__VA_ARGS__, f32_max_yzw                             , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_MAX             , .mask = 0xe) \
     MACRO(__VA_ARGS__, f32_max_xyzw                            , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_MAX             , .mask = 0xf)
 #define SWS_FOR_F32_UNPACK(MACRO, ...)
 #define SWS_FOR_STRUCT_F32_UNPACK(MACRO, ...)
@@ -1065,6 +1069,7 @@
     MACRO(__VA_ARGS__, f32_linear_xyz_xxx0x_xxx0x_xxx0x        , 
SWS_PIXEL_F32, SWS_UOP_LINEAR          , 0x7, 0x40000, 0xba108) \
     MACRO(__VA_ARGS__, f32_linear_xyz_x0x0x_xxx0x_xx00x        , 
SWS_PIXEL_F32, SWS_UOP_LINEAR          , 0x7, 0x40000, 0xbb10a) \
     MACRO(__VA_ARGS__, f32_linear_xyz_xxx00_xxx0x_xxx0x        , 
SWS_PIXEL_F32, SWS_UOP_LINEAR          , 0x7, 0x40000, 0xba118) \
+    MACRO(__VA_ARGS__, f32_linear_xyz_xxx00_xxx00_xxx00        , 
SWS_PIXEL_F32, SWS_UOP_LINEAR          , 0x7, 0x40000, 0xbe318) \
     MACRO(__VA_ARGS__, f32_linear_xyz_x000x_0x00x_00x0x        , 
SWS_PIXEL_F32, SWS_UOP_LINEAR          , 0x7, 0x40000, 0xbadae) \
     MACRO(__VA_ARGS__, f32_linear_xyz_x0000_0x000_00x00        , 
SWS_PIXEL_F32, SWS_UOP_LINEAR          , 0x7, 0x40000, 0xbefbe) \
     MACRO(__VA_ARGS__, f32_linear_xyz_10x0x_1xx0x_1x00x        , 
SWS_PIXEL_F32, SWS_UOP_LINEAR          , 0x7, 0x40421, 0xbb10a) \
@@ -1082,6 +1087,7 @@
     MACRO(__VA_ARGS__, f32_linear_xyz_xxx0x_xxx0x_xxx0x        , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR          , .mask = 0x7, .par.lin.one = 
0x40000, .par.lin.zero = 0xba108) \
     MACRO(__VA_ARGS__, f32_linear_xyz_x0x0x_xxx0x_xx00x        , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR          , .mask = 0x7, .par.lin.one = 
0x40000, .par.lin.zero = 0xbb10a) \
     MACRO(__VA_ARGS__, f32_linear_xyz_xxx00_xxx0x_xxx0x        , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR          , .mask = 0x7, .par.lin.one = 
0x40000, .par.lin.zero = 0xba118) \
+    MACRO(__VA_ARGS__, f32_linear_xyz_xxx00_xxx00_xxx00        , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR          , .mask = 0x7, .par.lin.one = 
0x40000, .par.lin.zero = 0xbe318) \
     MACRO(__VA_ARGS__, f32_linear_xyz_x000x_0x00x_00x0x        , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR          , .mask = 0x7, .par.lin.one = 
0x40000, .par.lin.zero = 0xbadae) \
     MACRO(__VA_ARGS__, f32_linear_xyz_x0000_0x000_00x00        , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR          , .mask = 0x7, .par.lin.one = 
0x40000, .par.lin.zero = 0xbefbe) \
     MACRO(__VA_ARGS__, f32_linear_xyz_10x0x_1xx0x_1x00x        , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR          , .mask = 0x7, .par.lin.one = 
0x40421, .par.lin.zero = 0xbb10a) \
@@ -1108,6 +1114,7 @@
     MACRO(__VA_ARGS__, f32_linear_fma_xyz_xxx00_xxx0x_xxx0x    , 
SWS_PIXEL_F32, SWS_UOP_LINEAR_FMA      , 0x7, 0x40000, 0xba118, 0xfa118) \
     MACRO(__VA_ARGS__, f32_linear_fma_xyz_xXX00_XxX0x_XXX0x    , 
SWS_PIXEL_F32, SWS_UOP_LINEAR_FMA      , 0x7, 0x40000, 0xba118, 0xfbdbe) \
     MACRO(__VA_ARGS__, f32_linear_fma_xyz_xXX00_XXX0x_XXX0x    , 
SWS_PIXEL_F32, SWS_UOP_LINEAR_FMA      , 0x7, 0x40000, 0xba118, 0xfbdfe) \
+    MACRO(__VA_ARGS__, f32_linear_fma_xyz_xXX00_xXX00_xXX00    , 
SWS_PIXEL_F32, SWS_UOP_LINEAR_FMA      , 0x7, 0x40000, 0xbe318, 0xffbde) \
     MACRO(__VA_ARGS__, f32_linear_fma_xyz_X000x_0X00x_00X0x    , 
SWS_PIXEL_F32, SWS_UOP_LINEAR_FMA      , 0x7, 0x40000, 0xbadae, 0xfbdef) \
     MACRO(__VA_ARGS__, f32_linear_fma_xyz_x0000_0x000_00x00    , 
SWS_PIXEL_F32, SWS_UOP_LINEAR_FMA      , 0x7, 0x40000, 0xbefbe, 0xfefbe) \
     MACRO(__VA_ARGS__, f32_linear_fma_xyz_10X0x_1XX0x_1X00x    , 
SWS_PIXEL_F32, SWS_UOP_LINEAR_FMA      , 0x7, 0x40421, 0xbb10a, 0xfbdef) \
@@ -1137,6 +1144,7 @@
     MACRO(__VA_ARGS__, f32_linear_fma_xyz_xxx00_xxx0x_xxx0x    , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR_FMA      , .mask = 0x7, .par.lin.one = 
0x40000, .par.lin.zero = 0xba118, .par.lin.exact = 0xfa118) \
     MACRO(__VA_ARGS__, f32_linear_fma_xyz_xXX00_XxX0x_XXX0x    , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR_FMA      , .mask = 0x7, .par.lin.one = 
0x40000, .par.lin.zero = 0xba118, .par.lin.exact = 0xfbdbe) \
     MACRO(__VA_ARGS__, f32_linear_fma_xyz_xXX00_XXX0x_XXX0x    , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR_FMA      , .mask = 0x7, .par.lin.one = 
0x40000, .par.lin.zero = 0xba118, .par.lin.exact = 0xfbdfe) \
+    MACRO(__VA_ARGS__, f32_linear_fma_xyz_xXX00_xXX00_xXX00    , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR_FMA      , .mask = 0x7, .par.lin.one = 
0x40000, .par.lin.zero = 0xbe318, .par.lin.exact = 0xffbde) \
     MACRO(__VA_ARGS__, f32_linear_fma_xyz_X000x_0X00x_00X0x    , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR_FMA      , .mask = 0x7, .par.lin.one = 
0x40000, .par.lin.zero = 0xbadae, .par.lin.exact = 0xfbdef) \
     MACRO(__VA_ARGS__, f32_linear_fma_xyz_x0000_0x000_00x00    , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR_FMA      , .mask = 0x7, .par.lin.one = 
0x40000, .par.lin.zero = 0xbefbe, .par.lin.exact = 0xfefbe) \
     MACRO(__VA_ARGS__, f32_linear_fma_xyz_10X0x_1XX0x_1X00x    , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LINEAR_FMA      , .mask = 0x7, .par.lin.one = 
0x40421, .par.lin.zero = 0xbb10a, .par.lin.exact = 0xfbdef) \
@@ -1209,7 +1217,11 @@
     MACRO(__VA_ARGS__, f32_lut_3d_xyz_dynamic                  , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LUT_3D          , .mask = 0x7, .par.lut3d.dynamic 
= 1) \
     MACRO(__VA_ARGS__, f32_lut_3d_xyzw_static                  , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LUT_3D          , .mask = 0xf, .par.lut3d.dynamic 
= 0) \
     MACRO(__VA_ARGS__, f32_lut_3d_xyzw_dynamic                 , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LUT_3D          , .mask = 0xf, .par.lut3d.dynamic 
= 1)
-#define SWS_FOR_F32_LUT_1D(MACRO, ...)
-#define SWS_FOR_STRUCT_F32_LUT_1D(MACRO, ...)
+#define SWS_FOR_F32_LUT_1D(MACRO, ...) \
+    MACRO(__VA_ARGS__, f32_lut_1d_xyz_uniform                  , 
SWS_PIXEL_F32, SWS_UOP_LUT_1D          , 0x7, 0) \
+    MACRO(__VA_ARGS__, f32_lut_1d_xyz_log                      , 
SWS_PIXEL_F32, SWS_UOP_LUT_1D          , 0x7, 1)
+#define SWS_FOR_STRUCT_F32_LUT_1D(MACRO, ...) \
+    MACRO(__VA_ARGS__, f32_lut_1d_xyz_uniform                  , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LUT_1D          , .mask = 0x7, .par.lut1d.log = 
0) \
+    MACRO(__VA_ARGS__, f32_lut_1d_xyz_log                      , .type = 
SWS_PIXEL_F32, .uop = SWS_UOP_LUT_1D          , .mask = 0x7, .par.lut1d.log = 1)
 
 #endif /* SWSCALE_X86_UOPS_MACROS_H */
diff --git a/tests/ref/fate/sws-ops-list b/tests/ref/fate/sws-ops-list
index 89652b8541a..c262521cd74 100644
--- a/tests/ref/fate/sws-ops-list
+++ b/tests/ref/fate/sws-ops-list
@@ -1 +1 @@
-4e6b06a456afad8365188ff8ff897b79
+d84d5be444a53188452e91b95247c225
-- 
2.54.0


From 05de3d146030fa47e7bef59edf439f3399118229 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Mon, 2 Nov 2026 09:01:24 +0100
Subject: [PATCH 8/8] swscale/graph: skip the 3DLUT for an exact color map
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The ops backends convert an exact color map with tables of the
transfers and a matrix in linear light, which the 3DLUT could only
approximate. Keep the map with the graph and generate the 3DLUT only for
a map that is not exact, or when the legacy backend takes the
conversion, which maps colors with the 3DLUT only.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libswscale/graph.c | 55 ++++++++++++++++++++++++++++++++++++----------
 libswscale/graph.h |  7 ++++++
 2 files changed, 50 insertions(+), 12 deletions(-)

diff --git a/libswscale/graph.c b/libswscale/graph.c
index f4ae0f5871d..e36088dcf4f 100644
--- a/libswscale/graph.c
+++ b/libswscale/graph.c
@@ -733,19 +733,45 @@ static bool prefer_ops_backend(SwsContext *ctx, const 
SwsFormat *src, const SwsF
     return false; /* default to legacy for stability reasons */
 }
 
+static int graph_lut3d(SwsGraph *graph)
+{
+    if (graph->lut3d)
+        return 0;
+
+    graph->lut3d = ff_sws_lut3d_alloc();
+    if (!graph->lut3d)
+        return AVERROR(ENOMEM);
+
+    return ff_sws_lut3d_generate(graph->lut3d, &graph->map);
+}
+
+static int add_legacy_convert_pass(SwsGraph *graph, const SwsFormat *src,
+                                   const SwsFormat *dst, SwsPass *input,
+                                   SwsPass **output)
+{
+    if (graph->mapped) {
+        int ret = graph_lut3d(graph);
+        if (ret < 0)
+            return ret;
+    }
+
+    return add_legacy_sws_pass(graph, src, dst, graph->lut3d, input, output);
+}
+
 static int add_convert_pass(SwsGraph *graph, const SwsFormat *src,
-                            const SwsFormat *dst, const SwsLut3D *lut3d,
-                            SwsPass *input, SwsPass **output)
+                            const SwsFormat *dst, SwsPass *input,
+                            SwsPass **output)
 {
     SwsContext *ctx = graph->ctx;
+    const SwsLut3D *lut3d = graph->map_exact ? NULL : graph->lut3d;
     int ret;
 
     if (prefer_ops_backend(ctx, src, dst)) {
         ret = add_ops_convert_pass(graph, src, dst, lut3d, input, output);
         if (ret == AVERROR(ENOTSUP))
-            ret = add_legacy_sws_pass(graph, src, dst, lut3d, input, output);
+            ret = add_legacy_convert_pass(graph, src, dst, input, output);
     } else {
-        ret = add_legacy_sws_pass(graph, src, dst, lut3d, input, output);
+        ret = add_legacy_convert_pass(graph, src, dst, input, output);
         if (ret == AVERROR(ENOTSUP))
             ret = add_ops_convert_pass(graph, src, dst, lut3d, input, output);
     }
@@ -757,7 +783,7 @@ static int add_convert_pass(SwsGraph *graph, const 
SwsFormat *src,
  * Gamut and tone mapping *
  **************************/
 
-static int generate_3dlut(SwsGraph *graph, SwsFormat *src, SwsFormat *dst)
+static int init_color_map(SwsGraph *graph, SwsFormat *src, SwsFormat *dst)
 {
     SwsColorMap map = {0};
 
@@ -786,11 +812,16 @@ static int generate_3dlut(SwsGraph *graph, SwsFormat 
*src, SwsFormat *dst)
     if (src->hw_format != AV_PIX_FMT_NONE || dst->hw_format != AV_PIX_FMT_NONE)
         return AVERROR(ENOTSUP);
 
-    graph->lut3d = ff_sws_lut3d_alloc();
-    if (!graph->lut3d)
-        return AVERROR(ENOMEM);
+    graph->map    = map;
+    graph->mapped = true;
 
-    return ff_sws_lut3d_generate(graph->lut3d, &map);
+    /* The ops backends convert an exact map with tables and a matrix, the
+     * legacy backend needs the 3DLUT of any map */
+    graph->map_exact = ff_sws_color_map_exact(&map);
+    if (graph->map_exact)
+        return 0;
+
+    return graph_lut3d(graph);
 }
 
 /***************************************
@@ -806,12 +837,12 @@ static int init_passes(SwsGraph *graph)
 
     graph->incomplete |= ff_infer_chroma_loc(&src, &dst);
 
-    ret = generate_3dlut(graph, &src, &dst);
+    ret = init_color_map(graph, &src, &dst);
     if (ret < 0)
         return ret;
 
-    if (!ff_fmt_equal(&src, &dst) || graph->lut3d) {
-        ret = add_convert_pass(graph, &src, &dst, graph->lut3d, pass, &pass);
+    if (!ff_fmt_equal(&src, &dst) || graph->mapped) {
+        ret = add_convert_pass(graph, &src, &dst, pass, &pass);
         if (ret < 0)
             return ret;
     }
diff --git a/libswscale/graph.h b/libswscale/graph.h
index 7380a991ce5..9d19d4d7aed 100644
--- a/libswscale/graph.h
+++ b/libswscale/graph.h
@@ -169,6 +169,13 @@ typedef struct SwsGraph {
      */
     SwsLut3D *lut3d; /* refstruct */
 
+    /**
+     * Color map of the conversion when the colors differ, and whether the ops
+     * backends convert it exactly, without the 3DLUT.
+     */
+    SwsColorMap map;
+    bool mapped, map_exact;
+
     /**
      * Temporary execution state inside ff_sws_graph_run(); used to pass
      * data to worker threads.
-- 
2.54.0

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

Reply via email to