PR #25290 opened by Forgejo_Fairy
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25290
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25290.patch

Converting GBRPF16/GBRAPF16 to float32 currently passes through integer 
RGB-to-YUV conversion, clipping components to [0, 1] and quantizing them. This 
breaks half-float EXR pipelines after 0e917389fe73. The sample in #25027 should 
produce `(0.5, 2.0, 0.5, 1.5, 0.5, 4.0)`, but its highlights are clipped and 
even 0.5 changes.

Add a direct unscaled conversion to GBRPF32/GBRAPF32 using the existing 
half-to-float helper. Support both byte orders, preserve alpha, and fill newly 
added alpha with 1.0. Initialize the conversion tables for this path, which 
bypasses normal filter initialization. The change covers format conversion at 
unchanged dimensions.

Add `fate-sws-float16`, covering every binary16 bit pattern across endian and 
alpha combinations, whole-frame and sliced conversion, negative strides, and 
single-/multi-threaded frame conversion. Expected checksums are stored in 
`tests/ref/fate/sws-float16`; NaN representations are normalized for 
portability. The references were independently checked using Python's binary16 
and float32 packing support.

Validation:

- The attached EXR and equivalent rawvideo input produce the exact expected 
values with the patch.
- The new FATE test fails without the conversion fix and passes with it.
- `fate-sws-float16`, `fate-sws-unscaled-threaded`, `fate-sws-pixdesc-query`, 
and `fate-sws-floatimg-cmp` pass on AArch64 and x86-64, with ASan/UBSan enabled 
on x86-64. These exercise native and table-based half conversion respectively.

Fixes #25027.


>From 5a3ac9a62412e7b56a57fd2fe16c40ca310ed7e3 Mon Sep 17 00:00:00 2001
From: Michael Niedermayer <[email protected]>
Date: Wed, 21 Oct 2026 06:02:59 +0000
Subject: [PATCH] swscale: preserve HDR values in planar half-to-float
 conversion

Promote GBRPF16/GBRAPF16 directly to GBRPF32/GBRAPF32 when the
image dimensions are unchanged. The generic input path clips RGB
components to [0, 1] and quantizes them before a YUV round trip,
losing scene-linear highlights even for a format-only conversion.

Handle both byte orders, preserve an existing alpha channel and fill
new alpha with 1.0. Initialize the half conversion tables for the
unscaled path, which bypasses the normal filter initialization.

Add a sample-free FATE test covering all half-float bit patterns,
alpha addition/removal, endian combinations, both slice directions,
negative strides and single-/multi-threaded frame conversion. Normalize
NaN representations in the checksum for portability; expected output
is stored in tests/ref/fate/sws-float16.

Fixes: #25027
Assisted-by: Fairy
---
 libswscale/Makefile           |   1 +
 libswscale/swscale_unscaled.c |  42 +++++++++
 libswscale/tests/float16.c    | 165 ++++++++++++++++++++++++++++++++++
 libswscale/utils.c            |   6 ++
 tests/fate/libswscale.mak     |   4 +
 tests/ref/fate/sws-float16    |  16 ++++
 6 files changed, 234 insertions(+)
 create mode 100644 libswscale/tests/float16.c
 create mode 100644 tests/ref/fate/sws-float16

diff --git a/libswscale/Makefile b/libswscale/Makefile
index 4c12c888c67..f2875f40d59 100644
--- a/libswscale/Makefile
+++ b/libswscale/Makefile
@@ -50,6 +50,7 @@ SHLIBOBJS                    += log2_tab.o half2float.o
 SHLIBOBJS-$(HAVE_GNU_WINDRES) += swscaleres.o
 
 TESTPROGS = colorspace                                                  \
+            float16                                                     \
             floatimg_cmp                                                \
             pixdesc_query                                               \
             swscale                                                     \
diff --git a/libswscale/swscale_unscaled.c b/libswscale/swscale_unscaled.c
index ccd96fb204e..bea00f26b72 100644
--- a/libswscale/swscale_unscaled.c
+++ b/libswscale/swscale_unscaled.c
@@ -2092,6 +2092,42 @@ static int yvu9ToYv12Wrapper(SwsInternal *c, const 
uint8_t *const src[],
     return srcSliceH;
 }
 
+/* Keep HDR values intact when promoting planar half-float RGB. */
+static int planar_rgbf16_to_rgbf32_wrapper(SwsInternal *c,
+                                         const uint8_t *const src[],
+                                         const int srcStride[], int srcSliceY,
+                                         int srcSliceH, uint8_t *const dst[],
+                                         const int dstStride[])
+{
+    const int src_be = isBE(c->opts.src_format);
+    const int dst_be = isBE(c->opts.dst_format);
+    const int planes = isALPHA(c->opts.dst_format) ? 4 : 3;
+
+    for (int p = 0; p < planes; p++) {
+        const int fill_alpha = p == 3 && !isALPHA(c->opts.src_format);
+        const uint8_t *s = src[p];
+        uint8_t *d = dst[p] + (ptrdiff_t)srcSliceY * dstStride[p];
+
+        for (int y = 0; y < srcSliceH; y++) {
+            for (int x = 0; x < c->opts.src_w; x++) {
+                uint32_t f = av_float2int(1.0f);
+                if (!fill_alpha) {
+                    uint16_t h = src_be ? AV_RB16(s + 2 * x) : AV_RL16(s + 2 * 
x);
+                    f = half2float(h, c->h2f_tables);
+                }
+                if (dst_be)
+                    AV_WB32(d + 4 * x, f);
+                else
+                    AV_WL32(d + 4 * x, f);
+            }
+            if (!fill_alpha)
+                s += srcStride[p];
+            d += dstStride[p];
+        }
+    }
+    return srcSliceH;
+}
+
 static int uint_y_to_float_y_wrapper(SwsInternal *c, const uint8_t *const 
src[],
                                      const int srcStride[], int srcSliceY,
                                      int srcSliceH, uint8_t *const dst[], 
const int dstStride[])
@@ -2636,6 +2672,12 @@ void ff_get_unscaled_swscale(SwsInternal *c)
             c->convert_unscaled = yuv422pToUyvyWrapper;
     }
 
+    /* Planar half-float RGB to single-precision RGB, without an integer
+     * intermediate or range clipping. */
+    if (isPlanarRGB(srcFormat) && isFloat16(srcFormat) &&
+        isPlanarRGB(dstFormat) && isFloat(dstFormat) && !isFloat16(dstFormat))
+        c->convert_unscaled = planar_rgbf16_to_rgbf32_wrapper;
+
     /* uint Y to float Y */
     if (srcFormat == AV_PIX_FMT_GRAY8 && dstFormat == AV_PIX_FMT_GRAYF32){
         c->convert_unscaled = uint_y_to_float_y_wrapper;
diff --git a/libswscale/tests/float16.c b/libswscale/tests/float16.c
new file mode 100644
index 00000000000..46c1d6a1d8a
--- /dev/null
+++ b/libswscale/tests/float16.c
@@ -0,0 +1,165 @@
+/*
+ * 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 <inttypes.h>
+#include <stdio.h>
+
+#include "libavutil/adler32.h"
+#include "libavutil/frame.h"
+#include "libavutil/intreadwrite.h"
+#include "libavutil/opt.h"
+#include "libavutil/pixdesc.h"
+#include "libswscale/swscale.h"
+
+#define WIDTH  257
+#define HEIGHT 256
+
+static AVFrame *alloc_frame(enum AVPixelFormat format)
+{
+    AVFrame *frame = av_frame_alloc();
+    if (!frame)
+        return NULL;
+    frame->format = format;
+    frame->width = WIDTH;
+    frame->height = HEIGHT;
+    frame->colorspace = AVCOL_SPC_RGB;
+    frame->color_range = AVCOL_RANGE_JPEG;
+    if (av_frame_get_buffer(frame, 32) < 0)
+        av_frame_free(&frame);
+    return frame;
+}
+
+static void flip_frame(AVFrame *frame)
+{
+    for (int p = 0; p < 4 && frame->data[p]; p++) {
+        frame->data[p] += (HEIGHT - 1) * frame->linesize[p];
+        frame->linesize[p] = -frame->linesize[p];
+    }
+}
+
+static int test(enum AVPixelFormat src_fmt, enum AVPixelFormat dst_fmt, int 
mode)
+{
+    const AVPixFmtDescriptor *sd = av_pix_fmt_desc_get(src_fmt);
+    const AVPixFmtDescriptor *dd = av_pix_fmt_desc_get(dst_fmt);
+    AVFrame *src = alloc_frame(src_fmt);
+    AVFrame *dst = alloc_frame(dst_fmt);
+    struct SwsContext *sws = NULL;
+    unsigned long checksum = 1;
+    int ret = AVERROR(ENOMEM);
+
+    if (!src || !dst)
+        goto end;
+    /* Exercise every half-float bit pattern in every plane, with different
+     * plane offsets to detect swapped channels. WIDTH also tests row padding. 
*/
+    for (int p = 0; p < sd->nb_components; p++) {
+        for (int y = 0; y < HEIGHT; y++) {
+            for (int x = 0; x < WIDTH; x++) {
+                uint8_t *s = src->data[p] + y * src->linesize[p] + 2 * x;
+                uint16_t h = y * WIDTH + x + p * 103;
+                if (sd->flags & AV_PIX_FMT_FLAG_BE)
+                    AV_WB16(s, h);
+                else
+                    AV_WL16(s, h);
+            }
+        }
+    }
+
+    if (mode >= 4) {
+        sws = sws_alloc_context();
+        if (!sws)
+            goto end;
+        av_opt_set_int(sws, "threads", mode == 4 ? 1 : 4, 0);
+        ret = sws_scale_frame(sws, dst, src);
+        if (ret < 0)
+            goto end;
+    } else {
+        sws = sws_getContext(WIDTH, HEIGHT, src_fmt, WIDTH, HEIGHT, dst_fmt,
+                             SWS_BILINEAR, NULL, NULL, NULL);
+        if (!sws)
+            goto end;
+        if (mode == 3) {
+            flip_frame(src);
+            flip_frame(dst);
+        }
+        for (int y = 0; y < HEIGHT;) {
+            const int h = mode == 0 ? HEIGHT : FFMIN(7, HEIGHT - y);
+            const int slice_y = mode == 2 ? HEIGHT - y - h : y;
+            const uint8_t *slice[4] = { 0 };
+            for (int p = 0; p < sd->nb_components; p++)
+                slice[p] = src->data[p] + slice_y * src->linesize[p];
+            ret = sws_scale(sws, slice, src->linesize, slice_y, h,
+                            dst->data, dst->linesize);
+            if (ret < 0)
+                goto end;
+            y += h;
+        }
+        if (mode == 3) {
+            flip_frame(src);
+            flip_frame(dst);
+        }
+    }
+
+    for (int p = 0; p < dd->nb_components; p++) {
+        for (int y = 0; y < HEIGHT; y++) {
+            for (int x = 0; x < WIDTH; x++) {
+                const uint8_t *d = dst->data[p] + y * dst->linesize[p] + 4 * x;
+                uint32_t f = dd->flags & AV_PIX_FMT_FLAG_BE ? AV_RB32(d) : 
AV_RL32(d);
+                uint8_t bytes[4];
+                /* Hardware and table conversions may quiet NaNs differently.
+                 * Normalize their payload and sign for a portable reference. 
*/
+                if ((f & 0x7fffffff) > 0x7f800000)
+                    f = 0x7fc00000;
+                AV_WL32(bytes, f);
+                checksum = av_adler32_update(checksum, bytes, sizeof(bytes));
+            }
+        }
+    }
+    printf(" %08lx", checksum);
+    ret = 0;
+end:
+    sws_freeContext(sws);
+    av_frame_free(&src);
+    av_frame_free(&dst);
+    return ret;
+}
+
+int main(void)
+{
+    static const enum AVPixelFormat inputs[] = {
+        AV_PIX_FMT_GBRPF16LE, AV_PIX_FMT_GBRPF16BE,
+        AV_PIX_FMT_GBRAPF16LE, AV_PIX_FMT_GBRAPF16BE,
+    };
+    static const enum AVPixelFormat outputs[] = {
+        AV_PIX_FMT_GBRPF32LE, AV_PIX_FMT_GBRPF32BE,
+        AV_PIX_FMT_GBRAPF32LE, AV_PIX_FMT_GBRAPF32BE,
+    };
+
+    for (int i = 0; i < FF_ARRAY_ELEMS(inputs); i++) {
+        for (int j = 0; j < FF_ARRAY_ELEMS(outputs); j++) {
+            printf("%s -> %s:", av_get_pix_fmt_name(inputs[i]),
+                   av_get_pix_fmt_name(outputs[j]));
+            /* Whole image, top-down slices, bottom-up slices, negative 
strides,
+             * and the frame API with one and four threads, respectively. */
+            for (int mode = 0; mode < 6; mode++)
+                if (test(inputs[i], outputs[j], mode) < 0)
+                    return 1;
+            printf("\n");
+        }
+    }
+    return 0;
+}
diff --git a/libswscale/utils.c b/libswscale/utils.c
index e970b9f035c..9127a58aed3 100644
--- a/libswscale/utils.c
+++ b/libswscale/utils.c
@@ -1675,6 +1675,12 @@ av_cold int ff_sws_init_single_context(SwsContext *sws, 
SwsFilter *srcFilter,
         ff_get_unscaled_swscale(c);
 
         if (c->convert_unscaled) {
+            if (isFloat16(srcFormat) && isFloat(dstFormat) && 
!isFloat16(dstFormat)) {
+                c->h2f_tables = av_malloc(sizeof(*c->h2f_tables));
+                if (!c->h2f_tables)
+                    return AVERROR(ENOMEM);
+                ff_init_half2float_tables(c->h2f_tables);
+            }
             if (flags & SWS_PRINT_INFO)
                 av_log(c, AV_LOG_INFO,
                        "using unscaled %s -> %s special converter\n",
diff --git a/tests/fate/libswscale.mak b/tests/fate/libswscale.mak
index 2542869f57c..130d2b3dfd4 100644
--- a/tests/fate/libswscale.mak
+++ b/tests/fate/libswscale.mak
@@ -1,3 +1,7 @@
+FATE_LIBSWSCALE += fate-sws-float16
+fate-sws-float16: libswscale/tests/float16$(EXESUF)
+fate-sws-float16: CMD = run libswscale/tests/float16$(EXESUF)
+
 FATE_LIBSWSCALE += fate-sws-unscaled-threaded
 fate-sws-unscaled-threaded: libswscale/tests/unscaled_threaded$(EXESUF)
 fate-sws-unscaled-threaded: CMD = run 
libswscale/tests/unscaled_threaded$(EXESUF)
diff --git a/tests/ref/fate/sws-float16 b/tests/ref/fate/sws-float16
new file mode 100644
index 00000000000..d8fc898bba3
--- /dev/null
+++ b/tests/ref/fate/sws-float16
@@ -0,0 +1,16 @@
+gbrpf16le -> gbrpf32le: e83ae078 e83ae078 e83ae078 e83ae078 e83ae078 e83ae078
+gbrpf16le -> gbrpf32be: e83ae078 e83ae078 e83ae078 e83ae078 e83ae078 e83ae078
+gbrpf16le -> gbrapf32le: 8634aab8 8634aab8 8634aab8 8634aab8 8634aab8 8634aab8
+gbrpf16le -> gbrapf32be: 8634aab8 8634aab8 8634aab8 8634aab8 8634aab8 8634aab8
+gbrpf16be -> gbrpf32le: e83ae078 e83ae078 e83ae078 e83ae078 e83ae078 e83ae078
+gbrpf16be -> gbrpf32be: e83ae078 e83ae078 e83ae078 e83ae078 e83ae078 e83ae078
+gbrpf16be -> gbrapf32le: 8634aab8 8634aab8 8634aab8 8634aab8 8634aab8 8634aab8
+gbrpf16be -> gbrapf32be: 8634aab8 8634aab8 8634aab8 8634aab8 8634aab8 8634aab8
+gbrapf16le -> gbrpf32le: e83ae078 e83ae078 e83ae078 e83ae078 e83ae078 e83ae078
+gbrapf16le -> gbrpf32be: e83ae078 e83ae078 e83ae078 e83ae078 e83ae078 e83ae078
+gbrapf16le -> gbrapf32le: 227d26f7 227d26f7 227d26f7 227d26f7 227d26f7 227d26f7
+gbrapf16le -> gbrapf32be: 227d26f7 227d26f7 227d26f7 227d26f7 227d26f7 227d26f7
+gbrapf16be -> gbrpf32le: e83ae078 e83ae078 e83ae078 e83ae078 e83ae078 e83ae078
+gbrapf16be -> gbrpf32be: e83ae078 e83ae078 e83ae078 e83ae078 e83ae078 e83ae078
+gbrapf16be -> gbrapf32le: 227d26f7 227d26f7 227d26f7 227d26f7 227d26f7 227d26f7
+gbrapf16be -> gbrapf32be: 227d26f7 227d26f7 227d26f7 227d26f7 227d26f7 227d26f7
-- 
2.54.0

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

Reply via email to