Skip to content

perf(arm64): prefer the NEON nnedi3 over scalar znedi3 - #61

Merged
StuartCameronCode merged 3 commits into
mainfrom
feat/arm64-nnedi3-preference
Aug 7, 2026
Merged

perf(arm64): prefer the NEON nnedi3 over scalar znedi3#61
StuartCameronCode merged 3 commits into
mainfrom
feat/arm64-nnedi3-preference

Conversation

@StuartCameronCode

Copy link
Copy Markdown
Owner

On ARM, znedi3 falls back to a pure scalar path — download-deps-{macos,linux}.sh
build it X86=0 X86_AVX512=0 and its SIMD kernels are x86-only. The bundled
dubhater nnedi3 has real NEON kernels, implements the same network from the same
nnedi3_weights.bin, and has an identical signature for every argument QTGMC uses.

Measured on an M1 against the rebuilt R78 bundle, QTGMC Slow: the interpolator goes
from 138.2 to 29.7 CPU-seconds (~4.7x), worth ~13% end-to-end.

The choice is made at runtime via platform.machine(), so x86 keeps znedi3
unchanged
and the patch text stays identical on every platform. Delivered two ways:
a _nnedi3() helper in both templates, and havsfunc patch 6 rewriting the three
hardcoded call sites (daa, santiag, QTGMC).

Also fixes the Linux side of the deps build, which the new packaging guard caught:

  • linux-arm64 could never build nnedi3. dubhater's build system treats every ARM
    as 32-bit ARMv7 — -mfpu=neon is rejected by aarch64 gcc, and cpufeatures.cpp
    reads HWCAP_ARM_* constants that only exist on 32-bit. macOS only ever hit the
    first, via its __APPLE__ branch. Both patched, with a guard that hard-fails if
    either stops matching — nnedi3.cpp only does if (!cpu.neon) d->opt = 0, so a
    lapsed patch yields a correct picture at scalar speed.
  • linux-x64 no longer requires it. x86 keeps znedi3; macos-x64 and
    windows-x64 already omit nnedi3, so requiring it on linux-x64 was inconsistent.

Verification

  • test_92 fails the build if a template reintroduces a direct implementation call
  • All five bundles rebuilt on deps-v1.8.0-rc1; nightly green on all 4 platforms
  • Local arm64 debug build confirms havsfunc._nnedi3_impl() resolves to nnedi3.nnedi3

znedi3's SIMD kernels are x86-only, so the ARM bundles build it with X86=0 and
it runs fully scalar (PredictorC / PrescreenerOldC). The bundled dubhater
nnedi3 ships real NEON kernels and is 6.3x faster for the same call — measured
on an M1, QTGMC Slow, 400 frames of 720x576: 37.8s vs 5.95s of CPU, which is
30% of the whole arm64 QTGMC cost.

havsfunc hardcoded `core.znedi3.nnedi3 if hasattr(core, 'znedi3')` at three
call sites (daa, santiag, QTGMC), and both our templates named znedi3 directly
in the upscale path, so every ARM deinterlace paid it.

Both plugins implement the same network from the same nnedi3_weights.bin and
their signatures are identical for every argument used, so this is a drop-in
swap: measured mean output difference 0.045/255 for the interpolator alone and
0.072/255 end-to-end through QTGMC, against a tolerance of 2.0. Worst single
pixel is ~48/255 on hard edges, where the two implementations' float rounding
flips a prescreener decision.

The choice is made at runtime rather than by the build, so havsfunc patch 6 is
byte-identical on every platform and x86 keeps using znedi3 exactly as before.

Worth +10% (Preset Faster) to +40% (Slow) end-to-end on arm64.

- havsfunc patch 6 in all three download-deps scripts, adding _nnedi3_impl()
- a _nnedi3() helper in both templates, replacing the four direct calls
- test_92 fails the build if a template names an implementation directly; this
  is a bug that still produces a correct picture, just slowly, so nothing else
  would catch it
- nnedi3 added to the required-plugin list on ARM only (Windows and macOS-x64
  ship only znedi3 + nnedi3cl) and to deps-expected-plugins.json for Linux
The packaging guard added with the ARM interpolator change was right to
fail: libnnedi3.so genuinely was not in either Linux bundle. Two separate
reasons, one per arch.

linux-arm64 could never have built it. dubhater's build system treats
every ARM as 32-bit ARMv7, which breaks aarch64 twice over: -mfpu=neon is
an ARMv7 option gcc rejects outright, and cpufeatures.cpp reads HWCAP_ARM_*
out of getauxval(), constants that exist only for 32-bit ARM. macOS only
ever hit the first, because cpufeatures.cpp has an __APPLE__ branch that
skips the hwcap probe entirely -- so the macOS script has carried the
-mfpu sed for ages while Linux silently shipped no plugin at all.

Fix both in the nnedi3 block, with a guard that hard-fails if either
literal match stops applying. That guard matters more than usual here:
nnedi3.cpp only does "if (!cpu.neon) d->opt = 0", so a patch that quietly
stopped working would produce a correct picture at scalar speed, which is
precisely the failure this plugin is bundled to avoid.

linux-x64 is a different story -- it fails on a missing yasm, and should
not be building it in the first place. Patch 6 prefers nnedi3 only on ARM,
so x86 keeps znedi3; macos-x64 and windows-x64 already omit nnedi3 from
their expected-plugin lists. Requiring it on linux-x64 was inconsistent
with that contract, so drop it.

The NEON intrinsics themselves were never the problem: simd_neon.c is
pure arm_neon.h with no inline asm, and macOS arm64 -- also aarch64 --
has been compiling that same file all along.
The blank-line search after the _nnedi3() helper looked for "\n\n", but git
checks the templates out CRLF on Windows, where the file only ever contains
"\r\n\r\n" -- so the test panicked with "helper should be followed by a blank
line" on that platform alone.

Normalise line endings on read. Verified both ways locally by converting the
templates to CRLF and re-running.

Nightly did not catch this because it runs only the heavy Flutter suite; test_92
is a cargo test and lives in the per-push gate.
@StuartCameronCode
StuartCameronCode merged commit 4029ef8 into main Aug 7, 2026
4 checks passed
@StuartCameronCode
StuartCameronCode deleted the feat/arm64-nnedi3-preference branch August 7, 2026 11:38
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.

1 participant