mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
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:
co-authored by
Claude Opus 4.8
parent
47229e4ff3
commit
2eb9ec659c
+13
-2
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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; }
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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: 64–256KB — 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, ¶m) == 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user