arm64: SPU2 mixer NEON paths

ARM64 NEON implementations for the SPU2 mixer hot paths.

Co-Authored-By: Ryan Walklin <ryan@testtoast.com>
Co-Authored-By: Brian Degenhardt <bmd@bmdhacks.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Brian Degenhardt
2026-06-20 20:27:55 -07:00
co-authored by Ryan Walklin Claude Opus 4.8
parent 3f01f39e9a
commit ecff9140a5
2 changed files with 68 additions and 0 deletions
+53
View File
@@ -9,6 +9,10 @@
#include "common/Assertions.h"
#if defined(__aarch64__)
#include <arm_neon.h>
#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)
+15
View File
@@ -9,6 +9,10 @@
#include <array>
#include <cstdio>
#if defined(__aarch64__)
#include <arm_neon.h>
#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