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]
