mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
Android: bring unified core to refresh-experimental parity
Vulkan render fixes (GS device backends), Oboe audio backend, GameDB armsx2_overrides.yaml override loader, EE+VU mac-port recompiler graft, PGO=optimize, and the VU-slam fix: an Android-only force-float in VMManager::SetEmuThreadAffinities (the slam was thread pinning locking the VU1 worker off the prime core, not the VU codegen). Also: CPU-name SoC-property fallback, Mali VK attachment-feedback-loop crash gate, MediaTek fbfetch disable, and on-device thread-placement diagnostic helpers. The recompiler grafts and GS deltas are unguarded and land in the shared core (they reach the mac/Linux arm64 builds) - see REFACTOR_STATUS.md items 2 and 3 for the reconciliation this still needs; the VU graft is reducible to just the force-float fix.
This commit is contained in:
@@ -42,6 +42,19 @@ changed yet.
|
||||
*(commit: "move iOS frontend …")*
|
||||
- [x] `.github/workflows/build-all.yml` — one push builds PC macOS-arm64,
|
||||
PC Linux-arm64, Android APK, iOS .app; each uploads its own artifact.
|
||||
- [x] **Android brought compile-green AND up to refresh-experimental parity
|
||||
(2026-07-10).** GoW2 boots and runs full-speed (60fps) on RP6 (Adreno 740 /
|
||||
QCS8550 / Turnip). Landed this pass — but read the caveats in Remaining #2/#3,
|
||||
some of it touches shared code:
|
||||
Vulkan render fixes (GS device backends), Oboe audio backend, GameDB
|
||||
`armsx2_overrides.yaml` override loader, OGL/GLES fixes, EE+VU mac-port
|
||||
recompiler graft, PGO=optimize, and the **VU-slam fix**. The VU slam was
|
||||
root-caused to Android thread PINNING hard-locking the VU1 worker off the
|
||||
prime core (diagnostic showed `VU=core3` while the X3 sat idle); fixed with an
|
||||
Android-only force-float in `VMManager::SetEmuThreadAffinities` — VU 20→14ms,
|
||||
82%→100% speed. Also (all `#if __ANDROID__`-guarded / runtime-gated, mac-safe):
|
||||
"CPU: Unknown" SoC-name fallback, Mali VK attachment-feedback-loop crash gate,
|
||||
MediaTek fbfetch disable.
|
||||
|
||||
## Remaining (the long pole — needs real toolchains / CI iteration)
|
||||
|
||||
@@ -50,6 +63,10 @@ changed yet.
|
||||
`build-all.yml` Android + iOS jobs are `continue-on-error: true` on purpose —
|
||||
they are the mechanism to surface and fix the remaining issues. Flip to
|
||||
blocking once green.
|
||||
> ✅ 2026-07-10: **Android is compile-green + runtime-verified** (dual-core 4k/16k
|
||||
> APK, PGO, running games on device). PC-macOS-arm64 / PC-Linux-arm64 / iOS still
|
||||
> need a CI green pass — and #2/#3 below may have perturbed the shared core, so
|
||||
> re-check the mac/Linux arm64 build after this push.
|
||||
2. **Shared-file reconciliation (~250 files).** The Android core fork also
|
||||
*modified* shared files (FullscreenUI, Achievements, GS device backends,
|
||||
VMManager, MemoryCardFile). Those deltas are **not** yet merged — only the
|
||||
@@ -57,9 +74,31 @@ changed yet.
|
||||
fold in the genuinely mobile-specific changes behind platform guards; discard
|
||||
stale-upstream noise. Source of truth for the deltas:
|
||||
`git diff macOS refresh-experimental -- pcsx2 common`.
|
||||
> ⚠️ 2026-07-10: to get Android green fast, the GS device backends (~47 `pcsx2/GS/*`
|
||||
> files) + `VMManager`/`GameDatabase`/`AudioStream`/`Config`/`Pcsx2Config`/`MTVU`/
|
||||
> `Semaphore`/`Threading` deltas were brought over by **wholesale graft/copy from
|
||||
> refresh-experimental, mostly UNGUARDED** — i.e. they now ride into the mac/Linux
|
||||
> arm64 builds, not just Android. This is the fast-but-dirty version of this item.
|
||||
> The genuinely Android-only pieces (force-float, Oboe, SoC-name, Mali/MediaTek VK
|
||||
> gates, OSD label) ARE `#if __ANDROID__`/runtime-gated and mac-safe. The rest still
|
||||
> needs the careful per-file pass: keep mobile-specific behind guards, verify PC/mac
|
||||
> unaffected, drop stale-upstream noise.
|
||||
3. **arm64 JIT fixes to port** (3 real Android commits):
|
||||
`45b4b68d10` (microVU PQ lanes), `75351e8545` (FTOI NaN / SQRT clamp),
|
||||
`ec06302ccf` (skip microVU emit on jump-cache hits).
|
||||
> ⚠️ 2026-07-10 — **HEADS-UP, this contradicts the "mac backend dropped" decision.**
|
||||
> To reach refresh-experimental EE/VU perf on Android quickly, this pass GRAFTED the
|
||||
> mac-port EE+VU backend into the shared canonical files (`pcsx2/arm64/aR5900*`,
|
||||
> `aVU*`) — i.e. it re-introduced the "superseded experiment" logic *under the
|
||||
> canonical filenames*, UNGUARDED. **This changes the mac/Linux arm64 recompiler too**,
|
||||
> so the mac build needs a re-verify after this push.
|
||||
> - The **VU graft is now REDUNDANT**: the VU slam was Android thread-pinning, not the
|
||||
> VU codegen, and the fix (force-float, `#if __ANDROID__`) is backend-agnostic. So
|
||||
> `aVU*` can be reverted to canonical with **zero** Android perf loss — recommended.
|
||||
> - The **EE graft** is the one genuine Android win (canonical Phase-7 EE was ~98%
|
||||
> slammed pre-graft). Decision needed: guard it Android-only, fold the improvement
|
||||
> into the canonical JIT for all arm64, or bench canonical-EE+PGO to see if the gap
|
||||
> is real. Until decided, mac inherits the mac-port EE.
|
||||
4. **3rdparty de-duplication.** `platforms/android/.../cpp/3rdparty` (adrenotools
|
||||
+ others) is still vendored for the NDK build. Keep adrenotools/oboe
|
||||
(Android-only); evaluate sourcing the rest from root `3rdparty/` once the
|
||||
|
||||
@@ -192,6 +192,24 @@ bool Threading::ThreadHandle::SetAffinity(u64 processor_mask) const
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Threading::ThreadHandle::SetNicePriority(int nice) const
|
||||
{
|
||||
// Darwin uses Mach thread policies; nice values on pthreads are advisory
|
||||
// and not reliably honored. No-op for now.
|
||||
return false;
|
||||
}
|
||||
|
||||
u64 Threading::ThreadHandle::GetAffinity() const
|
||||
{
|
||||
// No per-thread affinity introspection on Darwin.
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Threading::ThreadHandle::GetCurrentCpu() const
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
Threading::Thread::Thread() = default;
|
||||
|
||||
Threading::Thread::Thread(Thread&& thread)
|
||||
|
||||
+24
-1
@@ -9,6 +9,10 @@
|
||||
#include "cpuinfo.h"
|
||||
#endif
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
#include <sys/system_properties.h>
|
||||
#endif
|
||||
|
||||
static u32 PAUSE_TIME = 0;
|
||||
|
||||
static void MultiPause()
|
||||
@@ -145,7 +149,26 @@ void AbortWithMessage(const char* msg)
|
||||
static CPUInfo CalcCPUInfo()
|
||||
{
|
||||
CPUInfo out;
|
||||
out.name = cpuinfo_get_package(0)->name;
|
||||
const cpuinfo_package* pkg = cpuinfo_get_package(0);
|
||||
out.name = (pkg && pkg->name[0] != '\0') ? pkg->name : "Unknown";
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
// cpuinfo's bundled SoC database may not recognise newer chips (e.g. QCS8550),
|
||||
// leaving the package name empty or "Unknown". Fall back to the Android SoC build
|
||||
// properties so the OSD shows a real name instead of "Unknown".
|
||||
if (out.name.empty() || out.name.find("Unknown") != std::string::npos)
|
||||
{
|
||||
char model[PROP_VALUE_MAX] = {};
|
||||
char manuf[PROP_VALUE_MAX] = {};
|
||||
__system_property_get("ro.soc.model", model);
|
||||
__system_property_get("ro.soc.manufacturer", manuf);
|
||||
if (model[0] != '\0')
|
||||
out.name = (manuf[0] != '\0') ? (std::string(manuf) + " " + model) : std::string(model);
|
||||
else if (manuf[0] != '\0')
|
||||
out.name = manuf;
|
||||
}
|
||||
#endif
|
||||
|
||||
out.num_threads = cpuinfo_get_processors_count();
|
||||
out.num_clusters = cpuinfo_get_clusters_count();
|
||||
out.num_big_cores = 0;
|
||||
|
||||
@@ -9,13 +9,18 @@
|
||||
#include "common/Assertions.h"
|
||||
|
||||
#include <memory>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include <pthread.h>
|
||||
#include <unistd.h>
|
||||
#if defined(__linux__)
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/resource.h>
|
||||
#include <sys/types.h>
|
||||
#include <sched.h>
|
||||
#include <cerrno>
|
||||
|
||||
// glibc < v2.30 doesn't define gettid...
|
||||
#if __GLIBC__ == 2 && __GLIBC_MINOR__ < 30
|
||||
@@ -183,6 +188,85 @@ bool Threading::ThreadHandle::SetAffinity(u64 processor_mask) const
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Threading::ThreadHandle::SetNicePriority(int nice) const
|
||||
{
|
||||
#if defined(__linux__)
|
||||
if (m_native_id == 0)
|
||||
return false;
|
||||
// PRIO_PROCESS + a tid sets the nice value of that specific thread on Linux.
|
||||
// Silently tolerate EPERM — the process rlimit may forbid going negative.
|
||||
errno = 0;
|
||||
if (setpriority(PRIO_PROCESS, static_cast<id_t>(m_native_id), nice) == 0)
|
||||
return true;
|
||||
return errno == 0;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
u64 Threading::ThreadHandle::GetAffinity() const
|
||||
{
|
||||
#if defined(__linux__)
|
||||
if (m_native_id == 0)
|
||||
return 0;
|
||||
cpu_set_t set;
|
||||
CPU_ZERO(&set);
|
||||
if (sched_getaffinity((pid_t)m_native_id, sizeof(set), &set) < 0)
|
||||
return 0;
|
||||
u64 mask = 0;
|
||||
for (u32 i = 0; i < 64; i++)
|
||||
{
|
||||
if (CPU_ISSET(i, &set))
|
||||
mask |= (static_cast<u64>(1) << i);
|
||||
}
|
||||
return mask;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
int Threading::ThreadHandle::GetCurrentCpu() const
|
||||
{
|
||||
#if defined(__linux__)
|
||||
if (m_native_id == 0)
|
||||
return -1;
|
||||
// /proc/self/task/<tid>/stat field 39 (1-indexed) is the last CPU the thread ran on.
|
||||
// The comm field (2) is wrapped in parens and may contain spaces, so scan from the
|
||||
// last ')' and count whitespace-separated tokens (first token after it = field 3).
|
||||
char path[64];
|
||||
std::snprintf(path, sizeof(path), "/proc/self/task/%u/stat", m_native_id);
|
||||
FILE* fp = std::fopen(path, "re");
|
||||
if (!fp)
|
||||
return -1;
|
||||
char buf[1024];
|
||||
const size_t n = std::fread(buf, 1, sizeof(buf) - 1, fp);
|
||||
std::fclose(fp);
|
||||
if (n == 0)
|
||||
return -1;
|
||||
buf[n] = '\0';
|
||||
const char* p = std::strrchr(buf, ')');
|
||||
if (!p)
|
||||
return -1;
|
||||
p++;
|
||||
int token = 0; // first token after ')' is field 3 (state); processor is field 39
|
||||
while (*p)
|
||||
{
|
||||
while (*p == ' ')
|
||||
p++;
|
||||
if (!*p)
|
||||
break;
|
||||
token++;
|
||||
if (token == 37) // field 39 == 37th token after ')'
|
||||
return std::atoi(p);
|
||||
while (*p && *p != ' ')
|
||||
p++;
|
||||
}
|
||||
return -1;
|
||||
#else
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
Threading::Thread::Thread() = default;
|
||||
|
||||
Threading::Thread::Thread(Thread&& thread)
|
||||
|
||||
@@ -137,6 +137,33 @@ void Threading::WorkSema::Reset()
|
||||
m_state = STATE_RUNNING_0;
|
||||
}
|
||||
|
||||
void Threading::UserspaceSemaphore::WaitWithSpin()
|
||||
{
|
||||
// See header for rationale. The peek-and-CAS spin path is the win — when
|
||||
// the producer is about to Post (within ~50µs), we acquire in user space
|
||||
// and skip the futex syscall entirely.
|
||||
int32_t counter = m_counter.load(std::memory_order_relaxed);
|
||||
u32 waited = 0;
|
||||
while (true)
|
||||
{
|
||||
while (counter > 0)
|
||||
{
|
||||
if (m_counter.compare_exchange_weak(counter, counter - 1,
|
||||
std::memory_order_acquire, std::memory_order_relaxed))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (waited >= SPIN_TIME_NS)
|
||||
break;
|
||||
waited += ShortSpin();
|
||||
counter = m_counter.load(std::memory_order_relaxed);
|
||||
}
|
||||
// Spin window expired — block in the kernel (same as plain Wait()).
|
||||
if (m_counter.fetch_sub(1, std::memory_order_acquire) <= 0)
|
||||
m_sema.Wait();
|
||||
}
|
||||
|
||||
#if !defined(__APPLE__) // macOS implementations are in DarwinThreads
|
||||
|
||||
Threading::KernelSemaphore::KernelSemaphore()
|
||||
|
||||
+62
-4
@@ -75,6 +75,18 @@ namespace Threading
|
||||
/// Obviously, only works up to 64 processors.
|
||||
bool SetAffinity(u64 processor_mask) const;
|
||||
|
||||
/// Nudges the thread's scheduling priority (nice value on POSIX).
|
||||
/// Negative = higher priority. Silently no-ops on platforms without
|
||||
/// per-thread priority support. Returns true on success.
|
||||
bool SetNicePriority(int nice) const;
|
||||
|
||||
/// Diagnostic: current CPU-affinity mask (cores this thread may run on),
|
||||
/// reflecting any cpuset/cgroup clamp. 0 if unsupported/failed.
|
||||
u64 GetAffinity() const;
|
||||
|
||||
/// Diagnostic: the CPU core this thread last executed on, or -1 if unknown.
|
||||
int GetCurrentCpu() const;
|
||||
|
||||
protected:
|
||||
void* m_native_handle = nullptr;
|
||||
|
||||
@@ -148,14 +160,19 @@ namespace Threading
|
||||
/// Usage:
|
||||
/// - Processing thread loops on `WaitForWork()` followed by processing all work in the queue
|
||||
/// - Threads adding work first add their work to the queue, then call `NotifyOfWork()`
|
||||
class WorkSema
|
||||
class alignas(__cachelinesize) WorkSema
|
||||
{
|
||||
/// Semaphore for sleeping the worker thread
|
||||
KernelSemaphore m_sema;
|
||||
/// Semaphore for sleeping thread waiting on worker queue empty
|
||||
KernelSemaphore m_empty_sema;
|
||||
/// Current state (see enum below)
|
||||
std::atomic<s32> m_state{0};
|
||||
///
|
||||
/// Isolated to its own cache line: m_state is hammered on every
|
||||
/// NotifyOfWork/WaitForWork. Sharing a line with m_sema/m_empty_sema
|
||||
/// caused cross-core invalidations on the rare wake path. On ARM64
|
||||
/// big.LITTLE this false-sharing was visible in EE/MTVU traffic.
|
||||
alignas(__cachelinesize) std::atomic<s32> m_state{0};
|
||||
|
||||
// Expected call frequency is NotifyOfWork > WaitForWork > WaitForEmpty
|
||||
// So optimize states for fast NotifyOfWork
|
||||
@@ -222,10 +239,14 @@ namespace Threading
|
||||
};
|
||||
|
||||
/// A semaphore that definitely has a fast userspace path
|
||||
class UserspaceSemaphore
|
||||
class alignas(__cachelinesize) UserspaceSemaphore
|
||||
{
|
||||
KernelSemaphore m_sema;
|
||||
std::atomic<int32_t> m_counter{0};
|
||||
/// Isolated to its own cache line: m_counter is hot (Post/Wait fast path,
|
||||
/// WaitWithSpin spins on .load()) while m_sema is touched only on the
|
||||
/// slow blocking fallback. Sharing a line caused cross-core invalidation
|
||||
/// of m_sema state on every counter tick.
|
||||
alignas(__cachelinesize) std::atomic<int32_t> m_counter{0};
|
||||
|
||||
public:
|
||||
UserspaceSemaphore() = default;
|
||||
@@ -237,12 +258,49 @@ namespace Threading
|
||||
m_sema.Post();
|
||||
}
|
||||
|
||||
/// Post `count` times atomically. Used to batch signals to a single
|
||||
/// waiter — when `count > 1` and the counter was negative (waiters
|
||||
/// blocked), wakes only as many waiters as were actually queued
|
||||
/// (`min(count, -prev)`); the rest stay accumulated in the counter
|
||||
/// for subsequent Wait() callers to drain without a syscall.
|
||||
///
|
||||
/// MTVU uses this to coalesce per-VU-execute Posts into one batch
|
||||
/// per ring-buffer drain — reduces sem_post syscall storm during
|
||||
/// heavy GIF traffic (FFXII intro-style cinematic VU bursts).
|
||||
void Post(int count)
|
||||
{
|
||||
if (count <= 0)
|
||||
return;
|
||||
const int prev = m_counter.fetch_add(count, std::memory_order_release);
|
||||
if (prev < 0)
|
||||
{
|
||||
const int to_wake = std::min(count, -prev);
|
||||
for (int i = 0; i < to_wake; i++)
|
||||
m_sema.Post();
|
||||
}
|
||||
}
|
||||
|
||||
void Wait()
|
||||
{
|
||||
if (m_counter.fetch_sub(1, std::memory_order_acquire) <= 0)
|
||||
m_sema.Wait();
|
||||
}
|
||||
|
||||
/// Adaptive spin-before-block. Same semantics as Wait(), but spends up
|
||||
/// to SPIN_TIME_NS in a userspace busy-wait checking the counter
|
||||
/// before falling back to the kernel sema. Designed for high-frequency
|
||||
/// producer-consumer pairs where the producer typically posts within
|
||||
/// microseconds of the consumer's wait — avoids the futex syscall on
|
||||
/// the common case (perf data showed ~27% of MTVU thread time was
|
||||
/// inside `syscall` for sem_wait → futex).
|
||||
///
|
||||
/// Implementation note: peek-and-CAS BEFORE the fetch_sub, so a
|
||||
/// successful spin acquisition doesn't race with concurrent Posts the
|
||||
/// way a fetch_sub-then-rollback would. If the spin window expires
|
||||
/// without seeing a positive counter, fall through to the same code
|
||||
/// path as Wait() (fetch_sub + m_sema.Wait()).
|
||||
void WaitWithSpin();
|
||||
|
||||
bool TryWait()
|
||||
{
|
||||
int32_t counter = m_counter.load(std::memory_order_relaxed);
|
||||
|
||||
@@ -131,6 +131,32 @@ bool Threading::ThreadHandle::SetAffinity(u64 processor_mask) const
|
||||
return (SetThreadAffinityMask(GetCurrentThread(), (DWORD_PTR)processor_mask) != 0 || GetLastError() != ERROR_SUCCESS);
|
||||
}
|
||||
|
||||
bool Threading::ThreadHandle::SetNicePriority(int nice) const
|
||||
{
|
||||
if (!m_native_handle)
|
||||
return false;
|
||||
int win_priority = THREAD_PRIORITY_NORMAL;
|
||||
if (nice <= -15)
|
||||
win_priority = THREAD_PRIORITY_HIGHEST;
|
||||
else if (nice <= -5)
|
||||
win_priority = THREAD_PRIORITY_ABOVE_NORMAL;
|
||||
else if (nice >= 15)
|
||||
win_priority = THREAD_PRIORITY_LOWEST;
|
||||
else if (nice >= 5)
|
||||
win_priority = THREAD_PRIORITY_BELOW_NORMAL;
|
||||
return SetThreadPriority(static_cast<HANDLE>(m_native_handle), win_priority) != 0;
|
||||
}
|
||||
|
||||
u64 Threading::ThreadHandle::GetAffinity() const
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Threading::ThreadHandle::GetCurrentCpu() const
|
||||
{
|
||||
return static_cast<int>(GetCurrentProcessorNumber());
|
||||
}
|
||||
|
||||
Threading::Thread::Thread() = default;
|
||||
|
||||
Threading::Thread::Thread(Thread&& thread)
|
||||
|
||||
@@ -1122,9 +1122,11 @@ if(ANDROID)
|
||||
list(APPEND pcsx2HostSources
|
||||
Host/OboeAudioStream.cpp)
|
||||
list(APPEND pcsx2GSSources
|
||||
GS/Renderers/OpenGL/GLContextEGL.cpp
|
||||
GS/Renderers/OpenGL/GLContextEGLAndroid.cpp
|
||||
GS/Renderers/Common/GSGPUProfile.cpp)
|
||||
list(APPEND pcsx2GSHeaders
|
||||
GS/Renderers/OpenGL/GLContextEGL.h
|
||||
GS/Renderers/OpenGL/GLContextEGLAndroid.h
|
||||
GS/Renderers/Common/GSGPUProfile.h)
|
||||
list(APPEND pcsx2Sources
|
||||
|
||||
@@ -980,7 +980,11 @@ struct Pcsx2Config
|
||||
};
|
||||
|
||||
static constexpr s32 MAX_VOLUME = 200;
|
||||
#ifdef __ANDROID__
|
||||
static constexpr AudioBackend DEFAULT_BACKEND = AudioBackend::Oboe;
|
||||
#else
|
||||
static constexpr AudioBackend DEFAULT_BACKEND = AudioBackend::Cubeb;
|
||||
#endif
|
||||
static constexpr SPU2SyncMode DEFAULT_SYNC_MODE = SPU2SyncMode::TimeStretch;
|
||||
|
||||
static std::optional<SPU2SyncMode> ParseSyncMode(const char* str);
|
||||
|
||||
+53
-39
@@ -144,7 +144,9 @@ static bool OpenGSDevice(GSRendererType renderer, bool clear_state_on_fail, bool
|
||||
bool okay = g_gs_device->Create(vsync_mode, allow_present_throttle);
|
||||
if (okay)
|
||||
{
|
||||
Console.WriteLn("@@ANDROID_GL_INIT@@ stage=device_created");
|
||||
okay = ImGuiManager::Initialize();
|
||||
Console.WriteLn("@@ANDROID_GL_INIT@@ stage=imgui_mgr_done");
|
||||
if (!okay)
|
||||
Console.Error("Failed to initialize ImGuiManager");
|
||||
}
|
||||
@@ -153,18 +155,52 @@ static bool OpenGSDevice(GSRendererType renderer, bool clear_state_on_fail, bool
|
||||
Console.Error("Failed to create GS device");
|
||||
}
|
||||
|
||||
#if defined(ENABLE_VULKAN) && defined(ENABLE_OPENGL)
|
||||
// Some devices (particularly Android) advertise Vulkan but fail to actually create a
|
||||
// usable device. The SW renderer only uses the graphics API as a display backend, so if
|
||||
// Vulkan fell over we can retry with OpenGL/GLES and keep the user in SW instead of
|
||||
// crashing out.
|
||||
if (!okay && new_api == RenderAPI::Vulkan && renderer == GSRendererType::SW)
|
||||
{
|
||||
Console.Warning("Vulkan device creation failed for SW renderer; falling back to OpenGL.");
|
||||
ImGuiManager::Shutdown(clear_state_on_fail);
|
||||
if (g_gs_device)
|
||||
{
|
||||
g_gs_device->Destroy();
|
||||
g_gs_device.reset();
|
||||
}
|
||||
Host::ReleaseRenderWindow();
|
||||
|
||||
g_gs_device = std::make_unique<GSDeviceOGL>();
|
||||
okay = g_gs_device->Create(vsync_mode, allow_present_throttle);
|
||||
if (okay)
|
||||
{
|
||||
okay = ImGuiManager::Initialize();
|
||||
if (!okay)
|
||||
Console.Error("Failed to initialize ImGuiManager after OpenGL fallback");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error("OpenGL fallback also failed to create a GS device");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!okay)
|
||||
{
|
||||
ImGuiManager::Shutdown(clear_state_on_fail);
|
||||
g_gs_device->Destroy();
|
||||
g_gs_device.reset();
|
||||
if (g_gs_device)
|
||||
{
|
||||
g_gs_device->Destroy();
|
||||
g_gs_device.reset();
|
||||
}
|
||||
Host::ReleaseRenderWindow();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!g_gs_device->SetGPUTimingEnabled(true))
|
||||
GSConfig.OsdShowGPU = false;
|
||||
if (!g_gs_device->SetGPUPipelineStatisticsEnabled(true))
|
||||
if (!g_gs_device->SetGPUPipelineStatisticsEnabled(GSConfig.OsdShowGPUStats))
|
||||
GSConfig.OsdShowGPUStats = false;
|
||||
|
||||
Console.WriteLn(Color_StrongGreen, "%s Graphics Driver Info:", GSDevice::RenderAPIToString(new_api));
|
||||
@@ -226,6 +262,7 @@ static bool OpenGSRenderer(GSRendererType renderer, u8* basemem)
|
||||
g_gs_renderer->ResetPCRTC();
|
||||
g_gs_renderer->UpdateRenderFixes();
|
||||
g_perfmon.Reset();
|
||||
Console.WriteLn("@@ANDROID_GL_INIT@@ stage=renderer_open_done");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -936,9 +973,20 @@ void GSUpdateConfig(const Pcsx2Config::GSOptions& new_config)
|
||||
if (!g_gs_device->SetGPUTimingEnabled(true))
|
||||
GSConfig.OsdShowGPU = false;
|
||||
}
|
||||
else if (!GSConfig.OsdShowGPU && old_config.OsdShowGPU)
|
||||
{
|
||||
// Turning the GPU readout off must also stop the GPU timing queries
|
||||
// (timestamp queries + per-frame readback have real overhead) — that
|
||||
// is the actual perf win of disabling this overlay element on Android.
|
||||
g_gs_device->SetGPUTimingEnabled(false);
|
||||
}
|
||||
|
||||
if (GSConfig.OsdShowGPUStats != old_config.OsdShowGPUStats)
|
||||
{
|
||||
// GPU pipeline-statistics queries (VS/PS invocations) are only real on
|
||||
// Vulkan here (GLES has no pipeline_statistics_query). Enabling toggles
|
||||
// the per-frame query on the device; if the backend can't do it we fall
|
||||
// back to leaving the overlay line off rather than showing garbage.
|
||||
if (!g_gs_device->SetGPUPipelineStatisticsEnabled(GSConfig.OsdShowGPUStats))
|
||||
GSConfig.OsdShowGPUStats = false;
|
||||
}
|
||||
@@ -979,6 +1027,7 @@ void* GSAllocateWrappedMemory(size_t size, size_t repeat)
|
||||
{
|
||||
pxAssertRel(!s_fh, "Has no file mapping");
|
||||
|
||||
const char* file_name = "/GS.mem";
|
||||
s_fh = CreateFileMapping(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, size, nullptr);
|
||||
if (s_fh == NULL)
|
||||
{
|
||||
@@ -1075,12 +1124,11 @@ void* GSAllocateWrappedMemory(size_t size, size_t repeat)
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (ftruncate(s_shm_fd, repeat * size) < 0)
|
||||
fprintf(stderr, "Failed to reserve memory due to %s\n", strerror(errno));
|
||||
|
||||
void* fifo = mmap(nullptr, size * repeat, PROT_READ | PROT_WRITE, MAP_SHARED, s_shm_fd, 0);
|
||||
|
||||
#endif
|
||||
for (size_t i = 1; i < repeat; i++)
|
||||
{
|
||||
void* base = (u8*)fifo + size * i;
|
||||
@@ -1105,8 +1153,6 @@ void GSFreeWrappedMemory(void* ptr, size_t size, size_t repeat)
|
||||
s_shm_fd = -1;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
std::pair<u8, u8> GSGetRGBA8AlphaMinMax(const void* data, u32 width, u32 height, u32 stride)
|
||||
{
|
||||
GSVector4i minc = GSVector4i::xffffffff();
|
||||
@@ -1230,40 +1276,8 @@ static void HotkeyAdjustUpscaleMultiplier(const float delta)
|
||||
MTGS::ApplySettings();
|
||||
}
|
||||
|
||||
static bool s_osd_hotkey_forced_simple = false;
|
||||
|
||||
static bool HasConfiguredOSD()
|
||||
{
|
||||
return EmuConfig.GS.OsdShowSpeed || EmuConfig.GS.OsdShowFPS || EmuConfig.GS.OsdShowVPS ||
|
||||
EmuConfig.GS.OsdShowResolution || EmuConfig.GS.OsdShowGSStats || EmuConfig.GS.OsdShowCPU ||
|
||||
EmuConfig.GS.OsdShowGPU || EmuConfig.GS.OsdShowGPUDebug || EmuConfig.GS.OsdShowIndicators ||
|
||||
EmuConfig.GS.OsdShowFrameTimes || EmuConfig.GS.OsdShowHardwareInfo || EmuConfig.GS.OsdShowVersion ||
|
||||
EmuConfig.GS.OsdShowSettings || EmuConfig.GS.OsdshowPatches || EmuConfig.GS.OsdShowInputs ||
|
||||
EmuConfig.GS.OsdShowInputRec || EmuConfig.GS.OsdShowVideoCapture || EmuConfig.GS.OsdShowTextureReplacements;
|
||||
}
|
||||
|
||||
static void SetForcedSimpleOSD(bool enabled)
|
||||
{
|
||||
s_osd_hotkey_forced_simple = enabled;
|
||||
GSConfig.OsdShowFPS = enabled;
|
||||
GSConfig.OsdShowVPS = enabled;
|
||||
GSConfig.OsdShowSpeed = enabled;
|
||||
GSConfig.OsdShowVersion = enabled;
|
||||
GSConfig.OsdShowIndicators = enabled;
|
||||
GSConfig.OsdMessagesPos = enabled ? OsdOverlayPos::TopLeft : OsdOverlayPos::None;
|
||||
GSConfig.OsdPerformancePos = enabled ? OsdOverlayPos::TopRight : OsdOverlayPos::None;
|
||||
}
|
||||
|
||||
static void HotkeyToggleOSD()
|
||||
{
|
||||
if (!HasConfiguredOSD())
|
||||
{
|
||||
SetForcedSimpleOSD(!s_osd_hotkey_forced_simple || GSConfig.OsdPerformancePos == OsdOverlayPos::None);
|
||||
return;
|
||||
}
|
||||
|
||||
s_osd_hotkey_forced_simple = false;
|
||||
|
||||
GSConfig.OsdShowSettings ^= EmuConfig.GS.OsdShowSettings;
|
||||
GSConfig.OsdshowPatches ^= EmuConfig.GS.OsdshowPatches;
|
||||
GSConfig.OsdShowInputs ^= EmuConfig.GS.OsdShowInputs;
|
||||
|
||||
+17
-14
@@ -2402,7 +2402,7 @@ void GSState::FlushWrite()
|
||||
|
||||
if (m_draw_transfers.size() > 0 && m_tr.m_blit.DBP == m_draw_transfers.back().blit.DBP)
|
||||
{
|
||||
m_draw_transfers.back().rect = m_draw_transfers.back().rect.runion(r);
|
||||
m_draw_transfers.back().rect = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5740,12 +5740,20 @@ __forceinline void GSState::HandleAutoFlush()
|
||||
}
|
||||
}
|
||||
|
||||
bool GSState::CheckOverlapVerts(u32 n)
|
||||
// yaps2 ce186a0a: called once per vertex kick, so the common case (no recent buffer
|
||||
// switch, or draw buffering off) must stay inline — an out-of-line call here costs
|
||||
// caller-saved spills in every GIF vertex handler loop on top of the call itself.
|
||||
__fi bool GSState::CheckOverlapVerts(u32 n)
|
||||
{
|
||||
if (!GSConfig.UserHacks_DrawBuffering)
|
||||
if (!m_recent_buffer_switch || !GSConfig.UserHacks_DrawBuffering)
|
||||
return false;
|
||||
|
||||
if (m_recent_buffer_switch && ((m_vertex->tail + 1) - m_vertex->head) == n)
|
||||
return CheckOverlapVertsSlow(n);
|
||||
}
|
||||
|
||||
__noinline bool GSState::CheckOverlapVertsSlow(u32 n)
|
||||
{
|
||||
if (((m_vertex->tail + 1) - m_vertex->head) == n)
|
||||
{
|
||||
m_recent_buffer_switch = false;
|
||||
|
||||
@@ -5884,7 +5892,11 @@ __forceinline void GSState::VertexKick(u32 skip)
|
||||
u32 next = m_vertex->next;
|
||||
u32 xy_tail = m_vertex->xy_tail;
|
||||
|
||||
if (GSIsHardwareRenderer() && GSLocalMemory::m_psm[m_context->ZBUF.PSM].bpp == 32)
|
||||
// yaps2 ce186a0a: config test first — this runs per vertex kick, and with the hack
|
||||
// disabled (the default) the reorder short-circuits the GSIsHardwareRenderer() call
|
||||
// and the psm-table walk on every kick.
|
||||
if (GSConfig.UserHacks_Limit24BitDepth != GSLimit24BitDepth::Disabled &&
|
||||
GSIsHardwareRenderer() && GSLocalMemory::m_psm[m_context->ZBUF.PSM].bpp == 32)
|
||||
{
|
||||
if (GSConfig.UserHacks_Limit24BitDepth == GSLimit24BitDepth::PrioritizeUpper)
|
||||
m_v.XYZ.Z = ((m_v.XYZ.Z >> 8) & ~0xFF) | (m_v.XYZ.Z & 0xFF);
|
||||
@@ -6902,16 +6914,7 @@ bool GSState::GSTransferBuffer::Update(int tw, int th, int bpp, int& len)
|
||||
const int remaining = total - end;
|
||||
|
||||
if (len > remaining)
|
||||
{
|
||||
if (len > packet_size)
|
||||
{
|
||||
#if defined(_DEBUG)
|
||||
Console.Warning("GS transfer buffer overflow len %d remaining %d, tex_size %d tw %d th %d bpp %d", len, remaining, tex_size, tw, th, bpp);
|
||||
#endif
|
||||
}
|
||||
|
||||
len = remaining;
|
||||
}
|
||||
|
||||
return len > 0;
|
||||
}
|
||||
|
||||
@@ -189,6 +189,7 @@ protected:
|
||||
bool EarlyDetectShuffle(u32 prim);
|
||||
void CheckCLUTValidity(u32 prim);
|
||||
bool CheckOverlapVerts(u32 n);
|
||||
bool CheckOverlapVertsSlow(u32 n);
|
||||
|
||||
template <u32 prim, bool auto_flush> void VertexKick(u32 skip);
|
||||
|
||||
|
||||
@@ -288,6 +288,20 @@ GSRendererType GSUtil::GetPreferredRenderer()
|
||||
#elif defined(_WIN32)
|
||||
// Use D3D device info to select renderer.
|
||||
preferred_renderer = D3D::GetPreferredRenderer();
|
||||
#elif defined(__ANDROID__)
|
||||
// Android: prefer OpenGL HW. Vulkan's suitability probe is fragile
|
||||
// across the wide spread of mobile driver stacks, and falling through
|
||||
// to SW for alpha-heavy games (FFX battles, smoke effects) is far
|
||||
// worse than running OGL HW even on devices where Vulkan would also
|
||||
// have worked. Users can still pick Vulkan or SW explicitly via the
|
||||
// renderer setting; this only steers the Auto resolution.
|
||||
#if defined(ENABLE_OPENGL)
|
||||
preferred_renderer = GSRendererType::OGL;
|
||||
#elif defined(ENABLE_VULKAN)
|
||||
preferred_renderer = GSRendererType::VK;
|
||||
#else
|
||||
preferred_renderer = GSRendererType::SW;
|
||||
#endif
|
||||
#else
|
||||
// Linux: Prefer Vulkan if the driver isn't buggy.
|
||||
#if defined(ENABLE_VULKAN)
|
||||
|
||||
@@ -249,7 +249,7 @@ GSDevice::GSDevice()
|
||||
GSDevice::~GSDevice()
|
||||
{
|
||||
// should've been cleaned up in Destroy()
|
||||
pxAssert(m_pool[0].empty() && m_pool[1].empty() && !m_merge && !m_weavebob && !m_blend && !m_mad && !m_target_tmp && !m_cas && !m_mfx_output);
|
||||
pxAssert(m_pool[0].empty() && m_pool[1].empty() && !m_merge && !m_weavebob && !m_blend && !m_mad && !m_target_tmp && !m_cas);
|
||||
}
|
||||
|
||||
const char* GSDevice::RenderAPIToString(RenderAPI api)
|
||||
@@ -451,6 +451,11 @@ void GSDevice::ClearDepth(GSTexture* t, float d)
|
||||
t->SetClearDepth(d);
|
||||
}
|
||||
|
||||
void GSDevice::HintReadbackSource(GSTexture* tex)
|
||||
{
|
||||
// Default: no scheduling hint. See GSDeviceVK for a backend that uses it.
|
||||
}
|
||||
|
||||
bool GSDevice::ProcessClearsBeforeCopy(GSTexture* sTex, GSTexture* dTex, const bool full_copy)
|
||||
{
|
||||
pxAssert(sTex->GetState() == GSTexture::State::Cleared && dTex->IsRenderTargetOrDepthStencil());
|
||||
@@ -502,67 +507,68 @@ void GSDevice::UpdateImGuiTextures()
|
||||
case ImTextureStatus_Destroyed:
|
||||
continue;
|
||||
case ImTextureStatus_WantCreate:
|
||||
if (GSTexture* gs_tex = g_gs_device->CreateTexture(im_tex->Width, im_tex->Height, 1, GSTexture::Format::Color))
|
||||
{
|
||||
GSTexture* gs_tex = g_gs_device->CreateTexture(im_tex->Width, im_tex->Height, 1, GSTexture::Format::Color);
|
||||
if (!gs_tex)
|
||||
pxFailRel("Failed to create ImGui texture");
|
||||
|
||||
im_tex->SetTexID(reinterpret_cast<ImTextureID>(gs_tex->GetNativeHandle()));
|
||||
im_tex->BackendUserData = gs_tex;
|
||||
[[fallthrough]];
|
||||
}
|
||||
case ImTextureStatus_WantUpdates:
|
||||
{
|
||||
// If we fell through from WantCreate, then we are uploading the full size
|
||||
// Otherwise, we are just updating the specified region
|
||||
// clange-format off
|
||||
const int upload_x = (im_tex->Status == ImTextureStatus_WantCreate) ? 0 : im_tex->UpdateRect.x;
|
||||
const int upload_y = (im_tex->Status == ImTextureStatus_WantCreate) ? 0 : im_tex->UpdateRect.y;
|
||||
const int upload_w = (im_tex->Status == ImTextureStatus_WantCreate) ? im_tex->Width : im_tex->UpdateRect.w;
|
||||
const int upload_h = (im_tex->Status == ImTextureStatus_WantCreate) ? im_tex->Height : im_tex->UpdateRect.h;
|
||||
const int upload_pitch = upload_w * im_tex->BytesPerPixel;
|
||||
// clange-format on
|
||||
|
||||
const GSVector4i rect{
|
||||
upload_x,
|
||||
upload_y,
|
||||
upload_x + upload_w,
|
||||
upload_y + upload_h,
|
||||
};
|
||||
|
||||
GSTexture* gs_tex = static_cast<GSTexture*>(im_tex->BackendUserData);
|
||||
GSTexture::GSMap map;
|
||||
if (gs_tex->Map(map, &rect))
|
||||
{
|
||||
im_tex->SetTexID(reinterpret_cast<ImTextureID>(gs_tex->GetNativeHandle()));
|
||||
im_tex->BackendUserData = gs_tex;
|
||||
for (int y = 0; y < upload_h; y++)
|
||||
std::memcpy(map.bits + map.pitch * y, im_tex->GetPixelsAt(rect.x, rect.y + y), upload_pitch);
|
||||
|
||||
gs_tex->Unmap();
|
||||
}
|
||||
else
|
||||
{
|
||||
pxFailRel("Failed to create ImGui texture");
|
||||
break;
|
||||
for (int y = 0; y < upload_h; y++)
|
||||
gs_tex->Update({rect.left, rect.top + y, rect.right, rect.top + y + 1},
|
||||
im_tex->GetPixelsAt(rect.x, rect.y + y), upload_pitch);
|
||||
}
|
||||
[[fallthrough]];
|
||||
case ImTextureStatus_WantUpdates:
|
||||
if (GSTexture* gs_tex = static_cast<GSTexture*>(im_tex->BackendUserData))
|
||||
{
|
||||
// If we fell through from WantCreate, then we are uploading the full size
|
||||
// Otherwise, we are just updating the specified region
|
||||
// clange-format off
|
||||
const int upload_x = (im_tex->Status == ImTextureStatus_WantCreate) ? 0 : im_tex->UpdateRect.x;
|
||||
const int upload_y = (im_tex->Status == ImTextureStatus_WantCreate) ? 0 : im_tex->UpdateRect.y;
|
||||
const int upload_w = (im_tex->Status == ImTextureStatus_WantCreate) ? im_tex->Width : im_tex->UpdateRect.w;
|
||||
const int upload_h = (im_tex->Status == ImTextureStatus_WantCreate) ? im_tex->Height : im_tex->UpdateRect.h;
|
||||
const int upload_pitch = upload_w * im_tex->BytesPerPixel;
|
||||
// clange-format on
|
||||
|
||||
const GSVector4i rect{
|
||||
upload_x,
|
||||
upload_y,
|
||||
upload_x + upload_w,
|
||||
upload_y + upload_h,
|
||||
};
|
||||
|
||||
GSTexture::GSMap map;
|
||||
if (gs_tex->Map(map, &rect))
|
||||
{
|
||||
for (int y = 0; y < upload_h; y++)
|
||||
std::memcpy(map.bits + map.pitch * y, im_tex->GetPixelsAt(rect.x, rect.y + y), upload_pitch);
|
||||
|
||||
gs_tex->Unmap();
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int y = 0; y < upload_h; y++)
|
||||
gs_tex->Update({rect.left, rect.top + y, rect.right, rect.top + y + 1},
|
||||
im_tex->GetPixelsAt(rect.x, rect.y + y), upload_pitch);
|
||||
}
|
||||
|
||||
im_tex->Status = ImTextureStatus_OK;
|
||||
}
|
||||
im_tex->Status = ImTextureStatus_OK;
|
||||
break;
|
||||
}
|
||||
case ImTextureStatus_WantDestroy:
|
||||
if (GSTexture* gs_tex = static_cast<GSTexture*>(im_tex->BackendUserData))
|
||||
{
|
||||
// While it's unlikely we're going to reuse the same size as imgui for rendering,
|
||||
// imgui may request a new atlas of the same size if old font sizes are evicted.
|
||||
Recycle(gs_tex);
|
||||
{
|
||||
GSTexture* gs_tex = static_cast<GSTexture*>(im_tex->BackendUserData);
|
||||
if (gs_tex == nullptr)
|
||||
break;
|
||||
|
||||
im_tex->SetTexID(ImTextureID_Invalid);
|
||||
im_tex->BackendUserData = nullptr;
|
||||
im_tex->Status = ImTextureStatus_Destroyed;
|
||||
}
|
||||
// While it's unlikely we're going to reuse the same size as imgui for rendering,
|
||||
// imgui may request a new atlas of the same size if old font sizes are evicted.
|
||||
Recycle(gs_tex);
|
||||
|
||||
im_tex->SetTexID(ImTextureID_Invalid);
|
||||
im_tex->BackendUserData = nullptr;
|
||||
im_tex->Status = ImTextureStatus_Destroyed;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
pxAssert(false);
|
||||
break;
|
||||
@@ -925,7 +931,6 @@ void GSDevice::ClearCurrent()
|
||||
delete m_mad;
|
||||
delete m_target_tmp;
|
||||
delete m_cas;
|
||||
delete m_mfx_output;
|
||||
|
||||
m_merge = nullptr;
|
||||
m_weavebob = nullptr;
|
||||
@@ -933,7 +938,6 @@ void GSDevice::ClearCurrent()
|
||||
m_mad = nullptr;
|
||||
m_target_tmp = nullptr;
|
||||
m_cas = nullptr;
|
||||
m_mfx_output = nullptr;
|
||||
}
|
||||
|
||||
void GSDevice::Merge(GSTexture* sTex[3], GSVector4* sRect, GSVector4* dRect, const GSVector2i& fs, const GSRegPMODE& PMODE, const GSRegEXTBUF& EXTBUF, u32 c)
|
||||
@@ -1197,37 +1201,6 @@ void GSDevice::CAS(GSTexture*& tex, GSVector4i& src_rect, GSVector4& src_uv, con
|
||||
src_uv = GSVector4(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
}
|
||||
|
||||
void GSDevice::MetalFXUpscale(GSTexture*& tex, GSVector4i& src_rect, GSVector4& src_uv, const GSVector4& draw_rect)
|
||||
{
|
||||
const int dst_width = static_cast<int>(std::ceil(draw_rect.z - draw_rect.x));
|
||||
const int dst_height = static_cast<int>(std::ceil(draw_rect.w - draw_rect.y));
|
||||
if (dst_width <= 0 || dst_height <= 0)
|
||||
return;
|
||||
|
||||
GSTexture* src_tex = tex;
|
||||
if (!m_mfx_output || m_mfx_output->GetWidth() != dst_width || m_mfx_output->GetHeight() != dst_height)
|
||||
{
|
||||
delete m_mfx_output;
|
||||
m_mfx_output = CreateSurface(GSTexture::ShaderWriteTexture, dst_width, dst_height, 1, GSTexture::Format::Color);
|
||||
if (!m_mfx_output)
|
||||
{
|
||||
Console.Error("Failed to allocate MetalFX output texture.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!DoMetalFXSpatial(src_tex, m_mfx_output))
|
||||
{
|
||||
// leave textures intact if we failed
|
||||
Console.Warning("Applying MetalFX spatial upscale failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
tex = m_mfx_output;
|
||||
src_rect = GSVector4i(0, 0, dst_width, dst_height);
|
||||
src_uv = GSVector4(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
}
|
||||
|
||||
bool GSHWDrawConfig::BlendState::IsEffective(ColorMaskSelector colormask) const
|
||||
{
|
||||
return enable && (((colormask.key & 7u) && (src_factor != GSDevice::CONST_ONE || dst_factor != GSDevice::CONST_ZERO)) ||
|
||||
|
||||
@@ -900,7 +900,7 @@ struct alignas(16) GSHWDrawConfig
|
||||
|
||||
__fi bool HasDepthROV() const
|
||||
{
|
||||
return rov_depth != PS_ROV_DEPTH::NONE;
|
||||
return rov_depth == PS_ROV_DEPTH::READ_ONLY || rov_depth == PS_ROV_DEPTH::READ_WRITE;
|
||||
}
|
||||
|
||||
__fi bool HasDepthROVWrite() const
|
||||
@@ -1402,7 +1402,6 @@ public:
|
||||
bool depth_feedback : 1; ///< Depth feedback loops can be done with DS directly (otherwise need to copy to separate RT). Implies `feedback_loops`.
|
||||
bool aa1 : 1; ///< Supports the GS AA1 feature.
|
||||
bool rov : 1; ///< Supports rasterizer ordered views for both depth and color.
|
||||
bool metalfx_spatial : 1; ///< Supports Apple MetalFX spatial upscaling (Metal backend, macOS 13+).
|
||||
FeatureSupport()
|
||||
{
|
||||
memset(this, 0, sizeof(*this));
|
||||
@@ -1486,7 +1485,6 @@ protected:
|
||||
GSTexture* m_target_tmp = nullptr;
|
||||
GSTexture* m_current = nullptr;
|
||||
GSTexture* m_cas = nullptr;
|
||||
GSTexture* m_mfx_output = nullptr; ///< MetalFX spatial upscale destination (Metal backend).
|
||||
GSTexture* m_colclip_rt = nullptr; ///< Temp hw colclip texture
|
||||
GSTexture* m_ds_as_rt = nullptr; ///< Depth as color
|
||||
|
||||
@@ -1505,10 +1503,6 @@ protected:
|
||||
/// Applies CAS and writes to the destination texture, which should be a shader writeable texture.
|
||||
virtual bool DoCAS(GSTexture* sTex, GSTexture* dTex, bool sharpen_only, const std::array<u32, NUM_CAS_CONSTANTS>& constants) = 0;
|
||||
|
||||
/// Upscales sTex into dTex using a backend-specific spatial upscaler (MetalFX on Metal).
|
||||
/// Base implementation is a no-op; only the Metal backend overrides it.
|
||||
virtual bool DoMetalFXSpatial(GSTexture* sTex, GSTexture* dTex) { return false; }
|
||||
|
||||
/// Perform texture operations for ImGui
|
||||
void UpdateImGuiTextures();
|
||||
|
||||
@@ -1564,6 +1558,7 @@ 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); }
|
||||
@@ -1668,6 +1663,13 @@ public:
|
||||
|
||||
virtual std::unique_ptr<GSDownloadTexture> CreateDownloadTexture(u32 width, u32 height, GSTexture::Format format) = 0;
|
||||
|
||||
/// Hints that a synchronous CPU readback of `tex` is being performed. Games that read
|
||||
/// back every frame (e.g. small occlusion-test targets) will typically draw into the
|
||||
/// same texture again shortly before the next readback; backends can use this to
|
||||
/// schedule command submission so that readback has minimal GPU backlog to wait on.
|
||||
/// (Ported from yaps2 27984e96/2a5c0b1b — the mid-frame readback kick.)
|
||||
virtual void HintReadbackSource(GSTexture* tex);
|
||||
|
||||
virtual void CopyRect(GSTexture* sTex, GSTexture* dTex, const GSVector4i& r, u32 destX, u32 destY) = 0;
|
||||
|
||||
// StretchRect - all options
|
||||
@@ -1719,10 +1721,6 @@ public:
|
||||
|
||||
void CAS(GSTexture*& tex, GSVector4i& src_rect, GSVector4& src_uv, const GSVector4& draw_rect, bool sharpen_only);
|
||||
|
||||
/// Spatially upscales the merged display texture (MetalFX) to the draw-rect size, rewriting
|
||||
/// tex/src_rect/src_uv to point at the upscaled result, mirroring CAS().
|
||||
void MetalFXUpscale(GSTexture*& tex, GSVector4i& src_rect, GSVector4& src_uv, const GSVector4& draw_rect);
|
||||
|
||||
bool ResizeRenderTarget(GSTexture** t, int w, int h, bool preserve_contents, bool recycle);
|
||||
|
||||
void AgePool();
|
||||
|
||||
@@ -114,6 +114,37 @@ static bool LooksLikeMali(std::string_view lowered_hints)
|
||||
// on Mali-specific markers. "valhall"/"bifrost"/"midgard" are Mali GPU arches.
|
||||
return ContainsAny(lowered_hints, {"mali", "valhall", "bifrost", "midgard"});
|
||||
}
|
||||
|
||||
static bool LooksLikeMediaTekSoc(std::string_view lowered_hints)
|
||||
{
|
||||
// Ported from sashkinbro/EmuCoreX. Detect MediaTek (Dimensity/Helio) SoCs so
|
||||
// we can disable the broken Vulkan fbfetch path on their Mali stacks. The SoC
|
||||
// props (ro.soc.manufacturer/model/platform) are already folded into the hints
|
||||
// string by BuildHints().
|
||||
if (ContainsAny(lowered_hints, {"mediatek", "dimensity", "helio", "mtk"}))
|
||||
return true;
|
||||
|
||||
// MediaTek board/platform properties commonly use compact part numbers such as
|
||||
// mt6877 or mt6989z without spelling out the vendor. Require a token boundary and
|
||||
// four digits so an unrelated "mt" isn't treated as a chipset id.
|
||||
for (size_t i = 0; i + 6 <= lowered_hints.size(); i++)
|
||||
{
|
||||
if (lowered_hints[i] != 'm' || lowered_hints[i + 1] != 't' ||
|
||||
(i > 0 && std::isalnum(static_cast<unsigned char>(lowered_hints[i - 1]))))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool has_four_digits = true;
|
||||
for (size_t digit = i + 2; digit < i + 6; digit++)
|
||||
has_four_digits &= (std::isdigit(static_cast<unsigned char>(lowered_hints[digit])) != 0);
|
||||
|
||||
if (has_four_digits)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
GpuProfileOverride GpuProfileDetector::ParseOverride(std::string_view value)
|
||||
@@ -181,6 +212,9 @@ GpuProfileSelection GpuProfileDetector::Resolve(std::string_view override_value,
|
||||
GpuProfileSelection selection;
|
||||
selection.override_mode = ParseOverride(override_value);
|
||||
selection.hints = BuildHints(gpu_vendor, gpu_renderer_or_name);
|
||||
// Detected from the SoC hints regardless of any GPU-profile override — the
|
||||
// MediaTek-Mali Vulkan fbfetch breakage is orthogonal to the Mali/Adreno profile.
|
||||
selection.is_mediatek_soc = LooksLikeMediaTekSoc(ToLowerASCII(selection.hints));
|
||||
|
||||
if (selection.override_mode == GpuProfileOverride::Mali)
|
||||
{
|
||||
|
||||
@@ -27,6 +27,11 @@ struct GpuProfileSelection
|
||||
{
|
||||
GpuProfileOverride override_mode = GpuProfileOverride::Auto;
|
||||
RuntimeGpuProfile runtime_profile = RuntimeGpuProfile::Adreno;
|
||||
// True when the SoC hints look like a MediaTek chipset (Dimensity/Helio). Used
|
||||
// to disable the Vulkan framebuffer-fetch/ROAA path on MediaTek Mali stacks,
|
||||
// whose driver returns zero/stale destination color (black or missing textures)
|
||||
// across GPU generations. Ported from sashkinbro/EmuCoreX.
|
||||
bool is_mediatek_soc = false;
|
||||
std::string hints;
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "common/Path.h"
|
||||
#include "common/StringUtil.h"
|
||||
#include "common/Timer.h"
|
||||
#include "common/HostSys.h"
|
||||
|
||||
#include "fmt/format.h"
|
||||
#include "IconsFontAwesome.h"
|
||||
@@ -630,6 +631,62 @@ void GSRenderer::VSync(u32 field, bool registers_written, bool idle_frame)
|
||||
}
|
||||
}
|
||||
|
||||
// Manual frameskip (Android low-end devices): present 1 of every (N+1)
|
||||
// frames, skipping presentation of N. Reuses the duplicate-frame skip path
|
||||
// (skips present + post-processing). Emulation still runs every frame, so
|
||||
// this trades smoothness for GPU/present headroom.
|
||||
if (const u32 manual_skip = GSGetManualFrameSkip(); manual_skip > 0 && !GSCapture::IsCapturingVideo())
|
||||
{
|
||||
if (m_manual_frameskip_counter < manual_skip)
|
||||
{
|
||||
m_manual_frameskip_counter++;
|
||||
skip_frame = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_manual_frameskip_counter = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_manual_frameskip_counter = 0;
|
||||
}
|
||||
|
||||
// Max-FPS cap (Android): hold the *presented* frame rate at/below a target
|
||||
// without slowing emulation (decoupled from the speed limiter / Speed %). The
|
||||
// EE keeps running full speed, only presents are dropped. Set via setFpsCap.
|
||||
//
|
||||
// ACCUMULATOR pacer: schedule the next present at +cap_interval and drop every
|
||||
// frame until that time. This holds the AVERAGE present rate at ANY target
|
||||
// (e.g. 47 / 55 fps for the per-game "golden spot" tuning), not just whole
|
||||
// divisions of the source rate. Targets between the clean divisions get
|
||||
// slightly uneven spacing (a periodic doubled frame) — the price of an
|
||||
// arbitrary cap. Resync after a stall so we never burst-present to catch up.
|
||||
if (const u64 cap_interval = GSGetMaxPresentInterval(); cap_interval > 0 && !skip_frame && !GSCapture::IsCapturingVideo())
|
||||
{
|
||||
if (GSGetPresentCapSuspended())
|
||||
{
|
||||
// Fast-forward (Turbo): don't drop presents, so the speed-up is
|
||||
// actually visible. Re-prime the schedule so the cap resumes cleanly
|
||||
// the instant FF ends (no burst-present to "catch up").
|
||||
m_fps_cap_next_present = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
const u64 now = GetCPUTicks();
|
||||
if (m_fps_cap_next_present == 0)
|
||||
m_fps_cap_next_present = now; // first present primes the schedule
|
||||
if (now < m_fps_cap_next_present)
|
||||
skip_frame = true; // not yet time for the next present → drop it
|
||||
else
|
||||
{
|
||||
m_fps_cap_next_present += cap_interval;
|
||||
if (m_fps_cap_next_present < now)
|
||||
m_fps_cap_next_present = now + cap_interval; // stalled → resync, no burst
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bool blank_frame = !Merge(field);
|
||||
|
||||
m_last_draw_n = s_n;
|
||||
@@ -666,27 +723,6 @@ void GSRenderer::VSync(u32 field, bool registers_written, bool idle_frame)
|
||||
GetVideoMode() == GSVideoMode::SDTV_480P);
|
||||
s_last_draw_rect = draw_rect;
|
||||
|
||||
// MetalFX spatial upscale runs before CAS/present, and only when actually upscaling
|
||||
// (source smaller than the on-screen draw rect). CAS can still sharpen afterward.
|
||||
if (GSConfig.Upscaler == GSUpscaler::MetalFXSpatial)
|
||||
{
|
||||
static bool mfx_log_once = false;
|
||||
if (g_gs_device->Features().metalfx_spatial)
|
||||
{
|
||||
const int draw_w = static_cast<int>(std::ceil(draw_rect.z - draw_rect.x));
|
||||
const int draw_h = static_cast<int>(std::ceil(draw_rect.w - draw_rect.y));
|
||||
if (current->GetWidth() < draw_w && current->GetHeight() < draw_h)
|
||||
g_gs_device->MetalFXUpscale(current, src_rect, src_uv, draw_rect);
|
||||
}
|
||||
else if (!mfx_log_once)
|
||||
{
|
||||
Host::AddIconOSDMessage("MetalFXUnsupported", ICON_FA_TRIANGLE_EXCLAMATION,
|
||||
TRANSLATE_SV("GS", "MetalFX upscaling is not available on this system (requires a Metal GPU on macOS 13 or newer)."),
|
||||
10.0f);
|
||||
mfx_log_once = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (GSConfig.CASMode != GSCASMode::Disabled)
|
||||
{
|
||||
static bool cas_log_once = false;
|
||||
|
||||
@@ -19,7 +19,12 @@ private:
|
||||
std::string m_snapshot;
|
||||
u32 m_dump_frames = 0;
|
||||
u32 m_skipped_duplicate_frames = 0;
|
||||
u32 m_manual_frameskip_counter = 0;
|
||||
// Scheduled CPU-tick time of the next allowed present for the display FPS cap
|
||||
// (accumulator pacer — see GSRenderer::VSync). 0 = not primed yet.
|
||||
u64 m_fps_cap_next_present = 0;
|
||||
|
||||
private:
|
||||
// Tracking draw counters for idle frame detection.
|
||||
u64 m_last_draw_n = 0;
|
||||
u64 m_last_transfer_n = 0;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <d3d11.h>
|
||||
#include <directx/d3d12.h>
|
||||
#include <d3d12.h>
|
||||
#include <d3dcompiler.h>
|
||||
#include <fstream>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user