PR #25253 opened by Aaditya Agarwal (aaditya8979)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25253
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25253.patch

Add NEON-optimized SAO band filter for 10-bit and 12-bit content, shared
between HEVC and VVC via the h26x/ directory (H.265/H.266 clause 8.7.3
SAO band offset process).

The 8-bit implementation already existed (ff_h26x_sao_band_filter_8x8_8_neon
and ff_h26x_sao_band_filter_16x16_8_neon). This patch adds the HBD variants
for 10 and 12-bit pixel depths.

Implementation uses a 32-entry int16_t offset table (64 bytes, loaded into
NEON registers v16-v19). Per-pixel: the 5-bit band index is computed via
unsigned right shift (BIT_DEPTH - 5), converted to a byte-pair index via
shl+xtn+zip1, and used in a 4-register tbl lookup to reconstruct the signed
16-bit offset. Pixels are clipped to [0, (1<<BIT_DEPTH)-1] with smax/smin.

The VVC dsp_init registers the new functions for bd==10 and bd==12.

Benchmarks on Apple M4 (arm64, checkasm --bench):

| function              | C (ns) | NEON (ns) | speedup |
|-----------------------|--------|-----------|---------|
| vvc_sao_band_8_10     |   22.5 |       5.5 |  4.12x  |
| vvc_sao_band_8_12     |   23.0 |       5.5 |  4.19x  |
| vvc_sao_band_16_10    |   99.8 |      23.8 |  4.19x  |
| vvc_sao_band_16_12    |   99.2 |      23.8 |  4.17x  |
| vvc_sao_band_64_10    | 1648.2 |     390.4 |  4.22x  |
| vvc_sao_band_64_12    | 1651.7 |     391.2 |  4.22x  |
| vvc_sao_band_128_10   | 6687.7 |    1563.0 |  4.28x  |
| vvc_sao_band_128_12   | 7299.4 |    1567.4 |  4.66x  |

checkasm --test=vvc_sao: all 54 tests passed (NEON, 8/10/12-bit)
checkasm --test=hevc_sao: all 20 tests passed (no regression)



>From 81465cca79b361ca678be77010199c08c07fd8a0 Mon Sep 17 00:00:00 2001
From: Aaditya Agarwal <[email protected]>
Date: Tue, 20 Oct 2026 19:24:49 +0530
Subject: [PATCH] aarch64/h26x: add NEON optimizations for 10/12-bit SAO band
 filter

Add NEON-optimized SAO band filter functions for 10-bit and 12-bit
content, shared between HEVC and VVC decoders via the h26x/ directory.

The implementation uses a 32-entry int16_t offset table (64 bytes)
stored in NEON registers v16-v19. For each group of 8 pixels, the
5-bit band index is computed via unsigned right shift (BIT_DEPTH - 5),
converted to a byte-pair offset, and used in a 4-register tbl lookup
to reconstruct the signed 16-bit offset value. Pixel values are
clipped to [0, (1 << BIT_DEPTH) - 1] using smax/smin.

The VVC dsp_init is updated to register the new functions for 10 and
12-bit depths, matching the existing 8-bit registration pattern.

Benchmarks on Apple M4 (arm64):
  vvc_sao_band_8x8_10:    22.5 ns -> 5.5 ns  (4.12x)
  vvc_sao_band_8x8_12:    23.0 ns -> 5.5 ns  (4.19x)
  vvc_sao_band_16x16_10:  99.8 ns -> 23.8 ns (4.19x)
  vvc_sao_band_16x16_12:  99.2 ns -> 23.8 ns (4.17x)
  vvc_sao_band_128_10:  6687.7 ns -> 1563 ns (4.28x)
  vvc_sao_band_128_12:  7299.4 ns -> 1567 ns (4.66x)

Signed-off-by: Aaditya Agarwal <[email protected]>
---
 libavcodec/aarch64/h26x/dsp.h      |  16 ++++
 libavcodec/aarch64/h26x/sao_neon.S | 117 +++++++++++++++++++++++++++++
 libavcodec/aarch64/vvc/dsp_init.c  |   6 ++
 3 files changed, 139 insertions(+)

diff --git a/libavcodec/aarch64/h26x/dsp.h b/libavcodec/aarch64/h26x/dsp.h
index 2b8eae130a1..70dec558f93 100644
--- a/libavcodec/aarch64/h26x/dsp.h
+++ b/libavcodec/aarch64/h26x/dsp.h
@@ -32,6 +32,22 @@ void ff_h26x_sao_band_filter_16x16_8_neon(uint8_t *_dst, 
const uint8_t *_src,
                                         ptrdiff_t stride_dst, ptrdiff_t 
stride_src,
                                         const int16_t *sao_offset_val, int 
sao_left_class,
                                         int width, int height);
+void ff_h26x_sao_band_filter_8x8_10_neon(uint8_t *_dst, const uint8_t *_src,
+                                        ptrdiff_t stride_dst, ptrdiff_t 
stride_src,
+                                        const int16_t *sao_offset_val, int 
sao_left_class,
+                                        int width, int height);
+void ff_h26x_sao_band_filter_16x16_10_neon(uint8_t *_dst, const uint8_t *_src,
+                                        ptrdiff_t stride_dst, ptrdiff_t 
stride_src,
+                                        const int16_t *sao_offset_val, int 
sao_left_class,
+                                        int width, int height);
+void ff_h26x_sao_band_filter_8x8_12_neon(uint8_t *_dst, const uint8_t *_src,
+                                        ptrdiff_t stride_dst, ptrdiff_t 
stride_src,
+                                        const int16_t *sao_offset_val, int 
sao_left_class,
+                                        int width, int height);
+void ff_h26x_sao_band_filter_16x16_12_neon(uint8_t *_dst, const uint8_t *_src,
+                                        ptrdiff_t stride_dst, ptrdiff_t 
stride_src,
+                                        const int16_t *sao_offset_val, int 
sao_left_class,
+                                        int width, int height);
 #define SAO_EDGE_FILTER_PROTO(codec, size, depth)                          \
     void ff_##codec##_sao_edge_filter_##size##_##depth##_neon(             \
         uint8_t *dst, const uint8_t *src, ptrdiff_t stride_dst,            \
diff --git a/libavcodec/aarch64/h26x/sao_neon.S 
b/libavcodec/aarch64/h26x/sao_neon.S
index 083f0eb89d5..97a02b47604 100644
--- a/libavcodec/aarch64/h26x/sao_neon.S
+++ b/libavcodec/aarch64/h26x/sao_neon.S
@@ -100,6 +100,123 @@ function ff_h26x_sao_band_filter_16x16_8_neon, export=1
         ret
 endfunc
 
+// HBD (10/12-bit) SAO band filter: pixels are uint16_t.
+// Builds a 32-entry int16_t offset table (64 bytes) on the stack,
+// loads it into v16-v19, and uses dual tbl for the low/high bytes.
+
+// 12-bit 8x8 entry
+function ff_h26x_sao_band_filter_8x8_12_neon, export=1
+        mov             w9, #7                      // shift = BIT_DEPTH - 5 = 
12 - 5
+        mvni            v31.8h, #0xf0, lsl #8       // clip max = 4095
+        b               .Lsao_band_8x8_hbd
+endfunc
+
+// 10-bit 8x8 entry
+function ff_h26x_sao_band_filter_8x8_10_neon, export=1
+        mov             w9, #5                      // shift = BIT_DEPTH - 5 = 
10 - 5
+        mvni            v31.8h, #0xfc, lsl #8       // clip max = 1023
+.Lsao_band_8x8_hbd:
+        // Build 32-entry int16_t offset table on stack (64 bytes)
+        stp             xzr, xzr, [sp, #-64]!
+        stp             xzr, xzr, [sp, #16]
+        stp             xzr, xzr, [sp, #32]
+        stp             xzr, xzr, [sp, #48]
+        mov             w8, #4
+0:
+        ldrsh           x10, [x4, x8, lsl #1]      // sao_offset_val[k+1]
+        subs            w8, w8, #1
+        add             w11, w8, w5                 // k + sao_left_class
+        and             w11, w11, #0x1F
+        strh            w10, [sp, x11, lsl #1]      // store int16_t at 
table[idx]
+        bne             0b
+        ldp             q16, q17, [sp]              // load 64-byte table into 
v16-v19
+        ldp             q18, q19, [sp, #32]
+        add             sp, sp, #64
+        movi            v30.8h, #0                  // clip min = 0
+        neg             w9, w9
+        dup             v20.8h, w9                  // negative shift for ushl
+1:
+        ld1             {v0.8h}, [x1], x3           // load 8 pixels (uint16_t)
+        subs            w7, w7, #1
+        ushl            v2.8h, v0.8h, v20.8h       // pixel >> shift (5-bit 
index)
+        // Convert 16-bit index to byte offset pair for tbl
+        shl             v3.8h, v2.8h, #1            // byte offset = index * 2
+        xtn             v4.8b, v3.8h                // narrow to 8-bit (low 
byte index)
+        movi            v5.16b, #1
+        add             v5.16b, v4.16b, v5.16b      // high byte index = low + 
1
+        zip1            v6.16b, v4.16b, v5.16b      // interleave: 
[lo0,hi0,lo1,hi1,...]
+        // Dual tbl lookup across 64-byte table (v16-v19)
+        tbl             v7.16b, {v16.16b-v19.16b}, v6.16b
+        // v7 now has pairs of (lo,hi) bytes = int16_t offsets
+        // Add offset to pixel, clip to [0, max]
+        add             v0.8h, v0.8h, v7.8h
+        smax            v0.8h, v0.8h, v30.8h
+        smin            v0.8h, v0.8h, v31.8h
+        st1             {v0.8h}, [x0], x2
+        bne             1b
+        ret
+endfunc
+
+// 12-bit 16x16 entry
+function ff_h26x_sao_band_filter_16x16_12_neon, export=1
+        mov             w9, #7                      // shift = 12 - 5
+        mvni            v31.8h, #0xf0, lsl #8       // clip max = 4095
+        b               .Lsao_band_16x16_hbd
+endfunc
+
+// 10-bit 16x16 entry
+function ff_h26x_sao_band_filter_16x16_10_neon, export=1
+        mov             w9, #5                      // shift = 10 - 5
+        mvni            v31.8h, #0xfc, lsl #8       // clip max = 1023
+.Lsao_band_16x16_hbd:
+        // Build 32-entry int16_t offset table on stack (64 bytes)
+        stp             xzr, xzr, [sp, #-64]!
+        stp             xzr, xzr, [sp, #16]
+        stp             xzr, xzr, [sp, #32]
+        stp             xzr, xzr, [sp, #48]
+        mov             w8, #4
+0:
+        ldrsh           x10, [x4, x8, lsl #1]      // sao_offset_val[k+1]
+        subs            w8, w8, #1
+        add             w11, w8, w5                 // k + sao_left_class
+        and             w11, w11, #0x1F
+        strh            w10, [sp, x11, lsl #1]
+        bne             0b
+        ldp             q16, q17, [sp]
+        ldp             q18, q19, [sp, #32]
+        add             sp, sp, #64
+        movi            v30.8h, #0
+        neg             w9, w9
+        dup             v20.8h, w9                  // negative shift for ushl
+        add             w6, w6, #7
+        bic             w6, w6, #7                  // width rounded up to 8
+        lsl             w8, w6, #1                  // width in bytes (pixels 
* 2)
+        sub             x2, x2, x8                  // dst stride adjustment
+        sub             x3, x3, x8                  // src stride adjustment
+1:
+        mov             w8, w6                      // pixel count for this row
+2:
+        ld1             {v0.8h}, [x1], #16          // load 8 pixels
+        subs            w8, w8, #8
+        ushl            v2.8h, v0.8h, v20.8h       // pixel >> shift
+        shl             v3.8h, v2.8h, #1            // byte offset = index * 2
+        xtn             v4.8b, v3.8h
+        movi            v5.16b, #1
+        add             v5.16b, v4.16b, v5.16b
+        zip1            v6.16b, v4.16b, v5.16b
+        tbl             v7.16b, {v16.16b-v19.16b}, v6.16b
+        add             v0.8h, v0.8h, v7.8h
+        smax            v0.8h, v0.8h, v30.8h
+        smin            v0.8h, v0.8h, v31.8h
+        st1             {v0.8h}, [x0], #16
+        bne             2b
+        subs            w7, w7, #1
+        add             x0, x0, x2
+        add             x1, x1, x3
+        bne             1b
+        ret
+endfunc
+
 .Lhevc_sao_edge_pos_8:
 .word 1 // horizontal
 .word HEVC_SAO_STRIDE // vertical
diff --git a/libavcodec/aarch64/vvc/dsp_init.c 
b/libavcodec/aarch64/vvc/dsp_init.c
index 8936a4606ab..c53fd5bd2bd 100644
--- a/libavcodec/aarch64/vvc/dsp_init.c
+++ b/libavcodec/aarch64/vvc/dsp_init.c
@@ -372,6 +372,9 @@ void ff_vvc_dsp_init_aarch64(VVCDSPContext *const c, const 
int bd)
         c->inter.put[1][5][1][1] =
         c->inter.put[1][6][1][1] = ff_vvc_put_chroma_hv_x16_10_neon;
 
+        c->sao.band_filter[0] = ff_h26x_sao_band_filter_8x8_10_neon;
+        for (int i = 1; i < FF_ARRAY_ELEMS(c->sao.band_filter); i++)
+            c->sao.band_filter[i] = ff_h26x_sao_band_filter_16x16_10_neon;
         c->sao.edge_filter[0] = ff_vvc_sao_edge_filter_8x8_10_neon;
         for (int i = 1; i < FF_ARRAY_ELEMS(c->sao.edge_filter); i++)
             c->sao.edge_filter[i] = ff_vvc_sao_edge_filter_16x16_10_neon;
@@ -428,6 +431,9 @@ void ff_vvc_dsp_init_aarch64(VVCDSPContext *const c, const 
int bd)
         c->inter.put[1][5][1][1] =
         c->inter.put[1][6][1][1] = ff_vvc_put_chroma_hv_x16_12_neon;
 
+        c->sao.band_filter[0] = ff_h26x_sao_band_filter_8x8_12_neon;
+        for (int i = 1; i < FF_ARRAY_ELEMS(c->sao.band_filter); i++)
+            c->sao.band_filter[i] = ff_h26x_sao_band_filter_16x16_12_neon;
         c->sao.edge_filter[0] = ff_vvc_sao_edge_filter_8x8_12_neon;
         for (int i = 1; i < FF_ARRAY_ELEMS(c->sao.edge_filter); i++)
             c->sao.edge_filter[i] = ff_vvc_sao_edge_filter_16x16_12_neon;
-- 
2.54.0

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

Reply via email to