refactor: move Android frontend to platforms/android on single shared core

Snapshot the refresh-experimental Android app (Gradle + JNI + Android-only
3rdparty) into platforms/android/. Delete its vendored PCSX2 core copy and
relocate the ~24 genuinely Android-specific core additions (Oboe audio, Android
stubs, EGL-Android GL context, NEON SPU2, GSGPUProfile, VU1Fingerprint, Android
HTTP downloader) into the root core, guarded by if(ANDROID) in
pcsx2/CMakeLists.txt and common/CMakeLists.txt.

The superseded arm64 JIT experiment (arm64/mac/* IR-VU backend, split
aVU0/aDMAC/aVTLB/aR5900COP*) is dropped: the root macOS arm64 JIT (127 unique
commits, newer) is the canonical recompiler.

Rewire the Android native build to a thin CMakeLists that sources the root
{common,pcsx2,3rdparty} instead of the deleted vendored copy.

NOT yet compiled against a real NDK -- build validation is CI's job.
See REFACTOR_STATUS.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
David Isztl
2026-07-08 20:29:23 +02:00
co-authored by Claude Opus 4.8
parent 47229e4ff3
commit 2eb9ec659c
8410 changed files with 3252612 additions and 2 deletions
+13 -2
View File
@@ -157,6 +157,17 @@ elseif(APPLE)
"-framework Foundation"
"-framework IOKit"
)
elseif(ANDROID)
# Android reuses the Linux host layer plus a JNI-backed HTTP downloader
# (relocated from the former android core fork). Source wiring only:
# validate against a real NDK build. See REFACTOR_STATUS.md.
target_sources(common PRIVATE
Linux/LnxHostSys.cpp
Linux/LnxThreads.cpp
Linux/LnxMisc.cpp
HTTPDownloaderAndroid.cpp
HTTPDownloaderAndroid.h
)
else()
target_sources(common PRIVATE
Linux/LnxHostSys.cpp
@@ -189,8 +200,8 @@ if (USE_GCC AND CMAKE_INTERPROCEDURAL_OPTIMIZATION)
set_source_files_properties(FastJmp.cpp PROPERTIES COMPILE_FLAGS -fno-lto)
endif()
if(NOT WIN32)
# libcurl-based HTTPDownloader
if(NOT WIN32 AND NOT ANDROID)
# libcurl-based HTTPDownloader (Android uses HTTPDownloaderAndroid instead)
target_sources(common PRIVATE
HTTPDownloaderCurl.cpp
HTTPDownloaderCurl.h
+285
View File
@@ -0,0 +1,285 @@
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "common/HTTPDownloaderAndroid.h"
#include "common/Console.h"
#include "common/Timer.h"
#include "fmt/format.h"
#include <chrono>
// Cached at JNI_OnLoad / NativeApp.initialize via BindFromJNI() since
// the worker thread doesn't have a Java class loader and must use a
// global ref looked up earlier.
static jclass s_HttpClient_class = nullptr; // GlobalRef
static jmethodID s_HttpClient_doRequest = nullptr;
static jclass s_Response_class = nullptr; // GlobalRef
static jfieldID s_Response_statusCode = nullptr;
static jfieldID s_Response_contentType = nullptr;
static jfieldID s_Response_data = nullptr;
static JavaVM* s_jvm = nullptr;
// Helper: clear a pending JNI exception (FindClass / GetMethodID throw on
// failure; the pending exception will abort the JVM the moment we return
// to Java code if not cleared). Returns whether one was pending.
static bool ClearPendingException(JNIEnv* env)
{
if (!env->ExceptionCheck())
return false;
env->ExceptionDescribe(); // logs to logcat for triage
env->ExceptionClear();
return true;
}
bool HTTPDownloaderAndroid::BindFromJNI(JNIEnv* env)
{
if (s_HttpClient_class)
return true;
if (env->GetJavaVM(&s_jvm) != JNI_OK)
return false;
// IMPORTANT: every JNI call below can throw. After each, clear any
// pending exception BEFORE deciding success/failure — otherwise the
// JVM aborts the moment we return to Java code, even if we wanted to
// soft-fail (e.g. cover-art download not strictly required).
jclass local = env->FindClass("kr/co/iefriends/pcsx2/HttpClient");
ClearPendingException(env);
if (!local)
{
Console.Error("HTTPDownloaderAndroid: FindClass(HttpClient) failed");
return false;
}
s_HttpClient_class = static_cast<jclass>(env->NewGlobalRef(local));
env->DeleteLocalRef(local);
s_HttpClient_doRequest = env->GetStaticMethodID(s_HttpClient_class, "doRequest",
"(Ljava/lang/String;Ljava/lang/String;[BLjava/lang/String;I)"
"Lkr/co/iefriends/pcsx2/HttpClient$Response;");
ClearPendingException(env);
if (!s_HttpClient_doRequest)
{
Console.Error("HTTPDownloaderAndroid: GetStaticMethodID(doRequest) failed");
return false;
}
jclass response_local = env->FindClass("kr/co/iefriends/pcsx2/HttpClient$Response");
ClearPendingException(env);
if (!response_local)
{
Console.Error("HTTPDownloaderAndroid: FindClass(Response) failed");
return false;
}
s_Response_class = static_cast<jclass>(env->NewGlobalRef(response_local));
env->DeleteLocalRef(response_local);
s_Response_statusCode = env->GetFieldID(s_Response_class, "statusCode", "I");
ClearPendingException(env);
s_Response_contentType = env->GetFieldID(s_Response_class, "contentType", "Ljava/lang/String;");
ClearPendingException(env);
s_Response_data = env->GetFieldID(s_Response_class, "data", "[B");
ClearPendingException(env);
if (!s_Response_statusCode || !s_Response_contentType || !s_Response_data)
{
Console.Error("HTTPDownloaderAndroid: GetFieldID failed");
return false;
}
return true;
}
std::unique_ptr<HTTPDownloader> HTTPDownloader::Create(std::string user_agent)
{
if (!s_jvm || !s_HttpClient_class)
{
Console.Error("HTTPDownloaderAndroid: BindFromJNI hasn't run; can't create downloader");
return {};
}
auto inst = std::make_unique<HTTPDownloaderAndroid>();
if (!inst->Initialize(s_jvm, std::move(user_agent)))
return {};
return inst;
}
HTTPDownloaderAndroid::HTTPDownloaderAndroid()
: HTTPDownloader()
{
}
HTTPDownloaderAndroid::~HTTPDownloaderAndroid() = default;
bool HTTPDownloaderAndroid::Initialize(JavaVM* jvm, std::string user_agent)
{
m_jvm = jvm;
m_user_agent = std::move(user_agent);
return true;
}
HTTPDownloader::Request* HTTPDownloaderAndroid::InternalCreateRequest()
{
return new Request();
}
void HTTPDownloaderAndroid::InternalPollRequests()
{
// Worker threads update Request::state directly (Complete on return),
// so no per-poll reconciliation needed here. The base poll loop
// observes the atomic transition and fires the callback.
}
bool HTTPDownloaderAndroid::StartRequest(HTTPDownloader::Request* request)
{
auto* req = static_cast<Request*>(request);
req->state.store(Request::State::Started, std::memory_order_release);
req->start_time = Common::Timer::GetCurrentValue();
// Detach the std::thread inside RunRequest after it completes — but
// we still need to join in CloseRequest to make sure JNI cleanup is
// done before deletion. So keep it joinable.
req->worker = std::thread(&HTTPDownloaderAndroid::RunRequest, this, req);
return true;
}
void HTTPDownloaderAndroid::CloseRequest(HTTPDownloader::Request* request)
{
auto* req = static_cast<Request*>(request);
if (req->worker.joinable())
req->worker.join();
delete req;
}
// Helper: log + clear any pending exception on the JNI worker thread.
// Worker JNI calls (NewStringUTF / NewByteArray / SetByteArrayRegion)
// can throw OutOfMemoryError / ArrayIndexOutOfBoundsException — leaving
// one pending poisons subsequent JNI calls and aborts the JVM the moment
// we DetachCurrentThread.
static bool ClearWorkerException(JNIEnv* env, const char* tag)
{
if (!env->ExceptionCheck())
return false;
Console.Error(fmt::format("HTTPDownloaderAndroid: pending exception at {}", tag));
env->ExceptionDescribe();
env->ExceptionClear();
return true;
}
void HTTPDownloaderAndroid::RunRequest(Request* req)
{
if (!m_jvm || !s_HttpClient_class || !s_HttpClient_doRequest || !s_Response_class)
{
Console.Error("HTTPDownloaderAndroid: JNI bind state missing — failing request");
req->status_code = HTTP_STATUS_ERROR;
req->state.store(Request::State::Complete, std::memory_order_release);
return;
}
JNIEnv* env = nullptr;
bool attached = false;
if (m_jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK)
{
if (m_jvm->AttachCurrentThread(&env, nullptr) != JNI_OK)
{
Console.Error(fmt::format("HTTPDownloaderAndroid: AttachCurrentThread failed for {}", req->url));
req->status_code = HTTP_STATUS_ERROR;
req->state.store(Request::State::Complete, std::memory_order_release);
return;
}
attached = true;
}
// Defensive: clear any pending exception we may have inherited from
// the caller's JNI frame (shouldn't happen since we just attached,
// but cheap insurance).
ClearWorkerException(env, "entry");
jstring j_url = env->NewStringUTF(req->url.c_str());
ClearWorkerException(env, "NewStringUTF(url)");
jstring j_method = env->NewStringUTF(req->type == Request::Type::Post ? "POST" : "GET");
ClearWorkerException(env, "NewStringUTF(method)");
jstring j_ua = env->NewStringUTF(m_user_agent.c_str());
ClearWorkerException(env, "NewStringUTF(ua)");
jbyteArray j_post = nullptr;
if (req->type == Request::Type::Post && !req->post_data.empty())
{
j_post = env->NewByteArray(static_cast<jsize>(req->post_data.size()));
ClearWorkerException(env, "NewByteArray(post)");
if (j_post)
{
env->SetByteArrayRegion(j_post, 0, static_cast<jsize>(req->post_data.size()),
reinterpret_cast<const jbyte*>(req->post_data.data()));
ClearWorkerException(env, "SetByteArrayRegion(post)");
}
}
bool transport_failed = !j_url || !j_method || !j_ua;
jobject response = nullptr;
if (!transport_failed)
{
const jint timeout_ms = static_cast<jint>(m_timeout * 1000.0f);
response = env->CallStaticObjectMethod(
s_HttpClient_class, s_HttpClient_doRequest,
j_url, j_method, j_post, j_ua, timeout_ms);
if (ClearWorkerException(env, "CallStaticObjectMethod"))
transport_failed = true;
}
if (transport_failed)
{
req->status_code = HTTP_STATUS_ERROR;
}
else if (response)
{
req->status_code = env->GetIntField(response, s_Response_statusCode);
ClearWorkerException(env, "GetIntField(statusCode)");
jstring j_ct = static_cast<jstring>(env->GetObjectField(response, s_Response_contentType));
ClearWorkerException(env, "GetObjectField(contentType)");
if (j_ct)
{
const char* utf = env->GetStringUTFChars(j_ct, nullptr);
if (utf)
{
req->content_type = utf;
env->ReleaseStringUTFChars(j_ct, utf);
}
env->DeleteLocalRef(j_ct);
}
jbyteArray j_data = static_cast<jbyteArray>(env->GetObjectField(response, s_Response_data));
ClearWorkerException(env, "GetObjectField(data)");
if (j_data)
{
const jsize len = env->GetArrayLength(j_data);
req->data.resize(static_cast<size_t>(len));
if (len > 0)
{
env->GetByteArrayRegion(j_data, 0, len, reinterpret_cast<jbyte*>(req->data.data()));
ClearWorkerException(env, "GetByteArrayRegion(data)");
}
req->content_length = static_cast<u32>(len);
env->DeleteLocalRef(j_data);
}
env->DeleteLocalRef(response);
}
else
{
// HttpClient.doRequest returned null — shouldn't happen since
// the Java side always allocates Response, but treat as transport
// error.
req->status_code = HTTP_STATUS_ERROR;
}
if (j_url) env->DeleteLocalRef(j_url);
if (j_method) env->DeleteLocalRef(j_method);
if (j_ua) env->DeleteLocalRef(j_ua);
if (j_post) env->DeleteLocalRef(j_post);
// Final clear before detach — any leaked exception aborts the JVM.
ClearWorkerException(env, "exit");
if (attached)
m_jvm->DetachCurrentThread();
req->state.store(Request::State::Complete, std::memory_order_release);
}
+56
View File
@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "common/HTTPDownloader.h"
#include <atomic>
#include <jni.h>
#include <memory>
#include <string>
#include <thread>
// HTTPDownloader implementation backed by Java's HttpURLConnection.
// One worker std::thread per request; the worker attaches to the JVM,
// invokes kr.co.iefriends.pcsx2.HttpClient.doRequest synchronously, and
// flips the request's atomic state to Complete on return. The base
// HTTPDownloader's poll loop owns lifecycle (state observation,
// Cancelled/Timeout transitions, callback invocation) — we just provide
// the actual transport.
//
// Why not curl? On Android we'd need to bundle libcurl + an OpenSSL/
// mbedTLS build into 3rdparty/ to get TLS — significant build-system
// surface. HttpURLConnection uses Android's system networking stack
// (system CA store, OS-managed proxy, TLS via the platform's OpenSSL),
// so this approach has zero third-party dependencies.
class HTTPDownloaderAndroid final : public HTTPDownloader
{
public:
HTTPDownloaderAndroid();
~HTTPDownloaderAndroid() override;
bool Initialize(JavaVM* jvm, std::string user_agent);
// Static one-time setup of the HttpClient class + method ID. Called
// from JNI_OnLoad / NativeApp.initialize once we have a Java env.
// Subsequent calls are no-ops.
static bool BindFromJNI(JNIEnv* env);
protected:
struct Request : HTTPDownloader::Request
{
std::thread worker;
};
HTTPDownloader::Request* InternalCreateRequest() override;
void InternalPollRequests() override;
bool StartRequest(HTTPDownloader::Request* request) override;
void CloseRequest(HTTPDownloader::Request* request) override;
private:
void RunRequest(Request* req);
JavaVM* m_jvm = nullptr;
std::string m_user_agent;
};
+24
View File
@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
// PCAPAdapter stubs for Android (no libpcap available).
#include "PrecompiledHeader.h"
#include "DEV9/pcap_io.h"
PCAPAdapter::PCAPAdapter() {}
PCAPAdapter::~PCAPAdapter() {}
bool PCAPAdapter::blocks() { return false; }
bool PCAPAdapter::isInitialised() { return false; }
bool PCAPAdapter::recv(NetPacket* pkt) { return false; }
bool PCAPAdapter::send(NetPacket* pkt) { return false; }
void PCAPAdapter::reloadSettings() {}
std::vector<AdapterEntry> PCAPAdapter::GetAdapters() { return {}; }
AdapterOptions PCAPAdapter::GetAdapterOptions() { return AdapterOptions::None; }
bool PCAPAdapter::InitPCAP(const std::string& adapter, bool promiscuous) { return false; }
bool PCAPAdapter::SetMACSwitchedFilter(PacketReader::MAC_Address mac) { return false; }
void PCAPAdapter::SetMACBridgedRecv(NetPacket* pkt) {}
void PCAPAdapter::SetMACBridgedSend(NetPacket* pkt) {}
void PCAPAdapter::HandleFrameCheckSequence(NetPacket* pkt) {}
bool PCAPAdapter::ValidateEtherFrame(NetPacket* pkt) { return false; }
+89
View File
@@ -0,0 +1,89 @@
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
// Stubs for functionality not available/needed on Android.
#include "PrecompiledHeader.h"
#include "Input/InputManager.h"
#include "CDVD/CDVDdiscReader.h"
// g_host_hotkeys - normally defined in pcsx2-qt
BEGIN_HOTKEY_LIST(g_host_hotkeys)
END_HOTKEY_LIST()
// Host::SetMouseLock - no mouse lock on Android
void Host::SetMouseLock(bool state)
{
}
// HTTPDownloader::Create is now provided by common/HTTPDownloaderAndroid.cpp,
// which bridges to java.net.HttpURLConnection via JNI. The stub that
// returned null lived here previously — RA login + cover downloads
// silently failed and the cleanup path crashed when the unique_ptr was
// dereferenced. See HTTPDownloaderAndroid.{h,cpp}.
// Optical drive / disc reader stubs - no physical disc on Android
std::vector<std::string> GetOpticalDriveList()
{
return {};
}
void GetValidDrive(std::string& drive)
{
}
// IOCtlSrc stubs
IOCtlSrc::IOCtlSrc(std::string filename)
{
}
IOCtlSrc::~IOCtlSrc()
{
}
bool IOCtlSrc::Reopen(Error* error)
{
return false;
}
bool IOCtlSrc::DiscReady()
{
return false;
}
u32 IOCtlSrc::GetSectorCount() const
{
return 0;
}
s32 IOCtlSrc::GetMediaType() const
{
return 0;
}
const std::vector<toc_entry>& IOCtlSrc::ReadTOC() const
{
static const std::vector<toc_entry> empty;
return empty;
}
bool IOCtlSrc::ReadSectors2048(u32 sector, u32 count, u8* buffer) const
{
return false;
}
bool IOCtlSrc::ReadSectors2352(u32 sector, u32 count, u8* buffer) const
{
return false;
}
bool IOCtlSrc::ReadTrackSubQ(cdvdSubQ* subq) const
{
return false;
}
u32 IOCtlSrc::GetLayerBreakAddress() const
{
return 0;
}
+29
View File
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "common/Pcsx2Types.h"
// Gated EE opcode histogram for diagnosing interpreter-fallback hotspots on the mac
// ARM64 backend. Counts how often each opcode runs through the two fallback paths:
// - STEP : block-terminating single-step (intExecuteOneInst) — the expensive one
// - INLINE : in-block interpreter call (recEmitInterpInline)
// Every ~8M recorded fallback ops the EE thread prints "@@ANDROID_EE_OPHIST@@" lines
// listing the hottest primary opcodes + SPECIAL/REGIMM/COP2/MMI sub-ops, then resets.
// EE-thread-only (no atomics). Default 0 = zero overhead; flip to 1 for a diag build.
#ifndef ARMSX2_ANDROID_EE_OPHIST
#define ARMSX2_ANDROID_EE_OPHIST 0
#endif
#if ARMSX2_ANDROID_EE_OPHIST
namespace AndroidEEOpHist
{
// path: 0 = STEP (single-step), 1 = INLINE (in-block interp).
void Record(int path, u32 op);
// Emit-time tally of natively-compiled trap ops (proves the trap codegen engaged).
void NoteTrapCompiled();
}
// Counting thunk emitted by recEmitInterpInline in place of the raw interp handler
// when the histogram is on: records cpuRegs.code on the INLINE path, then dispatches.
void recOphistInlineThunk();
#endif
+189
View File
@@ -0,0 +1,189 @@
// SPDX-FileCopyrightText: 2026 ARMSX2
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "common/Console.h"
#include "common/Pcsx2Defs.h"
#include <atomic>
#include <chrono>
// Master switch for the Android EE/VU/GS/VIF perf-bucket instrumentation.
//
// When enabled this wraps hot per-op paths with two steady_clock reads + relaxed
// atomic adds and a periodic report check. The heaviest of these is the EE
// interpreter single-step thunk (recInterpStepThunk), which fires millions of
// times per second — so the instrumentation tanks real performance and MUST stay
// compiled out for any build whose FPS matters.
//
// The 2026-06-17 diagnostic run already used these buckets to pin the dominant EE
// cost on ee_interp_step (the EE single-stepping un-compilable ops through the
// interpreter); see the @@ANDROID_PERF_BUCKETS@@ markers. Flip this to 1 — or
// build with -DARMSX2_ANDROID_PERF_BUCKETS=1 — to collect another window. The
// atomic counters and every call site stay compiled in either way; only their
// bodies are gated, so re-enabling is a one-line change with no churn.
#ifndef ARMSX2_ANDROID_PERF_BUCKETS
#define ARMSX2_ANDROID_PERF_BUCKETS 0
#endif
namespace AndroidPerfBuckets
{
#if defined(__ANDROID__)
static constexpr u64 REPORT_WINDOW_US = 5'000'000;
inline std::atomic<u64> s_window_start_us{0};
inline std::atomic<u64> s_ee_recompile_count{0};
inline std::atomic<u64> s_ee_recompile_us{0};
inline std::atomic<u64> s_ee_recompile_interp_blocks{0};
inline std::atomic<u64> s_ee_recompile_ops{0};
inline std::atomic<u64> s_ee_interp_inline_count{0};
inline std::atomic<u64> s_ee_interp_inline_us{0};
inline std::atomic<u64> s_ee_interp_step_count{0};
inline std::atomic<u64> s_ee_interp_step_us{0};
inline std::atomic<u64> s_vu0_compile_count{0};
inline std::atomic<u64> s_vu0_compile_us{0};
inline std::atomic<u64> s_vu1_compile_count{0};
inline std::atomic<u64> s_vu1_compile_us{0};
inline std::atomic<u64> s_vu0_execute_count{0};
inline std::atomic<u64> s_vu0_execute_us{0};
inline std::atomic<u64> s_vu1_execute_count{0};
inline std::atomic<u64> s_vu1_execute_us{0};
inline std::atomic<u64> s_mtvu_wait_count{0};
inline std::atomic<u64> s_mtvu_wait_us{0};
inline std::atomic<u64> s_mtvu_exec_count{0};
inline std::atomic<u64> s_mtvu_exec_us{0};
inline std::atomic<u64> s_mtvu_reserve_wait_count{0};
inline std::atomic<u64> s_mtvu_reserve_wait_us{0};
inline std::atomic<u64> s_mtvu_reserve_spin_iters{0};
inline std::atomic<u64> s_waitvu_count{0};
inline std::atomic<u64> s_waitvu_us{0};
inline std::atomic<u64> s_xgkick_wait_count{0};
inline std::atomic<u64> s_xgkick_wait_us{0};
inline std::atomic<u64> s_mtgs_wait_count{0};
inline std::atomic<u64> s_mtgs_wait_us{0};
inline std::atomic<u64> s_mtgs_weak_spin_iters{0};
inline std::atomic<u64> s_vif_cpu_dyn_count{0};
inline std::atomic<u64> s_vif_cpu_dyn_us{0};
inline std::atomic<u64> s_vif_cpu_int_count{0};
inline std::atomic<u64> s_vif_cpu_int_us{0};
inline std::atomic<u64> s_vif_mtvu_queue_count{0};
inline std::atomic<u64> s_vif_mtvu_dyn_count{0};
inline std::atomic<u64> s_vif_mtvu_dyn_us{0};
inline std::atomic<u64> s_vif_mtvu_int_count{0};
inline std::atomic<u64> s_vif_mtvu_int_us{0};
inline std::atomic<u64> s_vif_dyn_overflow_fallback_count{0};
inline std::atomic<u64> s_vif_dyn_overflow_fallback_us{0};
#if ARMSX2_ANDROID_PERF_BUCKETS
inline u64 NowUs()
{
using clock = std::chrono::steady_clock;
return static_cast<u64>(std::chrono::duration_cast<std::chrono::microseconds>(
clock::now().time_since_epoch()).count());
}
inline void Add(std::atomic<u64>& counter, u64 amount = 1)
{
counter.fetch_add(amount, std::memory_order_relaxed);
}
inline u64 Take(std::atomic<u64>& counter)
{
return counter.exchange(0, std::memory_order_relaxed);
}
inline void MaybeReport(const char* source)
{
const u64 now = NowUs();
u64 start = s_window_start_us.load(std::memory_order_relaxed);
if (start == 0)
{
u64 expected = 0;
s_window_start_us.compare_exchange_strong(expected, now, std::memory_order_relaxed);
return;
}
if (now - start < REPORT_WINDOW_US)
return;
if (!s_window_start_us.compare_exchange_strong(start, now, std::memory_order_relaxed))
return;
Console.WriteLnFmt(
"@@ANDROID_PERF_BUCKETS@@ src={} win_ms={} "
"ee_recomp={}/{}us ee_recomp_ops={} ee_interp_blocks={} ee_interp_inline={}/{}us ee_interp_step={}/{}us "
"vu0_compile={}/{}us vu1_compile={}/{}us vu0_exec={}/{}us vu1_exec={}/{}us "
"mtvu_wait={}/{}us mtvu_exec={}/{}us mtvu_reserve_wait={}/{}us mtvu_reserve_spins={} waitvu={}/{}us "
"xgkick_wait={}/{}us mtgs_wait={}/{}us mtgs_weak_spin_iters={} "
"vif_cpu_dyn={}/{}us vif_cpu_int={}/{}us vif_mtvu_queue={} vif_mtvu_dyn={}/{}us vif_mtvu_int={}/{}us "
"vif_dyn_overflow_fb={}/{}us",
source, (now - start) / 1000,
Take(s_ee_recompile_count), Take(s_ee_recompile_us),
Take(s_ee_recompile_ops), Take(s_ee_recompile_interp_blocks),
Take(s_ee_interp_inline_count), Take(s_ee_interp_inline_us),
Take(s_ee_interp_step_count), Take(s_ee_interp_step_us),
Take(s_vu0_compile_count), Take(s_vu0_compile_us),
Take(s_vu1_compile_count), Take(s_vu1_compile_us),
Take(s_vu0_execute_count), Take(s_vu0_execute_us),
Take(s_vu1_execute_count), Take(s_vu1_execute_us),
Take(s_mtvu_wait_count), Take(s_mtvu_wait_us),
Take(s_mtvu_exec_count), Take(s_mtvu_exec_us),
Take(s_mtvu_reserve_wait_count), Take(s_mtvu_reserve_wait_us),
Take(s_mtvu_reserve_spin_iters),
Take(s_waitvu_count), Take(s_waitvu_us),
Take(s_xgkick_wait_count), Take(s_xgkick_wait_us),
Take(s_mtgs_wait_count), Take(s_mtgs_wait_us),
Take(s_mtgs_weak_spin_iters),
Take(s_vif_cpu_dyn_count), Take(s_vif_cpu_dyn_us),
Take(s_vif_cpu_int_count), Take(s_vif_cpu_int_us),
Take(s_vif_mtvu_queue_count),
Take(s_vif_mtvu_dyn_count), Take(s_vif_mtvu_dyn_us),
Take(s_vif_mtvu_int_count), Take(s_vif_mtvu_int_us),
Take(s_vif_dyn_overflow_fallback_count), Take(s_vif_dyn_overflow_fallback_us));
}
struct ScopedTimer
{
std::atomic<u64>& count;
std::atomic<u64>& total_us;
const char* source;
u64 start_us;
ScopedTimer(std::atomic<u64>& count_, std::atomic<u64>& total_us_, const char* source_)
: count(count_)
, total_us(total_us_)
, source(source_)
, start_us(NowUs())
{
}
~ScopedTimer()
{
Add(count);
Add(total_us, NowUs() - start_us);
MaybeReport(source);
}
};
#else // __ANDROID__ but instrumentation compiled out — no-op so call sites still build.
inline u64 NowUs() { return 0; }
inline void Add(std::atomic<u64>&, u64 = 1) {}
inline void MaybeReport(const char*) {}
struct ScopedTimer
{
ScopedTimer(std::atomic<u64>&, std::atomic<u64>&, const char*) {}
};
#endif // ARMSX2_ANDROID_PERF_BUCKETS
#else // !__ANDROID__
inline u64 NowUs() { return 0; }
inline void MaybeReport(const char*) {}
inline void Add(std::atomic<u64>&, u64 = 1) {}
#endif // __ANDROID__
} // namespace AndroidPerfBuckets
+35
View File
@@ -1085,6 +1085,41 @@ set(pcsx2arm64Headers
arm64/aVU_Compile.inl
)
# Android platform sources, relocated from the former android core fork
# (refresh-experimental). Source wiring only: a real NDK build is required to
# validate include paths and link deps. See REFACTOR_STATUS.md.
if(ANDROID)
list(APPEND pcsx2HostSources
Host/OboeAudioStream.cpp)
list(APPEND pcsx2SPU2Sources
SPU2/spu2_neon.cpp)
list(APPEND pcsx2SPU2Headers
SPU2/spu2_neon.h
SPU2/spu2_neon_dcfilter.h
SPU2/spu2_neon_mixer.h
SPU2/spu2_neon_reverb_ex.h
SPU2/spu2_optimize.h
SPU2/spu2_sve2_fir.h
SPU2/spu2_mt6899_tuning.h)
list(APPEND pcsx2GSSources
GS/Renderers/OpenGL/GLContextEGLAndroid.cpp
GS/Renderers/Common/GSGPUProfile.cpp)
list(APPEND pcsx2GSHeaders
GS/Renderers/OpenGL/GLContextEGLAndroid.h
GS/Renderers/Common/GSGPUProfile.h)
list(APPEND pcsx2Sources
Android/AndroidStubs.cpp
Android/AndroidPcapStubs.cpp
VU1Fingerprint.cpp
EEDiffVerify.cpp)
list(APPEND pcsx2Headers
VU1Fingerprint.h
EEDiffVerify.h
AndroidEEOpHist.h
AndroidPerfBuckets.h
PS1DrvTrace.h)
endif()
# These ones benefit a lot from LTO
set(pcsx2LTOSources
${pcsx2Sources}
+356
View File
@@ -0,0 +1,356 @@
// SPDX-License-Identifier: GPL-3.0+
//
// @@EEDIFF@@ THROWAWAY DIAGNOSTIC — EE recompiler-vs-interpreter differential verifier.
// See EEDiffVerify.h for the design overview. This file is the core: snapshot, the
// interpreter re-run with store-capture, and the compare/report.
#include "EEDiffVerify.h"
#include "R5900.h"
#include "R5900OpcodeTables.h"
#include "Memory.h"
#include "common/Console.h"
#include <cstdio>
#include <cstring>
// --------------------------------------------------------------------------------------
// Global switches
// --------------------------------------------------------------------------------------
volatile bool g_ee_diff_verify = false;
bool g_ee_diff_capture_stores = false;
void eeDiffSetEnabled(bool enabled) { g_ee_diff_verify = enabled; }
bool eeDiffGetEnabled() { return g_ee_diff_verify; }
namespace
{
// Snapshot of the guest register state taken BEFORE the recompiled op runs (via
// eeDiffSnapshotPre). This is the interpreter's input for the re-run.
struct EeDiffRegs
{
GPR_reg gpr[32];
GPR_reg hi;
GPR_reg lo;
u32 sa;
u32 pc;
};
EeDiffRegs g_diff_pre; // regs at op entry (rec == interp input here)
EeDiffRegs g_diff_recpost; // regs after the recompiled op ran (rec ground-truth to match)
// Captured interpreter stores for the op currently being verified.
struct EeDiffStore
{
u32 addr;
u32 bits; // 8/16/32/64/128
u64 lo;
u64 hi; // only for 128-bit
};
constexpr u32 EEDIFF_MAX_STORES = 8; // a single EE op stores at most a 128-bit quad
EeDiffStore g_diff_stores[EEDIFF_MAX_STORES];
u32 g_diff_store_count = 0;
// Log a bounded number of divergences (enough to characterize a systematic decompressor
// miscompile across several ops) without flooding logcat. False positives are now filtered at
// the source (RAM-only), so this budget is spent on real divergences.
u32 g_diff_reported = 0;
constexpr u32 EEDIFF_MAX_REPORTS = 64;
void snapshotInto(EeDiffRegs& dst)
{
std::memcpy(dst.gpr, cpuRegs.GPR.r, sizeof(dst.gpr));
dst.hi = cpuRegs.HI;
dst.lo = cpuRegs.LO;
dst.sa = cpuRegs.sa;
dst.pc = cpuRegs.pc;
}
void restoreFrom(const EeDiffRegs& src)
{
std::memcpy(cpuRegs.GPR.r, src.gpr, sizeof(src.gpr));
cpuRegs.HI = src.hi;
cpuRegs.LO = src.lo;
cpuRegs.sa = src.sa;
cpuRegs.pc = src.pc;
}
// Read the real (rec-written) memory at a captured store's address so we can compare it
// against what the interpreter WOULD have written. Uses the same vtlb read path the
// interpreter loads use, so it sees exactly what the rec committed. Reads happen with
// capture mode OFF, so they are real reads.
bool memMatchesCapture(const EeDiffStore& s, u64& real_out)
{
switch (s.bits)
{
case 8: real_out = memRead8(s.addr); return real_out == (s.lo & 0xffu);
case 16: real_out = memRead16(s.addr); return real_out == (s.lo & 0xffffu);
case 32: real_out = memRead32(s.addr); return real_out == (s.lo & 0xffffffffu);
case 64: real_out = memRead64(s.addr); return real_out == s.lo;
case 128:
{
u128 v;
memRead128(s.addr, &v);
real_out = v.lo; // report low half; compare both
return v.lo == s.lo && v.hi == s.hi;
}
default: real_out = 0; return true;
}
}
// True iff the effective address lands in EE main RAM (32 MB, incl. KSEG0/KSEG1 mirrors) or
// the 16 KB scratchpad — the ONLY regions where "read the value back and compare it to what
// the interpreter would have written" is a valid store check. Everything else is memory-mapped
// I/O: EE registers 0x10000000-0x1000FFFF (DMAC/GIF/VIF/IPU/timers/INTC), GS privileged
// registers 0x12000000, VU/GS mem, BIOS ROM. A read of those does NOT return the last written
// value, so comparing a store there against a read-back is a GUARANTEED FALSE POSITIVE — and
// re-running such an access on the interpreter is unsafe (DMA kicks, counter/event side effects,
// Cpu->CancelInstruction longjmps). The texture-decompression bug we hunt writes to RAM, so
// RAM + scratchpad is exactly — and only — the region we verify.
inline bool eeDiffAddrIsRam(u32 addr)
{
if ((addr & 0xFFFFC000u) == 0x70000000u) // scratchpad (0x70000000, 16 KB)
return true;
return (addr & 0x1FFFFFFFu) < 0x02000000u; // main RAM (0x00000000, 32 MB) + KSEG0/KSEG1 mirrors
}
// A memory op whose effective address is NOT RAM/scratchpad must be SKIPPED entirely: the
// interpreter re-run risks event-test/longjmp/DMA-kick side effects, and the store-compare
// would be a false positive (I/O doesn't read back what you wrote). Only RAM/scratchpad
// accesses are verified. Effective address = GPR[rs].UL[0] + s16(imm) (standard EE base+offset;
// LWL/SWL/LQ/SQ mask low bits but stay in the same region, so the RAM test still holds).
// Returns true = "skip".
bool opTouchesNonRam(u32 op, const EeDiffRegs& pre)
{
const u32 primary = op >> 26;
bool is_mem;
switch (primary)
{
case 0x1e: // LQ
case 0x1f: // SQ
case 0x20: case 0x21: case 0x22: case 0x23: // LB LH LWL LW
case 0x24: case 0x25: case 0x26: case 0x27: // LBU LHU LWR LWU
case 0x28: case 0x29: case 0x2a: case 0x2b: // SB SH SWL SW
case 0x2c: case 0x2d: case 0x2e: case 0x2f: // SDL SDR SWR CACHE
case 0x1a: case 0x1b: // LDL LDR
case 0x37: case 0x3f: // LD SD
is_mem = true;
break;
default:
is_mem = false;
break;
}
if (!is_mem)
return false;
const u32 rs = (op >> 21) & 0x1f;
const u32 addr = pre.gpr[rs].UL[0] + static_cast<u32>(static_cast<s32>(static_cast<s16>(op)));
return !eeDiffAddrIsRam(addr);
}
// Ops whose interpreter re-run would raise a CPU exception: SYSCALL/BREAK and the conditional
// TRAPs. The interpreter services these via cpuException -> CP0 mutation + a longjmp we neither
// snapshot (no CP0 in EeDiffRegs) nor can survive mid-block. The rec ends the block on these
// anyway, so they carry no straight-line divergence to check. Skip the re-run. (Trapping-arith
// signed-overflow is handled separately by opWouldTrapOverflow.)
bool opRaisesException(u32 op)
{
const u32 primary = op >> 26;
if (primary == 0x00) // SPECIAL
{
const u32 funct = op & 0x3f;
if (funct == 0x0c || funct == 0x0d) return true; // SYSCALL, BREAK
if (funct >= 0x30 && funct <= 0x36) return true; // TGE TGEU TLT TLTU TEQ (.34) TNE (.36)
return false;
}
if (primary == 0x01) // REGIMM
{
const u32 rt = (op >> 16) & 0x1f;
return (rt >= 0x08 && rt <= 0x0e); // TGEI TGEIU TLTI TLTIU TEQI TNEI
}
return false;
}
// The EE's trapping arithmetic (ADD/ADDI/SUB/DADD/DADDI/DSUB) raises a signed-overflow
// exception in the INTERPRETER (cpuException -> mutates CP0 + pc, possibly a longjmp), but
// the recompiler intentionally SKIPS that trap (treats them as ADDU/…, matching the x86
// JIT — see aR5900Arith.cpp). So on a real overflow they legitimately diverge and re-running
// the interpreter would corrupt CP0 irreversibly (we don't snapshot CP0). Detect overflow
// from the pre-op operands (mirroring _add32_Overflow/_add64_Overflow exactly) and SKIP the
// re-run when it would trap. The far-more-common non-overflow path is still fully verified.
// Returns true = "skip this op (would trap)".
bool add32WouldOverflow(s32 x, s32 y)
{
GPR_reg64 r;
r.SD[0] = static_cast<s64>(x) + y;
return (r.UL[0] >> 31) != (r.UL[1] & 1);
}
bool add64WouldOverflow(s64 x, s64 y)
{
const s64 result = x + y;
return ((~(x ^ y)) & (x ^ result)) < 0;
}
bool opWouldTrapOverflow(u32 op, const EeDiffRegs& pre)
{
const u32 primary = op >> 26;
const u32 rs = (op >> 21) & 0x1f;
const u32 rt = (op >> 16) & 0x1f;
const s32 imm = static_cast<s16>(op);
const s64 srs = pre.gpr[rs].SD[0];
const s64 srt = pre.gpr[rt].SD[0];
switch (primary)
{
case 0x08: return add32WouldOverflow(static_cast<s32>(srs), imm); // ADDI
case 0x18: return add64WouldOverflow(srs, imm); // DADDI
case 0x00: // SPECIAL — funct disambiguates the trapping R-type adds/subs
switch (op & 0x3f)
{
case 0x20: return add32WouldOverflow(static_cast<s32>(srs), static_cast<s32>(srt)); // ADD
case 0x22: return add32WouldOverflow(static_cast<s32>(srs), static_cast<s32>(-srt)); // SUB
case 0x2c: return add64WouldOverflow(srs, srt); // DADD
case 0x2e: return add64WouldOverflow(srs, -srt); // DSUB
default: return false;
}
default: return false;
}
}
void reportDivergence(u32 pc, u32 op, const char* what,
const char* detail, u64 rec_val, u64 interp_val)
{
const u32 primary = op >> 26;
const u32 rs = (op >> 21) & 0x1f;
const u32 rt = (op >> 16) & 0x1f;
const u32 rd = (op >> 11) & 0x1f;
const u32 sa = (op >> 6) & 0x1f;
const u32 funct = op & 0x3f;
const char* name = ::R5900::GetInstruction(op).Name;
Console.WriteLn(
"@@EEDIFF@@ pc=%08x op=%08x %-8s DIVERGE %s(%s) rec=%016llx interp=%016llx "
"| primary=%02x funct=%02x rs=%u rt=%u rd=%u sa=%u",
pc, op, name, what, detail,
static_cast<unsigned long long>(rec_val),
static_cast<unsigned long long>(interp_val),
primary, funct, rs, rt, rd, sa);
}
} // namespace
// --------------------------------------------------------------------------------------
// Store capture (called from vtlb_memWrite hook)
// --------------------------------------------------------------------------------------
void eeDiffCaptureStore(u32 addr, u32 bits, u64 lo, u64 hi)
{
if (g_diff_store_count >= EEDIFF_MAX_STORES)
return; // shouldn't happen for one op; drop extras defensively
EeDiffStore& s = g_diff_stores[g_diff_store_count++];
s.addr = addr;
s.bits = bits;
s.lo = lo;
s.hi = hi;
}
// --------------------------------------------------------------------------------------
// Emitted-code entry points
// --------------------------------------------------------------------------------------
extern "C" void eeDiffSnapshotPre()
{
snapshotInto(g_diff_pre);
}
extern "C" void eeDiffVerify(u32 pc, u32 op)
{
// The recompiled op has just run: cpuRegs == REC-post. Save it first so we can always
// restore it (the rec owns guest state from here on).
snapshotInto(g_diff_recpost);
// Skip memory ops whose effective address is not RAM/scratchpad: the interpreter re-run
// would touch memory-mapped I/O (event test / DMA kick / longjmp risk) and the store-compare
// there is a false positive. cpuRegs is already REC-post; nothing to do. See opTouchesNonRam.
if (opTouchesNonRam(op, g_diff_pre))
return;
// Skip ops whose interpreter re-run would raise a CPU exception (SYSCALL/BREAK/TRAP) — the
// longjmp would escape mid-block and we don't snapshot CP0. See opRaisesException.
if (opRaisesException(op))
return;
// Skip trapping-arithmetic ops that would raise a signed-overflow exception in the
// interpreter (the rec deliberately doesn't trap — a legitimate, known divergence that
// would also corrupt CP0 in the re-run). See opWouldTrapOverflow.
if (opWouldTrapOverflow(op, g_diff_pre))
return;
restoreFrom(g_diff_pre); // cpuRegs = interpreter input (op-entry state)
g_diff_store_count = 0;
const u32 saved_code = cpuRegs.code;
cpuRegs.code = op; // the interpreter's operand macros (_Rs_/_Rt_/_Imm_/...) read this
g_ee_diff_capture_stores = true;
::R5900::GetInstruction(op).interpret(); // one op; stores captured, not applied
g_ee_diff_capture_stores = false;
cpuRegs.code = saved_code;
// cpuRegs is now INTERP-post (regs updated in place; stores diverted to the log).
bool diverged = false;
// (i) compare GPRs (full 128-bit), then HI, LO.
if (g_diff_reported < EEDIFF_MAX_REPORTS)
{
for (u32 r = 0; r < 32 && !diverged; r++)
{
const GPR_reg& ri = cpuRegs.GPR.r[r]; // interp-post
const GPR_reg& rr = g_diff_recpost.gpr[r]; // rec-post
if (ri.UD[0] != rr.UD[0] || ri.UD[1] != rr.UD[1])
{
char detail[24];
std::snprintf(detail, sizeof(detail), "GPR%u.lo", r);
reportDivergence(pc, op, "reg", detail, rr.UD[0], ri.UD[0]);
if (ri.UD[1] != rr.UD[1])
reportDivergence(pc, op, "reg", "GPRhi", rr.UD[1], ri.UD[1]);
diverged = true;
}
}
if (!diverged && (cpuRegs.HI.UD[0] != g_diff_recpost.hi.UD[0] ||
cpuRegs.HI.UD[1] != g_diff_recpost.hi.UD[1]))
{
reportDivergence(pc, op, "reg", "HI", g_diff_recpost.hi.UD[0], cpuRegs.HI.UD[0]);
diverged = true;
}
if (!diverged && (cpuRegs.LO.UD[0] != g_diff_recpost.lo.UD[0] ||
cpuRegs.LO.UD[1] != g_diff_recpost.lo.UD[1]))
{
reportDivergence(pc, op, "reg", "LO", g_diff_recpost.lo.UD[0], cpuRegs.LO.UD[0]);
diverged = true;
}
// (ii) compare each captured interpreter store against the real memory the rec
// already wrote. Read memory with capture mode OFF (real reads).
for (u32 i = 0; i < g_diff_store_count && !diverged; i++)
{
const EeDiffStore& s = g_diff_stores[i];
if (!eeDiffAddrIsRam(s.addr))
continue; // 2nd-layer guard: never compare an I/O store against a read-back
u64 real_val = 0;
if (!memMatchesCapture(s, real_val))
{
char detail[40];
std::snprintf(detail, sizeof(detail), "mem[%08x]/%ub", s.addr, s.bits);
reportDivergence(pc, op, "mem", detail, real_val, s.lo);
diverged = true;
}
}
if (diverged)
g_diff_reported++;
}
// CRITICAL: restore cpuRegs to REC-post so the real recompiled run is unaffected —
// the recompiler owns guest state and continues from here.
restoreFrom(g_diff_recpost);
cpuRegs.code = saved_code;
}
+63
View File
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: GPL-3.0+
//
// @@EEDIFF@@ THROWAWAY DIAGNOSTIC — EE recompiler-vs-interpreter differential verifier.
//
// Purpose: pin the exact ARM64 EE guest instruction whose recompiled result diverges
// from the C++ interpreter. Built to hunt the True Crime NYC (SLUS-21106) texture
// decompressor corruption (displaced paletted texture bytes) that GS-dump replay
// proved is an EE data-gen bug, not the renderer.
//
// This whole feature is gated behind g_ee_diff_verify (default false). When it is
// false there is ZERO overhead — no hooks are emitted into recompiled blocks, and the
// store-capture branch in vtlb_memWrite is a single never-taken bool test. Flip it on
// (Recompiler tab -> "EE Diff Verify (diag)") and the EE block cache is cleared so
// blocks recompile WITH the per-op verify hooks.
//
// How it works (see EEDiffVerify.cpp for detail):
// * The recompiler, when compiling a straight-line op through the native generators
// (recTranslateOp), emits around each op: snapshot cpuRegs -> real op -> verify.
// * eeDiffVerify() re-runs that ONE op on the C++ interpreter from the pre-op
// snapshot, with interpreter stores CAPTURED (recorded, not applied — the rec
// already wrote real memory), then compares the interpreter's post-state (regs +
// each captured store vs the real memory the rec wrote) against the rec's post
// state. First mismatch logs "@@EEDIFF@@ ... DIVERGE ...".
//
// Everything here is wrapped with // @@EEDIFF@@ so it is trivially greppable/revertable.
#pragma once
#include "common/Pcsx2Defs.h"
// Master enable. Set via the Recompiler-tab toggle -> NativeApp.setEeDiffVerify() ->
// eeDiffSetEnabled(). Read at recompile time (to decide whether to emit hooks) and by
// the vtlb store-capture fast-out. `volatile` because it is toggled from the UI thread
// while the EE thread reads it; the block-cache clear that the setter forces is the
// real synchronisation point.
extern volatile bool g_ee_diff_verify;
// Store-capture mode. Set to true ONLY for the brief window while eeDiffVerify re-runs
// the interpreter op; vtlb_memWrite* checks it and records {addr,size,value} into the
// capture log instead of touching real memory. Never true outside eeDiffVerify.
extern bool g_ee_diff_capture_stores;
// Record one interpreter store while g_ee_diff_capture_stores is set. Called from the
// vtlb_memWrite<T> / vtlb_memWrite128 hook. `bits` = 8/16/32/64/128. For 128-bit the
// value is passed as two u64 halves (lo, hi); narrower sizes pass the value in lo.
void eeDiffCaptureStore(u32 addr, u32 bits, u64 lo, u64 hi);
// Emitted-code entry points (called from recompiled blocks — plain C ABI, no args
// except pc/op which the rec bakes in as immediates).
extern "C" {
// Snapshot the whole guest register file (GPR[0..31], HI, LO, PC, sa) into g_diff_pre.
// Emitted BEFORE the real recompiled op runs.
void eeDiffSnapshotPre();
// Re-run `op` on the interpreter from g_diff_pre with stores captured, then compare
// against the rec's post-state (current cpuRegs + real memory). Emitted AFTER the op.
void eeDiffVerify(u32 pc, u32 op);
}
// UI/JNI setter: set the enable flag. Returns nothing; the caller (native-lib JNI) is
// responsible for also clearing the EE block cache so blocks recompile with/without the
// hooks. Kept separate so the header has no dependency on the recompiler internals.
void eeDiffSetEnabled(bool enabled);
bool eeDiffGetEnabled();
+232
View File
@@ -0,0 +1,232 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "GS/Renderers/Common/GSGPUProfile.h"
#include <array>
#include <cctype>
#include <initializer_list>
#if defined(__ANDROID__)
#include <sys/system_properties.h>
#endif
namespace
{
static std::string ToLowerASCII(std::string_view value)
{
std::string lowered;
lowered.reserve(value.size());
for (const char ch : value)
lowered.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
return lowered;
}
static bool Contains(std::string_view haystack, std::string_view needle)
{
return (haystack.find(needle) != std::string_view::npos);
}
static bool ContainsAny(std::string_view haystack, std::initializer_list<const char*> needles)
{
for (const char* needle : needles)
{
if (Contains(haystack, needle))
return true;
}
return false;
}
static void AppendHint(std::string& hints, std::string_view key, std::string_view value)
{
if (value.empty())
return;
if (!hints.empty())
hints.append(" | ");
if (!key.empty())
{
hints.append(key);
hints.push_back('=');
}
hints.append(value);
}
#if defined(__ANDROID__)
static std::string GetAndroidProperty(const char* name)
{
std::array<char, PROP_VALUE_MAX> value = {};
const int length = __system_property_get(name, value.data());
return (length > 0) ? std::string(value.data(), static_cast<size_t>(length)) : std::string();
}
#endif
static std::string BuildHints(std::string_view gpu_vendor, std::string_view gpu_renderer_or_name)
{
std::string hints;
AppendHint(hints, "gpu_vendor", gpu_vendor);
AppendHint(hints, "gpu", gpu_renderer_or_name);
#if defined(__ANDROID__)
static constexpr const char* property_names[] = {
"ro.soc.manufacturer",
"ro.soc.model",
"ro.soc.platform",
"ro.board.platform",
"ro.hardware",
"ro.product.board",
"ro.product.cpu.abi",
"ro.vendor.product.cpu.abilist",
};
for (const char* property_name : property_names)
AppendHint(hints, property_name, GetAndroidProperty(property_name));
#endif
return hints;
}
static bool LooksLikeAdreno(std::string_view lowered_hints)
{
const bool has_adreno = ContainsAny(lowered_hints, {"adreno"});
const bool has_qualcomm = ContainsAny(lowered_hints, {"qualcomm", "qcom", "snapdragon"});
return (has_adreno || has_qualcomm);
}
static bool LooksLikePowerVR(std::string_view lowered_hints)
{
// "imagination" is unambiguous. "powervr" appears in PowerVR renderer strings
// (e.g. "PowerVR B-Series BXM-8-256"). "img" is a common Imagination prefix
// in SoC manifests (Mediatek MT68xx/MT69xx use "ro.soc.manufacturer=Mediatek"
// with "img" markers on some boards). We deliberately do NOT match bare "vr"
// to avoid false positives.
return ContainsAny(lowered_hints, {"imagination", "powervr", "img"});
}
static bool LooksLikeMali(std::string_view lowered_hints)
{
// "arm" alone is too broad (it matches the CPU ABI string "arm64-v8a") — gate
// on Mali-specific markers. "valhall"/"bifrost"/"midgard" are Mali GPU arches.
return ContainsAny(lowered_hints, {"mali", "valhall", "bifrost", "midgard"});
}
} // namespace
GpuProfileOverride GpuProfileDetector::ParseOverride(std::string_view value)
{
const std::string lowered = ToLowerASCII(value);
if (lowered == "mali")
return GpuProfileOverride::Mali;
if (lowered == "adreno")
return GpuProfileOverride::Adreno;
if (lowered == "powervr")
return GpuProfileOverride::PowerVR;
return GpuProfileOverride::Auto;
}
const char* GpuProfileDetector::OverrideToConfigString(GpuProfileOverride value)
{
switch (value)
{
case GpuProfileOverride::Mali:
return "mali";
case GpuProfileOverride::Adreno:
return "adreno";
case GpuProfileOverride::PowerVR:
return "powervr";
case GpuProfileOverride::Auto:
default:
return "auto";
}
}
const char* GpuProfileDetector::OverrideToString(GpuProfileOverride value)
{
switch (value)
{
case GpuProfileOverride::Mali:
return "Force Mali";
case GpuProfileOverride::Adreno:
return "Force Adreno";
case GpuProfileOverride::PowerVR:
return "Force PowerVR";
case GpuProfileOverride::Auto:
default:
return "Auto";
}
}
const char* GpuProfileDetector::RuntimeProfileToString(RuntimeGpuProfile value)
{
switch (value)
{
case RuntimeGpuProfile::Mali:
return "Mali";
case RuntimeGpuProfile::PowerVR:
return "PowerVR";
case RuntimeGpuProfile::Adreno:
default:
return "Adreno";
}
}
GpuProfileSelection GpuProfileDetector::Resolve(std::string_view override_value, std::string_view gpu_vendor,
std::string_view gpu_renderer_or_name)
{
GpuProfileSelection selection;
selection.override_mode = ParseOverride(override_value);
selection.hints = BuildHints(gpu_vendor, gpu_renderer_or_name);
if (selection.override_mode == GpuProfileOverride::Mali)
{
selection.runtime_profile = RuntimeGpuProfile::Mali;
return selection;
}
if (selection.override_mode == GpuProfileOverride::Adreno)
{
selection.runtime_profile = RuntimeGpuProfile::Adreno;
return selection;
}
if (selection.override_mode == GpuProfileOverride::PowerVR)
{
selection.runtime_profile = RuntimeGpuProfile::PowerVR;
return selection;
}
const std::string lowered_hints = ToLowerASCII(selection.hints);
#if defined(__ANDROID__)
if (LooksLikeAdreno(lowered_hints))
{
selection.runtime_profile = RuntimeGpuProfile::Adreno;
}
else if (LooksLikePowerVR(lowered_hints))
{
selection.runtime_profile = RuntimeGpuProfile::PowerVR;
}
else if (LooksLikeMali(lowered_hints))
{
selection.runtime_profile = RuntimeGpuProfile::Mali;
}
else
{
// No vendor/renderer string matched. Mali used to be the catch-all on this
// fork but that classified Imagination/PowerVR — which only has EXT fbfetch,
// not ARM — as Mali and broke device init. EXT-style fbfetch is the de-facto
// standard on every modern non-Mali mobile GPU, so default unknowns to
// Adreno profile instead.
selection.runtime_profile = RuntimeGpuProfile::Adreno;
}
#else
selection.runtime_profile = RuntimeGpuProfile::Adreno;
#endif
return selection;
}
+43
View File
@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "common/Pcsx2Defs.h"
#include <string>
#include <string_view>
enum class GpuProfileOverride : u8
{
Auto,
Mali,
Adreno,
PowerVR,
};
enum class RuntimeGpuProfile : u8
{
Mali,
Adreno,
PowerVR,
};
struct GpuProfileSelection
{
GpuProfileOverride override_mode = GpuProfileOverride::Auto;
RuntimeGpuProfile runtime_profile = RuntimeGpuProfile::Adreno;
std::string hints;
};
class GpuProfileDetector
{
public:
static GpuProfileOverride ParseOverride(std::string_view value);
static const char* OverrideToConfigString(GpuProfileOverride value);
static const char* OverrideToString(GpuProfileOverride value);
static const char* RuntimeProfileToString(RuntimeGpuProfile value);
static GpuProfileSelection Resolve(std::string_view override_value, std::string_view gpu_vendor,
std::string_view gpu_renderer_or_name);
};
@@ -0,0 +1,47 @@
#include "GLContextEGLAndroid.h"
#include "common/Console.h"
#include <android/native_window.h>
GLContextEGLAndroid::GLContextEGLAndroid(const WindowInfo& wi) : GLContextEGL(wi) {}
GLContextEGLAndroid::~GLContextEGLAndroid() = default;
std::unique_ptr<GLContext> GLContextEGLAndroid::Create(const WindowInfo& wi, const Version* versions_to_try,
size_t num_versions_to_try)
{
std::unique_ptr<GLContextEGLAndroid> context = std::make_unique<GLContextEGLAndroid>(wi);
if (!context->Initialize(std::span<const Version>(versions_to_try, num_versions_to_try), nullptr))
return nullptr;
return context;
}
std::unique_ptr<GLContext> GLContextEGLAndroid::CreateSharedContext(const WindowInfo& wi, Error* error)
{
std::unique_ptr<GLContextEGLAndroid> context = std::make_unique<GLContextEGLAndroid>(wi);
context->m_display = m_display;
if (!context->CreateContextAndSurface(m_version, m_context, false))
return nullptr;
return context;
}
void GLContextEGLAndroid::ResizeSurface(u32 new_surface_width, u32 new_surface_height)
{
GLContextEGL::ResizeSurface(new_surface_width, new_surface_height);
}
EGLNativeWindowType GLContextEGLAndroid::GetNativeWindow(EGLConfig config)
{
EGLint native_visual_id = 0;
if (!eglGetConfigAttrib(m_display, m_config, EGL_NATIVE_VISUAL_ID, &native_visual_id))
{
Console.Error("Failed to get native visual ID");
return 0;
}
ANativeWindow_setBuffersGeometry(static_cast<ANativeWindow*>(m_wi.window_handle), 0, 0, static_cast<int32_t>(native_visual_id));
m_wi.surface_width = ANativeWindow_getWidth(static_cast<ANativeWindow*>(m_wi.window_handle));
m_wi.surface_height = ANativeWindow_getHeight(static_cast<ANativeWindow*>(m_wi.window_handle));
return static_cast<EGLNativeWindowType>(m_wi.window_handle);
}
@@ -0,0 +1,18 @@
#pragma once
#include "GLContextEGL.h"
class GLContextEGLAndroid final : public GLContextEGL
{
public:
GLContextEGLAndroid(const WindowInfo& wi);
~GLContextEGLAndroid() override;
static std::unique_ptr<GLContext> Create(const WindowInfo& wi, const Version* versions_to_try,
size_t num_versions_to_try);
std::unique_ptr<GLContext> CreateSharedContext(const WindowInfo& wi, Error* error) override;
void ResizeSurface(u32 new_surface_width = 0, u32 new_surface_height = 0) override;
protected:
EGLNativeWindowType GetNativeWindow(EGLConfig config) override;
};
+310
View File
@@ -0,0 +1,310 @@
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "Host/AudioStream.h"
#include "VMManager.h"
#include "common/Assertions.h"
#include "common/Console.h"
#include "common/Error.h"
#include "oboe/Oboe.h"
#include <atomic>
#include <chrono>
#include <thread>
#if defined(__ANDROID__)
#include <sched.h>
#include <sys/syscall.h>
#include <unistd.h>
#endif
namespace {
class OboeAudioStream final : public AudioStream,
oboe::AudioStreamDataCallback,
oboe::AudioStreamErrorCallback
{
public:
OboeAudioStream(u32 sample_rate, const AudioStreamParameters& parameters);
~OboeAudioStream() override;
void SetPaused(bool paused) override;
bool Initialize(bool stretch_enabled);
bool Open();
bool Start();
void Stop();
void Close();
oboe::DataCallbackResult onAudioReady(oboe::AudioStream* p_audioStream,
void* p_audioData, int32_t p_numFrames) override;
bool onError(oboe::AudioStream* oboeStream, oboe::Result error) override;
private:
bool m_playing = false;
bool m_stop_requested = false;
std::shared_ptr<oboe::AudioStream> m_stream;
// Performance mode the stream is (re)opened with. Starts at LowLatency;
// Initialize() downgrades it to None if the device refuses the fast path
// (some Adreno/AAudio devices fail requestStart() with ErrorDisconnected
// at boot). onError()'s reopen then reuses whatever mode actually worked.
oboe::PerformanceMode m_perf_mode = oboe::PerformanceMode::LowLatency;
// Affinity pin latch. Oboe spawns its own audio data thread; we don't
// see its TID until the callback fires the first time. After the
// first callback we apply the perf-cluster affinity once. Audio
// callbacks compete with EE for cache lines + share the same big
// cluster — without pinning, the audio thread can land on a little
// core (jitter) or migrate onto EE's core (L2 pollution).
std::atomic<bool> m_audio_thread_pinned{false};
};
} // namespace
oboe::DataCallbackResult OboeAudioStream::onAudioReady(oboe::AudioStream* p_audioStream,
void* p_audioData, int32_t p_numFrames)
{
#if defined(__ANDROID__)
// Affinity pin. Oboe owns the audio data thread; we only see its TID
// inside this callback. Pin onto the same perf-cluster as EE/VU/GS so
// the audio thread doesn't (a) get scheduled to a little core and
// inject jitter into the callback's deadline, or (b) land on EE's
// core and pollute L2.
//
// VMManager's SetEmuThreadAffinities runs when the VM transitions to
// Running, which is typically AFTER Oboe has opened its stream and
// fired the first callback. So the first few callbacks see
// perf_mask=0 (pinning not yet active) and skip. Latch only after a
// SUCCESSFUL pin so we keep polling cheaply (one atomic-acquire +
// `s_thread_affinities_set` bool check inside
// GetPerformanceClusterAffinityMask) until pinning actually turns on.
if (!m_audio_thread_pinned.load(std::memory_order_acquire))
{
const u64 perf_mask = VMManager::Internal::GetPerformanceClusterAffinityMask();
if (perf_mask != 0)
{
const pid_t tid = static_cast<pid_t>(syscall(SYS_gettid));
cpu_set_t set;
CPU_ZERO(&set);
for (u32 i = 0; i < 64; i++)
{
if (perf_mask & (static_cast<u64>(1) << i))
CPU_SET(i, &set);
}
if (sched_setaffinity(tid, sizeof(set), &set) == 0)
{
INFO_LOG("(Oboe) audio thread tid={} pinned to perf-cluster mask 0x{:x}", tid, perf_mask);
m_audio_thread_pinned.store(true, std::memory_order_release);
}
else
{
WARNING_LOG("(Oboe) sched_setaffinity tid={} failed (errno {}) — will retry next callback", tid, errno);
}
}
// else: pinning not active yet (VM hasn't reached Running). Skip
// the syscall + don't latch — next callback retries.
}
#endif
if (p_audioData != nullptr)
ReadFrames(reinterpret_cast<SampleType*>(p_audioData), p_numFrames);
return oboe::DataCallbackResult::Continue;
}
bool OboeAudioStream::onError(oboe::AudioStream* oboeStream, oboe::Result error)
{
Console.Error("(Oboe) ErrorCB %d", error);
if (error == oboe::Result::ErrorDisconnected && !m_stop_requested)
{
Console.Error("(Oboe) Stream disconnected, reopening...");
Stop();
Close();
if (!Open() || !Start())
Console.Error("(Oboe) Failed to reopen stream after disconnection.");
return true;
}
return false;
}
bool OboeAudioStream::Initialize(bool stretch_enabled)
{
static constexpr const std::array<SampleReader, static_cast<size_t>(AudioExpansionMode::Count)> sample_readers = {{
&StereoSampleReaderImpl,
&SampleReaderImpl<AudioExpansionMode::StereoLFE,
READ_CHANNEL_FRONT_LEFT, READ_CHANNEL_FRONT_RIGHT, READ_CHANNEL_LFE>,
&SampleReaderImpl<AudioExpansionMode::Quadraphonic,
READ_CHANNEL_FRONT_LEFT, READ_CHANNEL_FRONT_RIGHT,
READ_CHANNEL_REAR_LEFT, READ_CHANNEL_REAR_RIGHT>,
&SampleReaderImpl<AudioExpansionMode::QuadraphonicLFE,
READ_CHANNEL_FRONT_LEFT, READ_CHANNEL_FRONT_RIGHT, READ_CHANNEL_LFE,
READ_CHANNEL_REAR_LEFT, READ_CHANNEL_REAR_RIGHT>,
&SampleReaderImpl<AudioExpansionMode::Surround51,
READ_CHANNEL_FRONT_LEFT, READ_CHANNEL_FRONT_RIGHT, READ_CHANNEL_FRONT_CENTER,
READ_CHANNEL_LFE, READ_CHANNEL_REAR_LEFT, READ_CHANNEL_REAR_RIGHT>,
&SampleReaderImpl<AudioExpansionMode::Surround71,
READ_CHANNEL_FRONT_LEFT, READ_CHANNEL_FRONT_RIGHT, READ_CHANNEL_FRONT_CENTER,
READ_CHANNEL_LFE, READ_CHANNEL_SIDE_LEFT, READ_CHANNEL_SIDE_RIGHT,
READ_CHANNEL_REAR_LEFT, READ_CHANNEL_REAR_RIGHT>,
}};
BaseInitialize(sample_readers[static_cast<size_t>(m_parameters.expansion_mode)], stretch_enabled);
// Resilient open: some devices (seen on Adreno/AAudio) refuse a low-latency /
// fast-path output stream at boot and fail requestStart() with
// ErrorDisconnected — the audio device was reclaimed the instant we tried to
// start it. Rather than fall straight to permanent silent null output, retry,
// and if the fast path keeps failing drop to the most compatible
// PerformanceMode::None (shared slow-path) stream before giving up.
static constexpr oboe::PerformanceMode kModes[] = {
oboe::PerformanceMode::LowLatency,
oboe::PerformanceMode::None,
};
for (const oboe::PerformanceMode mode : kModes)
{
m_perf_mode = mode;
for (int attempt = 0; attempt < 2; attempt++)
{
if (Open() && Start())
{
if (mode != oboe::PerformanceMode::LowLatency || attempt != 0)
Console.WriteLn("(Oboe) Audio stream opened with performance mode %d (attempt %d).",
static_cast<int>(mode), attempt);
return true;
}
// Open() failed, or Open() succeeded but Start() failed: tear the
// half-open stream down before the next attempt / mode, then pause
// briefly to let a transient device-reclaim settle.
Close();
std::this_thread::sleep_for(std::chrono::milliseconds(60));
}
Console.Warning("(Oboe) performance mode %d failed; trying a more compatible mode...",
static_cast<int>(mode));
}
Console.Error("(Oboe) All open/start attempts failed; audio will be silent.");
return false;
}
bool OboeAudioStream::Open()
{
// Each Open() spawns a fresh Oboe audio thread with a new TID, so the
// per-stream pin latch needs to clear here. Without this, an error-
// recovery re-Open() (onError → Stop/Close/Open) keeps the latch set
// from the previous instance and the new audio thread runs un-pinned.
m_audio_thread_pinned.store(false, std::memory_order_release);
oboe::AudioStreamBuilder builder;
builder.setDirection(oboe::Direction::Output);
builder.setPerformanceMode(m_perf_mode);
builder.setSharingMode(oboe::SharingMode::Shared);
builder.setFormat(oboe::AudioFormat::Float);
builder.setSampleRate(m_sample_rate);
builder.setChannelCount(m_output_channels == 2 ? oboe::ChannelCount::Stereo : oboe::ChannelCount::Mono);
builder.setDeviceId(oboe::kUnspecified);
builder.setBufferCapacityInFrames(2048 * 2);
builder.setFramesPerDataCallback(2048);
builder.setDataCallback(this);
builder.setErrorCallback(this);
Console.WriteLn("(Oboe) Opening stream...");
oboe::Result result = builder.openStream(m_stream);
if (result != oboe::Result::OK)
{
Console.Error("(Oboe) openStream() failed: %d", result);
return false;
}
return true;
}
bool OboeAudioStream::Start()
{
if (m_playing)
return true;
Console.WriteLn("(Oboe) Starting stream...");
m_stop_requested = false;
oboe::Result result = m_stream->requestStart();
if (result != oboe::Result::OK)
{
Console.Error("(Oboe) requestStart() failed: %d", result);
return false;
}
m_playing = true;
return true;
}
void OboeAudioStream::Stop()
{
if (!m_playing)
return;
Console.WriteLn("(Oboe) Stopping stream...");
m_stop_requested = true;
oboe::Result result = m_stream->requestStop();
if (result != oboe::Result::OK)
Console.Error("(Oboe) requestStop() failed: %d", result);
m_playing = false;
}
void OboeAudioStream::Close()
{
Console.WriteLn("(Oboe) Closing stream...");
if (m_playing)
Stop();
if (m_stream)
{
m_stream->close();
m_stream.reset();
}
}
void OboeAudioStream::SetPaused(bool paused)
{
if (m_paused == paused)
return;
if (paused)
{
if (m_stream)
{
oboe::Result result = m_stream->requestPause();
if (result != oboe::Result::OK)
Console.Error("(Oboe) requestPause() failed: %d", result);
}
// Mark not-playing even if requestPause() failed, so the paused/
// playing bookkeeping can't desync and strand a later resume.
m_playing = false;
}
else
{
// Resume must be authoritative. If m_playing desynced to true (e.g.
// an error-recovery reopen ran while we thought the stream was
// paused), Start()'s `if (m_playing) return true;` guard would
// swallow the restart and leave audio dead. Clear it first so the
// resume always actually re-issues requestStart().
m_playing = false;
Start();
}
m_paused = paused;
}
OboeAudioStream::OboeAudioStream(u32 sample_rate, const AudioStreamParameters& parameters)
: AudioStream(sample_rate, parameters)
{
}
OboeAudioStream::~OboeAudioStream()
{
Close();
}
std::unique_ptr<AudioStream> AudioStream::CreateOboeAudioStream(u32 sample_rate,
const AudioStreamParameters& parameters, bool stretch_enabled, Error* error)
{
std::unique_ptr<OboeAudioStream> stream = std::make_unique<OboeAudioStream>(sample_rate, parameters);
if (!stream->Initialize(stretch_enabled))
stream.reset();
return stream;
}
+150
View File
@@ -0,0 +1,150 @@
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
// Rate-limited / categorized trace helpers for PS1-mode runtime debugging.
// Toggles live in arm64/InterpFlags.h: PS1DRV_TRACE_<CAT> where CAT is one
// of CDROM / DMA / IRQ / GPU / SIO / MDEC. Logs prefixed with
// [PS1DRV-<CAT>] for grep-friendly filtering.
//
// Capture: `adb logcat -s STDOUT:W | grep PS1DRV` while the device runs.
//
// Macros (no-op when toggle off):
// PS1DRV_TRACE_LOG(CAT, fmt, ...)
// Unconditional log if PS1DRV_TRACE_<CAT> is defined.
//
// PS1DRV_TRACE_RATE(CAT, key, every_n_cycles, fmt, ...)
// Log only if at least every_n_cycles IOP cycles have elapsed since the
// last hit at this `key` (a string literal — used as the dedupe key).
// Use for events that fire every frame / every block.
//
// PS1DRV_TRACE_CHANGE(CAT, key, var, fmt, ...)
// Log only when `var` differs from the value seen at the previous call
// with the same `key`. Use to surface state transitions without
// spamming on no-change polls.
#pragma once
#include "Common.h"
#include "R3000A.h"
#include "arm64/InterpFlags.h"
#include <unordered_map>
#include <cstdint>
#include <string>
namespace PS1DrvTrace
{
struct RateState
{
u64 last_cycle = 0;
};
struct ChangeState
{
bool seen = false;
u64 last_value = 0;
};
inline std::unordered_map<std::string, RateState>& rateMap()
{
static std::unordered_map<std::string, RateState> m;
return m;
}
inline std::unordered_map<std::string, ChangeState>& changeMap()
{
static std::unordered_map<std::string, ChangeState> m;
return m;
}
inline bool rateAllow(const char* key, u64 every_n_cycles)
{
auto& s = rateMap()[key];
const u64 now = psxRegs.cycle;
if (now - s.last_cycle < every_n_cycles && s.last_cycle != 0)
return false;
s.last_cycle = now;
return true;
}
inline bool changeAllow(const char* key, u64 value)
{
auto& s = changeMap()[key];
if (s.seen && s.last_value == value)
return false;
s.seen = true;
s.last_value = value;
return true;
}
}
#define PS1DRV_TRACE_LOG_IMPL(CAT, ...) \
Console.WriteLn("[PS1DRV-" CAT "] " __VA_ARGS__)
#define PS1DRV_TRACE_RATE_IMPL(CAT, key, every_n, ...) \
do { if (PS1DrvTrace::rateAllow("PS1DRV-" CAT ":" key, (every_n))) \
Console.WriteLn("[PS1DRV-" CAT "] " __VA_ARGS__); } while (0)
#define PS1DRV_TRACE_CHANGE_IMPL(CAT, key, var, ...) \
do { if (PS1DrvTrace::changeAllow("PS1DRV-" CAT ":" key, static_cast<u64>(var))) \
Console.WriteLn("[PS1DRV-" CAT "] " __VA_ARGS__); } while (0)
#if defined(PS1DRV_TRACE_CDROM)
#define PS1DRV_LOG_CDROM(...) PS1DRV_TRACE_LOG_IMPL("CDROM", __VA_ARGS__)
#define PS1DRV_RATE_CDROM(k, n, ...) PS1DRV_TRACE_RATE_IMPL("CDROM", k, n, __VA_ARGS__)
#define PS1DRV_CHG_CDROM(k, v, ...) PS1DRV_TRACE_CHANGE_IMPL("CDROM", k, v, __VA_ARGS__)
#else
#define PS1DRV_LOG_CDROM(...) do {} while (0)
#define PS1DRV_RATE_CDROM(k, n, ...) do {} while (0)
#define PS1DRV_CHG_CDROM(k, v, ...) do {} while (0)
#endif
#if defined(PS1DRV_TRACE_DMA)
#define PS1DRV_LOG_DMA(...) PS1DRV_TRACE_LOG_IMPL("DMA", __VA_ARGS__)
#define PS1DRV_RATE_DMA(k, n, ...) PS1DRV_TRACE_RATE_IMPL("DMA", k, n, __VA_ARGS__)
#define PS1DRV_CHG_DMA(k, v, ...) PS1DRV_TRACE_CHANGE_IMPL("DMA", k, v, __VA_ARGS__)
#else
#define PS1DRV_LOG_DMA(...) do {} while (0)
#define PS1DRV_RATE_DMA(k, n, ...) do {} while (0)
#define PS1DRV_CHG_DMA(k, v, ...) do {} while (0)
#endif
#if defined(PS1DRV_TRACE_IRQ)
#define PS1DRV_LOG_IRQ(...) PS1DRV_TRACE_LOG_IMPL("IRQ", __VA_ARGS__)
#define PS1DRV_RATE_IRQ(k, n, ...) PS1DRV_TRACE_RATE_IMPL("IRQ", k, n, __VA_ARGS__)
#define PS1DRV_CHG_IRQ(k, v, ...) PS1DRV_TRACE_CHANGE_IMPL("IRQ", k, v, __VA_ARGS__)
#else
#define PS1DRV_LOG_IRQ(...) do {} while (0)
#define PS1DRV_RATE_IRQ(k, n, ...) do {} while (0)
#define PS1DRV_CHG_IRQ(k, v, ...) do {} while (0)
#endif
#if defined(PS1DRV_TRACE_GPU)
#define PS1DRV_LOG_GPU(...) PS1DRV_TRACE_LOG_IMPL("GPU", __VA_ARGS__)
#define PS1DRV_RATE_GPU(k, n, ...) PS1DRV_TRACE_RATE_IMPL("GPU", k, n, __VA_ARGS__)
#define PS1DRV_CHG_GPU(k, v, ...) PS1DRV_TRACE_CHANGE_IMPL("GPU", k, v, __VA_ARGS__)
#else
#define PS1DRV_LOG_GPU(...) do {} while (0)
#define PS1DRV_RATE_GPU(k, n, ...) do {} while (0)
#define PS1DRV_CHG_GPU(k, v, ...) do {} while (0)
#endif
#if defined(PS1DRV_TRACE_SIO)
#define PS1DRV_LOG_SIO(...) PS1DRV_TRACE_LOG_IMPL("SIO", __VA_ARGS__)
#define PS1DRV_RATE_SIO(k, n, ...) PS1DRV_TRACE_RATE_IMPL("SIO", k, n, __VA_ARGS__)
#define PS1DRV_CHG_SIO(k, v, ...) PS1DRV_TRACE_CHANGE_IMPL("SIO", k, v, __VA_ARGS__)
#else
#define PS1DRV_LOG_SIO(...) do {} while (0)
#define PS1DRV_RATE_SIO(k, n, ...) do {} while (0)
#define PS1DRV_CHG_SIO(k, v, ...) do {} while (0)
#endif
#if defined(PS1DRV_TRACE_MDEC)
#define PS1DRV_LOG_MDEC(...) PS1DRV_TRACE_LOG_IMPL("MDEC", __VA_ARGS__)
#define PS1DRV_RATE_MDEC(k, n, ...) PS1DRV_TRACE_RATE_IMPL("MDEC", k, n, __VA_ARGS__)
#define PS1DRV_CHG_MDEC(k, v, ...) PS1DRV_TRACE_CHANGE_IMPL("MDEC", k, v, __VA_ARGS__)
#else
#define PS1DRV_LOG_MDEC(...) do {} while (0)
#define PS1DRV_RATE_MDEC(k, n, ...) do {} while (0)
#define PS1DRV_CHG_MDEC(k, v, ...) do {} while (0)
#endif
+201
View File
@@ -0,0 +1,201 @@
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
//
// SPU2/spu2_mt6899_tuning.h — Platform detection and tuning for MediaTek MT6899
// (Dimensity 9400) and general ARM64 performance targets.
//
// MT6899 SoC layout:
// Cortex-X925 (Prime) @ 3.62 GHz — L1D=64KB, L2=1MB
// Cortex-X4 (Perf) x3 @ 2.80 GHz — L1D=64KB, L2=512KB
// Cortex-A720 (Eff) x4 @ 2.00 GHz — L1D=64KB, L2=256KB
// L3: 12MB shared | SLC: 8MB | LPDDR5X-8533
// SVE2: 128-bit VL | SME | ARMv9.2-A
//
// Key facts for SPU2:
// _spu2mem is 2MB — fits in X925 L2 (1MB) partially, fully in L3.
// Reverb working area: 64256KB — fits in L2.
// 24 voice structs: ~6KB — fits in L1D.
// interpTable: 2KB — fits in L1D.
// Cache line: 64 bytes on ALL cores.
#pragma once
#include <cstdint>
namespace spu2_mt6899 {
// ============================================================================
// Cache hierarchy constants
// ============================================================================
static constexpr uint32_t CACHE_LINE_BYTES = 64;
static constexpr uint32_t L1D_BYTES = 64 * 1024;
static constexpr uint32_t L2_BYTES_PRIME = 1024 * 1024;
static constexpr uint32_t L3_BYTES = 12 * 1024 * 1024;
// SPU2 memory budget:
// _spu2mem[0x200000] = 2,097,152 bytes (2MB)
// RevbDownBuf/RevbUpBuf = 2 x 2 x 128 x sizeof(s16) = 1KB
// Voice structs = 24 x ~256 bytes = 6KB
// interpTable = 256 x 4 x 2 = 2048 bytes
// adsr_shift_table = 32 x 8 = 256 bytes
// Prefetch distances — tuned for MT6899 cache hierarchy.
// PLDL1KEEP (temporal, all levels) — use for data accessed within ~20 cycles
// PLDL2KEEP (temporal, L2+) — use for data accessed within ~100 cycles
// PLDL3KEEP (temporal, L3+) — use for data accessed within ~300 cycles
// PSTL1KEEP (write, all levels) — use for stores about to happen
static constexpr uint32_t PREFETCH_READ_L1 = CACHE_LINE_BYTES; // 64B ahead
static constexpr uint32_t PREFETCH_READ_L2 = CACHE_LINE_BYTES * 4; // 256B ahead
static constexpr uint32_t PREFETCH_WRITE_L1 = CACHE_LINE_BYTES; // 64B ahead
// Alignment macros for SIMD-friendly layouts
#define SPU2_CACHE_ALIGN alignas(64)
#define SPU2_NEON_ALIGN alignas(16)
#define SPU2_SVE_ALIGN alignas(32) // For potential 256-bit SVE
} // namespace spu2_mt6899
// ============================================================================
// CPU Feature Detection — Runtime (Android/Linux)
// ============================================================================
#if defined(__aarch64__) || defined(_M_ARM64)
#if defined(__ANDROID__) || defined(__linux__)
#include <sys/auxv.h>
#include <asm/hwcap.h>
// Some NDK <asm/hwcap.h> revisions (e.g. NDK 28) omit a few HWCAP2 feature
// bits. Provide 0 fallbacks so feature detection compiles everywhere — these
// flags are informational only; the NEON reverb FIR does not depend on them.
#ifndef HWCAP2_SVE2
#define HWCAP2_SVE2 0
#endif
#ifndef HWCAP2_I8MM
#define HWCAP2_I8MM 0
#endif
#ifndef HWCAP2_FHM
#define HWCAP2_FHM 0
#endif
#ifndef HWCAP2_USCAT
#define HWCAP2_USCAT 0
#endif
namespace spu2_mt6899 {
struct ARM64Features {
bool neon = false;
bool sve = false;
bool sve2 = false;
bool i8mm = false; // Int8 matrix multiply
bool fhm = false; // FP16 multiply-accumulate
bool aes = false;
bool sha2 = false;
bool atomics = false; // LSE atomics (big perf win on X925)
bool uscat = false; // Unaligned single-copy atomicity
bool detected = false;
void detect() {
if (detected) return;
const unsigned long hwcap = getauxval(AT_HWCAP);
const unsigned long hwcap2 = getauxval(AT_HWCAP2);
neon = (hwcap & HWCAP_ASIMD) != 0;
sve = (hwcap & HWCAP_SVE) != 0;
aes = (hwcap & HWCAP_AES) != 0;
sha2 = (hwcap & HWCAP_SHA2) != 0;
sve2 = (hwcap2 & HWCAP2_SVE2) != 0;
i8mm = (hwcap2 & HWCAP2_I8MM) != 0;
fhm = (hwcap2 & HWCAP2_FHM) != 0;
atomics = (hwcap & HWCAP_ATOMICS) != 0;
uscat = (hwcap2 & HWCAP2_USCAT) != 0;
detected = true;
}
};
inline ARM64Features& GetFeatures() {
static ARM64Features feat;
feat.detect();
return feat;
}
} // namespace spu2_mt6899
#endif // __ANDROID__ || __linux__
#endif // __aarch64__
// ============================================================================
// Thread Affinity — Pin audio thread to prime core
// ============================================================================
// On MT6899, CPU 0 is typically the X925 prime core.
// Pinning the SPU2 mixing thread to the prime core reduces latency jitter
// from ~15us (on A720) to ~5us (on X925) per mix cycle.
#if defined(__aarch64__) && defined(__ANDROID__)
#include <sched.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/prctl.h>
namespace spu2_mt6899 {
// Pin current thread to the fastest available core.
// Returns the core number pinned to, or -1 on failure.
inline int PinToFastCore() {
cpu_set_t mask;
CPU_ZERO(&mask);
// MT6899 topology: CPU 0 = X925, CPU 1-3 = X4, CPU 4-7 = A720
// Strategy: try CPU 0 first (prime), then CPU 1 (first perf core).
for (int candidate = 0; candidate <= 3; candidate++) {
CPU_SET(candidate, &mask);
if (sched_setaffinity(0, sizeof(mask), &mask) == 0) {
return candidate;
}
CPU_CLR(candidate, &mask);
}
return -1;
}
// Set thread name for debugging (shows in systrace/perfetto)
inline void SetThreadName(const char* name) {
prctl(PR_SET_NAME, name, 0, 0, 0);
}
// Set thread to SCHED_FIFO real-time priority for lowest latency.
// Requires CAP_SYS_NICE or appropriate Android permissions.
// Returns true on success.
inline bool SetRealtimePriority(int priority = 10) {
struct sched_param param;
param.sched_priority = priority;
return (pthread_setschedparam(pthread_self(), SCHED_FIFO, &param) == 0);
}
// Query which core we're currently running on.
inline int GetCurrentCore() {
return sched_getcpu();
}
// Check if we're on a big core (X925 or X4, not A720)
inline bool IsOnBigCore() {
int cpu = GetCurrentCore();
return (cpu >= 0 && cpu <= 3);
}
} // namespace spu2_mt6899
#endif // __aarch64__ && __ANDROID__
// ============================================================================
// LSE Atomics Helper
// ============================================================================
// On Cortex-X925, LSE atomics (LDADD, STADD, CAS, SWP) are significantly
// faster than LL/SC (LDXR/STXR) loops — 1-2 cycles vs 10+ contended.
// MT6899 supports LSE. Use std::atomic with appropriate memory ordering.
#if defined(__aarch64__) && (__has_include(<atomic>))
#include <atomic>
namespace spu2_mt6899 {
// Type alias for lock-free counters on MT6899
// (LSE makes atomic<u32> fast enough for hot paths)
using AtomicCounter = std::atomic<uint32_t>;
} // namespace spu2_mt6899
#endif
+148
View File
@@ -0,0 +1,148 @@
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
//
// SPU2/spu2_neon.cpp — ARM64 backend registration for SPU2.
//
// Registers NEON (and optionally SVE2) optimized implementations by
// overriding the global function pointers at runtime.
// Called once from SPU2::Open() after Multi-ISA default init.
//
// Only compiled on ARM64 targets. On x86, this file produces no object code.
#if defined(__aarch64__) || defined(_M_ARM64)
#include "SPU2/spu2_neon.h"
#include "SPU2/spu2_neon_mixer.h"
#include "SPU2/spu2_neon_reverb_ex.h"
#include "SPU2/spu2_neon_dcfilter.h"
#include "SPU2/spu2_sve2_fir.h"
#include "SPU2/spu2_mt6899_tuning.h"
#include "SPU2/defs.h"
#include <arm_neon.h>
#include <cstdio>
// ============================================================================
// Reverb FIR — NEON implementations (from original spu2_neon.cpp)
// ============================================================================
static constexpr int NEON_NUM_TAPS = 39;
static constexpr std::array<int16_t, 48> neon_down_coefs alignas(16) = {
-1, 0, 2, 0, -10, 0, 35, 0,
-103, 0, 266, 0, -616, 0, 1332, 0,
-2960, 0, 10246, 16384, 10246, 0, -2960, 0,
1332, 0, -616, 0, 266, 0, -103, 0,
35, 0, -10, 0, 2, 0, -1,
};
static constexpr std::array<int16_t, 48> make_neon_up_coefs()
{
std::array<int16_t, 48> ret = {};
for (int i = 0; i < NEON_NUM_TAPS; i++)
{
ret[i] = static_cast<int16_t>(
std::clamp<int32_t>(neon_down_coefs[i] * 2, INT16_MIN, INT16_MAX));
}
return ret;
}
static constexpr std::array<int16_t, 48> neon_up_coefs alignas(16) = make_neon_up_coefs();
// NEON ReverbDownsample — 39-tap FIR
static int32_t ReverbDownsample_neon(V_Core& core, bool right)
{
const int index = (core.RevbSampleBufPos - NEON_NUM_TAPS) & 63;
int16x8_t acc = vdupq_n_s16(0);
int16x8_t coef, samp;
coef = vld1q_s16(&neon_down_coefs[0]);
samp = vld1q_s16(&core.RevbDownBuf[right][index]);
acc = vqaddq_s16(acc, vqrdmulhq_s16(samp, coef));
coef = vld1q_s16(&neon_down_coefs[8]);
samp = vld1q_s16(&core.RevbDownBuf[right][index + 8]);
acc = vqaddq_s16(acc, vqrdmulhq_s16(samp, coef));
coef = vld1q_s16(&neon_down_coefs[16]);
samp = vld1q_s16(&core.RevbDownBuf[right][index + 16]);
acc = vqaddq_s16(acc, vqrdmulhq_s16(samp, coef));
coef = vld1q_s16(&neon_down_coefs[24]);
samp = vld1q_s16(&core.RevbDownBuf[right][index + 24]);
acc = vqaddq_s16(acc, vqrdmulhq_s16(samp, coef));
coef = vld1q_s16(&neon_down_coefs[32]);
samp = vld1q_s16(&core.RevbDownBuf[right][index + 32]);
acc = vqaddq_s16(acc, vqrdmulhq_s16(samp, coef));
int32x4_t sum32 = vpaddlq_s16(acc);
int32x2_t pair = vadd_s32(vget_low_s32(sum32), vget_high_s32(sum32));
int32_t sum = vget_lane_s32(pair, 0) + vget_lane_s32(pair, 1);
return clamp_mix(sum);
}
// NEON ReverbUpsample — 39-tap FIR, L/R channels
static StereoOut32 ReverbUpsample_neon(V_Core& core)
{
const int index = (core.RevbSampleBufPos - NEON_NUM_TAPS) & 63;
int16x8_t l_acc = vdupq_n_s16(0);
int16x8_t r_acc = vdupq_n_s16(0);
struct { int offset; } groups[] = {{0}, {8}, {16}, {24}, {32}};
for (auto& g : groups)
{
int16x8_t coef = vld1q_s16(&neon_up_coefs[g.offset]);
int16x8_t l_s = vld1q_s16(&core.RevbUpBuf[0][index + g.offset]);
int16x8_t r_s = vld1q_s16(&core.RevbUpBuf[1][index + g.offset]);
l_acc = vqaddq_s16(l_acc, vqrdmulhq_s16(l_s, coef));
r_acc = vqaddq_s16(r_acc, vqrdmulhq_s16(r_s, coef));
}
int32x4_t l_s32 = vpaddlq_s16(l_acc);
int32x2_t l_p = vadd_s32(vget_low_s32(l_s32), vget_high_s32(l_s32));
int32_t l = vget_lane_s32(l_p, 0) + vget_lane_s32(l_p, 1);
int32x4_t r_s32 = vpaddlq_s16(r_acc);
int32x2_t r_p = vadd_s32(vget_low_s32(r_s32), vget_high_s32(r_s32));
int32_t r = vget_lane_s32(r_p, 0) + vget_lane_s32(r_p, 1);
return {clamp_mix(l), clamp_mix(r)};
}
// ============================================================================
// Registration — override global function pointers
// ============================================================================
//
// NOTE (ARMSX2): the original contributor build also pinned the calling thread
// to a fixed "prime" core (sched_setaffinity to CPU 0-3) and forced SCHED_FIFO.
// That is dropped here: this runs on the VM/EE thread (not the Oboe audio
// thread), the CPU-topology assumption is device-specific, and ARMSX2 already
// manages audio-thread affinity. Only the FIR pointer override is kept.
//
// SVE2 is intentionally left out of this build: it is gated by
// SPU2_HAS_SVE2_COMPILER (off on the default arm64-v8a target), and the
// contributor's SVE2 upsample coefficients overflow s16. Only the NEON FIR
// path is wired in.
namespace SPU2
{
void RegisterNEONBackend()
{
bool using_sve2 = false;
#if SPU2_HAS_SVE2_COMPILER
using_sve2 = spu2_neon::TryRegisterSVE2FIR();
#endif
if (!using_sve2)
{
ReverbDownsample = ReverbDownsample_neon;
ReverbUpsample = ReverbUpsample_neon;
}
}
} // namespace SPU2
#endif // __aarch64__ || _M_ARM64
+23
View File
@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
//
// SPU2/spu2_neon.h — ARM64 SPU2 backend registration API.
//
// Declares RegisterNEONBackend(), which overrides the reverb FIR function
// pointers (ReverbDownsample / ReverbUpsample, see SPU2/defs.h) with NEON
// implementations at runtime. Only meaningful on ARM64; on other targets
// spu2_neon.cpp produces no object code and this entry point is never called.
//
// Call once from SPU2::InternalReset(), AFTER the Multi-ISA defaults are
// assigned, and only when the user has opted in via the "NeonReverbSIMD"
// setting (gated by the caller).
#pragma once
namespace SPU2
{
// Point ReverbDownsample / ReverbUpsample at the NEON FIR implementations.
// Safe to call repeatedly; falls back to the scalar reference if the NEON
// path is unavailable.
void RegisterNEONBackend();
} // namespace SPU2
+138
View File
@@ -0,0 +1,138 @@
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
//
// SPU2/spu2_neon_dcfilter.h — NEON-optimized DC blocking filter.
//
// The DC filter is a simple first-order IIR high-pass filter:
// output[n] = input[n] - input[n-1] + 0.995 * output[n-1]
//
// This is applied per-sample to both L and R channels.
// NEON processes both channels simultaneously.
//
// On MT6899 (Cortex-X925), this replaces 2 scalar subtracts + 2 FMUL + 2 FADD
// with 1 NEON vector operation.
#pragma once
#include <cstdint>
#if defined(__aarch64__) || defined(_M_ARM64)
#include <arm_neon.h>
#define SPU2_DCFILTER_HAS_NEON 1
#else
#define SPU2_DCFILTER_HAS_NEON 0
#endif
namespace spu2_neon {
// ============================================================================
// DC Filter State — Holds previous input/output for both channels
// ============================================================================
// Aligned to 16 bytes for optimal NEON loads/stores.
struct DCFilterState {
#if SPU2_DCFILTER_HAS_NEON
alignas(16) float prev_in[2] = {0.0f, 0.0f};
alignas(16) float prev_out[2] = {0.0f, 0.0f};
#else
float prev_in[2] = {0.0f, 0.0f};
float prev_out[2] = {0.0f, 0.0f};
#endif
void reset() {
prev_in[0] = prev_in[1] = 0.0f;
prev_out[0] = prev_out[1] = 0.0f;
}
};
// ============================================================================
// DC Filter — Process one stereo sample
// ============================================================================
// Replaces the scalar code in spu2.cpp's DCFilter():
// output[0] = input[0] - DCFilterIn[0] + 0.995f * DCFilterOut[0];
// output[1] = input[1] - DCFilterIn[1] + 0.995f * DCFilterOut[1];
//
// NEON processes both channels in a single fused multiply-accumulate.
static __forceinline void DCFilterStereo(float input[2], DCFilterState& state)
{
#if SPU2_DCFILTER_HAS_NEON
float32x2_t inp = vld1_f32(input);
float32x2_t prev_in = vld1_f32(state.prev_in);
float32x2_t prev_out= vld1_f32(state.prev_out);
float32x2_t coeff = vdup_n_f32(0.995f);
// output = (input - prev_in) + 0.995 * prev_out
float32x2_t diff = vsub_f32(inp, prev_in);
float32x2_t out = vmla_f32(diff, prev_out, coeff); // diff + coeff*prev_out
// Store results
vst1_f32(input, out);
vst1_f32(state.prev_in, inp);
vst1_f32(state.prev_out, out);
#else
float out0 = input[0] - state.prev_in[0] + 0.995f * state.prev_out[0];
float out1 = input[1] - state.prev_in[1] + 0.995f * state.prev_out[1];
state.prev_in[0] = input[0];
state.prev_in[1] = input[1];
state.prev_out[0] = out0;
state.prev_out[1] = out1;
input[0] = out0;
input[1] = out1;
#endif
}
// ============================================================================
// Batch DC Filter — Process multiple stereo samples at once
// ============================================================================
// For the output chunk pipeline, processes N stereo frames.
// Each frame still has the IIR dependency, but L/R are parallel.
static __forceinline void DCFilterBatch(float* interleaved_stereo, uint32_t frame_count,
DCFilterState& state)
{
for (uint32_t i = 0; i < frame_count; i++) {
DCFilterStereo(&interleaved_stereo[i * 2], state);
}
}
// ============================================================================
// Batch s16-to-float conversion with DC filter — single pass
// ============================================================================
// Combines clamping, s16→float conversion, and DC filtering in one pass.
// Avoids writing intermediate values to memory.
static __forceinline void ConvertClampDCFilter(
int32_t raw_L, int32_t raw_R, // Raw mixer output (s32)
float* out_L, float* out_R, // Output float samples
DCFilterState& state)
{
// Clamp to s16 range
#if SPU2_DCFILTER_HAS_NEON
int32x2_t raw = {raw_L, raw_R};
int32x2_t lo = vdup_n_s32(-0x8000);
int32x2_t hi = vdup_n_s32(0x7FFF);
raw = vmax_s32(raw, lo);
raw = vmin_s32(raw, hi);
// s32 → f32
float32x2_t fval = vcvt_f32_s32(raw);
fval = vmul_n_f32(fval, 1.0f / 32767.0f);
// DC filter
float conv[2];
vst1_f32(conv, fval);
DCFilterStereo(conv, state);
*out_L = conv[0];
*out_R = conv[1];
#else
float clamped_L = static_cast<float>(std::clamp(raw_L, -0x8000, 0x7FFF)) / 32767.0f;
float clamped_R = static_cast<float>(std::clamp(raw_R, -0x8000, 0x7FFF)) / 32767.0f;
float conv[2] = {clamped_L, clamped_R};
DCFilterStereo(conv, state);
*out_L = conv[0];
*out_R = conv[1];
#endif
}
} // namespace spu2_neon

Some files were not shown because too many files have changed in this diff Show More