Skip to content

Build opus with NEON intrinsics on non-Android 64-bit ARM#3833

Open
mcfnord wants to merge 1 commit into
jamulussoftware:mainfrom
mcfnord:fix-2806-arm64-opus-neon
Open

Build opus with NEON intrinsics on non-Android 64-bit ARM#3833
mcfnord wants to merge 1 commit into
jamulussoftware:mainfrom
mcfnord:fix-2806-arm64-opus-neon

Conversation

@mcfnord

@mcfnord mcfnord commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Note

📡 STAND BY FOR AN LLM-AUTHORED MESSAGE.

Fixes #2806.

Follows up on the root-cause analysis posted in #2806 (comment): the opus arch-optimisation block in Jamulus.pro matches only the Android ABI names (armeabi-v7a, arm64-v8a). Everywhere else, Qt reports 64-bit ARM as QT_ARCH=arm64 — Apple Silicon macOS (mac/deploy_mac.sh passes it explicitly per architecture slice), iOS, and Linux aarch64 — so all of those builds compile opus as plain C with no NEON acceleration, even though the NEON intrinsic sources ship in-tree.

While implementing the fix, a deeper problem surfaced: the ARM NEON sources have never been compiled on any target, including Android. Two independent gaps:

  1. SOURCES += $$SOURCES_OPUS_ARCH only exists in the x86 consumption branch (around line 1150), so the Android arch match populated SOURCES_OPUS_ARCH but nothing ever built it.
  2. Even if the files had been compiled, opus gates the inclusion of its arm/*.h headers on OPUS_ARM_MAY_HAVE_NEON_INTR (see libs/opus/celt/pitch.h:49 and libs/opus/celt/cpu_support.h), which the block never defines — the PRESUME defines alone are inert.

That the Android builds link cleanly is itself the proof: with the defines inert, the generic C paths are used and the never-compiled NEON objects are never referenced.

What this PR does

  • Adds an else:contains(QT_ARCH, arm64) branch to the arch block that defines OPUS_ARM_MAY_HAVE_NEON_INTR=1 OPUS_ARM_PRESUME_NEON_INTR=1 OPUS_ARM_PRESUME_AARCH64_NEON_INTR=1. NEON is part of the base AArch64 ISA, so presuming it is always safe on this arch — with PRESUME set, opus calls the NEON functions directly and no runtime CPU detection machinery is engaged.
  • Adds a matching consumption branch that compiles the NEON intrinsic sources into SOURCES. Unlike the x86 SSE files, they need no special compiler flags on AArch64, so no QMAKE_EXTRA_COMPILERS dance is required.
  • Splits SOURCES_OPUS_ARM into the nine self-contained NEON intrinsic files and the two NE10 files (celt_fft_ne10.c, celt_mdct_ne10.c). The NE10 pair #include <NE10_dsp.h> from the external Ne10 library, which Jamulus does not bundle, so only the NEON subset is compiled.

Deliberately not included: activating NEON for the Android ABIs. That would change currently-shipping builds and armeabi-v7a needs its own decision about NEON baseline guarantees (#2475 territory). This PR leaves the Android and x86 branches untouched — their generated Makefiles are byte-identical before and after this change (verified by diffing qmake-generated Makefile.Release for QT_ARCH=x86_64 and QT_ARCH=arm64-v8a).

Why it's worth having

Opus encode/decode is the dominant per-channel CPU cost of a Jamulus server and a large share of the client's audio-thread budget. The NEON intrinsics cover the hot inner loops: pitch cross-correlation and inner products in CELT (celt_pitch_xcorr_float_neon, celt_inner_prod_neon, dual_inner_prod_neon) plus the SILK NSQ/LPC kernels. Every Apple Silicon Mac client, every iOS build, and every Linux aarch64 server (a rapidly growing hosting class — AWS Graviton, Oracle Ampere, Raspberry Pi 5) currently runs the scalar C fallback. This change turns the optimizations that already ship in the tree on for that hardware, with zero effect on any other platform.

Testing (cross-verified on Linux, aarch64-linux-gnu-gcc 13.3):

  • qmake "QT_ARCH=arm64" on the patched tree selects the nine NEON sources and the three defines; the NE10 files are correctly absent.
  • The full opus source set the patched arm64 build compiles (138 files) cross-compiles cleanly for aarch64 with the exact DEFINES_OPUS from the generated Makefile, archives into a static library with all nine *_neon symbols defined and zero unresolved NEON references.
  • Disassembly of the resulting objects shows real NEON vector code (470 SIMD vector ops across the intrinsic objects, e.g. fmla v1.4s chains in celt_pitch_xcorr_float_neon) — genuinely vectorized, not scalar fallback.
  • QT_ARCH=x86_64 and QT_ARCH=arm64-v8a Makefiles are byte-identical pre/post patch (regression guard).
  • CI provides the remaining native proof: the macOS runners build the arm64 slice with this branch active.

🤖 Generated with Claude Code

…tware#2806)

The opus arch-optimisation block only matched the Android ABI names
`armeabi-v7a` and `arm64-v8a`. Everywhere else Qt reports 64-bit ARM as
`QT_ARCH=arm64` (Apple Silicon macOS, iOS, Linux aarch64), so those
builds fell through to the plain-C opus path with no NEON acceleration.

In fact the ARM NEON sources were never compiled on any target: the
`SOURCES += $$SOURCES_OPUS_ARCH` line only lived in the x86 branch, so
even the Android arch match populated the variable but never built it.

Add an `arm64` branch that:
- defines OPUS_ARM_MAY_HAVE_NEON_INTR (required for opus to include its
  arm/*.h headers) plus the PRESUME_NEON_INTR / PRESUME_AARCH64_NEON_INTR
  defines (NEON is part of the base AArch64 ISA, so it is always present);
- compiles the NEON intrinsic sources, which need no special compiler
  flags, straight into SOURCES.

Split SOURCES_OPUS_ARM into the self-contained NEON intrinsic files and
the two NE10 files, which #include <NE10_dsp.h> from the external Ne10
library that Jamulus does not bundle; only the NEON subset is compiled.

The x86 and Android arch matches are untouched: their generated
Makefiles are byte-identical before and after this change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mac ARM: Possibly missing opus optimizations

1 participant