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

## The bug

For input of more than 8 bits, `vf_xpsnr` reads the frames of the second
input in place (`prec[c] = ref->data[c]` in `do_xpsnr()`), but walks their
rows with the plane width as the stride (`s_rec = s->plane_width[c]` in
`get_wsse()`) instead of the frame's `linesize`. The first input is handled
right: its stride comes from `master->linesize`. 8-bit input is unaffected,
because it is copied into packed buffers first.

So any high-bit-depth frame whose rows are padded is read misaligned from
the second row on. With the usual alignment that is every 10-bit frame whose
width is not a multiple of 32, such as 1080x1920 portrait video from phones:

    ffmpeg -lavfi 
"testsrc2=s=1080x1920:d=0.2,format=yuv420p10le,split[a][b];[a][b]xpsnr" -f null 
-

prints `XPSNR y: 1.1954` for a frame compared with itself; it should be `inf`.
The existing refcmp test with yuv422p10le instead of yuv422p (300x200, so
padded rows) gives about -3 dB where the 8-bit test gives 15.4.

## The fix

Keep the second input's linesizes beside the first's (`line_sizes_rec`) and
use them for the reference stride in `get_wsse()`, as is already done for
the first input.

## Tests

- New `fate-filter-refcmp-xpsnr-yuv10`: the refcmp test in yuv422p10le. It
  fails without the fix and passes with it; its scores agree with the 8-bit
  test's within 0.03 dB (Y 26.026 against 26.000 on the first frame).
- `fate-filter-refcmp-xpsnr-yuv` and `fate-filter-refcmp-xpsnr-rgb` still
  pass; 8-bit scores are unchanged.
- 1080x1920 yuv420p10le compared with itself: `inf` (was 1.2 dB).

Until this is merged, users can work around it by cropping both inputs to
a width that is a multiple of 32 and passing them through `copy`, so that
their rows are packed.



>From 753b685d72c99cd8abef27b030a738c8d73bbf83 Mon Sep 17 00:00:00 2001
From: Viktor Turskyi <[email protected]>
Date: Wed, 21 Oct 2026 23:21:06 +0300
Subject: [PATCH] avfilter/vf_xpsnr: use the reference frame's stride for high
 bit depth

For input of more than 8 bits, the frames of the second input are read in
place, but their rows were walked with the plane width as the stride
instead of the frame's linesize. Frames whose rows are padded, such as any
10-bit frame whose width is not a multiple of 32 (1080x1920 portrait video,
for one), were read misaligned from the second row on, and the scores were
meaningless: a 1080x1920 yuv420p10le frame compared with itself scored
1.2 dB instead of inf.

Use the second input's linesize, as is already done for the first. 8-bit
input is copied into packed buffers first and is unaffected.

Add a 10-bit variant of the refcmp test, whose 300-pixel width pads the
rows. Its scores now agree with those of the 8-bit test within 0.03 dB;
before, they were around -3 dB.
---
 libavfilter/vf_xpsnr.c                   | 10 +++++++---
 tests/fate/filter-video.mak              |  3 +++
 tests/ref/fate/filter-refcmp-xpsnr-yuv10 | 20 ++++++++++++++++++++
 3 files changed, 30 insertions(+), 3 deletions(-)
 create mode 100644 tests/ref/fate/filter-refcmp-xpsnr-yuv10

diff --git a/libavfilter/vf_xpsnr.c b/libavfilter/vf_xpsnr.c
index 89f4f279ba4..8c4c354cd2e 100644
--- a/libavfilter/vf_xpsnr.c
+++ b/libavfilter/vf_xpsnr.c
@@ -52,6 +52,7 @@ typedef struct XPSNRContext {
     unsigned        frame_rate;
     FFFrameSync     fs;
     int             line_sizes[4];
+    int             line_sizes_rec[4];
     int             plane_height[4];
     int             plane_width[4];
     uint8_t         rgba_map[4];
@@ -279,6 +280,7 @@ static int get_wsse(AVFilterContext *ctx, int16_t **org, 
int16_t *org_m1,
     const double   avg_act = sqrt(16.0 * (double) (1 << (2 * s->depth - 9)) / 
sqrt(FFMAX(0.00001,
                                                                                
    r))); /* the sqrt(a_pic) */
     const int  *stride_org = (s->bpp == 1 ? s->plane_width : s->line_sizes);
+    const int  *stride_rec = (s->bpp == 1 ? s->plane_width : 
s->line_sizes_rec);
     uint32_t x, y, idx_blk = 0; /* the "16.0" above is due to fixed-point code 
*/
     double *const sse_luma = s->sse_luma;
     double *const  weights = s->weights;
@@ -298,7 +300,7 @@ static int get_wsse(AVFilterContext *ctx, int16_t **org, 
int16_t *org_m1,
         const int16_t *p_org = org[0];
         const uint32_t s_org = stride_org[0] / s->bpp;
         const int16_t *p_rec = rec[0];
-        const uint32_t s_rec = s->plane_width[0];
+        const uint32_t s_rec = stride_rec[0] / s->bpp;
         double     wsse_luma = 0.0;
 
         for (y = 0; y < h; y += b) { /* calculate block SSE and perceptual 
weights */
@@ -349,7 +351,7 @@ static int get_wsse(AVFilterContext *ctx, int16_t **org, 
int16_t *org_m1,
         const int16_t *p_org = org[c];
         const uint32_t s_org = stride_org[c] / s->bpp;
         const int16_t *p_rec = rec[c];
-        const uint32_t s_rec = s->plane_width[c];
+        const uint32_t s_rec = stride_rec[c] / s->bpp;
         const uint32_t w_pln = s->plane_width[c];
         const uint32_t h_pln = s->plane_height[c];
 
@@ -422,8 +424,10 @@ static int do_xpsnr(FFFrameSync *fs)
     if (!s->weights)
         s->weights  = av_malloc_array(w_blk * h_blk, sizeof(double));
 
-    for (c = 0; c < s->num_comps; c++)  /* create temporal org buffer memory */
+    for (c = 0; c < s->num_comps; c++) { /* create temporal org buffer memory 
*/
         s->line_sizes[c] = master->linesize[c];
+        s->line_sizes_rec[c] = ref->linesize[c];
+    }
 
     stride_org_bpp = (s->bpp == 1 ? s->plane_width[0] : s->line_sizes[0] / 
s->bpp);
 
diff --git a/tests/fate/filter-video.mak b/tests/fate/filter-video.mak
index 4a065ec0320..859ee737026 100644
--- a/tests/fate/filter-video.mak
+++ b/tests/fate/filter-video.mak
@@ -978,6 +978,9 @@ fate-filter-refcmp-xpsnr-rgb: CMD = refcmp_metadata xpsnr 
rgb24 0.002
 FATE_FILTER_REFCMP_METADATA-$(CONFIG_XPSNR_FILTER) += 
fate-filter-refcmp-xpsnr-yuv
 fate-filter-refcmp-xpsnr-yuv: CMD = refcmp_metadata xpsnr yuv422p 0.0015
 
+FATE_FILTER_REFCMP_METADATA-$(CONFIG_XPSNR_FILTER) += 
fate-filter-refcmp-xpsnr-yuv10
+fate-filter-refcmp-xpsnr-yuv10: CMD = refcmp_metadata xpsnr yuv422p10le 0.0015
+
 FATE_FILTER-$(call ALLYES, TESTSRC2_FILTER SPLIT_FILTER AVGBLUR_FILTER        \
                            METADATA_FILTER WRAPPED_AVFRAME_ENCODER NULL_MUXER \
                            PIPE_PROTOCOL) += $(FATE_FILTER_REFCMP_METADATA-yes)
diff --git a/tests/ref/fate/filter-refcmp-xpsnr-yuv10 
b/tests/ref/fate/filter-refcmp-xpsnr-yuv10
new file mode 100644
index 00000000000..e964da32643
--- /dev/null
+++ b/tests/ref/fate/filter-refcmp-xpsnr-yuv10
@@ -0,0 +1,20 @@
+frame:0    pts:0       pts_time:0
+lavfi.xpsnr.xpsnr.y=26.025789
+lavfi.xpsnr.xpsnr.u=24.723721
+lavfi.xpsnr.xpsnr.v=21.420650
+frame:1    pts:1       pts_time:1
+lavfi.xpsnr.xpsnr.y=14.155715
+lavfi.xpsnr.xpsnr.u=11.917146
+lavfi.xpsnr.xpsnr.v=6.415221
+frame:2    pts:2       pts_time:2
+lavfi.xpsnr.xpsnr.y=13.656163
+lavfi.xpsnr.xpsnr.u=11.398902
+lavfi.xpsnr.xpsnr.v=6.802186
+frame:3    pts:3       pts_time:3
+lavfi.xpsnr.xpsnr.y=13.737943
+lavfi.xpsnr.xpsnr.u=11.593648
+lavfi.xpsnr.xpsnr.v=6.581923
+frame:4    pts:4       pts_time:4
+lavfi.xpsnr.xpsnr.y=13.999208
+lavfi.xpsnr.xpsnr.u=11.171051
+lavfi.xpsnr.xpsnr.v=6.165915
-- 
2.54.0

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

Reply via email to