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>
@ann0see

ann0see commented Jul 25, 2026

Copy link
Copy Markdown
Member

Should have Autobuild please build all targets

Comment thread Jamulus.pro
libs/opus/src/repacketizer.c

SOURCES_OPUS_ARM = libs/opus/celt/arm/armcpu.c \
# NEON intrinsic sources: self-contained, need no external library.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# NEON intrinsic sources: self-contained, need no external library.

@ann0see

ann0see commented Jul 25, 2026

Copy link
Copy Markdown
Member

Also this needs to be built, tested and analyzed on Apple Silicon. The arm handling in general is sketchy. For macOS Apple Silicon the explicit variable setting of arm64 is needed otherwise it tries to compile SSE for arm64 which obviously doesn't work

Comment thread Jamulus.pro
# NE10 sources: FFT/MDCT acceleration via the external Ne10 library, which
# Jamulus does not bundle (they #include <NE10_dsp.h> unconditionally and are
# gated on HAVE_ARM_NE10, which we never define). Kept listed for completeness
# but never added to SOURCES.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then investigate if we can use it and it makes sense to use it - or drop it otherwise

Comment thread Jamulus.pro
SOURCES_OPUS_ARCH += $$SOURCES_OPUS_ARM
DEFINES_OPUS += OPUS_ARM_PRESUME_NEON=1 OPUS_ARM_PRESUME_NEON_INTR=1
contains(QT_ARCH, arm64-v8a):DEFINES_OPUS += OPUS_ARM_PRESUME_AARCH64_NEON_INTR
} else:contains(QT_ARCH, arm64) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to merge this with the above case to avoid duplication

@mcfnord

mcfnord commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

Note

📡 STAND BY FOR AN LLM-AUTHORED MESSAGE.

Native verification on a Raspberry Pi 4 (Cortex-A72, aarch64 Linux, not cross-compiled), including a result I didn't expect.

Symbols: built this branch and the same base commit without it, both natively. nm on the shipped binary confirms all 9 *_neon functions defined in the fixed build, 0 in the unfixed one — the fix links and ships correctly.

CPU benefit: interleaved A/B benchmark (server pinned to one core, bot load, perf stat task-clock), at OPUS_SET_COMPLEXITY(1) (Jamulus's actual setting) I measured no significant difference — and re-tested at OPUS_SET_COMPLEXITY(10) in case low complexity was masking a win: still no significant difference.

Why: confirmed with perf record -g under load — Jamulus uses opus_custom_* (Opus Custom, CELT-only; SILK never runs). 6 of the 9 accelerated functions are SILK-only and are dead code in Jamulus regardless of this fix. Of the 3 CELT functions, only 2 ever appeared under profiling, combined under 1% of server CPU.

Bottom line: still worth merging — dead ARM code silently no-op'ing is a real defect on its own terms — but I'd frame it as a correctness/build-hygiene fix rather than a proven Jamulus performance win. This only covers Linux aarch64; it doesn't address the Apple Silicon question above.

@pljones

pljones commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

I'd love to get my local (Ubuntu 24.04 LTS) Android build working. Currently I rely on Github.

@pljones

pljones commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

#2806 is "mac" -- this is "android". Is #2806 mis-titled?

@mcfnord

mcfnord commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Note

📡 STAND BY FOR AN LLM-AUTHORED MESSAGE.

Catching up on the thread:

@ann0see, re: build all targets — CI already did, on this PR's own run: Android APK, Linux arm64 .deb, armhf .deb, macOS, macOS Legacy, iOS, Windows, Windows JACK all pass (https://github.com/jamulussoftware/jamulus/actions/runs/30064659704). The macOS job builds a universal binary and mac/deploy_mac.sh explicitly sets QT_ARCH=arm64 for the Apple Silicon slice, so that slice does compile clean with this fix. I don't have Apple hardware, so that's a compile proof only — no perf number for Apple Silicon from me, same caveat as my Pi 4 result below.

@pljones, re: is #2806 mis-titled — fair catch, and a bit of both: #2806's title says "Mac ARM," which matches how the issue was originally reported, but this PR's actual scope is wider than that title suggests. It touches every non-Android 64-bit ARM target that reports QT_ARCH=arm64 (macOS Apple Silicon, iOS, Linux aarch64) — and separately, while implementing that, it surfaced a genuine Android-specific bug: Android's own arch match populated the NEON source list but two other gaps meant those sources were never actually compiled or linked, so Android has been silently running plain-C opus too, this whole time. That second part isn't really "about" #2806 at all — it's its own defect that happened to surface in the same investigation.

My conclusion, for the record: native Pi 4 (Cortex-A72, Linux aarch64) testing confirms the fix is real and correct — all 9 NEON symbols ship where 0 did before — but produces no measurable CPU win for Jamulus specifically, at either complexity=1 (Jamulus's actual setting) or complexity=10 (max), because Jamulus's CELT-only Opus Custom mode only ever touches 2 of those 9 functions, combined under 1% of server CPU. That's a Jamulus-specific result, not a NEON-doesn't-help result — other opus consumers using SILK or the general encoder path would very plausibly see more. I'd still call this worth merging on correctness/build-hygiene grounds (dead ARM code silently no-op'ing across four platforms is a real defect on its own), just not on a proven performance claim. That's everything I can verify from here — Apple Silicon and a from-scratch Android build both remain open if anyone has the hardware.

@dingodoppelt dingodoppelt added the AI AI generated or potentially AI generated label Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI AI generated or potentially AI generated android Android runtime issue

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

ARM: Possibly missing opus optimizations

4 participants