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

For 4:2:0 input, `pseudocolor` output depends on the number of filter threads. 
`filter_slice()` runs `ff_slice_pos()` separately on the height of each plane 
and on the height of the index plane. The subsampled filters 
(`pseudocolor_filter_11`/`_11d` and their 16-bit versions) address the index 
plane relative to its slice start, and they assume luma row 2*y lines up with 
chroma row y. That stops being true once a luma slice boundary isn't exactly 
twice the chroma one. Take 50 rows on 2 threads: the second luma slice starts 
at row 25, but the second chroma slice starts at row 12. With `i=0` the U/V 
planes then read the wrong luma rows, and with `i=1`/`i=2` the luma rows of a 
slice that starts on an odd row read the wrong chroma rows:

```
SRC='testsrc2=s=64x50:d=0.04,format=yuv420p,geq=lum=Y*5:cb=128+Y:cr=X*2'
for t in 1 2 3; do ffmpeg -v error -f lavfi -i "$SRC" -filter_threads $t -vf 
pseudocolor=p=magma:i=0 -f md5 -; done
```

Before: 37fd75a1806b0be4aeb4f628b98a698e, 2ebd3e7a6030de0c74011fc4d83414fb, 
bf7d22c9fda6fed02970a647888ef893 for 1, 2 and 3 threads. `i=1` and yuv420p10le 
show the same pattern, three different hashes each. After: all thread counts 
give the single-thread result (37fd75a1806b0be4aeb4f628b98a698e here), and 
single-threaded output is unchanged.

The patch splits every plane on chroma rows and derives each luma slice 
boundary from the chroma one (`chroma_pos << vsub`, clamped to the plane 
height). That keeps luma slices starting at twice the chroma slice start. 
4:4:4, 4:2:2, RGB and gray behave as before, since their slices are identical 
for every plane. I also checked odd heights (64x51) and the alpha index (`i=3` 
on yuva420p/yuva420p16le) with 1, 2, 3, 5 and 26/64 threads: every thread count 
gives the same output.

The second commit adds `fate-filter-pseudocolor-index0` and 
`fate-filter-pseudocolor-index1`. They run yuv420p 64x50 with 
`-filter_complex_threads 3`, a setting where the luma and chroma boundaries 
don't line up. Both fail without the fix and pass with it (THREADS=1 and 4). 
Their references match the single-threaded output. pseudocolor had no FATE 
coverage before.

This issue was found by an LLM-Enhanced Static Analysis Engine and verified 
manually.

🤖 Generated with [Claude Code](https://claude.com/claude-code)



From 624d84d9bf8810ee584986a906e6715520a664fa Mon Sep 17 00:00:00 2001
From: Martin Mirchev <[email protected]>
Date: Wed, 21 Oct 2026 18:19:10 +0800
Subject: [PATCH 1/2] avfilter/vf_pseudocolor: align luma and chroma slices

filter_slice() computed the slice of each plane and of the index plane
independently with ff_slice_pos() on the plane heights. For 4:2:0, the
subsampled filters address the index plane relative to its slice start
and assume that luma row 2 * y corresponds to chroma row y, which does
not hold when a luma slice boundary is not twice the chroma one (e.g.
50 rows on 2 threads: the second luma slice starts at row 25, the
second chroma slice at row 12). With index=0 the chroma planes then
read the wrong luma rows, and with index=1/2 luma slices starting on an
odd row read the wrong chroma rows, so the output depended on the
number of slice threads.

Split all planes on chroma rows and derive the luma slice boundaries
from them, so luma slices always start at twice the chroma slice start.

Signed-off-by: Martin Mirchev <[email protected]>
---
 libavfilter/vf_pseudocolor.c | 18 +++++++++++++++---
 1 file changed, 15 insertions(+), 3 deletions(-)

diff --git a/libavfilter/vf_pseudocolor.c b/libavfilter/vf_pseudocolor.c
index 793278ce89f..4ceb2afadf9 100644
--- a/libavfilter/vf_pseudocolor.c
+++ b/libavfilter/vf_pseudocolor.c
@@ -318,6 +318,7 @@ typedef struct PseudoColorContext {
     int color;
     int linesize[4];
     int width[4], height[4];
+    int vsub;
     double var_values[VAR_VARS_NB];
     char   *comp_expr_str[4];
     AVExpr *comp_expr[4];
@@ -729,6 +730,7 @@ static int config_input(AVFilterLink *inlink)
 
     hsub = desc->log2_chroma_w;
     vsub = desc->log2_chroma_h;
+    s->vsub = vsub;
     s->height[1] = s->height[2] = AV_CEIL_RSHIFT(inlink->h, vsub);
     s->height[0] = s->height[3] = inlink->h;
     s->width[1]  = s->width[2]  = AV_CEIL_RSHIFT(inlink->w, hsub);
@@ -973,6 +975,16 @@ typedef struct ThreadData {
     AVFrame *in, *out;
 } ThreadData;
 
+/* Slices are split on chroma rows so that luma and chroma slices line up. */
+static int slice_pos(const PseudoColorContext *s, int plane, int jobnr, int 
nb_jobs)
+{
+    const int pos = ff_slice_pos(s->height[1], jobnr, nb_jobs);
+
+    if (plane == 1 || plane == 2)
+        return pos;
+    return FFMIN(pos << s->vsub, s->height[plane]);
+}
+
 static int filter_slice(AVFilterContext *ctx, void *arg, int jobnr, int 
nb_jobs)
 {
     PseudoColorContext *s = ctx->priv;
@@ -981,9 +993,9 @@ static int filter_slice(AVFilterContext *ctx, void *arg, 
int jobnr, int nb_jobs)
     AVFrame *out = td->out;
 
     for (int plane = 0; plane < s->nb_planes; plane++) {
-        const int slice_start = ff_slice_pos(s->height[plane], jobnr, nb_jobs);
-        const int slice_end = ff_slice_pos(s->height[plane], jobnr + 1, 
nb_jobs);
-        const int islice_start = ff_slice_pos(s->height[s->index], jobnr, 
nb_jobs);
+        const int slice_start = slice_pos(s, plane, jobnr, nb_jobs);
+        const int slice_end = slice_pos(s, plane, jobnr + 1, nb_jobs);
+        const int islice_start = slice_pos(s, s->index, jobnr, nb_jobs);
         ptrdiff_t ilinesize = in->linesize[s->index];
         ptrdiff_t slinesize = in->linesize[plane];
         ptrdiff_t dlinesize = out->linesize[plane];
-- 
2.54.0


From 41fae6f461623ce06cc4ce49cf68f99243eb652e Mon Sep 17 00:00:00 2001
From: Martin Mirchev <[email protected]>
Date: Wed, 21 Oct 2026 18:20:32 +0800
Subject: [PATCH 2/2] fate/filter-video: add pseudocolor tests for 4:2:0 with
 slice threads

Run pseudocolor on yuv420p with index 0 and 1 using three filter
threads and a frame height whose luma and chroma slice boundaries do
not line up, so a mismatch between the luma and chroma slices shows
up as a different output.

Signed-off-by: Martin Mirchev <[email protected]>
---
 tests/fate/filter-video.mak              |  4 ++++
 tests/ref/fate/filter-pseudocolor-index0 | 10 ++++++++++
 tests/ref/fate/filter-pseudocolor-index1 | 10 ++++++++++
 3 files changed, 24 insertions(+)
 create mode 100644 tests/ref/fate/filter-pseudocolor-index0
 create mode 100644 tests/ref/fate/filter-pseudocolor-index1

diff --git a/tests/fate/filter-video.mak b/tests/fate/filter-video.mak
index 4a065ec0320..720755dcd47 100644
--- a/tests/fate/filter-video.mak
+++ b/tests/fate/filter-video.mak
@@ -483,6 +483,10 @@ fate-filter-decimate: CMD = framecrc -lavfi 
testsrc2=r=24:d=10,fps=60,decimate=5
 FATE_FILTER-$(call FILTERFRAMECRC, TESTSRC2 FPS MPDECIMATE) += 
fate-filter-mpdecimate
 fate-filter-mpdecimate: CMD = framecrc -lavfi 
testsrc2=r=2:d=10,fps=3,mpdecimate -pix_fmt yuv420p
 
+FATE_FILTER-$(call FILTERFRAMECRC, TESTSRC2 FORMAT GEQ PSEUDOCOLOR) += 
fate-filter-pseudocolor-index0 fate-filter-pseudocolor-index1
+fate-filter-pseudocolor-index0: CMD = framecrc -filter_complex_threads 3 
-lavfi 
'testsrc2=s=64x50:r=5:d=1,format=yuv420p,geq=lum=Y*5+N*2:cb=128+Y-N:cr=X*2+N,pseudocolor=p=magma:i=0'
 -pix_fmt yuv420p
+fate-filter-pseudocolor-index1: CMD = framecrc -filter_complex_threads 3 
-lavfi 
'testsrc2=s=64x50:r=5:d=1,format=yuv420p,geq=lum=Y*5+N*2:cb=128+Y-N:cr=X*2+N,pseudocolor=p=magma:i=1'
 -pix_fmt yuv420p
+
 FATE_FILTER-$(call FILTERFRAMECRC, TESTSRC2 FPS MPDECIMATE) += 
fate-filter-mpdecimate-keep
 fate-filter-mpdecimate-keep: CMD = framecrc -lavfi 
testsrc2=r=2:d=5,fps=4,mpdecimate=keep=3 -pix_fmt yuv420p
 
diff --git a/tests/ref/fate/filter-pseudocolor-index0 
b/tests/ref/fate/filter-pseudocolor-index0
new file mode 100644
index 00000000000..ef36ece797b
--- /dev/null
+++ b/tests/ref/fate/filter-pseudocolor-index0
@@ -0,0 +1,10 @@
+#tb 0: 1/5
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 64x50
+#sar 0: 1/1
+0,          0,          0,        1,     4800, 0xf3be9529
+0,          1,          1,        1,     4800, 0x4e1ea9c9
+0,          2,          2,        1,     4800, 0x3d38bd09
+0,          3,          3,        1,     4800, 0x11c8d209
+0,          4,          4,        1,     4800, 0xca81e689
diff --git a/tests/ref/fate/filter-pseudocolor-index1 
b/tests/ref/fate/filter-pseudocolor-index1
new file mode 100644
index 00000000000..579d8471f46
--- /dev/null
+++ b/tests/ref/fate/filter-pseudocolor-index1
@@ -0,0 +1,10 @@
+#tb 0: 1/5
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 64x50
+#sar 0: 1/1
+0,          0,          0,        1,     4800, 0xf4faf109
+0,          1,          1,        1,     4800, 0x9c76e8a9
+0,          2,          2,        1,     4800, 0x7e8be0a9
+0,          3,          3,        1,     4800, 0x8272d889
+0,          4,          4,        1,     4800, 0xa84bd089
-- 
2.54.0

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

Reply via email to