mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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,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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (;;)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user