|
| 1 | +# HiFi intrinsics opportunities in ffmpeg_dec |
| 2 | + |
| 3 | +Analysis of the hot audio-processing paths in the FFmpeg SOF module (decode / |
| 4 | +filter / encode) and where Xtensa **HiFi** SIMD intrinsics would give the biggest |
| 5 | +wins. It covers both the code this module owns and the FFmpeg code it links. |
| 6 | + |
| 7 | +## Background: why this matters on Xtensa |
| 8 | + |
| 9 | +- The FFmpeg archive is cross-built with **`--disable-asm`** (see `ffmpeg.cmake`), |
| 10 | + because FFmpeg has no Xtensa assembly. Every DSP kernel therefore runs as |
| 11 | + **scalar C** on the DSP — no SIMD, no fused multiply-add vectorisation. |
| 12 | +- FFmpeg dispatches its DSP through function-pointer contexts with per-architecture |
| 13 | + init hooks (`ff_*_init_aarch64/arm/x86/riscv`) — but **no `_xtensa`** variant. The |
| 14 | + generic `_c` kernels are what execute on our target. |
| 15 | +- SOF already ships HiFi-optimised DSP and the toolchain for it. Use it as the |
| 16 | + template: |
| 17 | + - ISA select + intrinsics header (`src/math/exp_fcn_hifi.c`): |
| 18 | + ```c |
| 19 | + #if XCHAL_HAVE_HIFI5 |
| 20 | + #include <xtensa/tie/xt_hifi5.h> |
| 21 | + #elif XCHAL_HAVE_HIFI4 |
| 22 | + #include <xtensa/tie/xt_hifi4.h> |
| 23 | + #else |
| 24 | + #include <xtensa/tie/xt_hifi3.h> |
| 25 | + #endif |
| 26 | + ``` |
| 27 | + - Existing HiFi kernels to mirror: `src/math/fir_hifi{2ep,3,5}.c`, |
| 28 | + `src/math/iir_df1_hifi{3,4,5}.c`, `src/math/fft/fft_{16,32}_hifi3.c`, |
| 29 | + `src/math/exp_fcn_hifi.c`. `ace30` (ptl) is HiFi4-class. |
| 30 | + |
| 31 | +There are two distinct bodies of code: |
| 32 | + |
| 33 | +- **(A) Module glue we own** — the PCM format-conversion loops in |
| 34 | + `ffmpeg_dec-*.c`. Small, per-sample, easy to hand-vectorise with HiFi, and we |
| 35 | + can change them freely. |
| 36 | +- **(B) FFmpeg DSP** — the actual codec compute (MDCT/FFT, LPC, windowing). Much |
| 37 | + higher cycle cost, but upstream; adding HiFi means either FFmpeg fork patches |
| 38 | + (an `_xtensa` DSP init, mirroring the other arch dirs) or routing to SOF's own |
| 39 | + HiFi kernels. |
| 40 | + |
| 41 | +## Hot-path summary |
| 42 | + |
| 43 | +| # | Path | File | Kind | Now | HiFi win | Effort | |
| 44 | +|---|------|------|------|-----|----------|--------| |
| 45 | +| 1 | MDCT/FFT (AAC/MP3/Opus/afftdn) | FFmpeg `libavutil/tx*` | float/fixed transform | scalar C | **very high** | high | |
| 46 | +| 2 | float vector kernels (fmul/window/overlap-add) | FFmpeg `libavutil/float_dsp` | float SIMD | scalar C | **high** | low–med | |
| 47 | +| 3 | FLAC LPC / residual | FFmpeg `libavcodec/flacdsp` | int32/64 MAC | scalar C | high | med | |
| 48 | +| 4 | MP3 synthesis window / imdct36 | FFmpeg `libavcodec/mpegaudiodsp` | float/fixed | scalar C | high | med | |
| 49 | +| 5 | AAC PS / SBR DSP | FFmpeg `aacpsdsp`/`sbrdsp` | float SIMD | scalar C | med (HE-AAC only) | med | |
| 50 | +| 6 | S32↔float PCM convert (filter) | `ffmpeg_dec-filter.c:230,255` | int↔float + scale | scalar C | med | **low** | |
| 51 | +| 7 | planar→interleaved PCM (decode) | `ffmpeg_dec-ffmpeg.c:231` | copy/interleave | scalar C | low–med | **low** | |
| 52 | +| 8 | S32→S16 pack (encode) | `ffmpeg_dec-encode.c:147` | narrow + interleave | scalar C | med | **low** | |
| 53 | +| 9 | fast float libm (powf/exp2f/...) | `fastmathf.c` | scalar poly/Newton | scalar C | med (per-band) | med | |
| 54 | + |
| 55 | +Priority order (cost/benefit): **2 → 6/7/8 → 3 → 1 → 4 → 9 → 5**. Start with the |
| 56 | +easy, high-use float vector kernels (2) and the conversion loops we own (6–8); |
| 57 | +tackle the transform (1) last because it is the most work despite the highest |
| 58 | +raw cost. |
| 59 | + |
| 60 | +## (A) Module conversion loops — we own these, do them first |
| 61 | + |
| 62 | +These run **per sample over a whole frame** (typically 1024–4608 samples × |
| 63 | +channels per call), so they are genuinely hot and are trivial, self-contained HiFi |
| 64 | +wins. Add HiFi versions guarded by `#if XCHAL_HAVE_HIFI*` with the current scalar |
| 65 | +loop as the `#else` fallback. |
| 66 | + |
| 67 | +1. **Filter S32→float deinterleave + normalise** — `ffmpeg_dec-filter.c:230` |
| 68 | + ```c |
| 69 | + ((float *)in->data[c])[i] = (float)src[i * ch + c] / FFMPEG_AF_S32_SCALE; |
| 70 | + ``` |
| 71 | + HiFi: load 32-bit lanes, convert int32→float (`AE_FLOAT32`-family), multiply by |
| 72 | + the reciprocal scale, store to the per-channel plane. Deinterleave with |
| 73 | + strided/`AE_SEL` moves. |
| 74 | + |
| 75 | +2. **Filter float→S32 interleave + denormalise + clip** — `ffmpeg_dec-filter.c:255` |
| 76 | + The scalar path multiplies, branches to clamp, then casts. HiFi does the |
| 77 | + float→int32 convert **with saturation** in one op (no per-sample branch), which |
| 78 | + is both faster and removes the mispredicted clamp branch. |
| 79 | + |
| 80 | +3. **Encode S32→S16 narrow + deinterleave** — `ffmpeg_dec-encode.c:147` |
| 81 | + ```c |
| 82 | + int16_t s = (int16_t)(in[i * ch + c] >> 16); |
| 83 | + ``` |
| 84 | + HiFi: pack 32→16 with rounding (`AE_ROUND16X4F32`-style) and store; SIMD handles |
| 85 | + 4–8 samples per iteration. |
| 86 | + |
| 87 | +4. **Decode planar→interleaved** — `ffmpeg_dec-ffmpeg.c:231` |
| 88 | + Per-element `memcpy`-equivalent; a HiFi interleave (vector load per plane, |
| 89 | + `AE_SEL` interleave, vector store) removes the per-sample loop overhead. Lower |
| 90 | + priority as many decoders already output the sink format. |
| 91 | + |
| 92 | +A single small `ffmpeg_dec-convert.c` with HiFi + scalar variants (like SOF's |
| 93 | +`*_generic.c` / `*_hifi3.c` split) would hold all four and be reusable across the |
| 94 | +three modes. |
| 95 | + |
| 96 | +## (B) FFmpeg DSP — the real compute |
| 97 | + |
| 98 | +FFmpeg's DSP contexts are function-pointer tables initialised per arch. The |
| 99 | +Xtensa-idiomatic fix is a **fork patch** adding `ff_<dsp>_init_xtensa()` with HiFi |
| 100 | +intrinsics, mirroring `libavcodec/aarch64/`, `x86/` etc. (our FFmpeg is already a |
| 101 | +SOF fork — see `west.yml` — so such patches have a home). |
| 102 | + |
| 103 | +- **`AVFloatDSPContext`** (`libavutil/float_dsp.h`) — `vector_fmul`, |
| 104 | + `vector_fmul_window`, `vector_fmul_add`, `vector_fmul_reverse`, |
| 105 | + `scalarproduct_float`. Used for windowing / overlap-add by **AAC, MP3 and Opus**. |
| 106 | + These are plain multiply/MAC over contiguous float arrays — the easiest and |
| 107 | + highest-leverage HiFi target (one small `float_dsp_init_xtensa` benefits three |
| 108 | + codecs). **Do this first on the FFmpeg side.** |
| 109 | +- **`FLACDSPContext`** (`libavcodec/flacdsp.h`) — `lpc16`/`lpc32`/`lpc33` and |
| 110 | + decorrelate. FLAC decode is dominated by the LPC prediction MAC loop over the |
| 111 | + residual: a natural fit for HiFi multiply-accumulate. `ff_flacdsp_init_xtensa`. |
| 112 | +- **`MPADSPContext`** (`libavcodec/mpegaudiodsp.h`) — `apply_window_{float,fixed}`, |
| 113 | + `imdct36_blocks_*`, `synth_filter`. The MP3 subband synthesis window is the MP3 |
| 114 | + decode hot loop. |
| 115 | +- **`libavutil/tx`** (MDCT/FFT) — the single largest cost for AAC/MP3/Opus decode |
| 116 | + and for the `afftdn` filter. It is a "codelet" system rather than one function |
| 117 | + pointer, so it is the most work. Two routes: |
| 118 | + 1. Add HiFi FFT/MDCT codelets to the fork (largest effort, cleanest for FFmpeg). |
| 119 | + 2. Bridge to **SOF's existing HiFi3 FFT** (`src/math/fft/fft_*_hifi3.c`) by |
| 120 | + replacing FFmpeg's transform behind `av_tx_init` for the sizes SOF supports. |
| 121 | + Less code, but a semantic bridge (twiddle/scaling/layout must match) and only |
| 122 | + covers power-of-two sizes. |
| 123 | +- **`PSDSPContext` / SBR DSP** — only relevant if HE-AAC (SBR/PS) is enabled; skip |
| 124 | + unless needed. |
| 125 | + |
| 126 | +## (C) fastmathf — the module's float libm |
| 127 | + |
| 128 | +`fastmathf.c` (`powf`/`exp2f`/`log2f`/`sinf`/`cosf`/`sqrtf`/`cbrtf`) is scalar |
| 129 | +polynomial/Newton code. These are called per-band / per-frame (not the innermost |
| 130 | +per-sample loop), so they are a secondary target. HiFi wins come from: |
| 131 | +- `sqrtf`/`cbrtf`: HiFi `RSQRT`/reciprocal seed instructions instead of the |
| 132 | + bit-trick + Newton. |
| 133 | +- `exp2f`/`log2f`/`sinf`/`cosf`: process 2/4 lanes at once when a decoder needs a |
| 134 | + vector of them (e.g. AAC scalefactor gains), and use FMA for the polynomials. |
| 135 | + Mirror `src/math/exp_fcn_hifi.c`. |
| 136 | + |
| 137 | +## Recommendations |
| 138 | + |
| 139 | +1. **Quick wins we own** — hand-vectorise the four conversion loops (§A) with |
| 140 | + `#if XCHAL_HAVE_HIFI*` + scalar fallback. Small, self-contained, no fork needed. |
| 141 | +2. **Biggest FFmpeg leverage-per-effort** — add `ff_float_dsp_init_xtensa` (HiFi |
| 142 | + `vector_fmul*`/`scalarproduct`) to the FFmpeg fork; benefits AAC + MP3 + Opus. |
| 143 | +3. **Per-codec kernels** — `flacdsp` (FLAC), `mpegaudiodsp` (MP3) HiFi inits as |
| 144 | + fork patches, gated on the enabled decoders. |
| 145 | +4. **Transform** — evaluate SOF-HiFi-FFT bridge vs. HiFi tx codelets for |
| 146 | + `libavutil/tx`; highest payoff, most work, do once the cheaper items land. |
| 147 | +5. Always keep the scalar C path as the fallback (`#else`) and behind the same ISA |
| 148 | + guards SOF uses, so non-HiFi / host/testbench builds still work. |
| 149 | + |
| 150 | +Measurement: profile on hardware per codec before and after; the ranking above is |
| 151 | +by expected cost, but the actual hot spot depends on the codec and content (FLAC |
| 152 | +is LPC-bound, AAC/MP3/Opus are transform+window-bound). |
0 commit comments