Skip to content

Commit cd703cf

Browse files
committed
iOS: Add optional NEON reverb FIR backend for SPU2
Port the SPU2 SIMD audio backend (upstream f0c025b) as an opt-in, off-by-default NEON fast path for the reverb down/up-sample FIRs. When the SPU2/NeonReverbSIMD setting is set, SPU2::InternalReset overrides the ReverbDownsample/ReverbUpsample function pointers with 39-tap NEON implementations (vqrdmulhq_s16 dot-product FIRs); otherwise the scalar Multi-ISA reference is used unchanged. The backend is shared-core C++: the active FIR path uses only arm_neon.h and the existing reverb buffer/function-pointer machinery. The Android- only tuning helpers (getauxval HWCAP feature detect, sched_setaffinity thread pinning) are kept in spu2_mt6899_tuning.h behind __ANDROID__ / __linux__ guards so they compile to nothing on Darwin; SVE2 is compiled out via SPU2_HAS_SVE2_COMPILER (off on the default arm64 target). Adds the eight backend files, registers spu2_neon.cpp + the seven headers in CMakeLists, and wires the InternalReset hook. Default off, so there is no behavioural change for existing audio output; reverb is bit-inexact vs the scalar reference when enabled (NEON rounding), which is why it remains experimental and opt-in.
1 parent a962484 commit cd703cf

10 files changed

Lines changed: 1328 additions & 0 deletions

File tree

app/src/main/cpp/pcsx2/CMakeLists.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ set(pcsx2SPU2Sources
254254
SPU2/spu2freeze.cpp
255255
SPU2/spu2sys.cpp
256256
SPU2/Wavedump_wav.cpp
257+
# Optional ARM64 NEON reverb FIR backend (self-guards with #if __aarch64__;
258+
# produces no object code on other targets). Opt-in via the NeonReverbSIMD
259+
# setting; overrides ReverbDownsample/ReverbUpsample at runtime.
260+
SPU2/spu2_neon.cpp
257261
)
258262

259263
set(pcsx2SPU2SourcesUnshared
@@ -270,6 +274,13 @@ set(pcsx2SPU2Headers
270274
SPU2/spu2.h
271275
SPU2/regs.h
272276
SPU2/spdif.h
277+
SPU2/spu2_neon.h
278+
SPU2/spu2_neon_mixer.h
279+
SPU2/spu2_neon_dcfilter.h
280+
SPU2/spu2_neon_reverb_ex.h
281+
SPU2/spu2_sve2_fir.h
282+
SPU2/spu2_mt6899_tuning.h
283+
SPU2/spu2_optimize.h
273284
)
274285

275286
# DEV9 sources

app/src/main/cpp/pcsx2/SPU2/spu2.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@
1212
#include "R3000A.h"
1313
#include "VMManager.h"
1414

15+
#include "common/Console.h"
1516
#include "common/Error.h"
1617

18+
#if defined(__aarch64__) || defined(_M_ARM64)
19+
#include "SPU2/spu2_neon.h"
20+
#endif
21+
1722
#include <cstdio>
1823

1924
#if defined(__APPLE__)
@@ -245,6 +250,18 @@ void SPU2::InternalReset(bool psxmode)
245250
ReverbDownsample = MULTI_ISA_SELECT(ReverbDownsample);
246251
ReverbUpsample = MULTI_ISA_SELECT(ReverbUpsample);
247252

253+
#if defined(__aarch64__) || defined(_M_ARM64)
254+
// Optional NEON reverb FIR (opt-in, default off). Overrides the Multi-ISA
255+
// scalar reverb resamplers assigned just above. Read fresh on every reset so
256+
// toggling the "SPU2 SIMD audio" setting and rebooting the game switches
257+
// backends. When off, the scalar reference (current default) is used.
258+
if (Host::GetBaseBoolSettingValue("SPU2", "NeonReverbSIMD", false))
259+
{
260+
SPU2::RegisterNEONBackend();
261+
Console.WriteLn("SPU2: NEON reverb SIMD backend enabled");
262+
}
263+
#endif
264+
248265
s_current_chunk_pos = 0;
249266
s_psxmode = psxmode;
250267
if (!s_psxmode)
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
2+
// SPDX-License-Identifier: GPL-3.0+
3+
//
4+
// SPU2/spu2_mt6899_tuning.h — Platform detection and tuning for MediaTek MT6899
5+
// (Dimensity 9400) and general ARM64 performance targets.
6+
//
7+
// MT6899 SoC layout:
8+
// Cortex-X925 (Prime) @ 3.62 GHz — L1D=64KB, L2=1MB
9+
// Cortex-X4 (Perf) x3 @ 2.80 GHz — L1D=64KB, L2=512KB
10+
// Cortex-A720 (Eff) x4 @ 2.00 GHz — L1D=64KB, L2=256KB
11+
// L3: 12MB shared | SLC: 8MB | LPDDR5X-8533
12+
// SVE2: 128-bit VL | SME | ARMv9.2-A
13+
//
14+
// Key facts for SPU2:
15+
// _spu2mem is 2MB — fits in X925 L2 (1MB) partially, fully in L3.
16+
// Reverb working area: 64–256KB — fits in L2.
17+
// 24 voice structs: ~6KB — fits in L1D.
18+
// interpTable: 2KB — fits in L1D.
19+
// Cache line: 64 bytes on ALL cores.
20+
21+
#pragma once
22+
23+
#include <cstdint>
24+
25+
namespace spu2_mt6899 {
26+
27+
// ============================================================================
28+
// Cache hierarchy constants
29+
// ============================================================================
30+
31+
static constexpr uint32_t CACHE_LINE_BYTES = 64;
32+
static constexpr uint32_t L1D_BYTES = 64 * 1024;
33+
static constexpr uint32_t L2_BYTES_PRIME = 1024 * 1024;
34+
static constexpr uint32_t L3_BYTES = 12 * 1024 * 1024;
35+
36+
// SPU2 memory budget:
37+
// _spu2mem[0x200000] = 2,097,152 bytes (2MB)
38+
// RevbDownBuf/RevbUpBuf = 2 x 2 x 128 x sizeof(s16) = 1KB
39+
// Voice structs = 24 x ~256 bytes = 6KB
40+
// interpTable = 256 x 4 x 2 = 2048 bytes
41+
// adsr_shift_table = 32 x 8 = 256 bytes
42+
43+
// Prefetch distances — tuned for MT6899 cache hierarchy.
44+
// PLDL1KEEP (temporal, all levels) — use for data accessed within ~20 cycles
45+
// PLDL2KEEP (temporal, L2+) — use for data accessed within ~100 cycles
46+
// PLDL3KEEP (temporal, L3+) — use for data accessed within ~300 cycles
47+
// PSTL1KEEP (write, all levels) — use for stores about to happen
48+
static constexpr uint32_t PREFETCH_READ_L1 = CACHE_LINE_BYTES; // 64B ahead
49+
static constexpr uint32_t PREFETCH_READ_L2 = CACHE_LINE_BYTES * 4; // 256B ahead
50+
static constexpr uint32_t PREFETCH_WRITE_L1 = CACHE_LINE_BYTES; // 64B ahead
51+
52+
// Alignment macros for SIMD-friendly layouts
53+
#define SPU2_CACHE_ALIGN alignas(64)
54+
#define SPU2_NEON_ALIGN alignas(16)
55+
#define SPU2_SVE_ALIGN alignas(32) // For potential 256-bit SVE
56+
57+
} // namespace spu2_mt6899
58+
59+
// ============================================================================
60+
// CPU Feature Detection — Runtime (Android/Linux)
61+
// ============================================================================
62+
63+
#if defined(__aarch64__) || defined(_M_ARM64)
64+
#if defined(__ANDROID__) || defined(__linux__)
65+
#include <sys/auxv.h>
66+
#include <asm/hwcap.h>
67+
68+
// Some NDK <asm/hwcap.h> revisions (e.g. NDK 28) omit a few HWCAP2 feature
69+
// bits. Provide 0 fallbacks so feature detection compiles everywhere — these
70+
// flags are informational only; the NEON reverb FIR does not depend on them.
71+
#ifndef HWCAP2_SVE2
72+
#define HWCAP2_SVE2 0
73+
#endif
74+
#ifndef HWCAP2_I8MM
75+
#define HWCAP2_I8MM 0
76+
#endif
77+
#ifndef HWCAP2_FHM
78+
#define HWCAP2_FHM 0
79+
#endif
80+
#ifndef HWCAP2_USCAT
81+
#define HWCAP2_USCAT 0
82+
#endif
83+
84+
namespace spu2_mt6899 {
85+
86+
struct ARM64Features {
87+
bool neon = false;
88+
bool sve = false;
89+
bool sve2 = false;
90+
bool i8mm = false; // Int8 matrix multiply
91+
bool fhm = false; // FP16 multiply-accumulate
92+
bool aes = false;
93+
bool sha2 = false;
94+
bool atomics = false; // LSE atomics (big perf win on X925)
95+
bool uscat = false; // Unaligned single-copy atomicity
96+
bool detected = false;
97+
98+
void detect() {
99+
if (detected) return;
100+
const unsigned long hwcap = getauxval(AT_HWCAP);
101+
const unsigned long hwcap2 = getauxval(AT_HWCAP2);
102+
103+
neon = (hwcap & HWCAP_ASIMD) != 0;
104+
sve = (hwcap & HWCAP_SVE) != 0;
105+
aes = (hwcap & HWCAP_AES) != 0;
106+
sha2 = (hwcap & HWCAP_SHA2) != 0;
107+
sve2 = (hwcap2 & HWCAP2_SVE2) != 0;
108+
i8mm = (hwcap2 & HWCAP2_I8MM) != 0;
109+
fhm = (hwcap2 & HWCAP2_FHM) != 0;
110+
atomics = (hwcap & HWCAP_ATOMICS) != 0;
111+
uscat = (hwcap2 & HWCAP2_USCAT) != 0;
112+
detected = true;
113+
}
114+
};
115+
116+
inline ARM64Features& GetFeatures() {
117+
static ARM64Features feat;
118+
feat.detect();
119+
return feat;
120+
}
121+
122+
} // namespace spu2_mt6899
123+
#endif // __ANDROID__ || __linux__
124+
#endif // __aarch64__
125+
126+
// ============================================================================
127+
// Thread Affinity — Pin audio thread to prime core
128+
// ============================================================================
129+
// On MT6899, CPU 0 is typically the X925 prime core.
130+
// Pinning the SPU2 mixing thread to the prime core reduces latency jitter
131+
// from ~15us (on A720) to ~5us (on X925) per mix cycle.
132+
133+
#if defined(__aarch64__) && defined(__ANDROID__)
134+
#include <sched.h>
135+
#include <unistd.h>
136+
#include <pthread.h>
137+
#include <sys/prctl.h>
138+
139+
namespace spu2_mt6899 {
140+
141+
// Pin current thread to the fastest available core.
142+
// Returns the core number pinned to, or -1 on failure.
143+
inline int PinToFastCore() {
144+
cpu_set_t mask;
145+
CPU_ZERO(&mask);
146+
147+
// MT6899 topology: CPU 0 = X925, CPU 1-3 = X4, CPU 4-7 = A720
148+
// Strategy: try CPU 0 first (prime), then CPU 1 (first perf core).
149+
for (int candidate = 0; candidate <= 3; candidate++) {
150+
CPU_SET(candidate, &mask);
151+
if (sched_setaffinity(0, sizeof(mask), &mask) == 0) {
152+
return candidate;
153+
}
154+
CPU_CLR(candidate, &mask);
155+
}
156+
return -1;
157+
}
158+
159+
// Set thread name for debugging (shows in systrace/perfetto)
160+
inline void SetThreadName(const char* name) {
161+
prctl(PR_SET_NAME, name, 0, 0, 0);
162+
}
163+
164+
// Set thread to SCHED_FIFO real-time priority for lowest latency.
165+
// Requires CAP_SYS_NICE or appropriate Android permissions.
166+
// Returns true on success.
167+
inline bool SetRealtimePriority(int priority = 10) {
168+
struct sched_param param;
169+
param.sched_priority = priority;
170+
return (pthread_setschedparam(pthread_self(), SCHED_FIFO, &param) == 0);
171+
}
172+
173+
// Query which core we're currently running on.
174+
inline int GetCurrentCore() {
175+
return sched_getcpu();
176+
}
177+
178+
// Check if we're on a big core (X925 or X4, not A720)
179+
inline bool IsOnBigCore() {
180+
int cpu = GetCurrentCore();
181+
return (cpu >= 0 && cpu <= 3);
182+
}
183+
184+
} // namespace spu2_mt6899
185+
#endif // __aarch64__ && __ANDROID__
186+
187+
// ============================================================================
188+
// LSE Atomics Helper
189+
// ============================================================================
190+
// On Cortex-X925, LSE atomics (LDADD, STADD, CAS, SWP) are significantly
191+
// faster than LL/SC (LDXR/STXR) loops — 1-2 cycles vs 10+ contended.
192+
// MT6899 supports LSE. Use std::atomic with appropriate memory ordering.
193+
194+
#if defined(__aarch64__) && (__has_include(<atomic>))
195+
#include <atomic>
196+
namespace spu2_mt6899 {
197+
// Type alias for lock-free counters on MT6899
198+
// (LSE makes atomic<u32> fast enough for hot paths)
199+
using AtomicCounter = std::atomic<uint32_t>;
200+
} // namespace spu2_mt6899
201+
#endif

0 commit comments

Comments
 (0)