Android 2.6.5: Adreno Vulkan default, low-latency default, UI sounds, RA client hardening, GameDB

Rendering
- Auto renderer now resolves to Vulkan HW on Adreno (OpenGL elsewhere).
- Mobile hardware ROV (Phase 0): tile-native depth feedback behind the ROV toggle.

Performance & input
- Low Latency frame pacing is the default on capable devices, with a one-time
  migration for existing installs; low-end devices keep the queued pacing.
- Reduce Android input latency and improve input handling (PR #403, Splaser).
- Experimental CPU clock hint (ADPF) toggle in Performance settings (default off).

Audio & UI
- Pop-up open/close sound cues (info, hardcore confirm, patches & cheats).
- Alternating controller navigation / slider tick sounds.

RetroAchievements
- Inject the RA client version from a build-time secret kept out of public source,
  with a stock-PCSX2 fallback (no hardcore) for secret-less builds. Applies to the
  iOS client token too. Prevents third parties from copying our User-Agent.

Game compatibility
- Delta Force: Black Hawk Down (SLUS-21124 / SLES-53299) GameDB fixes
  (PR #401, XDarkFallenX).
This commit is contained in:
jpolo1224
2026-07-24 02:40:17 -04:00
parent 6e5569a794
commit 8e23439b26
37 changed files with 1026 additions and 126 deletions
+4
View File
@@ -147,3 +147,7 @@ CMakeSettings.json
# Achievement sounds are shipped assets, not build output — keep them tracked
!platforms/android/app/src/main/assets/resources/sounds/**/*.wav
# RetroAchievements client User-Agent secret — the real pinned version is injected
# from this header at build time and must NEVER be committed (spoofing prevention).
ra_ua_secret.h
@@ -13,6 +13,19 @@
# Generated from the Android GameIndex delta vs bin plus the former runtime
# armsx2_overrides.yaml. Regenerate via devs/bmdhacks tooling; do not bulk
# hand-edit.
# Delta Force: Black Hawk Down — GameDB fixes (PR #401, XDarkFallenX)
SLUS-21124:
gameFixes:
- InstantDMAHack
- BlitInternalFPSHack
gsHWFixes:
hwDownloadMode: 3
SLES-53299:
gameFixes:
- InstantDMAHack
- BlitInternalFPSHack
gsHWFixes:
hwDownloadMode: 3
PAPX-90522:
clampModes:
eeClampMode: 3
+2
View File
@@ -493,6 +493,8 @@ static __fi void VSyncStart(u64 sCycle)
// Don't bother throttling if we're going to pause.
if (!VMManager::Internal::IsExecutionInterrupted())
VMManager::Internal::Throttle();
else
PerformanceMetrics::AdpfPauseFrameWork(); // interrupted → no Throttle → drop the ADPF period so the resume report excludes the pause
gsPostVsyncStart(); // MUST be after framelimit; doing so before causes funk with frame times!
+13 -7
View File
@@ -282,6 +282,12 @@ u32 GSUtil::GetChannelMask(u32 spsm, u32 fbmsk)
return mask;
}
#if defined(__ANDROID__)
// Set by the Android app from the GL_RENDERER string (NativeApp.setPreferVulkan): true steers the
// Auto renderer resolution to Vulkan HW on Adreno; false keeps OpenGL HW (Mali/Xclipse/others).
bool g_gs_android_prefer_vk = false;
#endif
GSRendererType GSUtil::GetPreferredRenderer()
{
// Memorize the value, so we don't keep re-querying it.
@@ -298,13 +304,13 @@ GSRendererType GSUtil::GetPreferredRenderer()
// 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)
// Android: Auto resolves to Vulkan HW on Adreno (the tile-memory framebuffer-fetch fast
// path), OpenGL HW elsewhere (Mali runs GL_ARM_shader_framebuffer_fetch; Xclipse has no
// working VK fbfetch). The app sets g_gs_android_prefer_vk from the GL_RENDERER string
// before the GS starts. This only steers Auto — an explicit Vulkan/OpenGL/SW pick still wins.
#if defined(ENABLE_VULKAN) && defined(ENABLE_OPENGL)
preferred_renderer = g_gs_android_prefer_vk ? GSRendererType::VK : GSRendererType::OGL;
#elif defined(ENABLE_OPENGL)
preferred_renderer = GSRendererType::OGL;
#elif defined(ENABLE_VULKAN)
preferred_renderer = GSRendererType::VK;
+15 -2
View File
@@ -15,6 +15,8 @@
#include "common/HashCombine.h"
#include "common/SmallString.h"
#include <atomic>
#include "fmt/format.h"
#include <cinttypes>
@@ -430,7 +432,14 @@ GSVector4i GSTextureCache::TranslateAlignedRectByPage(u32 tbp, u32 tebp, u32 tbw
// The width is mismatched to the page.
if (!is_invalidation && GSConfig.UserHacks_TextureInsideRt < GSTextureInRtMode::MergeTargets)
{
DevCon.Warning("Uneven pages mess up sbp %x dbp %x spgw %d dpgw %d src fmt %d dst fmt %d src_rect %d, %d, %d, %d draw %lld", sbp, tbp, src_pgw, dst_pgw, spsm, tpsm, in_rect.x, in_rect.y, in_rect.z, in_rect.w, GSRendererHW::GetInstance()->s_n);
// Rate-limited: on render-target churn this fires thousands of times/second (Rogue Galaxy:
// 5000+ per session) and a synchronous log per hit is itself a cost that drowns the log.
// Lock-free geometric backoff — atomic, not a plain static, since the TC can be reached
// off the GS back thread under pipelined GS. Each emitted line carries the running total.
static std::atomic<u64> s_uneven_hits{0};
const u64 uneven_n = s_uneven_hits.fetch_add(1, std::memory_order_relaxed) + 1;
if ((uneven_n & (uneven_n - 1)) == 0)
DevCon.Warning("Uneven pages mess up (#%llu) sbp %x dbp %x spgw %d dpgw %d src fmt %d dst fmt %d src_rect %d, %d, %d, %d draw %lld", uneven_n, sbp, tbp, src_pgw, dst_pgw, spsm, tpsm, in_rect.x, in_rect.y, in_rect.z, in_rect.w, GSRendererHW::GetInstance()->s_n);
return GSVector4i::zero();
}
@@ -573,7 +582,11 @@ GSVector4i GSTextureCache::TranslateAlignedRectByPage(u32 tbp, u32 tebp, u32 tbw
// Results won't be square, if it's not invalidation, it's a texture, which is problematic to translate, so let's not (FIFA 2005).
if (!is_invalidation)
{
DevCon.Warning("Uneven pages mess up sbp %x dbp %x spgw %d dpgw %d", sbp, tbp, src_pgw, dst_pgw);
// Rate-limited (see the sibling site above): lock-free geometric backoff.
static std::atomic<u64> s_uneven_hits2{0};
const u64 n = s_uneven_hits2.fetch_add(1, std::memory_order_relaxed) + 1;
if ((n & (n - 1)) == 0)
DevCon.Warning("Uneven pages mess up (#%llu) sbp %x dbp %x spgw %d dpgw %d", n, sbp, tbp, src_pgw, dst_pgw);
return GSVector4i::zero();
}
+70 -7
View File
@@ -730,6 +730,21 @@ bool GSDeviceVK::CreateDevice(VkSurfaceKHR surface, bool enable_validation_layer
if (m_optional_extensions.vk_ext_rasterization_order_attachment_access)
{
rasterization_order_access_feature.rasterizationOrderColorAttachmentAccess = VK_TRUE;
// Tile-native ordered DEPTH feedback ("mobile ROV"): the depth aspect of ROAA is an
// OPTIONAL sub-feature — a driver may expose the extension and color access yet not
// depth. The full feature reconcile (ProcessDeviceExtensions) runs only AFTER
// vkCreateDevice, too late to gate this, and requesting an unsupported feature fails
// device creation with VK_ERROR_FEATURE_NOT_PRESENT. So probe the depth bit up-front
// and only request it when the device actually advertises it.
VkPhysicalDeviceRasterizationOrderAttachmentAccessFeaturesEXT roaa_probe = {
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_FEATURES_EXT};
VkPhysicalDeviceFeatures2 roaa_probe2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &roaa_probe};
vkGetPhysicalDeviceFeatures2(m_physical_device, &roaa_probe2);
m_optional_extensions.vk_ext_roaa_depth = (roaa_probe.rasterizationOrderDepthAttachmentAccess == VK_TRUE);
if (m_optional_extensions.vk_ext_roaa_depth)
rasterization_order_access_feature.rasterizationOrderDepthAttachmentAccess = VK_TRUE;
Vulkan::AddPointerToChain(&device_info, &rasterization_order_access_feature);
}
if (m_optional_extensions.vk_ext_attachment_feedback_loop_layout)
@@ -869,6 +884,9 @@ bool GSDeviceVK::ProcessDeviceExtensions()
m_optional_extensions.vk_ext_provoking_vertex &= (provoking_vertex_features.provokingVertexLast == VK_TRUE);
m_optional_extensions.vk_ext_rasterization_order_attachment_access &=
(rasterization_order_access_feature.rasterizationOrderColorAttachmentAccess == VK_TRUE);
// Depth ROAA is meaningless (and its subpass/pipeline flags invalid) without the color
// extension being usable; keep them consistent after the post-create reconcile.
m_optional_extensions.vk_ext_roaa_depth &= m_optional_extensions.vk_ext_rasterization_order_attachment_access;
m_optional_extensions.vk_ext_attachment_feedback_loop_layout &=
(attachment_feedback_loop_feature.attachmentFeedbackLoopLayout == VK_TRUE);
@@ -897,6 +915,10 @@ bool GSDeviceVK::ProcessDeviceExtensions()
if (m_device_properties.vendorID == 0x13B5u && m_optional_extensions.vk_khr_driver_properties &&
std::string_view(m_device_driver_properties.driverInfo).find("r44p1") != std::string_view::npos)
{
// NOTE: this layout disable alone did NOT stop the DEVICE_LOST — the per-primitive barrier /
// fbfetch path it falls back to lowers to the same faulting in-tile silicon. The real fix
// forces r44p1 onto the RT-copy blend path by ALSO disabling texture_barrier; see the matching
// "Mali r44p1:" block where m_features.texture_barrier is resolved.
Console.WriteLn("Mali r44p1: disabling attachment-feedback-loop blend path (DEVICE_LOST workaround).");
m_optional_extensions.vk_ext_attachment_feedback_loop_layout = false;
}
@@ -1976,10 +1998,17 @@ VkRenderPass GSDeviceVK::CreateCachedRenderPass(RenderPassCacheKey key)
num_attachments++;
}
const VkSubpassDescriptionFlags subpass_flags =
VkSubpassDescriptionFlags subpass_flags =
(key.color_feedback_loop && m_optional_extensions.vk_ext_rasterization_order_attachment_access) ?
VK_SUBPASS_DESCRIPTION_RASTERIZATION_ORDER_ATTACHMENT_COLOR_ACCESS_BIT_EXT :
0;
// Mobile ordered depth feedback: on the framebuffer_fetch path the depth self-dependency above
// is skipped, so declare ordered depth access here to make the in-tile subpassLoad of depth
// coherent (mirror of the colour flag). Gated on depth_feedback (HWROV) so it never fires when
// the toggle is off; the pipeline built for this pass sets the matching depth-stencil flag.
if (key.depth_sampling && m_features.depth_feedback && m_features.framebuffer_fetch &&
m_optional_extensions.vk_ext_roaa_depth)
subpass_flags |= VK_SUBPASS_DESCRIPTION_RASTERIZATION_ORDER_ATTACHMENT_DEPTH_ACCESS_BIT_EXT;
const VkSubpassDescription subpass = {subpass_flags, VK_PIPELINE_BIND_POINT_GRAPHICS, num_subpass_inputs,
num_subpass_inputs ? input_reference.data() : nullptr, color_reference_ptr ? 1u : 0u,
color_reference_ptr ? color_reference_ptr : nullptr, nullptr, depth_reference_ptr, 0, nullptr};
@@ -3331,6 +3360,21 @@ bool GSDeviceVK::CheckFeatures()
m_features.framebuffer_fetch = m_optional_extensions.vk_ext_rasterization_order_attachment_access;
m_features.texture_barrier = true;
}
// Mali r44p1: the attachment-feedback-loop-layout disable in CreateDevice only swapped the
// RT-as-texture LAYOUT/descriptor — it never removed the in-tile RT self-read itself, so Maximum
// blending kept sampling the colour attachment IN-TILE (fbfetch/ROAA subpassLoad OR the
// texture-barrier feedback loop; both lower to the same faulting tile-feedback silicon) and the
// device kept dying (VK_ERROR_DEVICE_LOST at vkWaitForFences, Rogue Galaxy). Force texture_barrier
// off so accurate blending routes through the RT-COPY path (draw_rt_clone) instead — the
// "fbfetch needs barriers" line below (framebuffer_fetch &= texture_barrier) then also turns
// fbfetch off, so the RT is only ever read from a SEPARATE copy, never in-tile. Slower but stable,
// and strictly r44p1-only (no other GPU, not even other Mali-G615 units, is touched).
if (is_mali_vk && m_optional_extensions.vk_khr_driver_properties &&
std::string_view(m_device_driver_properties.driverInfo).find("r44p1") != std::string_view::npos)
{
Console.WriteLn("Mali r44p1: forcing RT-copy blend path (texture_barrier off) — in-tile self-read faults.");
m_features.texture_barrier = false;
}
m_features.multidraw_fb_copy = false;
m_features.broken_point_sampler = false;
@@ -3443,13 +3487,24 @@ bool GSDeviceVK::CheckFeatures()
m_features.line_expand =
(m_device_features.wideLines && limits.lineWidthRange[0] <= f_upscale && limits.lineWidthRange[1] >= f_upscale);
// Same class of issue as framebuffer_fetch above: the upstream-sync SW-Z
// depth feedback (depth bound as input attachment + shader depth test/write)
// is untested on the Android mobile GPUs and the pre-sync core never used it.
// Force it off on Android so the renderer takes the well-tested avoid/copy
// fallbacks (same as D3D11); desktop keeps canonical feedback-loop behavior.
// Mobile tile-native ordered depth feedback ("mobile ROV"), opt-in via HWROV. Reads the
// depth buffer in-tile (subpassLoad on a depth input attachment) instead of copying it to a
// colour RT (BeginDSAsRT), so SW-Z / DATE / alpha-test / AA1 depth passes fuse in-pass rather
// than round-tripping. It needs an ordered in-tile depth read: ROAA on the depth aspect on the
// framebuffer_fetch path (Mali-default / opt-in Adreno), or the render-pass self-dependency on
// the texture_barrier path. Gated behind HWROV so toggle-off is byte-for-byte the well-tested
// avoid/copy fallback (same as D3D11). Mali r44p1 excludes itself: texture_barrier is forced
// off for it above, so framebuffer_fetch is off and the ordered read is unavailable.
// Read at device init, so the depth half applies on game restart (HWROV's colour half is live).
// Desktop keeps canonical feedback-loop behaviour.
#if defined(__ANDROID__)
m_features.depth_feedback = false;
const bool depth_feedback_ordered =
m_features.framebuffer_fetch ? m_optional_extensions.vk_ext_roaa_depth : m_features.texture_barrier;
m_features.depth_feedback = GSConfig.HWROV && m_features.feedback_loops() && depth_feedback_ordered;
Console.WriteLn("Mobile depth feedback (ROV): %s [HWROV=%s fbfetch=%s roaa_depth=%s texbarrier=%s]",
m_features.depth_feedback ? "ENABLED" : "disabled", GSConfig.HWROV ? "on" : "off",
m_features.framebuffer_fetch ? "yes" : "no", m_optional_extensions.vk_ext_roaa_depth ? "yes" : "no",
m_features.texture_barrier ? "yes" : "no");
#else
m_features.depth_feedback = m_features.feedback_loops();
#endif
@@ -6196,6 +6251,14 @@ VkPipeline GSDeviceVK::CreateTFXPipeline(const PipelineSelector& p)
if (m_features.framebuffer_fetch && p.IsRTFeedbackLoop())
gpb.AddBlendFlags(VK_PIPELINE_COLOR_BLEND_STATE_CREATE_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_BIT_EXT);
// Mobile ordered depth feedback: the render pass declares ordered depth access (subpass flag
// above) whenever depth is sampled on the fbfetch path with the toggle on — the pipeline bound
// in that pass must carry the matching depth-stencil rasterization-order flag or it is invalid.
// Condition mirrors the subpass flag exactly (key.depth_sampling == p.IsTestingAndSamplingDepth()).
if (m_features.depth_feedback && m_features.framebuffer_fetch && p.IsTestingAndSamplingDepth() &&
m_optional_extensions.vk_ext_roaa_depth)
gpb.AddDepthStencilFlags(VK_PIPELINE_DEPTH_STENCIL_STATE_CREATE_RASTERIZATION_ORDER_ATTACHMENT_DEPTH_ACCESS_BIT_EXT);
VkPipeline pipeline = gpb.Create(m_device, g_vulkan_shader_cache->GetPipelineCache(true));
if (pipeline)
{
+1
View File
@@ -39,6 +39,7 @@ public:
bool vk_ext_memory_budget : 1;
bool vk_ext_calibrated_timestamps : 1;
bool vk_ext_rasterization_order_attachment_access : 1;
bool vk_ext_roaa_depth : 1; ///< ROAA depth sub-feature (rasterizationOrderDepthAttachmentAccess); optional, often absent when color ROAA is present.
bool vk_ext_full_screen_exclusive : 1;
bool vk_ext_line_rasterization : 1;
bool vk_swapchain_maintenance1 : 1;
+5
View File
@@ -505,6 +505,11 @@ void Vulkan::GraphicsPipelineBuilder::AddBlendFlags(u32 flags)
m_blend_state.flags |= flags;
}
void Vulkan::GraphicsPipelineBuilder::AddDepthStencilFlags(u32 flags)
{
m_depth_state.flags |= flags;
}
void Vulkan::GraphicsPipelineBuilder::SetBlendFlags(u32 flags)
{
m_blend_state.flags = flags;
+1
View File
@@ -124,6 +124,7 @@ namespace Vulkan
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT);
void AddBlendFlags(u32 flags);
void SetBlendFlags(u32 flags);
void AddDepthStencilFlags(u32 flags);
void ClearBlendAttachments();
void SetBlendConstants(float r, float g, float b, float a);
+48 -6
View File
@@ -20,6 +20,13 @@
#include <cstdarg>
#include <shared_mutex>
#if defined(__APPLE__)
#include <TargetConditionals.h>
#if TARGET_OS_IPHONE
#include <sys/sysctl.h>
#endif
#endif
namespace Host
{
static std::pair<const char*, u32> LookupTranslationString(
@@ -155,14 +162,49 @@ void Host::ReportFormattedErrorAsync(const std::string_view title, const char* f
ReportErrorAsync(title, message);
}
// The RetroAchievements client versions are injected from a gitignored header
// (ra_ua_secret.h) so the exact strings RA pins are not present in public source —
// third parties were spoofing our clients by copying the User-Agent verbatim. A fork or
// code-lift without the secret gets a stock UA that RA treats as unknown (no hardcore).
#if defined(__has_include)
# if __has_include("ra_ua_secret.h")
# include "ra_ua_secret.h"
# endif
#endif
#if defined(__APPLE__) && TARGET_OS_IPHONE
// Human-readable iOS version for the RA User-Agent detail (e.g. "iOS 17.5").
static std::string GetIOSVersionForUserAgent()
{
char version[64] = {};
size_t version_len = sizeof(version);
if (sysctlbyname("kern.osproductversion", version, &version_len, nullptr, 0) == 0 && version[0] != '\0')
return fmt::format("iOS {}", version);
return Host::GetOSVersionString();
}
#endif
std::string Host::GetHTTPUserAgent()
{
// RetroAchievements identifies the client by the leading Name/Version token of
// this User-Agent, so report ARMSX2 with the stable dotted version the RA server
// has registered/pinned (matches the refresh-experimental Android build). A
// "PCSX2 <gitrev>" UA is rejected as an outdated/unknown emulator, which disables
// hardcore unlocks ("Warning: Outdated Emulator").
return fmt::format("ARMSX2/2.7.407.0 ({})", GetOSVersionString());
// RetroAchievements identifies the client by the leading Name/Version token of this
// User-Agent. iOS ships the ARMSX2-iOS client, everything else the ARMSX2 client; the
// version comes from the gitignored ra_ua_secret.h. A build without the secret sends a
// token RA doesn't recognise (no hardcore) rather than leaking a real, still-valid one.
#if defined(__APPLE__) && TARGET_OS_IPHONE
#if defined(ARMSX2_IOS_RA_UA_VERSION)
const char* core_version = (BuildVersion::GitTag && BuildVersion::GitTag[0]) ? BuildVersion::GitTag : BuildVersion::GitRev;
return fmt::format("ARMSX2-iOS/v" ARMSX2_IOS_RA_UA_VERSION " ({}) pcsx2/{}", GetIOSVersionForUserAgent(), core_version);
#else
return fmt::format("PCSX2 {} ({})", BuildVersion::GitRev, GetOSVersionString());
#endif
#else
#if defined(ARMSX2_RA_UA_VERSION)
return fmt::format("ARMSX2/" ARMSX2_RA_UA_VERSION " ({})", GetOSVersionString());
#else
return fmt::format("PCSX2-community ({})", GetOSVersionString());
#endif
#endif
}
std::unique_lock<std::mutex> Host::GetSettingsLock()
+2
View File
@@ -7,6 +7,7 @@
#include "MTVU.h"
#include "Host.h"
#include "IconsFontAwesome.h"
#include "PerformanceMetrics.h"
#include "VMManager.h"
#include "common/FPControl.h"
@@ -144,6 +145,7 @@ void MTGS::ShutdownThread()
void MTGS::ThreadEntryPoint()
{
Threading::SetNameOfCurrentThread("GS");
PerformanceMetrics::AdpfRegisterCallingThread(); // ADPF: hint the GS thread's core (Android)
// GS can hit SMC write traps when executing InitAndReadFIFO
// As racey as it sounds, it should be safe, since InitAndReadFIFO is requested and immediately waited for,
+2
View File
@@ -4,6 +4,7 @@
#include "Common.h"
#include "Gif_Unit.h"
#include "MTVU.h"
#include "PerformanceMetrics.h"
#include "VMManager.h"
#include "Vif_Dynarec.h"
@@ -128,6 +129,7 @@ void VU_Thread::Reset()
void VU_Thread::ExecuteRingBuffer()
{
Threading::SetNameOfCurrentThread("MTVU");
PerformanceMetrics::AdpfRegisterCallingThread(); // ADPF: MTVU is often the limiting CPU thread (Android)
for (;;)
{
+228
View File
@@ -16,6 +16,16 @@
#include "MTVU.h"
#include "VMManager.h"
#if defined(__ANDROID__)
#include <algorithm>
#include <cstdint>
#include <mutex>
#include <string>
#include <dlfcn.h>
#include <sys/syscall.h>
#include <unistd.h>
#endif
static const float UPDATE_INTERVAL = 0.5f;
static float s_fps = 0.0f;
@@ -55,6 +65,103 @@ static u64 s_last_vu_time = 0;
static u64 s_last_capture_time = 0;
static u64 s_last_ticks = 0;
#if defined(__ANDROID__)
// ---- Android ADPF (PerformanceHintManager) ---------------------------------
// Tells the OS "these threads produce a frame every N ns, clock them to hit it."
// PS2 emulation is bursty and latency-sensitive, so Android's DVFS governor
// routinely under-clocks the CPU/GPU under it; ADPF is the purpose-built fix.
// Symbols are resolved at runtime from libandroid.so so the app still loads on
// pre-API-33 devices (there the session is never created — everything no-ops).
struct AdpfApi
{
void* (*getManager)() = nullptr;
void* (*createSession)(void*, const int32_t*, size_t, int64_t) = nullptr;
int (*updateTarget)(void*, int64_t) = nullptr;
int (*reportActual)(void*, int64_t) = nullptr;
void (*closeSession)(void*) = nullptr;
bool tried = false;
bool Available()
{
if (!tried)
{
tried = true;
if (void* lib = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL))
{
getManager = reinterpret_cast<decltype(getManager)>(dlsym(lib, "APerformanceHint_getManager"));
createSession = reinterpret_cast<decltype(createSession)>(dlsym(lib, "APerformanceHint_createSession"));
updateTarget = reinterpret_cast<decltype(updateTarget)>(dlsym(lib, "APerformanceHint_updateTargetWorkDuration"));
reportActual = reinterpret_cast<decltype(reportActual)>(dlsym(lib, "APerformanceHint_reportActualWorkDuration"));
closeSession = reinterpret_cast<decltype(closeSession)>(dlsym(lib, "APerformanceHint_closeSession"));
}
}
return getManager && createSession && updateTarget && reportActual && closeSession;
}
};
static std::mutex s_adpf_mutex;
static AdpfApi s_adpf;
static std::vector<int32_t> s_adpf_tids;
static void* s_adpf_manager = nullptr;
static void* s_adpf_session = nullptr;
static bool s_adpf_enabled = false; // experimental, opt-in
static bool s_adpf_create_failed = false;
static bool s_adpf_report_warned = false;
static bool s_adpf_paused = false; // reporting suspended (unlimited/vsync/interrupted), edge-logged
static int64_t s_adpf_target_ns = 0;
static Common::Timer::Value s_adpf_work_start = 0; // start of the current active-work period (0 = none)
// The frame deadline in ns: emulated refresh scaled by the limiter's target speed, so turbo /
// slow-motion move the deadline correctly. Returns 0 when there is no finite deadline (Unlimited,
// GetTargetSpeed()==0), so the caller pauses ADPF rather than feeding a bogus/inf target.
static int64_t AdpfTargetNs()
{
const double fps = VMManager::GetFrameRate() * static_cast<double>(VMManager::GetTargetSpeed());
return (fps > 1.0) ? static_cast<int64_t>(1.0e9 / fps) : 0;
}
// Must hold s_adpf_mutex. Creates the session once a perf thread has registered and a finite
// deadline exists, and LOGS the real outcome — so "ACTIVE" means a session genuinely exists,
// not merely that the user flipped the toggle.
static void AdpfEnsureSession()
{
if (s_adpf_session || s_adpf_create_failed || !s_adpf_enabled || s_adpf_tids.empty())
return;
if (!s_adpf.Available())
{
s_adpf_create_failed = true; // pre-API-33 / no libandroid; stop retrying
Console.WriteLn("ADPF: PerformanceHintManager symbols unavailable (needs Android 13+) — clock hint inactive.");
return;
}
if (!s_adpf_manager)
s_adpf_manager = s_adpf.getManager();
if (!s_adpf_manager)
{
s_adpf_create_failed = true;
Console.WriteLn("ADPF: getManager() returned null — clock hint inactive.");
return;
}
const int64_t target = AdpfTargetNs();
if (target <= 0)
return; // no finite deadline yet (Unlimited / VM not paced); retry once one exists
s_adpf_session = s_adpf.createSession(s_adpf_manager, s_adpf_tids.data(), s_adpf_tids.size(), target);
if (!s_adpf_session)
{
s_adpf_create_failed = true;
Console.Warning("ADPF: createSession() over %zu threads failed — clock hint inactive.", s_adpf_tids.size());
return;
}
s_adpf_target_ns = target;
s_adpf_report_warned = false;
s_adpf_paused = false;
std::string tid_list;
for (size_t i = 0; i < s_adpf_tids.size(); i++)
tid_list += (i ? "," : "") + std::to_string(s_adpf_tids[i]);
Console.WriteLn("ADPF: session ACTIVE over %zu threads [tids %s], deadline %.2f ms.", s_adpf_tids.size(),
tid_list.c_str(), static_cast<double>(target) / 1.0e6);
}
#endif // __ANDROID__
static double s_cpu_thread_usage = 0.0f;
static double s_cpu_thread_time = 0.0f;
static float s_gs_thread_usage = 0.0f;
@@ -290,6 +397,127 @@ void PerformanceMetrics::SetCPUThread(Threading::ThreadHandle thread)
s_cpu_thread_handle = std::move(thread);
}
void PerformanceMetrics::AdpfRegisterCallingThread()
{
#if defined(__ANDROID__)
const int32_t tid = static_cast<int32_t>(syscall(SYS_gettid));
std::lock_guard<std::mutex> lock(s_adpf_mutex);
if (std::find(s_adpf_tids.begin(), s_adpf_tids.end(), tid) != s_adpf_tids.end())
return;
s_adpf_tids.push_back(tid);
// The session's thread list is fixed at creation, so a newly-registered thread
// means the current session is missing it — drop it and let the next frame
// recreate it over the full set.
if (s_adpf_session)
{
s_adpf.closeSession(s_adpf_session);
s_adpf_session = nullptr;
}
s_adpf_create_failed = false;
#endif
}
void PerformanceMetrics::AdpfSetEnabled(bool enabled)
{
#if defined(__ANDROID__)
std::lock_guard<std::mutex> lock(s_adpf_mutex);
if (s_adpf_enabled == enabled)
return;
s_adpf_enabled = enabled;
if (!enabled && s_adpf_session)
{
s_adpf.closeSession(s_adpf_session);
s_adpf_session = nullptr;
}
if (enabled)
s_adpf_create_failed = false; // allow the next frame to recreate
#else
(void)enabled;
#endif
}
void PerformanceMetrics::AdpfShutdown()
{
#if defined(__ANDROID__)
std::lock_guard<std::mutex> lock(s_adpf_mutex);
if (s_adpf_session)
{
s_adpf.closeSession(s_adpf_session);
s_adpf_session = nullptr;
}
s_adpf_tids.clear();
s_adpf_create_failed = false;
s_adpf_work_start = 0;
#endif
}
void PerformanceMetrics::AdpfOnFrameWorkComplete()
{
#if defined(__ANDROID__)
// Sampled at Throttle() entry — the instant the frame's active CPU work finished, before the
// limiter sleep — so (now - work_start) excludes the deliberate limiter sleep. It is NOT pure
// CPU compute: the EE can still block behind a full MTGS queue that is itself stalled on
// presentation, so some present-wait can leak in. Acceptable for a first experiment, and a far
// better approximation of ADPF's "last workload cycle" than the present interval.
const Common::Timer::Value now = Common::Timer::GetCurrentValue();
std::lock_guard<std::mutex> lock(s_adpf_mutex);
if (!s_adpf_enabled)
return;
AdpfEnsureSession();
if (!s_adpf_session || s_adpf_work_start == 0)
return;
const int64_t target = AdpfTargetNs();
if (target > 0 && target != s_adpf_target_ns)
{
s_adpf.updateTarget(s_adpf_session, target);
s_adpf_target_ns = target;
}
const int64_t work_ns = static_cast<int64_t>(Common::Timer::ConvertValueToSeconds(now - s_adpf_work_start) * 1.0e9);
// Drop absurd outliers (savestate load, renderer recreation, debugger stall): a period several
// times the deadline is not a real frame and would spam a spurious max-frequency demand.
if (work_ns <= 0 || (s_adpf_target_ns > 0 && work_ns > s_adpf_target_ns * 4))
return;
if (s_adpf_paused)
{
Console.WriteLn("ADPF: reporting resumed.");
s_adpf_paused = false;
}
const int ret = s_adpf.reportActual(s_adpf_session, work_ns);
if (ret != 0 && !s_adpf_report_warned)
{
s_adpf_report_warned = true;
Console.Warning("ADPF: reportActualWorkDuration returned %d — driver is ignoring the hint.", ret);
}
#endif
}
void PerformanceMetrics::AdpfBeginFrameWork()
{
#if defined(__ANDROID__)
// Opens a work period at the post-sleep instant (Throttle exit), so the deliberate limiter
// sleep is excluded from the next reported duration.
const Common::Timer::Value now = Common::Timer::GetCurrentValue();
std::lock_guard<std::mutex> lock(s_adpf_mutex);
s_adpf_work_start = now;
#endif
}
void PerformanceMetrics::AdpfPauseFrameWork()
{
#if defined(__ANDROID__)
// Not frame-limiting (unlimited / host-vsync / interrupted) — invalidate the period so no
// wall-time-with-wait duration is reported, and edge-log so a tester never sees "ACTIVE" while
// nothing is actually being submitted.
std::lock_guard<std::mutex> lock(s_adpf_mutex);
if (s_adpf_session && !s_adpf_paused)
{
Console.WriteLn("ADPF: reporting paused (unlimited / host-vsync / interrupted) — no durations submitted.");
s_adpf_paused = true;
}
s_adpf_work_start = 0;
#endif
}
void PerformanceMetrics::SetGSSWThreadCount(u32 count)
{
s_gs_sw_threads.clear();
+21
View File
@@ -27,6 +27,27 @@ namespace PerformanceMetrics
/// wall time). Called at VM shutdown so every -logfile run records it.
void LogSessionSummary();
/// Android ADPF (PerformanceHintManager): register the calling thread as perf-critical
/// so the OS ramps its CPU/GPU clocks toward the frame deadline instead of leaving them
/// low under emulation's bursty load. Called on the EE (CPU), GS and MTVU threads.
/// No-op on non-Android and below API 33 (symbols resolved via dlsym).
void AdpfRegisterCallingThread();
/// Enable/disable ADPF hinting at runtime (settings toggle). Default OFF (experimental).
void AdpfSetEnabled(bool enabled);
/// Close the ADPF session and forget registered threads (VM shutdown).
void AdpfShutdown();
/// ADPF work-period brackets, driven by the frame limiter (VMManager::Internal::Throttle).
/// The reported duration must be the active EE/GS/VU work per frame, EXCLUDING the deliberate
/// limiter sleep and present wait, per the PerformanceHintManager contract (reportActualWork
/// = the last workload cycle, not the frame interval). OnFrameWorkComplete() reports the
/// period that just ended (called at Throttle entry, before the sleep); BeginFrameWork()
/// opens a new period after the sleep; PauseFrameWork() invalidates it when we are not
/// frame-limiting (unlimited / host-vsync pacing), so no bogus duration is reported.
void AdpfOnFrameWorkComplete();
void AdpfBeginFrameWork();
void AdpfPauseFrameWork();
/// Sets the EE thread for CPU usage calculations.
void SetCPUThread(Threading::ThreadHandle thread);
+13
View File
@@ -382,6 +382,7 @@ bool VMManager::Internal::CPUThreadInitialize()
{
Threading::SetNameOfCurrentThread("CPU Thread");
PerformanceMetrics::SetCPUThread(Threading::ThreadHandle::GetForCallingThread());
PerformanceMetrics::AdpfRegisterCallingThread(); // ADPF: hint the EE thread's core (Android)
// On Win32, we have a bunch of things which use COM (e.g. SDL, XAudio2, etc).
// We need to initialize COM first, before anything else does, because otherwise they might
@@ -460,6 +461,7 @@ void VMManager::Internal::CPUThreadShutdown()
WaitForSaveStateFlush();
PerformanceMetrics::SetCPUThread(Threading::ThreadHandle());
PerformanceMetrics::AdpfShutdown(); // ADPF: close the hint session on VM shutdown (Android)
USBshutdown();
@@ -2334,7 +2336,18 @@ void VMManager::ResetFrameLimiter()
void VMManager::Internal::Throttle()
{
if (s_target_speed == 0.0f || s_use_vsync_for_timing)
{
// Not frame-limiting this frame (unlimited / host-vsync pacing): invalidate the ADPF work
// period so no wall-time-with-wait duration is submitted.
PerformanceMetrics::AdpfPauseFrameWork();
return;
}
// ADPF: report the active-work period that just ended (before the limiter sleep below), then
// re-open a new period AFTER the sleep. The ScopedGuard fires on EVERY exit past here —
// including the missed-frame early return — so measurement survives the can't-hit-target case.
PerformanceMetrics::AdpfOnFrameWorkComplete();
ScopedGuard adpf_begin_next_work([]() { PerformanceMetrics::AdpfBeginFrameWork(); });
const u64 uExpectedEnd =
s_limiter_frame_start +
@@ -171,6 +171,8 @@
"fixes.opt.upper": "上部",
"fixes.optimalFramePacing.desc": "以零排队帧运行 GS 线程,获得最紧凑的帧调度和最低输入延迟(与 PCSX2 一致)。若较弱设备用小帧队列更平滑,可关闭。",
"fixes.optimalFramePacing.label": "最佳帧调度",
"perf.lowLatencyMode.label": "低延迟模式",
"perf.lowLatencyMode.description": "使用零帧 GS 队列,并请求与游戏帧率匹配的高刷新率显示模式,降低实体手柄到画面的延迟。推荐中高端掌机开启;如果较弱设备出现不流畅,请关闭。",
"fixes.overrideTextureBarriers.desc": "强制开/关渲染器的纹理屏障支持。推荐 Auto。",
"fixes.overrideTextureBarriers.label": "覆盖纹理屏障",
"fixes.preloadFrameData.desc": "在绘制前上传上一帧数据。修复部分效果。",
@@ -171,6 +171,8 @@
"fixes.opt.upper": "較高",
"fixes.optimalFramePacing.desc": "以零排隊影格執行 GS 執行緒,取得最緊湊的影格節奏與最低輸入延遲 (符合 PCSX2)。若較弱裝置搭配小影格佇列更順暢,請關閉。",
"fixes.optimalFramePacing.label": "最佳影格節奏",
"perf.lowLatencyMode.label": "低延遲模式",
"perf.lowLatencyMode.description": "使用零影格 GS 佇列,並要求與遊戲影格率匹配的高更新率顯示模式,降低實體控制器到畫面的延遲。建議中高階掌機開啟;若較弱裝置變得不順暢,請關閉。",
"fixes.overrideTextureBarriers.desc": "強制開/關渲染器的 texture-barrier 支援。建議使用 Auto。",
"fixes.overrideTextureBarriers.label": "覆寫 Texture Barriers",
"fixes.preloadFrameData.desc": "在繪製前上傳前一影格的資料。修正部分效果。",
@@ -230,6 +230,19 @@ Java_kr_co_iefriends_pcsx2_NativeApp_setEeDiffVerify(JNIEnv*, jclass, jboolean e
Cpu->Reset();
}
// ADPF (PerformanceHintManager): hint the OS scheduler to raise the EE/GS/MTVU threads' CPU
// frequency toward the frame deadline instead of the DVFS governor under-clocking emulation's
// bursty load. Basic API-33 path — CPU scheduling only, no explicit GPU timing. Applies live;
// no-op below API 33 (symbols dlsym'd from libandroid.so). This only records the user's request:
// whether a session actually exists is logged from PerformanceMetrics when a game runs.
extern "C"
JNIEXPORT void JNICALL
Java_kr_co_iefriends_pcsx2_NativeApp_setAdpfEnabled(JNIEnv*, jclass, jboolean enabled) {
PerformanceMetrics::AdpfSetEnabled(enabled == JNI_TRUE);
Console.WriteLnFmt("ADPF hint {} by user (session state logged separately when a game runs)",
enabled == JNI_TRUE ? "requested" : "disabled");
}
extern "C"
JNIEXPORT void JNICALL
Java_kr_co_iefriends_pcsx2_NativeApp_emulog(JNIEnv *env, jclass, jstring p_msg) {
@@ -782,6 +795,12 @@ Java_kr_co_iefriends_pcsx2_NativeApp_getFPS(JNIEnv *env, jclass clazz) {
return (jfloat)PerformanceMetrics::GetFPS();
}
extern "C"
JNIEXPORT jfloat JNICALL
Java_kr_co_iefriends_pcsx2_NativeApp_getNominalFrameRate(JNIEnv*, jclass) {
return VMManager::HasValidVM() ? static_cast<jfloat>(VMManager::GetFrameRate()) : 0.0f;
}
extern "C"
JNIEXPORT jstring JNICALL
Java_kr_co_iefriends_pcsx2_NativeApp_getPauseGameTitle(JNIEnv *env, jclass clazz) {
@@ -1855,6 +1874,16 @@ Java_kr_co_iefriends_pcsx2_NativeApp_renderOpenGL(JNIEnv *env, jclass clazz) {
}
}
// Android renderer Auto steering: g_gs_android_prefer_vk (GSUtil.cpp) makes GetPreferredRenderer's
// Auto resolution pick Vulkan HW on Adreno instead of OpenGL. The app sets it from GL_RENDERER
// before the GS starts — a plain global (no settings interface), so it's safe to set at startup.
extern bool g_gs_android_prefer_vk;
extern "C"
JNIEXPORT void JNICALL
Java_kr_co_iefriends_pcsx2_NativeApp_setPreferVulkan(JNIEnv*, jclass, jboolean enabled) {
g_gs_android_prefer_vk = (enabled == JNI_TRUE);
}
extern "C"
JNIEXPORT void JNICALL
Java_kr_co_iefriends_pcsx2_NativeApp_renderVulkan(JNIEnv *env, jclass clazz) {
@@ -39,8 +39,8 @@ object MenuSfx {
/** UI events a sound binds to. [fileName] is the base name to use when importing a custom pack
* (case- and extension-insensitive); [defaultRes] is the bundled fallback clip. */
enum class Event(val fileName: String, val defaultRes: Int) {
NAV("nav", R.raw.sfx_cursor), // controller moves the selection highlight
enum class Event(val fileName: String, val defaultRes: Int, val altRes: Int = 0) {
NAV("nav", R.raw.sfx_nav_a, R.raw.sfx_nav_b), // alternates two soft ticks as the highlight moves
SELECT("select", R.raw.sfx_select), // confirm / launch a game
SUBMENU("submenu", R.raw.sfx_submenu), // open a settings menu / sub-screen
MENU_OPEN("menu", R.raw.sfx_menu), // open the in-game pause menu
@@ -48,9 +48,11 @@ object MenuSfx {
TOGGLE_ON("toggle_on", R.raw.sfx_toggle_on),
TOGGLE_OFF("toggle_off", R.raw.sfx_toggle_off),
RESET("reset", R.raw.sfx_reset),
SLIDER("slider", R.raw.sfx_cursor), // shares the cursor tick with NAV
SLIDER("slider", R.raw.sfx_nav_a, R.raw.sfx_nav_b), // shares the alternating nav ticks
SLEEP("sleep", R.raw.sfx_sleep), // DS-lid-style chime when the device sleeps
WAKE("wake", R.raw.sfx_wake), // chime when waking back to the app
POPUP_OPEN("popup_open", R.raw.sfx_popup_open), // a dialog/popup appears (hardcore confirm, info)
POPUP_CLOSE("popup_close", R.raw.sfx_popup_close), // ...and dismisses
}
/** On by default — the bundled sounds give the launcher its "personality" out of the box. */
@@ -64,6 +66,10 @@ object MenuSfx {
private var pool: SoundPool? = null
/** event -> loaded SoundPool sample id. */
private val sampleIds = HashMap<Event, Int>()
/** Second sample for events with an [Event.altRes] (NAV/SLIDER): [play] alternates between
* this and the primary so a fast walk/drag varies the tick instead of machine-gunning one. */
private val altSampleIds = HashMap<Event, Int>()
private var navAlt = false
/** Per-event last-play time, to throttle rapid emitters (a slider drag fires onChange many
* times/second without this it buzzes instead of ticking). */
private val lastPlayMs = HashMap<Event, Long>()
@@ -144,12 +150,17 @@ object MenuSfx {
fun play(event: Event) {
if (!enabled.value) return
val p = pool ?: return
val id = sampleIds[event] ?: return
val now = SystemClock.uptimeMillis()
// NAV and SLIDER fire on every highlight move / value step — a longer floor keeps a fast
// D-pad walk or drag ticking pleasantly instead of buzzing.
val minGap = if (event == Event.SLIDER || event == Event.NAV) 45L else 18L
if (now - (lastPlayMs[event] ?: 0L) < minGap) return
// Alternate the two loaded ticks for events that have a second sample (NAV/SLIDER); flip
// only when we actually play (post-throttle) so the alternation follows audible ticks.
val id = if (altSampleIds.containsKey(event)) {
navAlt = !navAlt
(if (navAlt) altSampleIds[event] else sampleIds[event]) ?: return
} else (sampleIds[event] ?: return)
lastPlayMs[event] = now
lastPlayedMs = now
val g = gain()
@@ -179,6 +190,9 @@ object MenuSfx {
val id = if (custom.length() > 0L) sp.load(custom.absolutePath, 1)
else sp.load(context, ev.defaultRes, 1)
if (id != 0) sampleIds[ev] = id
// Second (alternating) sample: bundled-only. A custom "nav" import overrides the
// primary tick and alternates against this bundled hover tick.
if (ev.altRes != 0) sp.load(context, ev.altRes, 1).let { if (it != 0) altSampleIds[ev] = it }
}.onFailure { Log.w(TAG, "load failed for ${ev.fileName}", it) }
}
pool = sp
@@ -186,6 +200,7 @@ object MenuSfx {
private fun releasePool() {
sampleIds.clear()
altSampleIds.clear()
lastPlayMs.clear()
pool?.let { runCatching { it.release() } }
pool = null
@@ -177,6 +177,35 @@ object ConfigStore {
writeBackupMirror()
}
/**
* Persist capability-aware defaults only when this is genuinely a fresh install.
*
* Call after [reconcileReusedFolder]: a reused data directory gets first chance to
* restore its prior global settings, while an empty install starts with the
* zero-frame GS queue on capable devices. Low-end devices retain the smoother
* two-frame queue. Once persisted, this never changes an existing user's choice.
*/
fun seedFreshInstallDefaults(context: android.content.Context) {
if (MainActivityRuntime.prefs.getString(KEY_GLOBAL, null) != null) return
val queueSize = if (com.armsx2.DeviceTier.isLowEnd(context)) 2 else 0
saveGlobal(Settings(vsyncQueueSize = queueSize))
}
private const val KEY_LOWLATENCY_MIGRATED = "config.migrated.lowLatencyDefault"
/**
* One-time flip of EXISTING capable installs to Low Latency (zero-frame GS queue), matching the
* fresh-install default seeded above. Flagged so it runs exactly once a user who later turns it
* off keeps that choice. Low-end devices keep the smoother two-frame queue.
*/
fun migrateLowLatencyDefault(context: android.content.Context) {
if (MainActivityRuntime.prefs.getBoolean(KEY_LOWLATENCY_MIGRATED, false)) return
MainActivityRuntime.prefs.edit().putBoolean(KEY_LOWLATENCY_MIGRATED, true).apply()
// Fresh installs are handled by seedFreshInstallDefaults; only touch an existing global save.
if (MainActivityRuntime.prefs.getString(KEY_GLOBAL, null) == null || com.armsx2.DeviceTier.isLowEnd(context)) return
val g = loadGlobal()
if (g.vsyncQueueSize != 0) saveGlobal(g.copy(vsyncQueueSize = 0))
}
/** Load the sparse per-game override blob, or null if there are none. */
fun loadOverrides(serial: String): JSONObject? {
val raw = MainActivityRuntime.prefs.getString(keyForGame(serial), null) ?: return null

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