[PR] swscale/graph: fix missing chroma rows in threaded frame copies (PR #24261)
Kevin Watters via ffmpeg-devel <[email protected]>
| Newsgroups | gmane.comp.video.ffmpeg.devel |
|---|---|
| Message-ID | <[email protected]> |
PR #24261 opened by Kevin Watters (kevinw)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24261
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24261.patch
run_copy() calculated each plane's copy height from the height of the current luma slice. For vertically subsampled formats, this rounds every slice down independently. If a slice boundary falls on an odd luma row, the discarded remainder is lost and a chroma row may not be copied. The same problem occurs at the end of an odd-height frame.
For example, this command produces regular horizontal green lines:
```
ffmpeg -f lavfi -i "testsrc2=size=720x480:rate=1" \
-vf "scale=iw:ih:out_primaries=bt709:threads=13,format=yuv420p,scale=iw:ih:threads=1,format=rgb24" \
-frames:v 1 -update 1 -y repro.png
```
# Summary of changes
Calculate the plane range from the absolute start and end of the slice instead. Internal boundaries round down consistently, while the final frame boundary rounds up to include the last chroma row.
Add a regression test covering two-thread YUV420P copies with odd slice boundaries, odd frame heights, caller-allocated output, and unequal strides.
Fixes: bf738412e849 ("swscale/graph: add new high-level scaler dispatch mechanism")
>From 877bea6b9a1a3390218933b323946f1477308d4b Mon Sep 17 00:00:00 2001
From: Kevin Watters <[email protected]>
Date: Tue, 25 Aug 2026 09:36:53 +0200
Subject: [PATCH] swscale/graph: fix missing chroma rows in threaded frame
copies
run_copy() calculated each plane's copy height from the height of the
current luma slice. For vertically subsampled formats, this rounds every
slice down independently. If a slice boundary falls on an odd luma row,
the discarded remainder is lost and a chroma row may not be copied. The
same problem occurs at the end of an odd-height frame.
For example, this command produces regular horizontal green lines:
ffmpeg -f lavfi -i "testsrc2=size=720x480:rate=1" \
-vf "scale=iw:ih:out_primaries=bt709:threads=13,format=yuv420p,scale=iw:ih:threads=1,format=rgb24" \
-frames:v 1 -update 1 -y repro.png
Calculate the plane range from the absolute start and end of the slice
instead. Internal boundaries round down consistently, while the final
frame boundary rounds up to include the last chroma row.
Add a regression test covering two-thread YUV420P copies with odd slice
boundaries, odd frame heights, caller-allocated output, and unequal
strides.
Fixes: bf738412e849 ("swscale/graph: add new high-level scaler dispatch mechanism")
Signed-off-by: Kevin Watters <[email protected]>
---
libswscale/Makefile | 1 +
libswscale/graph.c | 8 ++-
libswscale/tests/.gitignore | 1 +
libswscale/tests/frame_copy.c | 92 +++++++++++++++++++++++++++++++++++
tests/fate/libswscale.mak | 5 ++
5 files changed, 106 insertions(+), 1 deletion(-)
create mode 100644 libswscale/tests/frame_copy.c
diff --git a/libswscale/Makefile b/libswscale/Makefile
index b53b815309..cc700e0a96 100644
--- a/libswscale/Makefile
+++ b/libswscale/Makefile
@@ -51,6 +51,7 @@ SHLIBOBJS-$(HAVE_GNU_WINDRES) += swscaleres.o
TESTPROGS = colorspace \
floatimg_cmp \
+ frame_copy \
pixdesc_query \
swscale \
diff --git a/libswscale/graph.c b/libswscale/graph.c
index 558d422531..0a754b0e66 100644
--- a/libswscale/graph.c
+++ b/libswscale/graph.c
@@ -287,7 +287,13 @@ static void run_copy(const SwsFrame *out, const SwsFrame *in, int y, int h,
frame_shift(out, y, out_data);
for (int i = 0; i < 4 && out_data[i]; i++) {
- const int lines = h >> ff_fmt_vshift(in->format, i);
+ const int vshift = ff_fmt_vshift(in->format, i);
+ const int slice_end = y + h;
+ const int plane_start = y >> vshift;
+ const int plane_end = slice_end == out->height
+ ? AV_CEIL_RSHIFT(slice_end, vshift)
+ : slice_end >> vshift;
+ const int lines = plane_end - plane_start;
av_assert1(in_data[i]);
if (in_data[i] == out_data[i]) {
diff --git a/libswscale/tests/.gitignore b/libswscale/tests/.gitignore
index f27fc891f5..e127ea3abb 100644
--- a/libswscale/tests/.gitignore
+++ b/libswscale/tests/.gitignore
@@ -1,5 +1,6 @@
/colorspace
/floatimg_cmp
+/frame_copy
/pixdesc_query
/swscale
/sws_ops
diff --git a/libswscale/tests/frame_copy.c b/libswscale/tests/frame_copy.c
new file mode 100644
index 0000000000..7cbdb62ffc
--- /dev/null
+++ b/libswscale/tests/frame_copy.c
@@ -0,0 +1,92 @@
+/*
+ * 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 <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "libavutil/error.h"
+#include "libavutil/frame.h"
+#include "libavutil/log.h"
+#include "libavutil/pixfmt.h"
+#include "libswscale/swscale.h"
+
+static int run_test(int width, int height, int src_align, int dst_align)
+{
+ AVFrame *src = av_frame_alloc();
+ AVFrame *dst = av_frame_alloc();
+ SwsContext *sws = sws_alloc_context();
+ int ret = AVERROR(ENOMEM);
+
+ if (!src || !dst || !sws)
+ goto end;
+
+ src->format = dst->format = AV_PIX_FMT_YUV420P;
+ src->width = dst->width = width;
+ src->height = dst->height = height;
+ if ((ret = av_frame_get_buffer(src, src_align)) < 0 ||
+ (ret = av_frame_get_buffer(dst, dst_align)) < 0)
+ goto end;
+
+ if (src_align != dst_align &&
+ src->linesize[0] == dst->linesize[0]) {
+ av_log(sws, AV_LOG_ERROR, "failed to create unequal strides\n");
+ ret = AVERROR(EINVAL);
+ goto end;
+ }
+
+ memset(src->buf[0]->data, 1, src->buf[0]->size);
+ memset(dst->buf[0]->data, 0, dst->buf[0]->size);
+
+ sws->threads = 2;
+ ret = sws_scale_frame(sws, dst, src);
+ if (ret < 0)
+ goto end;
+
+ for (int plane = 0; plane < 3; plane++) {
+ const int plane_width = plane ? width >> 1 : width;
+ const int plane_height = plane ? (height + 1) >> 1 : height;
+
+ for (int y = 0; y < plane_height; y++) {
+ if (memcmp(src->data[plane] + (ptrdiff_t)y * src->linesize[plane],
+ dst->data[plane] + (ptrdiff_t)y * dst->linesize[plane],
+ plane_width)) {
+ av_log(sws, AV_LOG_ERROR,
+ "mismatch in plane %d, row %d\n", plane, y);
+ ret = AVERROR_INVALIDDATA;
+ goto end;
+ }
+ }
+ }
+
+end:
+ sws_free_context(&sws);
+ av_frame_free(&dst);
+ av_frame_free(&src);
+ return ret;
+}
+
+int main(void)
+{
+ /* Three-row slices exercise an odd internal boundary. */
+ if (run_test(16, 6, 32, 32) < 0)
+ return 1;
+
+ /* Also exercise an odd final boundary and unequal strides. */
+ return run_test(16, 5, 64, 32) < 0;
+}
diff --git a/tests/fate/libswscale.mak b/tests/fate/libswscale.mak
index 9de79d736b..6d667026d5 100644
--- a/tests/fate/libswscale.mak
+++ b/tests/fate/libswscale.mak
@@ -6,6 +6,11 @@ FATE_LIBSWSCALE += fate-sws-floatimg-cmp
fate-sws-floatimg-cmp: libswscale/tests/floatimg_cmp$(EXESUF)
fate-sws-floatimg-cmp: CMD = run libswscale/tests/floatimg_cmp$(EXESUF)
+FATE_LIBSWSCALE += fate-sws-frame-copy
+fate-sws-frame-copy: libswscale/tests/frame_copy$(EXESUF)
+fate-sws-frame-copy: CMD = run libswscale/tests/frame_copy$(EXESUF)
+fate-sws-frame-copy: REF = /dev/null
+
SWS_SLICE_TEST-$(call DEMDEC, MATROSKA, VP9) += fate-sws-slice-yuv422-12bit-rgb48
fate-sws-slice-yuv422-12bit-rgb48: CMD = run tools/scale_slice_test$(EXESUF) $(TARGET_SAMPLES)/vp9-test-vectors/vp93-2-20-12bit-yuv422.webm 150 100 rgb48
--
2.52.0
_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]