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]
