PR #24991 opened by i7xre URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24991 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24991.patch
# Summary of changes Related: https://code.ffmpeg.org/FFmpeg/FFmpeg/issues/24871 Base: official FFmpeg master `f5d4fd186c68913240c4f6e9cda98443e1518b34` (2026-10-09 17:34:46 UTC). This is a native DSP port, not the Nuvio JNI/archive-hook integration. Review commit: `d8ff9512acfb06b8a0492999907d9577169a3f0b` (`avcodec/arm: add NEON H.264 High10 qpel functions`). Five source/test files only; no generated binaries, benchmark scripts or private media included. ## Scope Add 64 ARM32 High10 qpel callbacks: 16 fractional positions, 8/16-wide, put/avg. Four-wide qpel, chroma, 8-bit and other bit depths remain unchanged. Dispatch requires both compiler-supported NEON intrinsics and runtime NEON. Existing configure/INTRINSICS build machinery gates the object; assembly-only NEON and pure soft-float configurations retain generic High10 fallback. No configure changes, production compiler flags or threading changes. Filtering follows the six-tap and rounding definitions of FFmpeg's generic H.264 implementation. The 1-D saturation strategy follows the existing AArch64 implementation, but no AArch64 assembly was translated. This family originated as an experimental FFmpeg 7.0.2 Nuvio candidate; its numerical implementation is retained here. This is an RFC, not a claim that this implementation should be adopted as-is. `doc/developer.texi` prefers GAS for ARM and restricts intrinsics to special cases. These standalone callbacks do not establish that exception. Maintainer guidance on a GAS implementation or another preferred approach is welcome. ## Current-master verification on actual ARM32 hardware Xiaomi MiTV-AFMU0, Android 14, four Cortex-A55 cores, armeabi-v7a userspace. Cross-compiled with Android NDK r29/Clang 21, API 24, ARMv7-A/NEON, softfp/O3. No Nuvio APK was installed or replaced for these current-master checks. - Actual DSP-table comparison: baseline 0 High10 qpel overrides; candidate 64. - Configured full checkasm: baseline 302/302; candidate 366/366 passed. - Candidate qpel checkasm: 128/128 across seeds 1, 7, 42, 123 and 65535. Added padded 32/64-pixel strides and aligned-offset cases; original cases retained. These are not exhaustive input-space or negative-stride tests. - The expanded qpel checkasm also passed 238/238 on an x86-64 Ryzen 5 5600GT host (MMXEXT/SSE2/SSSE3). Destination alignment is retained for existing SIMD callbacks; only source alignment offsets are varied. - Seven official FATE conformance targets passed on device: `fate-h264-conformance-frext-pph10i{1..7}_panasonic_a`. This is a focused FATE subset, not the full FATE suite. - Episode 7 42–55 s reproduction: 312/312 complete decoded-frame hashes matched between current-master baseline and candidate. - Pure soft-float cross-build passed with `HAVE_NEON=1` and `HAVE_INTRINSICS_NEON=0`; actual device dispatch remained at zero High10 overrides and existing qpel checkasm passed 64/64. Its separate dispatch gate intentionally reports failure against an expected-64 assertion, demonstrating fallback rather than successful optimized dispatch. - Moving the object into the configured intrinsics build path left its `.text` byte-identical (SHA-256 `381e0a55a606cca771deaca8fa383d28adb36d417f8d632f90f82699893f2ee0`). ## Current-master decode-only measurements Three interleaved A/B pairs, same Episode 7 input via HTTP, 42–55 s interval, four frame workers, no audio/subtitles/hardware decode, null output. Each run produced 312 frames. CPU seconds below are FFmpeg `utime + stime`. | Pair | Baseline CPU s | Candidate CPU s | Reduction | Baseline rtime s | Candidate rtime s | | --- | ---: | ---: | ---: | ---: | ---: | | 1 | 40.697 | 29.486 | 27.5% | 16.810 | 14.123 | | 2 | 39.990 | 29.307 | 26.7% | 17.091 | 10.975 | | 3 | 40.085 | 28.779 | 28.2% | 21.831 | 11.829 | Current HAL start temperatures A/B: 43.2/42.7, 43.0/44.2, 43.8/44.4°C. Peaks: 47.1/47.1, 47.2/47.6, 47.7/47.7°C; reported Android thermal status remained 0. Paired conditions were similar, not identical: pair 2 starts differed by 1.2°C. Sampled policy frequency ranged 2.004–2.508 GHz except candidate run 3, which also sampled 1.608 GHz. Sampling does not prove identical frequency residency or absence of all thermal effects. `rtime` and CPU figures include FFmpeg input/probing/seek-preroll overhead; HTTP and scheduler variation affect wall time. These are decode-only CLI measurements, not rendered FPS, drop-rate, A/V-sync or physical-presentation measurements. No current-master player validation is claimed. Example command (replace URL with the same locally available input): ```sh ffmpeg -hide_banner -nostdin -nostats -loglevel info -benchmark \ -filter_threads 1 -filter_complex_threads 1 \ -ss 42 -threads 4 -thread_type frame -hwaccel none \ -rw_timeout 15000000 -i INPUT_URL -map 0:v:0 -an -sn -dn \ -t 13 -fps_mode passthrough -f null - ``` Correctness can be reproduced with checkasm and the public FATE samples; the anime episode is not included in the patch or uploaded. ## Historical evidence, separate from master The issue's FFmpeg 7.0.2 Xiaomi playback campaign found qpel/luma MC the dominant leaf profiling cost (~47.63%; chroma ~15.23%, not inclusive native stack attribution). The coherent candidate passed 151,680 bit-exact differential cases and 1,320 matching decoded-frame hashes. Historical three playback pairs reported decoded FPS 20.14→23.10, 19.84→21.02, 19.71→23.41; rendered telemetry 8.96→22.85, 10.03→18.89, 9.78→23.99; drops 61.3→4.2%, 55.6→16.8%, 57.3→0%. These historical figures used the earlier player measurement methodology; they are not exact-window current-master results. Successful Surface submission is not proof of physical display presentation. ## Limitations Performance measured on one Cortex-A55 ARM32 device/toolchain. No claim about other ARMv7 CPUs, AArch64, 4-wide acceleration, chroma acceleration, all H.264 streams, full FATE, or upstream acceptance of intrinsics. Independent source review found no numerical/bounds defect; that is not a formal proof. A GAS version would need fresh build/correctness/performance verification; this patch does not pre-empt maintainers' preferred design. From d8ff9512acfb06b8a0492999907d9577169a3f0b Mon Sep 17 00:00:00 2001 From: akai <[email protected]> Date: Sat, 10 Oct 2026 01:21:34 +0300 Subject: [PATCH] avcodec/arm: add NEON H.264 High10 qpel functions Install 64 8/16-wide put/avg callbacks through native ARM DSP initialization. Leave 4-wide, chroma, other bit depths and existing 8-bit dispatch unchanged. Use the configured NEON intrinsics build path and retain generic fallback when only external NEON assembly is available. Extend qpel checkasm with padded strides and source alignment offsets while preserving destination alignment required by existing SIMD callbacks. Current-master ARM32 Cortex-A55 validation: configured full checkasm 366/366, qpel 128/128 at five seeds, seven High10 FATE conformance targets, and 312 matching complete decoded-frame hashes. Expanded qpel tests also pass 238/238 on x86-64. Pure soft-float ARM build and generic fallback tested. Three interleaved four-worker Episode07 decode-only CLI pairs reduce CPU (utime+stime) from 40.697/39.990/40.085 s to 29.486/29.307/28.779 s. This is not rendered-FPS or drop-rate evidence; no current-master player integration or physical display presentation is claimed. Historical 7.0.2 player evidence and detailed thermal caveats accompany the RFC report. Related: https://code.ffmpeg.org/FFmpeg/FFmpeg/issues/24871 Submitted for implementation discussion: FFmpeg's preferred ARM SIMD style is GAS. This intrinsic port does not assert an exception to that policy or claim merge readiness. --- libavcodec/arm/h264qpel_init_arm.c | 7 + libavcodec/h264qpel.h | 1 + libavcodec/neon/Makefile | 4 + libavcodec/neon/h264qpel_10.c | 228 +++++++++++++++++++++++++++++ tests/checkasm/h264qpel.c | 24 ++- 5 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 libavcodec/neon/h264qpel_10.c diff --git a/libavcodec/arm/h264qpel_init_arm.c b/libavcodec/arm/h264qpel_init_arm.c index 71237be359..3a76e766f5 100644 --- a/libavcodec/arm/h264qpel_init_arm.c +++ b/libavcodec/arm/h264qpel_init_arm.c @@ -99,6 +99,13 @@ av_cold void ff_h264qpel_init_arm(H264QpelContext *c, int bit_depth) const int high_bit_depth = bit_depth > 8; int cpu_flags = av_get_cpu_flags(); +#if HAVE_INTRINSICS_NEON + if (bit_depth == 10 && have_neon(cpu_flags)) { + ff_h264qpel_init_neon_10(c); + return; + } +#endif + if (have_neon(cpu_flags) && !high_bit_depth) { c->put_h264_qpel_pixels_tab[0][ 0] = ff_put_h264_qpel16_mc00_neon; c->put_h264_qpel_pixels_tab[0][ 1] = ff_put_h264_qpel16_mc10_neon; diff --git a/libavcodec/h264qpel.h b/libavcodec/h264qpel.h index f198a9483c..6c48d08895 100644 --- a/libavcodec/h264qpel.h +++ b/libavcodec/h264qpel.h @@ -33,6 +33,7 @@ void ff_h264qpel_init(H264QpelContext *c, int bit_depth); void ff_h264qpel_init_aarch64(H264QpelContext *c, int bit_depth); void ff_h264qpel_init_arm(H264QpelContext *c, int bit_depth); +void ff_h264qpel_init_neon_10(H264QpelContext *c); void ff_h264qpel_init_ppc(H264QpelContext *c, int bit_depth); void ff_h264qpel_init_riscv(H264QpelContext *c, int bit_depth); void ff_h264qpel_init_x86(H264QpelContext *c, int bit_depth); diff --git a/libavcodec/neon/Makefile b/libavcodec/neon/Makefile index 83c2f0051c..5beb8118a7 100644 --- a/libavcodec/neon/Makefile +++ b/libavcodec/neon/Makefile @@ -2,3 +2,7 @@ clean:: $(RM) $(CLEANSUFFIXES:%=libavcodec/neon/%) OBJS-$(CONFIG_MPEGVIDEO) += neon/mpegvideo.o + +ifeq ($(ARCH),arm) +OBJS-$(CONFIG_H264QPEL) += neon/h264qpel_10.o +endif diff --git a/libavcodec/neon/h264qpel_10.c b/libavcodec/neon/h264qpel_10.c new file mode 100644 index 0000000000..d85c025515 --- /dev/null +++ b/libavcodec/neon/h264qpel_10.c @@ -0,0 +1,228 @@ +/* + * ARMv7 NEON H.264 High10 quarter-pel motion compensation. + * Ported from the experimental Nuvio High10 motion-compensation family. + * Six-tap filtering and rounding follow h264qpel_template.c (FFmpeg 7.0.2). + * The 1-D saturation strategy follows aarch64/h264qpel_neon.S + * (0f745b74ecd4); no AArch64 assembly was translated. + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +#include <arm_neon.h> +#include <stdint.h> +#include "libavcodec/h264qpel.h" +#include "libavutil/attributes.h" + +#define INLINE static av_always_inline +#define HELPER static av_noinline +#define PAD 10230 + +INLINE uint16x8_t finish1(uint16x8_t positive, uint16x8_t negative) +{ + return vminq_u16(vrshrq_n_u16(vqsubq_u16(positive, negative), 5), + vdupq_n_u16(1023)); +} +INLINE int16x8_t horizontal_raw(const uint16_t *s) +{ + uint16x8_t a = vld1q_u16(s - 2), tail = vld1q_u16(s + 3); + uint16x8_t b = vextq_u16(tail, tail, 3); + uint16x8_t p1 = vextq_u16(a, b, 1), p2 = vextq_u16(a, b, 2), + p3 = vextq_u16(a, b, 3); + uint16x8_t p4 = vextq_u16(a, b, 4), p5 = vextq_u16(a, b, 5); + uint16x8_t positive = vmlaq_n_u16(vaddq_u16(a, p5), vaddq_u16(p2, p3), 20); + uint16x8_t negative = vmulq_n_u16(vaddq_u16(p1, p4), 5); + /* Wrapping arithmetic intentionally preserves unrounded signed result. + * Final biased range [-20460,32736] fits int16 exactly. */ + return vreinterpretq_s16_u16( + vsubq_u16(vsubq_u16(positive, negative), vdupq_n_u16(PAD))); +} +INLINE uint16x8_t clip_raw1(int16x8_t raw) +{ + uint16x8_t positive = + vaddq_u16(vreinterpretq_u16_s16(raw), vdupq_n_u16(PAD)); + positive = vandq_u16(positive, vcgeq_s16(raw, vdupq_n_s16(-PAD))); + return vminq_u16(vrshrq_n_u16(positive, 5), vdupq_n_u16(1023)); +} +HELPER void horizontal(uint16_t *out, const uint16_t *src, ptrdiff_t stride, + int n) +{ + for (int y = 0; y < n; y++) + for (int x = 0; x < n; x += 8) + vst1q_u16(out + y * n + x, + clip_raw1(horizontal_raw(src + y * stride + x))); +} +INLINE uint16x8_t filter_vertical1(uint16x8_t a, uint16x8_t b, uint16x8_t c, + uint16x8_t d, uint16x8_t e, uint16x8_t f) +{ + uint16x8_t positive = vmlaq_n_u16(vaddq_u16(a, f), vaddq_u16(c, d), 20); + uint16x8_t negative = vmulq_n_u16(vaddq_u16(b, e), 5); + return finish1(positive, negative); +} +HELPER void vertical(uint16_t *out, const uint16_t *src, ptrdiff_t stride, + int n) +{ + for (int x = 0; x < n; x += 8) { + uint16x8_t a = vld1q_u16(src - 2 * stride + x), + b = vld1q_u16(src - stride + x); + uint16x8_t c = vld1q_u16(src + x), d = vld1q_u16(src + stride + x); + uint16x8_t e = vld1q_u16(src + 2 * stride + x), + f = vld1q_u16(src + 3 * stride + x); + for (int y = 0; y < n; y++) { + vst1q_u16(out + y * n + x, filter_vertical1(a, b, c, d, e, f)); + if (y + 1 < n) { + a = b; + b = c; + c = d; + d = e; + e = f; + f = vld1q_u16(src + (y + 4) * stride + x); + } + } + } +} +INLINE uint16x4_t finish2(int16x4_t a, int16x4_t b, int16x4_t c, int16x4_t d, + int16x4_t e, int16x4_t f) +{ + int32x4_t total = vmlaq_n_s32(vaddl_s16(a, f), vaddl_s16(c, d), 20); + total = vmlsq_n_s32(total, vaddl_s16(b, e), 5); + total = vaddq_s32(total, vdupq_n_s32(PAD * 32 + 512)); + total = vshrq_n_s32(total, 10); + total = vminq_s32(vmaxq_s32(total, vdupq_n_s32(0)), vdupq_n_s32(1023)); + return vqmovun_s32(total); +} +HELPER void horizontal_intermediate(int16_t *raw, const uint16_t *src, + ptrdiff_t stride, int n) +{ + for (int y = 0; y < n + 5; y++) + for (int x = 0; x < n; x += 8) + vst1q_s16(raw + y * n + x, + horizontal_raw(src + (y - 2) * stride + x)); +} +HELPER void two_axis(uint16_t *out, const int16_t *raw, int n) +{ + for (int x = 0; x < n; x += 8) { + int16x8_t a = vld1q_s16(raw + x), b = vld1q_s16(raw + n + x), + c = vld1q_s16(raw + 2 * n + x); + int16x8_t d = vld1q_s16(raw + 3 * n + x), + e = vld1q_s16(raw + 4 * n + x), + f = vld1q_s16(raw + 5 * n + x); + for (int y = 0; y < n; y++) { + uint16x4_t lo = + finish2(vget_low_s16(a), vget_low_s16(b), vget_low_s16(c), + vget_low_s16(d), vget_low_s16(e), vget_low_s16(f)); + uint16x4_t hi = + finish2(vget_high_s16(a), vget_high_s16(b), vget_high_s16(c), + vget_high_s16(d), vget_high_s16(e), vget_high_s16(f)); + vst1q_u16(out + y * n + x, vcombine_u16(lo, hi)); + if (y + 1 < n) { + a = b; + b = c; + c = d; + d = e; + e = f; + f = vld1q_s16(raw + (y + 6) * n + x); + } + } + } +} + +INLINE void predict(uint8_t *dst_bytes, const uint8_t *src_bytes, + ptrdiff_t bytes, int n, int fx, int fy, int avg) +{ + ptrdiff_t stride = bytes / 2; + uint16_t *dst = (uint16_t *)dst_bytes; + const uint16_t *src = (const uint16_t *)src_bytes; + uint16_t h[16 * 16], v[16 * 16], hv[16 * 16]; + int16_t raw[16 * 21]; + const uint16_t *p1 = NULL, *p2 = NULL; + ptrdiff_t st1 = n, st2 = n; + int raw_h = 0; + if (!fx && !fy) { + p1 = src; + st1 = stride; + } else if (!fy) { + horizontal(h, src, stride, n); + p1 = h; + if (fx != 2) { + p2 = src + (fx == 3); + st2 = stride; + } + } else if (!fx) { + vertical(v, src, stride, n); + p1 = v; + if (fy != 2) { + p2 = src + (fy == 3) * stride; + st2 = stride; + } + } else if ((fx & 1) && (fy & 1)) { + horizontal(h, src + (fy == 3) * stride, stride, n); + vertical(v, src + (fx == 3), stride, n); + p1 = h; + p2 = v; + } else { + horizontal_intermediate(raw, src, stride, n); + two_axis(hv, raw, n); + p1 = hv; + if (fx == 2 && fy != 2) { + raw_h = 1; + } else if (fy == 2 && fx != 2) { + vertical(v, src + (fx == 3), stride, n); + p2 = v; + } + } + for (int y = 0; y < n; y++) + for (int x = 0; x < n; x += 8) { + uint16x8_t value = vld1q_u16(p1 + y * st1 + x); + if (raw_h) + value = vrhaddq_u16( + value, + clip_raw1(vld1q_s16(raw + (y + 2 + (fy == 3)) * n + x))); + else if (p2) + value = vrhaddq_u16(value, vld1q_u16(p2 + y * st2 + x)); + if (avg) + value = vrhaddq_u16(value, vld1q_u16(dst + y * stride + x)); + vst1q_u16(dst + y * stride + x, value); + } +} +#define ENTRY(N, X, Y, A) \ + static void q##N##_##X##Y##_##A(uint8_t *d, const uint8_t *s, ptrdiff_t t) \ + { \ + predict(d, s, t, N, X, Y, A); \ + } +#define ROW(N, Y, A) \ + ENTRY(N, 0, Y, A) ENTRY(N, 1, Y, A) ENTRY(N, 2, Y, A) ENTRY(N, 3, Y, A) +#define SIZE(N, A) ROW(N, 0, A) ROW(N, 1, A) ROW(N, 2, A) ROW(N, 3, A) +SIZE(16, 0) +SIZE(16, 1) +SIZE(8, 0) +SIZE(8, 1) +#define ASSIGN(N, I, X, Y) \ + c->put_h264_qpel_pixels_tab[I][X + Y * 4] = q##N##_##X##Y##_0; \ + c->avg_h264_qpel_pixels_tab[I][X + Y * 4] = q##N##_##X##Y##_1; +#define SETROW(N, I, Y) \ + ASSIGN(N, I, 0, Y) ASSIGN(N, I, 1, Y) ASSIGN(N, I, 2, Y) ASSIGN(N, I, 3, Y) + +av_cold void ff_h264qpel_init_neon_10(H264QpelContext *c) +{ + SETROW(16, 0, 0) + SETROW(16, 0, 1) + SETROW(16, 0, 2) + SETROW(16, 0, 3) + SETROW(8, 1, 0) + SETROW(8, 1, 1) + SETROW(8, 1, 2) + SETROW(8, 1, 3) +} diff --git a/tests/checkasm/h264qpel.c b/tests/checkasm/h264qpel.c index 6f4a021faf..b586bf2891 100644 --- a/tests/checkasm/h264qpel.c +++ b/tests/checkasm/h264qpel.c @@ -29,7 +29,7 @@ static const uint32_t pixel_mask[3] = { 0xffffffff, 0x01ff01ff, 0x03ff03ff }; #define SIZEOF_PIXEL ((bit_depth + 7) / 8) -#define BUF_SIZE (2 * 16 * (16 + 3 + 4)) +#define BUF_SIZE (2 * 64 * (16 + 3 + 4)) #define randomize_buffers() \ do { \ @@ -45,8 +45,8 @@ static const uint32_t pixel_mask[3] = { 0xffffffff, 0x01ff01ff, 0x03ff03ff }; } \ } while (0) -#define src0 (buf0 + 3 * 2 * 16) /* h264qpel functions read data from negative src pointer offsets */ -#define src1 (buf1 + 3 * 2 * 16) +#define src0 (buf0 + 3 * 2 * 64 + 16) +#define src1 (buf1 + 3 * 2 * 64 + 16) void checkasm_check_h264qpel(void) { @@ -55,7 +55,7 @@ void checkasm_check_h264qpel(void) LOCAL_ALIGNED_16(uint8_t, dst0, [BUF_SIZE]); LOCAL_ALIGNED_16(uint8_t, dst1, [BUF_SIZE]); H264QpelContext h; - int op, bit_depth, i, j; + int op, bit_depth, i, j, stride, offset; declare_func_emms(AV_CPU_FLAG_MMX | AV_CPU_FLAG_MMXEXT, void, uint8_t *dst, const uint8_t *src, ptrdiff_t stride); for (op = 0; op < 2; op++) { @@ -73,6 +73,22 @@ void checkasm_check_h264qpel(void) call_new(dst1, src1, size * SIZEOF_PIXEL); if (memcmp(buf0, buf1, BUF_SIZE) || memcmp(dst0, dst1, BUF_SIZE)) fail(); + /* Exercise padded strides and source offsets while + * preserving 16-byte destination alignment required + * by existing optimized callbacks. */ + for (stride = 32; stride <= 64; stride *= 2) { + for (offset = 0; offset < (bit_depth > 8 ? 16 : 8); + offset += 8) { + randomize_buffers(); + call_ref(dst0, src0 + offset, + stride * SIZEOF_PIXEL); + call_new(dst1, src1 + offset, + stride * SIZEOF_PIXEL); + if (memcmp(buf0, buf1, BUF_SIZE) || + memcmp(dst0, dst1, BUF_SIZE)) + fail(); + } + } bench_new(dst1, src1, size * SIZEOF_PIXEL); } } -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
