From ecff9140a5203ee9ff7ca25a67c34f869b80e96f Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sat, 20 Jun 2026 20:27:55 -0700 Subject: [PATCH] arm64: SPU2 mixer NEON paths ARM64 NEON implementations for the SPU2 mixer hot paths. Co-Authored-By: Ryan Walklin Co-Authored-By: Brian Degenhardt Co-Authored-By: Claude Opus 4.8 --- pcsx2/SPU2/Mixer.cpp | 53 ++++++++++++++++++++++++++++++++++++++++++++ pcsx2/SPU2/defs.h | 15 +++++++++++++ 2 files changed, 68 insertions(+) diff --git a/pcsx2/SPU2/Mixer.cpp b/pcsx2/SPU2/Mixer.cpp index 6e3dabc451..ded43ca984 100644 --- a/pcsx2/SPU2/Mixer.cpp +++ b/pcsx2/SPU2/Mixer.cpp @@ -9,6 +9,10 @@ #include "common/Assertions.h" +#if defined(__aarch64__) +#include +#endif + // LOOP/END sets the ENDX bit and sets NAX to LSA, and the voice is muted if LOOP is not set // LOOP seems to only have any effect on the block with LOOP/END set, where it prevents muting the voice // (the documented requirement that every block in a loop has the LOOP bit set is nonsense according to tests) @@ -157,18 +161,50 @@ static __forceinline s32 ApplyVolume(s32 data, s32 volume) return (volume * data) >> 15; } +#if defined(__aarch64__) +// NEON helper: lanewise `(volume * data) >> 15` on s32x2. Bit-exact with the +// scalar code's `mul w*, w*` + `asr 15` — narrow the s64 product to s32 first +// (matching scalar's implicit s32 truncation) then arithmetic-shift right 15. +static __forceinline int32x2_t ApplyVolumeStereoNEON(int32x2_t data, int32x2_t volume) +{ + const int64x2_t prod = vmull_s32(data, volume); + const int32x2_t lo32 = vmovn_s64(prod); + return vshr_n_s32(lo32, 15); +} +#endif + static __forceinline StereoOut32 ApplyVolume(const StereoOut32& data, const V_VolumeLR& volume) { +#if defined(__aarch64__) + const int32x2_t d = vld1_s32(&data.Left); + const int32x2_t v = vld1_s32(&volume.Left); + StereoOut32 out; + vst1_s32(&out.Left, ApplyVolumeStereoNEON(d, v)); + return out; +#else return StereoOut32( ApplyVolume(data.Left, volume.Left), ApplyVolume(data.Right, volume.Right)); +#endif } static __forceinline StereoOut32 ApplyVolume(const StereoOut32& data, const V_VolumeSlideLR& volume) { +#if defined(__aarch64__) + // V_VolumeSlide is 12 bytes; .Value is the s32 at offset 8 (Reg_VOL u16 + pad, + // then u32 Counter, then s32 Value). Build {Left.Value, Right.Value} via two + // scalar loads — cheaper than a gather, no aliasing constraints. + static_assert(sizeof(V_VolumeSlide) == 12, "V_VolumeSlide layout assumed by NEON ApplyVolume"); + const int32x2_t d = vld1_s32(&data.Left); + const int32x2_t v = { volume.Left.Value, volume.Right.Value }; + StereoOut32 out; + vst1_s32(&out.Left, ApplyVolumeStereoNEON(d, v)); + return out; +#else return StereoOut32( ApplyVolume(data.Left, volume.Left.Value), ApplyVolume(data.Right, volume.Right.Value)); +#endif } static __forceinline void UpdateBlockHeader(V_Core& thiscore, uint voiceidx) @@ -424,6 +460,22 @@ static __forceinline void MixCoreVoices(VoiceMixSet& dest, const uint coreidx) { V_Core& thiscore(Cores[coreidx]); +#if defined(__aarch64__) + // dest is {Dry.L, Dry.R, Wet.L, Wet.R} = 4 contiguous s32, and each + // V_VoiceGates entry is the same {DryL, DryR, WetL, WetR} contiguous s32x4. + // Per voice: vval = {VVal.L, VVal.R, VVal.L, VVal.R}, accum += vval & gates. + // Bit-identical to the scalar version below. + int32x4_t accum = vld1q_s32(&dest.Dry.Left); + for (uint voiceidx = 0; voiceidx < V_Core::NumVoices; ++voiceidx) + { + const StereoOut32 VVal(MixVoice(coreidx, voiceidx)); + const int32x2_t lr = vld1_s32(&VVal.Left); + const int32x4_t vval = vcombine_s32(lr, lr); + const int32x4_t gate = vld1q_s32(&thiscore.VoiceGates[voiceidx].DryL); + accum = vaddq_s32(accum, vandq_s32(vval, gate)); + } + vst1q_s32(&dest.Dry.Left, accum); +#else for (uint voiceidx = 0; voiceidx < V_Core::NumVoices; ++voiceidx) { StereoOut32 VVal(MixVoice(coreidx, voiceidx)); @@ -435,6 +487,7 @@ static __forceinline void MixCoreVoices(VoiceMixSet& dest, const uint coreidx) dest.Wet.Left += VVal.Left & thiscore.VoiceGates[voiceidx].WetL; dest.Wet.Right += VVal.Right & thiscore.VoiceGates[voiceidx].WetR; } +#endif } static __forceinline StereoOut32 MixCore(const uint coreidx, const VoiceMixSet& inVoices, const StereoOut32& Input, const StereoOut32& Ext) diff --git a/pcsx2/SPU2/defs.h b/pcsx2/SPU2/defs.h index 3e240bc3b5..e517fefea3 100644 --- a/pcsx2/SPU2/defs.h +++ b/pcsx2/SPU2/defs.h @@ -9,6 +9,10 @@ #include #include +#if defined(__aarch64__) +#include +#endif + // -------------------------------------------------------------------------------------- // SPU2 Register Table LUT // -------------------------------------------------------------------------------------- @@ -83,7 +87,18 @@ static __forceinline s32 clamp_mix(s32 x) static __forceinline StereoOut32 clamp_mix(StereoOut32 sample) { +#if defined(__aarch64__) + // vmin/vmax on s32x2 — one cycle each on A53 vs scalar cmp+csel pair × 2. + const int32x2_t v = vld1_s32(&sample.Left); + const int32x2_t lo = vdup_n_s32(-0x8000); + const int32x2_t hi = vdup_n_s32(0x7fff); + const int32x2_t out = vmin_s32(vmax_s32(v, lo), hi); + StereoOut32 r; + vst1_s32(&r.Left, out); + return r; +#else return StereoOut32(clamp_mix(sample.Left), clamp_mix(sample.Right)); +#endif } struct V_VolumeLR