Android: bring the platform up on the unified monorepo core

Wire platforms/android to build against the shared core and reconcile the
pieces the Android native lib needs, so the Android target compiles and
links on this repo (produces libemucore_4k.so). Per-platform builds stay
independent; the PC and iOS targets are unchanged.

Build/CMake:
- Android-aware cmake modules under platforms/android/.../cmake with
  CMAKE_MODULE_PATH repointed at them (root desktop modules untouched)
- ryml::ryml alias, TOP_CMAKE_WAS_SOURCED, drop the fatal 3rdparty
  add_subdirectory; pcsx2 PCAP guard elseif(NOT ANDROID)
- git-sync-deps step for shaderc third_party (fetch-on-demand, not
  submodules); android .gitignore for keystores/artifacts/shaderc deps
- dual-core rotation-signed build scripts (release APK + Play AAB)

Core reconciliation (APIs the native lib expects):
- GS present-cap/frameskip, Adreno framebuffer-fetch GSOption round-trip
- SPU2 channel swap + output-pause suppression
- Achievements JSON, VMManager autosave slot, WindowInfo::Android
- FileSystem SAF hooks, custom Vulkan driver path, BIOS-from-fd
- Oboe audio stream, EGL native-window, Linux/Android source guards

refresh-experimental fixes layered onto the shared recompiler/GS:
- IOP direct block chaining (aR3000A, gated s_iopBlockLinkEnabled,
  default off so behaviour is byte-identical until enabled)
- COP2 CO-bit macro-mode gate (aVU_Macro)
- EE mixed-operand constant folding
- Adreno GL coherent depth feedback via gl_LastFragDepthARM, gated to the
  Adreno GPU profile (inert on every other GPU)
- Android GPU gates: GPU-profile shader-header keystone, FIFO_RELAXED
  present mode, Mali coherent readback, Adreno-650 boot-crash gates,
  ROAA VK_ARM alias
This commit is contained in:
jpolo1224
2026-07-08 21:46:23 -04:00
parent ece195771b
commit 11e9289454
54 changed files with 3813 additions and 53 deletions
+5
View File
@@ -73,6 +73,11 @@ jobs:
- name: Install NDK
run: yes | sdkmanager "ndk;27.2.12479018" "cmake;3.22.1" >/dev/null || true
# shaderc's SPIRV-Tools/glslang/etc. are fetched on demand (not vendored,
# not submodules), so `submodules: recursive` above does not provide them.
- name: Fetch shaderc third-party deps
run: python3 app/src/main/cpp/3rdparty/shaderc/utils/git-sync-deps
- name: Build release APK
run: ./gradlew :app:assembleRelease --stacktrace
+11
View File
@@ -123,6 +123,17 @@ namespace FileSystem
int OpenFDFile(const char* filename, int flags, int mode, Error* error = nullptr);
#if defined(__ANDROID__)
/// Open a content:// URI (passed through from the Java SAF layer) and return
/// its raw fd. Defined in the Android JNI layer (native-lib.cpp).
int OpenFDFileContent(const char* filename);
/// Create a directory tree via the Java File API (NativeApp.createDirectoryPath).
/// Fallback for when libc mkdir() is denied on FUSE-emulated external storage
/// (user-picked custom data folders) despite all-files access. Returns true if
/// the directory exists afterwards.
bool CreateDirectoryViaJava(const char* path);
#endif
/// Sharing modes for OpenSharedCFile().
enum class FileShareMode
{
+20
View File
@@ -22,6 +22,16 @@
#include "fmt/format.h"
#if defined(__ANDROID__)
#include <sys/syscall.h>
// bionic lacks shm_open until API 30; memfd_create is a libc wrapper only from
// API 30 too, but the raw syscall works from API 26 (our minSdk).
static int memfd_create_wrapper(const char* name, unsigned int flags)
{
return static_cast<int>(syscall(__NR_memfd_create, name, flags));
}
#endif
#if defined(__FreeBSD__)
#include "cpuinfo.h"
#endif
@@ -64,6 +74,15 @@ std::string HostSys::GetFileMappingName(const char* prefix)
void* HostSys::CreateSharedMemory(const char* name, size_t size)
{
#if defined(__ANDROID__)
// Android: memfd_create available since API 26 (our minSdk), no shm_open until API 30.
const int fd = memfd_create_wrapper(name, 0);
if (fd < 0)
{
std::fprintf(stderr, "memfd_create failed: %d\n", errno);
return nullptr;
}
#else
const int fd = shm_open(name, O_CREAT | O_EXCL | O_RDWR, 0600);
if (fd < 0)
{
@@ -73,6 +92,7 @@ void* HostSys::CreateSharedMemory(const char* name, size_t size)
// we're not going to be opening this mapping in other processes, so remove the file
shm_unlink(name);
#endif
// ensure it's the correct size
if (ftruncate(fd, static_cast<off_t>(size)) < 0)
+32
View File
@@ -13,14 +13,18 @@
#include "fmt/format.h"
#if !defined(__ANDROID__)
#include <dbus/dbus.h>
#endif
#include <spawn.h>
#include <sys/sysinfo.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <unistd.h>
#if !defined(__ANDROID__)
#include <X11/Xlib.h>
#include <X11/extensions/XInput2.h>
#endif
#include <cstdlib>
#include <cstring>
@@ -140,6 +144,7 @@ std::string GetOSVersionString()
#endif
}
#if !defined(__ANDROID__)
static bool SetScreensaverInhibitDBus(const bool inhibit_requested, const char* program_name, const char* reason)
{
static dbus_uint32_t s_cookie;
@@ -329,6 +334,32 @@ void Common::DetachMousePositionCb()
}
}
#else // __ANDROID__
// Android has no X11 or D-Bus screensaver/session service; these are no-ops.
bool Common::InhibitScreensaver(bool inhibit)
{
return false;
}
void Common::SetMousePosition(int x, int y)
{
}
bool Common::AttachMousePositionCb(std::function<void(int, int)> cb)
{
return false;
}
void Common::DetachMousePositionCb()
{
}
#endif // !__ANDROID__
// Desktop-Linux only: the aplay/gstreamer approach uses posix_spawnp (bionic API 30+)
// and external audio tools. Android provides its own Common::PlaySoundAsync (native-lib).
#ifndef __ANDROID__
bool Common::PlaySoundAsync(const char* path)
{
#ifdef __linux__
@@ -365,6 +396,7 @@ bool Common::PlaySoundAsync(const char* path)
return false;
#endif
}
#endif // !__ANDROID__ (PlaySoundAsync — Android impl lives in native-lib.cpp)
void Threading::Sleep(int ms)
{
+2 -1
View File
@@ -15,7 +15,8 @@ struct WindowInfo
Win32,
X11,
Wayland,
MacOS
MacOS,
Android
};
/// The type of the surface. Surfaceless indicates it will not be displayed on screen at all.
+210
View File
@@ -38,6 +38,7 @@
#include <algorithm>
#include <array>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <functional>
#include <limits>
@@ -401,6 +402,215 @@ bool Achievements::HasAchievements()
return s_has_achievements;
}
std::string Achievements::GetAchievementsAsJSON()
{
// JSON-quote a string into the buffer, escaping the bare minimum so
// org.json on the Java side can parse the payload. We don't expect
// achievement titles/descriptions to contain control chars beyond \n,
// but escape them defensively. Characters > 0x7F are passed through —
// the JSON spec allows raw UTF-8 in string contents.
auto append_json_string = [](std::string& dst, const char* src) {
dst += '"';
if (src)
{
for (const char* p = src; *p; ++p)
{
const unsigned char c = static_cast<unsigned char>(*p);
switch (c)
{
case '"': dst += "\\\""; break;
case '\\': dst += "\\\\"; break;
case '\n': dst += "\\n"; break;
case '\r': dst += "\\r"; break;
case '\t': dst += "\\t"; break;
default:
if (c < 0x20)
{
char esc[8];
std::snprintf(esc, sizeof(esc), "\\u%04x", c);
dst += esc;
}
else
{
dst += static_cast<char>(c);
}
break;
}
}
}
dst += '"';
};
std::string out;
out.reserve(4096);
out += '{';
auto lock = GetLock();
const bool active = HasActiveGame() && s_has_achievements;
// `s_client` is the persistent rc_client created by Achievements::Initialize,
// which only runs when a VM is initialized AND EmuConfig.Achievements.Enabled.
// On Android the user typically logs in BEFORE loading a game, via the
// overlay's right-side panel — that path uses a TEMPORARY client which is
// destroyed when Login returns. So `rc_client_get_user_info(s_client)` reports
// "not logged in" even though the auth token was just written to the secrets
// layer. Fall back to the persisted Username / Token to detect post-login
// state. Token lives in the LAYER_SECRETS in-memory store; Username in BASE.
const rc_client_user_t* user = s_client ? rc_client_get_user_info(s_client) : nullptr;
std::string display_name;
bool logged_in = false;
if (user && user->display_name)
{
display_name = user->display_name;
logged_in = true;
}
else
{
const std::string saved_user = Host::GetBaseStringSettingValue("Achievements", "Username", "");
const std::string saved_token = Host::GetStringSettingValue("Achievements", "Token");
if (!saved_user.empty() && !saved_token.empty())
{
display_name = saved_user;
logged_in = true;
}
}
out += "\"active\":";
out += active ? "true" : "false";
out += ",\"loggedIn\":";
out += logged_in ? "true" : "false";
out += ",\"hardcore\":";
out += s_hardcore_mode ? "true" : "false";
out += ",\"userName\":";
append_json_string(out, display_name.c_str());
// Player score. Only available from the persistent client (a game with
// achievements is loaded); the post-login temporary client is destroyed,
// so report -1 ("unknown") when we can't read it. The panel hides the
// points chip on -1 rather than showing a misleading 0.
out += ",\"score\":";
out += std::to_string(user ? static_cast<long long>(user->score) : -1LL);
out += ",\"softcoreScore\":";
out += std::to_string(user ? static_cast<long long>(user->score_softcore) : -1LL);
// RA presentation options (global [Achievements] settings) so the panel
// can show + toggle them without a second JNI poll. Defaults mirror
// Pcsx2Config::AchievementsOptions() (all on).
out += ",\"notifications\":";
out += Host::GetBaseBoolSettingValue("Achievements", "Notifications", true) ? "true" : "false";
out += ",\"leaderboardNotifications\":";
out += Host::GetBaseBoolSettingValue("Achievements", "LeaderboardNotifications", true) ? "true" : "false";
out += ",\"overlays\":";
out += Host::GetBaseBoolSettingValue("Achievements", "Overlays", true) ? "true" : "false";
out += ",\"lbOverlays\":";
out += Host::GetBaseBoolSettingValue("Achievements", "LBOverlays", true) ? "true" : "false";
out += ",\"soundEffects\":";
out += Host::GetBaseBoolSettingValue("Achievements", "SoundEffects", true) ? "true" : "false";
out += ",\"items\":[";
if (active && s_client)
{
// Build a fresh list — don't reuse s_achievement_list, that one's
// owned by the ImGui pause-menu Achievements window and we don't
// want to step on its lifecycle.
rc_client_achievement_list_t* list = rc_client_create_achievement_list(
s_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL,
RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS);
if (list)
{
// Same display order the ImGui window uses.
static constexpr u32 bucket_order[] = {
RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE,
RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED,
RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED,
RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE,
RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED,
RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL,
RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED,
};
bool first = true;
for (u32 bucket_type : bucket_order)
{
for (u32 b = 0; b < list->num_buckets; b++)
{
const rc_client_achievement_bucket_t& bucket = list->buckets[b];
if (bucket.bucket_type != bucket_type)
continue;
for (u32 a = 0; a < bucket.num_achievements; a++)
{
const rc_client_achievement_t* ach = bucket.achievements[a];
if (!ach)
continue;
if (!first)
out += ',';
first = false;
out += "{\"id\":";
out += std::to_string(ach->id);
out += ",\"title\":";
append_json_string(out, ach->title);
out += ",\"description\":";
append_json_string(out, ach->description);
out += ",\"points\":";
out += std::to_string(ach->points);
out += ",\"unlocked\":";
out += (ach->state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) ? "true" : "false";
out += ",\"bucket\":";
out += std::to_string(static_cast<int>(bucket.bucket_type));
out += ",\"rarity\":";
{
char buf[32];
std::snprintf(buf, sizeof(buf), "%.1f", ach->rarity);
out += buf;
}
out += ",\"measuredProgress\":";
append_json_string(out, ach->measured_progress);
out += ",\"measuredPercent\":";
{
char buf[32];
std::snprintf(buf, sizeof(buf), "%.1f", ach->measured_percent);
out += buf;
}
// Type lets the UI tag MISSABLE / PROGRESSION / WIN
// achievements (STANDARD = 0 → no tag). Unlocked is
// the SOFTCORE/HARDCORE bitmask — distinguishes how
// the user earned it for the HC indicator. unlockTime
// is unix-seconds (0 if locked) so the panel can
// render a relative timestamp.
out += ",\"type\":";
out += std::to_string(static_cast<int>(ach->type));
out += ",\"unlockedMask\":";
out += std::to_string(static_cast<int>(ach->unlocked));
out += ",\"unlockTime\":";
out += std::to_string(static_cast<long long>(ach->unlock_time));
// Badge image URL for the achievement's current state
// (unlocked variant vs `_lock` greyscale). Java side
// fetches via Coil into the persistent cover_cache —
// RA badge URLs are immutable per badge_name so they
// cache forever. Empty string on failure → panel
// falls back to the glyph placeholder.
{
char url_buf[256];
const int rc = rc_client_achievement_get_image_url(
ach, ach->state, url_buf, std::size(url_buf));
out += ",\"iconUrl\":";
append_json_string(out, rc == RC_OK ? url_buf : "");
}
out += '}';
}
}
}
rc_client_destroy_achievement_list(list);
}
}
out += "]}";
return out;
}
bool Achievements::HasLeaderboards()
{
return s_has_leaderboards;
+18
View File
@@ -105,6 +105,24 @@ namespace Achievements
/// Returns true if the current game has any achievements.
bool HasAchievements();
/// Snapshot the current game's achievements as JSON for the in-game
/// overlay's right-side panel. Walks rc_client buckets in display
/// order (active challenge → recently unlocked → unlocked → almost
/// there → locked → unofficial → unsupported). Empty array when no
/// active game / not logged in. Format:
/// {
/// "active": bool, // game has any achievements
/// "loggedIn": bool, // a user is logged in to RA
/// "userName": "string", // display name when loggedIn
/// "items": [
/// { "id": int, "title": "...", "description": "...",
/// "points": int, "unlocked": bool, "bucket": int,
/// "rarity": float, "measuredProgress": "..." }
/// ]
/// }
/// Self-contained — no rcheevos headers needed by the caller.
std::string GetAchievementsAsJSON();
/// Returns true if the current game has any leaderboards.
bool HasLeaderboards();
+15 -1
View File
@@ -650,6 +650,10 @@ if(USE_VULKAN)
)
target_link_libraries(PCSX2_FLAGS INTERFACE vulkan-headers)
target_include_directories(PCSX2_FLAGS INTERFACE ${SHADERC_INCLUDE_DIR})
if(ANDROID AND CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a")
# VKLoader.cpp's adrenotools_open_libvulkan splice needs adrenotools/driver.h.
target_link_libraries(PCSX2_FLAGS INTERFACE adrenotools)
endif()
endif()
set(pcsx2GSMetalShaders
@@ -1127,6 +1131,13 @@ if(ANDROID)
AndroidEEOpHist.h
AndroidPerfBuckets.h
PS1DrvTrace.h)
# Oboe is the Android audio backend consumed by Host/OboeAudioStream.cpp.
# The `oboe` target (created in SearchForStuff.cmake) exports its `include/`
# dir as PUBLIC, so linking it into PCSX2_FLAGS propagates <oboe/Oboe.h> to
# the PCSX2 translation units. (android/log libs are already linked on the
# emucore target in the Android entry CMakeLists, so only oboe is needed here.)
target_link_libraries(PCSX2_FLAGS INTERFACE
oboe)
endif()
if(ARMSX2_IOS)
@@ -1243,6 +1254,7 @@ target_link_libraries(PCSX2_FLAGS INTERFACE
ZLIB::ZLIB
LZ4::LZ4
SoundTouch::SoundTouch
JPEG::JPEG
PNG::PNG
LZMA::LZMA
Zstd::Zstd
@@ -1276,7 +1288,9 @@ if(WIN32)
dwmapi.lib
OneCore.lib
)
else()
elseif(NOT ANDROID)
# Android has no system libpcap; DEV9 uses Android/AndroidPcapStubs.cpp instead,
# so it must NOT link PCAP::PCAP (a bare else() here caught Android and broke it).
target_link_libraries(PCSX2_FLAGS INTERFACE
PCAP::PCAP
)
+2
View File
@@ -771,6 +771,7 @@ struct Pcsx2Config
UseBlitSwapChain : 1,
DisableShaderCache : 1,
DisableFramebufferFetch : 1,
EnableAdrenoFramebufferFetch : 1,
DisableVertexShaderExpand : 1,
SkipDuplicateFrames : 1,
OsdShowSpeed : 1,
@@ -938,6 +939,7 @@ struct Pcsx2Config
int AudioCaptureBitrate = DEFAULT_AUDIO_CAPTURE_BITRATE;
std::string Adapter;
std::string AndroidGpuProfileOverride = "auto";
std::string HWDumpDirectory;
std::string SWDumpDirectory;
+63
View File
@@ -52,6 +52,7 @@
#include "fmt/format.h"
#include <atomic>
#include <fstream>
Pcsx2Config::GSOptions GSConfig;
@@ -436,6 +437,55 @@ void GSgifTransfer3(u8* mem, u32 size)
g_gs_renderer->Transfer<2>(const_cast<u8*>(mem), size);
}
// Manual frameskip target (Android). Set from the UI thread via the JNI
// setFrameSkip, read on the GS thread in GSRenderer::VSync. Relaxed atomic — a
// stale read at most mis-skips a single frame, which is harmless.
static std::atomic<u32> s_manual_frameskip{0};
void GSSetManualFrameSkip(u32 frames)
{
s_manual_frameskip.store(frames, std::memory_order_relaxed);
}
u32 GSGetManualFrameSkip()
{
return s_manual_frameskip.load(std::memory_order_relaxed);
}
// Max presented-FPS cap (Android). Caps the DISPLAY frame rate without slowing
// emulation — read on the GS thread in GSRenderer::VSync, which drops a present
// only when ahead of the target interval (adaptive, no over-skip). 0 = off.
// s_max_present_fps is the cap value (for the OSD label); s_max_present_interval
// is the vsync-aligned minimum present spacing in CPU ticks, computed in
// native-lib setFpsCap where the native refresh is known, so display rates snap
// to whole vsync multiples (60/30/20/15…) and hold steady at the boundary.
static std::atomic<u32> s_max_present_fps{0};
static std::atomic<u64> s_max_present_interval{0};
// Fast-forward (Turbo) bypasses the present cap so the speed-up is visible. Set
// from the limiter-mode JNI (Turbo → true, anything else → false) and read on
// the GS thread in GSRenderer::VSync. Unlimited (frame-limit-off steady state)
// deliberately does NOT set this — there the present cap is still wanted.
static std::atomic<bool> s_present_cap_suspended{false};
void GSSetMaxPresentFps(u32 fps, u64 present_interval)
{
s_max_present_fps.store(fps, std::memory_order_relaxed);
s_max_present_interval.store(present_interval, std::memory_order_relaxed);
}
u32 GSGetMaxPresentFps()
{
return s_max_present_fps.load(std::memory_order_relaxed);
}
u64 GSGetMaxPresentInterval()
{
return s_max_present_interval.load(std::memory_order_relaxed);
}
void GSSetPresentCapSuspended(bool suspended)
{
s_present_cap_suspended.store(suspended, std::memory_order_relaxed);
}
bool GSGetPresentCapSuspended()
{
return s_present_cap_suspended.load(std::memory_order_relaxed);
}
void GSvsync(u32 field, bool registers_written)
{
// Update this here because we need to check if the pending draw affects the current frame, so our regs need to be updated.
@@ -994,6 +1044,10 @@ void GSFreeWrappedMemory(void* ptr, size_t size, size_t repeat)
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#if defined(__ANDROID__)
#include <sys/syscall.h>
#include <android/sharedmem.h>
#endif
static int s_shm_fd = -1;
@@ -1002,6 +1056,14 @@ void* GSAllocateWrappedMemory(size_t size, size_t repeat)
pxAssert(s_shm_fd == -1);
const char* file_name = "/GS.mem";
#if defined(__ANDROID__)
s_shm_fd = static_cast<int>(syscall(__NR_memfd_create, "GS.mem", 0));
if (s_shm_fd == -1)
{
fprintf(stderr, "Failed to create memfd due to %s\n", strerror(errno));
return nullptr;
}
#else
s_shm_fd = shm_open(file_name, O_RDWR | O_CREAT | O_EXCL, 0600);
if (s_shm_fd != -1)
{
@@ -1012,6 +1074,7 @@ void* GSAllocateWrappedMemory(size_t size, size_t repeat)
fprintf(stderr, "Failed to open %s due to %s\n", file_name, strerror(errno));
return nullptr;
}
#endif
if (ftruncate(s_shm_fd, repeat * size) < 0)
fprintf(stderr, "Failed to reserve memory due to %s\n", strerror(errno));
+14
View File
@@ -71,6 +71,20 @@ void GSgifTransfer1(u8* mem, u32 addr);
void GSgifTransfer2(u8* mem, u32 size);
void GSgifTransfer3(u8* mem, u32 size);
void GSvsync(u32 field, bool registers_written);
// Manual frameskip (Android low-end devices): present 1 of every (frames+1)
// VSyncs, skipping presentation of the rest. 0 disables. See GSRenderer::VSync.
void GSSetManualFrameSkip(u32 frames);
u32 GSGetManualFrameSkip();
// Max presented-FPS cap (Android). Caps the DISPLAY frame rate without touching
// emulation speed — dropped on the GS thread in GSRenderer::VSync. 0 disables.
void GSSetMaxPresentFps(u32 fps, u64 present_interval);
u32 GSGetMaxPresentFps();
u64 GSGetMaxPresentInterval();
// While true (set when the limiter enters Turbo / fast-forward), the present cap
// above is bypassed so the speed-up is actually visible. The cap resumes — with
// a clean re-prime, no catch-up burst — as soon as fast-forward ends.
void GSSetPresentCapSuspended(bool suspended);
bool GSGetPresentCapSuspended();
int GSfreeze(FreezeAction mode, freezeData* data);
std::string GSGetBaseSnapshotFilename();
std::string GSGetBaseVideoFilename();
+7
View File
@@ -7,6 +7,7 @@
#include "common/WindowInfo.h"
#include "GS/GS.h"
#include "GS/Renderers/Common/GSFastList.h"
#include "GS/Renderers/Common/GSGPUProfile.h"
#include "GS/Renderers/Common/GSShaderEnums.h"
#include "GS/Renderers/Common/GSTexture.h"
#include "GS/Renderers/Common/GSVertex.h"
@@ -1444,6 +1445,7 @@ protected:
std::string m_name = "Unknown";
FeatureSupport m_features;
u32 m_max_texture_size = 0;
RuntimeGpuProfile m_runtime_gpu_profile = RuntimeGpuProfile::Adreno;
struct
{
@@ -1562,6 +1564,11 @@ public:
__fi FeatureSupport Features() const { return m_features; }
__fi u32 GetMaxTextureSize() const { return m_max_texture_size; }
__fi void SetRuntimeGPUProfile(RuntimeGpuProfile p) { m_runtime_gpu_profile = p; }
__fi RuntimeGpuProfile GetRuntimeGPUProfile() const { return m_runtime_gpu_profile; }
__fi bool IsMaliGPUProfile() const { return (m_runtime_gpu_profile == RuntimeGpuProfile::Mali); }
__fi bool IsAdrenoGPUProfile() const { return (m_runtime_gpu_profile == RuntimeGpuProfile::Adreno); }
__fi bool IsPowerVRGPUProfile() const { return (m_runtime_gpu_profile == RuntimeGpuProfile::PowerVR); }
__fi const WindowInfo& GetWindowInfo() const { return m_window_info; }
__fi s32 GetWindowWidth() const { return static_cast<s32>(m_window_info.surface_width); }
@@ -125,6 +125,11 @@ bool GLContextEGL::Initialize(std::span<const Version> versions_to_try, Error* e
return false;
}
EGLNativeWindowType GLContextEGL::GetNativeWindow(EGLConfig config)
{
return {};
}
EGLDisplay GLContextEGL::GetPlatformDisplay(Error* error)
{
EGLDisplay dpy = TryGetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA, "EGL_MESA_platform_surfaceless");
+4
View File
@@ -31,6 +31,10 @@ public:
protected:
virtual EGLDisplay GetPlatformDisplay(Error* error);
virtual EGLSurface CreatePlatformSurface(EGLConfig config, void* win, Error* error);
// Overridden by GLContextEGLAndroid to return the ANativeWindow; base returns
// none. (Surface creation on this core sources the window directly; the Android
// subclass keeps this override for parity with the known-good EGL path.)
virtual EGLNativeWindowType GetNativeWindow(EGLConfig config);
EGLDisplay TryGetPlatformDisplay(EGLenum platform, const char* platform_ext);
EGLSurface TryCreatePlatformSurface(EGLConfig config, void* window, Error* error);
+12 -1
View File
@@ -312,8 +312,19 @@ namespace
std::unique_ptr<GLStreamBuffer> GLStreamBuffer::Create(GLenum target, u32 size)
{
// Adreno 650 (Snapdragon 855/865-era driver) tears out a persistent buffer
// mapping on an Android task-switch, so the GS thread's next write SIGSEGVs.
// Route ONLY that GPU to the orphan-per-draw glBufferData path below. Every other
// GPU -- including newer Adreno 7xx/8xx -- keeps the fast persistent path; a broad
// version of this gate over ALL Qualcomm/Adreno was a big GS perf regression, so
// this is deliberately narrowed to the single reported model.
const char* renderer = reinterpret_cast<const char*>(glGetString(GL_RENDERER));
const bool adreno650_no_persistent = renderer &&
std::strstr(renderer, "Adreno") && std::strstr(renderer, "650");
std::unique_ptr<GLStreamBuffer> buf;
if (GLAD_GL_VERSION_4_4 || GLAD_GL_ARB_buffer_storage || GLAD_GL_EXT_buffer_storage)
if (!adreno650_no_persistent &&
(GLAD_GL_VERSION_4_4 || GLAD_GL_ARB_buffer_storage || GLAD_GL_EXT_buffer_storage))
{
buf = BufferStorageStreamBuffer::Create(target, size);
if (buf)
+80 -3
View File
@@ -4,6 +4,7 @@
#include "GS/Renderers/OpenGL/GLContext.h"
#include "GS/Renderers/OpenGL/GSDeviceOGL.h"
#include "GS/Renderers/OpenGL/GLState.h"
#include "GS/Renderers/Common/GSGPUProfile.h"
#include "GS/GSState.h"
#include "GS/GSGL.h"
#include "GS/GSPerfMon.h"
@@ -578,7 +579,15 @@ bool GSDeviceOGL::Create(GSVSyncMode vsync_mode, bool allow_present_throttle)
return false;
// Image load store and GLSL 420pack is core in GL4.2, no need to check.
m_features.cas_sharpening = ((GLAD_GL_VERSION_4_2 && GLAD_GL_ARB_compute_shader) || GLAD_GL_ES_VERSION_3_2) && CreateCASPrograms();
// NOTE: CAS uses a desktop-GL compute shader (cas.glsl is "#version 420" +
// "#extension GL_ARB_compute_shader") -- that source is invalid GLSL ES. On
// devices whose driver only gives a GL ES 3.2 context (e.g. Adreno 650 on the
// Retroid Pocket Mini, where the desktop 4.2 context request fails and we fall
// back to ES), feeding the ES compiler that shader hard-crashes the driver
// during GS init. So gate CAS to a real desktop-GL 4.2 context only; ES devices
// just go without the sharpening filter (TV/CRT present shaders are unaffected --
// they use the ES-aware "#version 320 es" header and compile fine).
m_features.cas_sharpening = (GLAD_GL_VERSION_4_2 && GLAD_GL_ARB_compute_shader) && CreateCASPrograms();
// ****************************************************************
// rasterization configuration
@@ -742,6 +751,31 @@ bool GSDeviceOGL::CheckFeatures()
Console.WriteLn(Color_StrongBlue, "GL: Intel GPU detected.");
//vendor_id_intel = true;
}
// ARMSX2: resolve the runtime GPU profile (Mali/Adreno/PowerVR) so the tfx GL
// shaders can select their tile-based-GPU arms via GPU_PROFILE_* (emitted in
// GenGlslHeader below). The monorepo GS device base is a desktop PCSX2 base and
// has no Mali/Adreno vendor branch above, so we detect from the raw vendor/renderer
// strings here. Guard null pointers (glGetString may return null).
{
const char* renderer = (const char*)glGetString(GL_RENDERER);
const char* vendor_safe = vendor ? vendor : "";
const char* renderer_safe = renderer ? renderer : "";
#if defined(__ANDROID__)
const GpuProfileSelection profile_selection =
GpuProfileDetector::Resolve(GSConfig.AndroidGpuProfileOverride, vendor_safe, renderer_safe);
SetRuntimeGPUProfile(profile_selection.runtime_profile);
Console.WriteLn("GL: GPU profile override='%s' resolved='%s'.",
GpuProfileDetector::OverrideToConfigString(profile_selection.override_mode),
GpuProfileDetector::RuntimeProfileToString(profile_selection.runtime_profile));
DevCon.WriteLn("GL: GPU profile hints: %s", profile_selection.hints.c_str());
#else
// Desktop/non-Android GL: no tile-based-GPU profile arms are used, keep the
// device's profile initialized to a benign default.
SetRuntimeGPUProfile(RuntimeGpuProfile::Adreno);
(void)vendor_safe;
(void)renderer_safe;
#endif
}
GLint major_gl = 0;
GLint minor_gl = 0;
@@ -880,6 +914,28 @@ bool GSDeviceOGL::CheckFeatures()
m_features.depth_feedback |= GSConfig.DepthFeedbackMode == GSDepthFeedbackMode::Auto;
}
// ARMSX2 (Adreno GLES only; inert on every other GPU/profile). Adreno's driver
// rejects a fragment shader declaring TWO framebuffer-fetch `inout` outputs (o_col0
// colour + o_col1 depth), which the depth-as-colour SW-Z path emits for accurate-
// alpha-test draws -> link failure -> garbage (Everybody's Golf 4 / Minna no Golf 4).
// Route depth feedback through the depth path (a single fetch output) so it links, and
// read prior depth via the coherent ARM depth-stencil fetch (gl_LastFragDepthARM) when
// available -- the mode-1 depth sampler read is incoherent on GLES (no barrier on a
// sampled depth attachment) and makes occluded triangles poke through as white shards.
// Only overrides Auto; an explicit DepthFeedbackMode choice is honoured. The GPU
// profile is already resolved above (SetRuntimeGPUProfile), so IsAdrenoGPUProfile()
// is valid here.
if (m_features.framebuffer_fetch && IsAdrenoGPUProfile() &&
GSConfig.DepthFeedbackMode == GSDepthFeedbackMode::Auto)
{
m_features.depth_feedback = true;
m_arm_depth_fetch = GLAD_GL_ARM_shader_framebuffer_fetch_depth_stencil;
Console.WriteLn(m_arm_depth_fetch
? "GL: Adreno - depth feedback via coherent ARM depth-stencil fetch (gl_LastFragDepthARM)."
: "GL: Adreno - routing depth feedback through the depth sampler "
"(avoids the dual framebuffer-fetch output link failure).");
}
if (GLAD_GL_ARB_shader_storage_buffer_object)
{
GLint max_vertex_ssbos = 0;
@@ -925,7 +981,13 @@ void GSDeviceOGL::SetSwapInterval()
m_vsync_mode = (m_vsync_mode == GSVSyncMode::Mailbox) ? GSVSyncMode::FIFO : m_vsync_mode;
// Window framebuffer has to be bound to call SetSwapInterval.
const s32 interval = static_cast<s32>(m_vsync_mode == GSVSyncMode::FIFO);
s32 interval = static_cast<s32>(m_vsync_mode == GSVSyncMode::FIFO);
// ARM Mali GLES breaks with eglSwapInterval(0): after a handful of swaps the surface
// stops presenting entirely (frozen screen). Never pass 0 on Mali — force interval 1.
// A forced-on vsync beats a frozen display, and the FIFO present pacer still lets
// fast-forward exceed the panel rate via dropped presents. (cf. Dolphin BUG_BROKEN_VSYNC)
if (interval == 0 && IsMaliGPUProfile())
interval = 1;
GLint current_fbo = 0;
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &current_fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
@@ -1570,10 +1632,22 @@ std::string GSDeviceOGL::GenGlslHeader(const std::string_view entry, GLenum type
if (m_features.framebuffer_fetch && GLAD_GL_EXT_shader_framebuffer_fetch)
header += "#extension GL_EXT_shader_framebuffer_fetch : require\n";
// ARMSX2 (Adreno): coherent prior-depth read for SW-Z feedback (gl_LastFragDepthARM).
if (m_arm_depth_fetch)
header += "#extension GL_ARM_shader_framebuffer_fetch_depth_stencil : require\n";
if (m_features.framebuffer_fetch)
header += "#define HAS_FRAMEBUFFER_FETCH 1\n";
else
header += "#define HAS_FRAMEBUFFER_FETCH 0\n";
// ARMSX2: emit the runtime GPU-profile selectors so the tfx GL shaders can pick
// their tile-based-GPU (Mali/Adreno/PowerVR) arms. GenGlslHeader() is prepended to
// every GL shader (GetShaderSource / GetTfxVertexShader / GetTfxFragmentShader),
// so this is the header string the tfx_fs.glsl `#if GPU_PROFILE_MALI` guards read.
header += fmt::format("#define GPU_PROFILE_MALI {}\n", IsMaliGPUProfile() ? 1 : 0);
header += fmt::format("#define GPU_PROFILE_ADRENO {}\n", IsAdrenoGPUProfile() ? 1 : 0);
header += fmt::format("#define GPU_PROFILE_POWERVR {}\n", IsPowerVRGPUProfile() ? 1 : 0);
header += fmt::format("#define HAS_ARM_DEPTH_FETCH {}\n", m_arm_depth_fetch ? 1 : 0);
if (GLAD_GL_ARB_conservative_depth)
{
@@ -2975,7 +3049,10 @@ void GSDeviceOGL::RenderHW(GSHWDrawConfig& config)
if (m_features.texture_barrier && (config.require_one_barrier || config.require_full_barrier))
PSSetShaderResource(TEXTURE_RT, colclip_rt ? colclip_rt : config.rt);
if (m_features.texture_barrier && (config.require_one_barrier || config.require_full_barrier) && config.ps.IsFeedbackLoopDepth())
PSSetShaderResource(TEXTURE_DEPTH, m_features.depth_feedback ? config.ds : m_ds_as_rt);
// ARMSX2 (Adreno): with ARM depth-stencil fetch the shader reads gl_LastFragDepthARM,
// not a sampler, so don't bind the live depth attachment as a texture (avoids a
// feedback-loop bind the driver may flag).
PSSetShaderResource(TEXTURE_DEPTH, (m_features.depth_feedback && !m_arm_depth_fetch) ? config.ds : m_ds_as_rt);
SetupSampler(config.sampler);
+4
View File
@@ -158,6 +158,10 @@ private:
} m_bugs;
bool m_disable_download_pbo = false;
// ARMSX2 (Adreno GLES): read prior depth for SW-Z feedback via the coherent ARM
// depth-stencil fetch (gl_LastFragDepthARM) instead of the incoherent depth sampler,
// when GL_ARM_shader_framebuffer_fetch_depth_stencil is present. False everywhere else.
bool m_arm_depth_fetch = false;
GLuint m_fbo = 0; // frame buffer container
GLuint m_fbo_read = 0; // frame buffer container only for reading
+54 -2
View File
@@ -411,8 +411,15 @@ bool GSDeviceVK::SelectDeviceExtensions(ExtensionList* extension_list, bool enab
m_optional_extensions.vk_ext_memory_budget = SupportsExtension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME, false);
m_optional_extensions.vk_ext_calibrated_timestamps =
SupportsExtension(VK_EXT_CALIBRATED_TIMESTAMPS_EXTENSION_NAME, false);
// ROAA is DUAL-NAMED: ARM shipped VK_ARM_rasterization_order_attachment_access (Mali
// driver r36p0), and the promoted VK_EXT_ alias only landed at r40p0. The structs/enums
// are identical (alias), so accept EITHER - otherwise Mali on r36-r39 blobs (a big chunk
// of mid-tier, incl. Tensor G2/G3 on old blobs) exposes only the ARM name and gets
// silently demoted to the per-primitive-barrier slideshow. SupportsExtension enables
// whichever name it finds (EXT preferred via short-circuit). Matches upstream 5da4b7e.
m_optional_extensions.vk_ext_rasterization_order_attachment_access =
SupportsExtension(VK_EXT_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_EXTENSION_NAME, false);
SupportsExtension(VK_EXT_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_EXTENSION_NAME, false) ||
SupportsExtension(VK_ARM_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_EXTENSION_NAME, false);
m_optional_extensions.vk_ext_attachment_feedback_loop_layout =
SupportsExtension(VK_EXT_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_EXTENSION_NAME, false);
m_optional_extensions.vk_ext_line_rasterization = SupportsExtension(VK_EXT_LINE_RASTERIZATION_EXTENSION_NAME, false);
@@ -2757,7 +2764,40 @@ bool GSDeviceVK::CheckFeatures()
//const bool isAMD = (vendorID == 0x1002 || vendorID == 0x1022);
//const bool isNVIDIA = (vendorID == 0x10DE);
m_features.framebuffer_fetch =
// framebuffer_fetch: the tiler-native ordered Cd read (ROAA / subpassLoad in tile memory).
// It lets DetermineBarriers() drop every per-primitive barrier and makes ROV auto-disable --
// the fast, correct path for blend-heavy games on a TBDR.
//
// MALI (0x13B5): ENABLED by default when ROAA is present -- without it the per-PRIMITIVE
// texture-barrier path tanks blend-heavy games (GT4 = 10-20fps slideshow). No-op on any Mali
// lacking the extension.
//
// ADRENO / other non-Mali: OPT-IN only (GSConfig.EnableAdrenoFramebufferFetch, default off).
// The proprietary Adreno driver returned STALE ROAA reads above Basic blending (alpha cutouts /
// invisible floors, A/B 2026-06-10); gated behind a toggle to ship dark and be A/B-verified per
// device+driver. Gated on ROAA presence, so a no-op on any device lacking the extension.
// Keep the runtime GPU profile consistent on Vulkan too. The OpenGL device sets this from the
// GpuProfileDetector so the shared HW blend path (GSRendererHW.cpp alpha_mali_custom_set) can route
// Mali's broken HW dual-source blends through the in-shader SW-blend path; on Vulkan we reach Cd via
// texture-barriers instead of GL_ARM_shader_framebuffer_fetch. The GL-shader GPU_PROFILE_* defines are
// unused by the VK path, so this only affects that one cross-platform blend workaround.
if (IsDeviceMali())
SetRuntimeGPUProfile(RuntimeGpuProfile::Mali);
else if (IsDevicePowerVR())
SetRuntimeGPUProfile(RuntimeGpuProfile::PowerVR);
else if (IsDeviceAdreno())
SetRuntimeGPUProfile(RuntimeGpuProfile::Adreno);
const bool is_mali_vk = IsDeviceMali();
// Turnip/Mesa is the open Adreno driver and does NOT exhibit the proprietary blob's stale-ROAA
// reads (the reason Adreno fbfetch shipped opt-in), so default it ON there.
const bool is_turnip = (m_device_driver_properties.driverID == VK_DRIVER_ID_MESA_TURNIP);
// Samsung Xclipse (Exynos AMD-RDNA2) has no working ROAA-based framebuffer fetch -- force it off
// there. Inert if the 0x144D vendorID guess is wrong.
const bool is_xclipse_vk = IsDeviceXclipse();
const bool vendor_allows_fbfetch =
(is_mali_vk || is_turnip || GSConfig.EnableAdrenoFramebufferFetch) && !is_xclipse_vk;
m_features.framebuffer_fetch = vendor_allows_fbfetch &&
m_optional_extensions.vk_ext_rasterization_order_attachment_access && !GSConfig.DisableFramebufferFetch;
m_features.texture_barrier = GSConfig.OverrideTextureBarriers != 0;
m_features.multidraw_fb_copy = false;
@@ -2789,6 +2829,18 @@ bool GSDeviceVK::CheckFeatures()
// Use D32F depth instead of D32S8 when we have framebuffer fetch.
m_features.stencil_buffer &= !m_features.framebuffer_fetch;
// @@MALI_TELEMETRY@@ One-line device/driver banner so Mali (and Adreno) field reports are
// actionable: which GPU/driver, and - critically - which accurate-blend path was resolved:
// in-tile framebuffer_fetch (cheap) vs the per-primitive barrier fallback (the tile-flush
// slideshow). ROAA=yes but fbfetch=NO means the barrier path is active.
Console.WriteLn("VK: GPU '%s' vendor=0x%04X driver='%s' (%s) | ROAA=%s fbfetch=%s texbarrier=%s",
m_device_properties.deviceName,
m_device_properties.vendorID,
m_device_driver_properties.driverName,
m_device_driver_properties.driverInfo,
m_optional_extensions.vk_ext_rasterization_order_attachment_access ? "yes" : "NO",
m_features.framebuffer_fetch ? "yes(in-tile)" : "NO(barrier-fallback)",
m_features.texture_barrier ? "on" : "off");
// whether we can do point/line expand depends on the range of the device
const float f_upscale = static_cast<float>(GSConfig.UpscaleMultiplier);
+13
View File
@@ -81,6 +81,19 @@ public:
/// Returns true if running on an AMD GPU.
__fi bool IsDeviceAMD() const { return (m_device_properties.vendorID == 0x1002); }
/// Returns true if running on an ARM Mali GPU (vendorID 0x13B5).
__fi bool IsDeviceMali() const { return (m_device_properties.vendorID == 0x13B5u); }
/// Returns true if running on a Qualcomm Adreno GPU (vendorID 0x5143).
__fi bool IsDeviceAdreno() const { return (m_device_properties.vendorID == 0x5143u); }
/// Returns true if running on an Imagination PowerVR GPU (vendorID 0x1010).
__fi bool IsDevicePowerVR() const { return (m_device_properties.vendorID == 0x1010u); }
/// Returns true if running on a Samsung Xclipse (Exynos AMD-RDNA2) GPU.
/// NOTE: 0x144D (Samsung) is unverified across driver revisions -- a real Xclipse tester
/// must confirm this fires; if it reports a different vendorID the gate is simply inert.
__fi bool IsDeviceXclipse() const { return (m_device_properties.vendorID == 0x144Du); }
// Creates a simple render pass.
VkRenderPass GetRenderPass(VkFormat color_format, VkFormat depth_format,
+8 -1
View File
@@ -855,7 +855,14 @@ std::unique_ptr<GSDownloadTextureVK> GSDownloadTextureVK::Create(u32 width, u32
VmaAllocationCreateInfo aci = {};
aci.usage = VMA_MEMORY_USAGE_GPU_TO_CPU;
aci.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT;
aci.preferredFlags = VK_MEMORY_PROPERTY_HOST_CACHED_BIT;
// Cached host memory is normally the fastest to read back from on the CPU. On the ARM Mali
// Vulkan driver, however, cached readbacks are much slower than coherent memory: mapping a
// cached readback buffer spends most of its time inside the kernel cache-invalidation routine
// (__pi___inval_cache_range), pegging a CPU core. Prefer coherent memory on Mali so texture
// readbacks (GT4, Tales, any hardware-download game) skip that invalidation cost. Every other
// vendor keeps the cached preference. (Ports Dolphin BUG_SLOW_CACHED_READBACK_MEMORY.)
aci.preferredFlags = GSDeviceVK::GetInstance()->IsDeviceMali() ? VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
: VK_MEMORY_PROPERTY_HOST_CACHED_BIT;
VmaAllocationInfo ai = {};
VmaAllocation allocation;

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