GS: age the texture pool every frame, and fix runtime GPU profile detection

AgePool() ran only on frames that actually presented, but it is what trims
the texture pool AND the only place GSDevice::m_frame advances. With
SkipDuplicateFrames on by default the pool could go a long time untrimmed,
grow to its limit, and push FetchSurface into handing back textures recycled
in the current frame. Reported as a slowdown in some scenes that cleared the
moment you changed any setting - that is not the setting, it is the settings
path calling PurgePool() and emptying a bloated pool.

m_runtime_gpu_profile defaulted to Adreno, so every backend that never called
SetRuntimeGPUProfile identified as Adreno: Vulkan, Metal and DX12 never called
it at all, and desktop OpenGL resolved anything not-Mali to Adreno. That fired
Adreno-only workarounds on Apple silicon (found by Brian Degenhardt) and left
IsMaliGPUProfile() permanently false under Vulkan, which silently disabled the
Tekken 5 MediaTek Mali GameDB fix on the renderer Android defaults to. Default
is Unknown now, Vulkan sets the resolved profile, and Apple has its own value.

Also: bound vkAcquireNextImageKHR instead of waiting forever on a destroyed
surface, rate-limit the synchronous pipeline-cache write, restore the manual
frame-skip and present-FPS-cap readers with an OSD indicator, widen the
decompressed-chunk cache from 2 so CHD streaming stops re-decompressing, and
stop InvalidateContainedTargets asking for a rect that cannot exist.
This commit is contained in:
jpolo1224
2026-07-26 02:01:27 -04:00
parent 3410a08c2b
commit 8a161bddc5
14 changed files with 305 additions and 13 deletions
+19 -2
View File
@@ -72,8 +72,25 @@ private:
std::atomic<u32> size{0};
u32 cap = 0;
};
/// 2 buffers for readahead (current block, next block)
Buffer m_buffer[2];
/// Decompressed-chunk cache. For a CHD one entry is one HUNK, so this is the entire cache of
/// decompressed disc data — at the original 2 entries (current + next) any seek pattern that
/// revisits data even slightly out of order re-decompresses from scratch, off whatever storage
/// the image lives on.
///
/// ★ Raised from 2 after measuring on a Retroid Pocket 6: Ultimate Spider-Man (a .chd on an SD
/// card) streams its open city continuously, and after fast-forward — which moves the player
/// through the world ~4x faster than the streaming budget allows — the GAME stalls for seconds
/// waiting on disc data. The emulator is provably healthy through it (surface presenting at
/// 120 Hz, VSync producing new frames, EE running); the guest simply draws nothing and submits
/// no audio while it waits, which is exactly how it presents to the user: frozen picture,
/// silence, working UI, and internal FPS reading N/A. Enabling fastCDVD shortened the stall but
/// did not remove it, which isolates the remaining cost to decompression + storage latency.
///
/// Every access below is written against std::size(m_buffer) (search loops and the round-robin
/// eviction index alike), so this is safe to tune. Entries allocate lazily — an unused slot is
/// 24 bytes, and a used one is one chunk — so the real cost is bounded by chunks actually
/// touched, which matters on memory-tight handhelds.
Buffer m_buffer[8];
u32 m_nextBuffer = 0;
std::thread m_readThread;
+7 -1
View File
@@ -1460,7 +1460,13 @@ protected:
std::string m_name = "Unknown";
FeatureSupport m_features;
u32 m_max_texture_size = 0;
RuntimeGpuProfile m_runtime_gpu_profile = RuntimeGpuProfile::Adreno;
// ★ Unknown, NOT Adreno. Defaulting to a real vendor meant every backend that never calls
// SetRuntimeGPUProfile (Vulkan, Metal, DX12 — none of them did) silently identified as Adreno,
// and so did desktop OpenGL on anything not-Mali. That made IsAdrenoGPUProfile() fire
// Adreno-only workarounds on Apple Silicon, and made IsMaliGPUProfile() permanently false under
// Vulkan — which quietly disabled the Tekken 5 MediaTek-Mali GameDB fix on our default renderer.
// Unknown means "no vendor quirks", which is the only safe thing to assume before detection.
RuntimeGpuProfile m_runtime_gpu_profile = RuntimeGpuProfile::Unknown;
// Per-vendor mobile GPU identity + GS tuning (pool sizes / ages / constrained), resolved from the
// GPU-profile system (sashkinbro/EmuCoreX). Drives texture/target pool sizing on Android below.
MobileGpuIdentity m_mobile_gpu_identity;
@@ -211,6 +211,8 @@ const char* GpuProfileDetector::RuntimeProfileToString(RuntimeGpuProfile value)
return "Adreno";
case RuntimeGpuProfile::Xclipse:
return "Xclipse";
case RuntimeGpuProfile::Apple:
return "Apple";
case RuntimeGpuProfile::Unknown:
default:
return "Unknown";
+6
View File
@@ -24,6 +24,12 @@ enum class RuntimeGpuProfile : u8
Adreno,
PowerVR,
Xclipse,
/// Apple Silicon (M-series / A-series). A TBDR like the mobile parts, but it is NOT one of
/// them and must never inherit their workarounds — before this existed, desktop GL resolved
/// anything not-Mali to Adreno, so an M2 ran Adreno-only paths (reported by bmd: "GL: Adreno -
/// routing depth feedback through the depth sampler"). Distinct from Unknown so the tiler-ness
/// can be acted on deliberately later rather than by accident.
Apple,
};
enum class MobileGpuArchitecture : u8
+113 -2
View File
@@ -15,6 +15,8 @@
#endif
#include "Host.h"
#include "PerformanceMetrics.h"
#include "common/Console.h" // @@ANDROID_STALEFRAMES@@ diagnostic
#include "common/HostSys.h" // GetCPUTicks — present-cap pacer
#include "pcsx2/Config.h"
#include "VMManager.h"
@@ -710,11 +712,122 @@ void GSRenderer::VSync(u32 field, bool registers_written, bool idle_frame)
}
}
// ★ Manual frame skip and the max-presented-FPS cap. Both were fully implemented in GS.cpp with
// JNI setters wired to live UI controls, and both had ZERO readers — GSGetManualFrameSkip() and
// GSGetMaxPresentInterval() were never called, so the in-game "Frame Skip" picker (0..5) and the
// FPS cap silently did nothing. The GS.cpp comments named this exact function as the reader, so
// the consumer was lost rather than never written. Restored here.
//
// Both skip only the PRESENT: Merge() and the rest of the frame still run below, so emulation
// and GS state are untouched and only display rate changes.
// Set when the user ASKED for a dropped present, so the stale-frame diagnostic below doesn't
// report their own frame-skip/FPS-cap settings as a fault.
bool deliberate_present_skip = false;
{
const u32 manual_skip = GSGetManualFrameSkip();
if (manual_skip > 0)
{
// Present 1 frame in every (manual_skip + 1).
m_manual_frameskip_phase = (m_manual_frameskip_phase + 1) % (manual_skip + 1);
if (m_manual_frameskip_phase != 0)
{
skip_frame = true;
deliberate_present_skip = true;
}
}
else
{
m_manual_frameskip_phase = 0;
}
}
if (!skip_frame && !GSGetPresentCapSuspended())
{
// Accumulator pacer, not a simple "too soon?" test: advancing the deadline by exactly one
// interval holds the requested AVERAGE rate even when it isn't a whole division of the
// source (47 or 55 fps work, not just 30/20/15). Resynchronise when we fall more than one
// interval behind, so a hitch can't bank credit and then burst.
const u64 interval = GSGetMaxPresentInterval();
if (interval > 0)
{
const u64 now = GetCPUTicks();
if (m_next_present_deadline == 0 || now + interval < m_next_present_deadline)
m_next_present_deadline = now; // first frame, or the clock jumped backwards
if (now < m_next_present_deadline)
{
skip_frame = true;
deliberate_present_skip = true;
}
else if ((now - m_next_present_deadline) > interval)
m_next_present_deadline = now + interval; // far behind: restart the cadence
else
m_next_present_deadline += interval;
}
else
{
m_next_present_deadline = 0;
}
}
const bool blank_frame = !Merge(field);
// ★ @@ANDROID_STALEFRAMES@@ — diagnostic for "the picture freezes but emulation keeps running".
// Measured on a Retroid Pocket 6: SurfaceFlinger presents steadily at 120 Hz straight through
// the freeze (worst present gap across a whole session was 142 ms, frame count never dipped),
// so frame DELIVERY is healthy and a stale image can only come from frame PRODUCTION here.
// There are exactly three ways this function fails to put something new on screen: the
// duplicate-frame skip above, the device's present throttle, and Merge() yielding nothing.
// Logged only when a RUN of such frames ends, and only if it was long enough to be visible, so
// a healthy frame costs one branch. GS thread only, hence plain statics.
{
const bool stale = !deliberate_present_skip &&
(skip_frame || blank_frame || g_gs_device->ShouldSkipPresentingFrame());
static u32 s_stale_run = 0, s_stale_skipdup = 0, s_stale_blank = 0, s_stale_throttle = 0;
if (stale)
{
s_stale_run++;
if (skip_frame)
s_stale_skipdup++;
if (blank_frame)
s_stale_blank++;
if (g_gs_device->ShouldSkipPresentingFrame())
s_stale_throttle++;
}
else
{
// ~10 frames is the shortest run a person could notice; below that it is normal churn.
if (s_stale_run >= 10)
{
Console.Warning("@@ANDROID_STALEFRAMES@@ run=%u frames (skipdup=%u blank=%u "
"throttle=%u) fpsmethod=%d",
s_stale_run, s_stale_skipdup, s_stale_blank, s_stale_throttle,
static_cast<int>(PerformanceMetrics::GetInternalFPSMethod()));
}
s_stale_run = 0;
s_stale_skipdup = 0;
s_stale_blank = 0;
s_stale_throttle = 0;
}
}
m_last_draw_n = s_n;
m_last_transfer_n = s_transfer_n;
// ★ Age the texture pool on EVERY frame, including skipped presents. AgePool() is what trims
// stale textures and it is the ONLY place GSDevice::m_frame advances, so parking it on the skip
// path had two compounding costs:
// - the pool stops being trimmed and grows to its limit, at which point FetchSurface starts
// handing back textures recycled in the current frame instead of fresh ones;
// - m_frame freezes, so every texture recycled during the run looks "used this frame" and the
// fallback above is taken even more often.
// Reported as "the game runs slow in some scenes, and changing ANY on-screen-display option
// makes it full speed again" — that is not the OSD, it is the settings apply calling
// g_gs_device->PurgePool() (GS.cpp:334/:1029) and emptying the bloated pool. With
// SkipDuplicateFrames on by default, plus the frame-skip and FPS-cap paths above, skipped
// presents are common, so the pool could go a long time without aging. Aging is about texture
// lifetime, not presentation, so it belongs on both paths.
if (!idle_frame)
g_gs_device->AgePool();
// Skip presentation when running uncapped while vsync is on.
if (skip_frame || g_gs_device->ShouldSkipPresentingFrame())
{
@@ -725,8 +838,6 @@ void GSRenderer::VSync(u32 field, bool registers_written, bool idle_frame)
}
else
{
if (!idle_frame)
g_gs_device->AgePool();
g_perfmon.EndFrame(idle_frame);
+5
View File
@@ -19,6 +19,11 @@ private:
std::string m_snapshot;
u32 m_dump_frames = 0;
u32 m_skipped_duplicate_frames = 0;
/// Manual frame-skip phase counter (Android "Frame Skip" 0..5). GS thread only.
u32 m_manual_frameskip_phase = 0;
/// Accumulator pacer for the max-presented-FPS cap: the tick deadline at which the next
/// present is due. 0 = not started/disabled. GS thread only.
u64 m_next_present_deadline = 0;
// Tracking draw counters for idle frame detection.
u64 m_last_draw_n = 0;
+11 -1
View File
@@ -4716,7 +4716,17 @@ void GSTextureCache::InvalidateContainedTargets(u32 start_bp, u32 end_bp, u32 wr
const u32 end_width = write_bw * 64;
const u32 end_height = ((end_page_offset / std::max(write_bw, 1U)) * GSLocalMemory::m_psm[write_psm].pgs.y) + GSLocalMemory::m_psm[write_psm].pgs.y;
const GSVector4i r = GSVector4i(0, 0, end_width, end_height);
const GSVector4i invalidate_r = TranslateAlignedRectByPage(t, start_bp, write_psm, write_bw, r, false).rintersect(t->m_valid); // it is invalidation but we need a real rect.
// is_invalidation=TRUE. This used to pass false with the comment "it is invalidation
// but we need a real rect" — but when the source and destination page widths differ
// there IS no real rect: the source region is a staircase in destination space, so
// TranslateAlignedRectByPage bails and returns zero(). An empty invalidate_r then
// makes AddDirtyRectTarget below a no-op (a GS memory clear silently fails to
// invalidate the target it overlaps) and, when the dirty rect misses, lets control
// fall through to the delete path — destroying a target that was never read back.
// Passing true takes the conservative whole-row band instead: a SUPERSET, which is
// exactly what dirtying wants, and strictly safer than deleting. This is also the
// dominant emitter of the "Uneven pages mess up" spam.
const GSVector4i invalidate_r = TranslateAlignedRectByPage(t, start_bp, write_psm, write_bw, r, true).rintersect(t->m_valid);
if (offset == 0 || dirty_rect.rempty() || !dirty_rect.rintersect(invalidate_r).rempty())
{
+14 -1
View File
@@ -820,6 +820,10 @@ bool GSDeviceOGL::CheckFeatures()
// Matched on the renderer, not the vendor: Apple silicon reports the vendor of whoever
// wrote the driver ("Mesa" under Asahi, "Apple Inc." on macOS), while an Intel Mac reports
// vendor "Apple Inc." with an AMD or Intel GPU. The renderer names the actual GPU.
//
// Apple silicon is a TBDR, but it is not a mobile-vendor part and must not inherit their
// workarounds — detected explicitly so it resolves to its own profile instead of falling
// through to the old not-Mali-therefore-Adreno guess.
else if (std::strstr(renderer_str, "Apple"))
{
Console.WriteLn(Color_StrongCyan, "GL: Apple GPU detected.");
@@ -848,7 +852,16 @@ bool GSDeviceOGL::CheckFeatures()
bool use_adreno_profile = IsAdrenoGPUProfile();
bool use_powervr_profile = IsPowerVRGPUProfile();
#else
SetRuntimeGPUProfile(vendor_id_mali ? RuntimeGpuProfile::Mali : RuntimeGpuProfile::Adreno);
// ★ Was `vendor_id_mali ? Mali : Adreno`, which claimed ADRENO for every non-Mali desktop GPU —
// NVIDIA, AMD, Intel and Apple Silicon all identified as Adreno. The locals below were already
// correct (real per-vendor detection), so only the member misfired, which is why it hid: it
// surfaced as Adreno-only workarounds engaging on an M2 (reported by bmd: "GL: Adreno - routing
// depth feedback through the depth sampler"). Mirror the locals instead of guessing, and fall
// back to Unknown — desktop GPUs are not tilers and want none of the mobile vendor paths.
SetRuntimeGPUProfile(vendor_id_mali ? RuntimeGpuProfile::Mali :
vendor_id_adreno ? RuntimeGpuProfile::Adreno :
vendor_id_apple ? RuntimeGpuProfile::Apple :
RuntimeGpuProfile::Unknown);
bool use_mali_profile = vendor_id_mali;
bool use_adreno_profile = vendor_id_adreno;
bool use_powervr_profile = false;
+21
View File
@@ -3272,6 +3272,14 @@ bool GSDeviceVK::CheckFeatures()
// ro.soc.* props already folded into the profile hints (no new JNI needed).
const GpuProfileSelection mobile_profile = GpuProfileDetector::Resolve(
GSConfig.AndroidGpuProfileOverride, std::string_view(), m_device_properties.deviceName);
// ★ Vulkan resolved mobile_profile and pushed every OTHER piece of it into the device
// (MediaTek SoC, GPU identity, GS tuning) but never the runtime profile itself, so
// IsMaliGPUProfile()/IsAdrenoGPUProfile() answered from the default for the entire Vulkan
// lifetime. Consequence: ApplyAndroidGameDBOverrides()'s `IsMaliGPUProfile() && IsMediaTekSoC()`
// gate could never pass, so the Tekken 5 duplicated-framebuffer fix was dead on the renderer we
// default to on Android, and the profile printed in VK logs was whatever the default happened
// to be rather than the detected GPU.
SetRuntimeGPUProfile(mobile_profile.runtime_profile);
SetMediaTekSoC(mobile_profile.is_mediatek_soc);
force_xclipse_profile = (mobile_profile.override_mode == GpuProfileOverride::Xclipse) ||
(mobile_profile.runtime_profile == RuntimeGpuProfile::Xclipse);
@@ -6287,7 +6295,20 @@ VkPipeline GSDeviceVK::GetTFXPipeline(const PipelineSelector& p)
if (it != m_tfx_pipelines.end())
return it->second;
// A cache miss compiles SYNCHRONOUSLY on the GS thread, freezing the picture for as long as the
// driver takes. Normally invisible (a few ms, spread out), but fast-forward runs through content
// at 4x+ and hits a burst of new variants at once — which is the other half of "turn fast-forward
// off and it hangs for a few seconds". Timed so the stall is measurable instead of inferred;
// only slow compiles are logged, so this costs nothing in the common case.
const Common::Timer::Value tfx_compile_start = Common::Timer::GetCurrentValue();
VkPipeline pipeline = CreateTFXPipeline(p);
const double tfx_compile_ms =
Common::Timer::ConvertValueToMilliseconds(Common::Timer::GetCurrentValue() - tfx_compile_start);
if (tfx_compile_ms >= 20.0)
{
Console.Warning("@@ANDROID_TFXCOMPILE@@ %.1f ms for one pipeline (n=%u since last cache flush)",
tfx_compile_ms, m_tfx_pipeline_compile_counter + 1);
}
m_tfx_pipelines.emplace(p, pipeline);
// Persist the pipeline cache every N new compiles so an Android OOM-kill
+20 -2
View File
@@ -348,7 +348,7 @@ VKShaderCache::VKShaderCache() = default;
VKShaderCache::~VKShaderCache()
{
CloseShaderCache();
FlushPipelineCache();
FlushPipelineCache(true); // teardown: never skip, this is the last chance to persist
ClosePipelineCache();
}
@@ -602,11 +602,29 @@ bool VKShaderCache::ReadExistingPipelineCache()
return true;
}
bool VKShaderCache::FlushPipelineCache()
bool VKShaderCache::FlushPipelineCache(bool force)
{
if (m_pipeline_cache == VK_NULL_HANDLE || !m_pipeline_cache_dirty || m_pipeline_cache_filename.empty())
return false;
// ★ Rate-limited, because this whole function is synchronous ON THE GS THREAD: it re-serialises
// the ENTIRE cache (measured at 777 KB on a Retroid Pocket 6) and writes it to disk, and the
// emulator is frozen for the duration. GetTFXPipeline triggers it every 256 new compiles with
// no time bound, and fast-forward blasts through content at 4x+ — hitting many new pipeline
// variants in a burst, crossing the threshold repeatedly, and stalling the picture for seconds
// right as the user drops back to normal speed. That is the reported "turn FF off and it hangs
// for a few seconds", and it is intermittent precisely because it depends on whether the burst
// crossed the counter. Losing a flush costs nothing but recompiling those pipelines on the next
// cold start, so throttling is strictly a win; teardown passes force=true.
static constexpr std::chrono::seconds MIN_FLUSH_INTERVAL{120};
const auto now = std::chrono::steady_clock::now();
if (!force && m_last_pipeline_cache_flush.time_since_epoch().count() != 0 &&
(now - m_last_pipeline_cache_flush) < MIN_FLUSH_INTERVAL)
{
return false;
}
m_last_pipeline_cache_flush = now;
size_t data_size;
VkResult res =
vkGetPipelineCacheData(GSDeviceVK::GetInstance()->GetDevice(), m_pipeline_cache, &data_size, nullptr);
+6 -1
View File
@@ -7,6 +7,7 @@
#include "common/HashCombine.h"
#include <chrono>
#include <cstdio>
#include <memory>
#include <optional>
@@ -27,7 +28,9 @@ public:
VkPipelineCache GetPipelineCache(bool set_dirty = true);
/// Writes pipeline cache to file, saving all newly compiled pipelines.
bool FlushPipelineCache();
/// Serialises the pipeline cache to disk. This is SYNCHRONOUS and runs on the GS thread, so it
/// is rate-limited: pass force=true only where a missed flush actually loses data (teardown).
bool FlushPipelineCache(bool force = false);
VkShaderModule GetVertexShader(std::string_view shader_code);
VkShaderModule GetFragmentShader(std::string_view shader_code);
@@ -97,6 +100,8 @@ private:
VkPipelineCache m_pipeline_cache = VK_NULL_HANDLE;
bool m_pipeline_cache_dirty = false;
/// When the cache was last serialised, so the synchronous GS-thread write can be rate-limited.
std::chrono::steady_clock::time_point m_last_pipeline_cache_flush{};
};
extern std::unique_ptr<VKShaderCache> g_vulkan_shader_cache;
+21 -2
View File
@@ -877,14 +877,33 @@ VkResult VKSwapChain::AcquireNextImage()
const bool stats = s_stats_enabled.load(std::memory_order_relaxed);
const Common::Timer::Value t_start = stats ? Common::Timer::GetCurrentValue() : 0;
const VkResult res = vkAcquireNextImageKHR(GSDeviceVK::GetInstance()->GetDevice(), m_swap_chain, UINT64_MAX,
m_semaphores[m_current_semaphore].available_semaphore, VK_NULL_HANDLE, &m_current_image);
// ★ Bounded, NOT UINT64_MAX. On Android the surface can be destroyed under us (background,
// rotate, fold) while the GS thread is ALREADY inside this call — at which point an infinite
// timeout waits forever on a window nothing will ever present to. Everything that could
// rebuild the swapchain is marshalled through the CPU thread, and the CPU thread is itself
// blocked waiting on this GS thread, so the entire VM wedges: every thread asleep, 0% CPU,
// and no log output ever again. That is the observed "froze the whole emulator" / "sometimes
// it never unpauses" failure. A finite timeout turns a permanent deadlock into a recoverable
// one — BeginPresent already handles VK_ERROR_SURFACE_LOST_KHR by recreating the surface.
// 2 s is orders of magnitude beyond any legitimate acquire (a stalled compositor is tens of
// ms), so this cannot trip during normal rendering.
static constexpr u64 ACQUIRE_TIMEOUT_NS = 2'000'000'000ull;
VkResult res = vkAcquireNextImageKHR(GSDeviceVK::GetInstance()->GetDevice(), m_swap_chain,
ACQUIRE_TIMEOUT_NS, m_semaphores[m_current_semaphore].available_semaphore, VK_NULL_HANDLE,
&m_current_image);
if (stats)
{
const double elapsed_ms =
Common::Timer::ConvertValueToMilliseconds(Common::Timer::GetCurrentValue() - t_start);
NoteAcquire(elapsed_ms, res);
}
if (res == VK_TIMEOUT || res == VK_NOT_READY)
{
Console.Error("VK: vkAcquireNextImageKHR timed out after %llu ms — treating the surface as "
"lost so the GS thread cannot deadlock the VM.",
static_cast<unsigned long long>(ACQUIRE_TIMEOUT_NS / 1'000'000ull));
res = VK_ERROR_SURFACE_LOST_KHR;
}
m_image_acquire_result = res;
return res;
}
+19
View File
@@ -373,6 +373,25 @@ __ri void ImGuiManager::DrawPerformanceOverlay(float& position_y, float scale, f
const float speed = PerformanceMetrics::GetSpeed();
s_speed_line.clear();
#if defined(__ANDROID__)
// Display-rate limiters, FIRST in the line so they read as a qualifier on the FPS that
// follows rather than a stray value. Shown only while ACTIVE, so they cost nothing and
// need no toggle of their own. Both lower the ON-SCREEN rate while emulation keeps
// running full speed, so without a label a capped display is indistinguishable from the
// emulator running badly — which is a good part of why these looked broken.
if (const u32 fps_cap = GSGetMaxPresentFps(); fps_cap > 0)
{
s_speed_line.append_format("FPS CAP: {}", fps_cap);
// The cap is deliberately bypassed while fast-forwarding so the speed-up stays
// visible; say so, or it looks like the cap is simply being ignored.
if (GSGetPresentCapSuspended())
s_speed_line.append(" (off: FF)");
}
if (const u32 skip = GSGetManualFrameSkip(); skip > 0)
s_speed_line.append_format("{}SKIP: {}", s_speed_line.empty() ? "" : " | ", skip);
#endif
if (GSConfig.OsdShowFPS)
{
switch (PerformanceMetrics::GetInternalFPSMethod())
+41 -1
View File
@@ -393,9 +393,30 @@ const std::string& VMManager::GetCurrentELF()
return s_elf_path;
}
// Identity of the CPU thread, for IsOnCPUThread(). The CPU thread owns EmuConfig and is the
// sole producer into the MTGS ring, so a good deal of core state may only be touched from it;
// this backs the dev asserts that catch a frontend calling in from its UI thread instead of
// marshalling via Host::RunOnCPUThread().
static std::atomic<std::thread::id> s_cpu_thread_id{};
bool VMManager::Internal::IsOnCPUThread()
{
const std::thread::id owner = s_cpu_thread_id.load(std::memory_order_acquire);
// Permissive before CPUThreadInitialize() and after CPUThreadShutdown(): during startup and
// teardown there is no CPU thread to marshal onto, and the frontends legitimately drive core
// setup inline (e.g. VMManager::ApplySettings before the first Initialize). Test harnesses
// which never register a CPU thread at all are covered by the same allowance.
if (owner == std::thread::id())
return true;
return owner == std::this_thread::get_id();
}
bool VMManager::Internal::CPUThreadInitialize()
{
Threading::SetNameOfCurrentThread("CPU Thread");
s_cpu_thread_id.store(std::this_thread::get_id(), std::memory_order_release);
PerformanceMetrics::SetCPUThread(Threading::ThreadHandle::GetForCallingThread());
PerformanceMetrics::AdpfRegisterCallingThread(); // ADPF: hint the EE thread's core (Android)
@@ -495,6 +516,9 @@ void VMManager::Internal::CPUThreadShutdown()
Log::SetFileOutputLevel(LOGLEVEL_NONE, std::string());
R5900SymbolImporter.ShutdownWorkerThread();
// Last: everything above still runs as the CPU thread and may hit an IsOnCPUThread() assert.
s_cpu_thread_id.store(std::thread::id(), std::memory_order_release);
}
u64 VMManager::Internal::GetPerformanceClusterAffinityMask()
@@ -809,6 +833,19 @@ void VMManager::ApplyGameFixes()
void VMManager::ApplySettings()
{
// Must run on the CPU thread. This move-constructs and then reconstructs the whole global
// EmuConfig — every std::string in it is freed and reallocated, and there is a window where
// EmuConfig is moved-from — and CheckForCPUConfigChanges() below goes on to call
// ClearCPUExecutionCaches(), i.e. it resets the recompiler code caches. That function
// documents that it expects to be re-entered from inside the running CPU ("we're still
// executing the cpu when this function is called"), which is only true on the CPU thread.
// Called from a UI thread instead, it frees the JIT code buffer under an executing JIT.
//
// Note the WaitVU/WaitGS below is a *drain*, not a park: it does not stop the EE, so it is no
// substitute for being on the right thread.
pxAssertMsg(Internal::IsOnCPUThread(),
"VMManager::ApplySettings() off the CPU thread — marshal via Host::RunOnCPUThread()");
Console.WriteLn("Applying settings...");
// If we're running, ensure the threads are synced.
@@ -3476,7 +3513,10 @@ void VMManager::WarnAboutUnsafeSettings()
append(ICON_FA_PAINTBRUSH,
TRANSLATE_SV("VMManager", "Blending Accuracy is below Basic, this may break effects in some games."));
}
if (EmuConfig.GS.HWDownloadMode > GSHardwareDownloadMode::EnabledForceFull)
// NOT a relational comparison: Asynchronous (5) was appended after Disabled (4), so the
// enum is no longer ordered by accuracy and `> EnabledForceFull` wrongly flags Async while
// the ordering trap is exactly what the rest of the port was rewritten to avoid.
if (!IsHardwareDownloadReadbackEnabled(EmuConfig.GS.HWDownloadMode))
{
append(ICON_FA_DOWNLOAD,
TRANSLATE_SV("VMManager", "Hardware Download Mode is not set to Accurate, this may break rendering in some games."));