diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/DolphinApplication.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/DolphinApplication.java index c197c002b0..e0816e408b 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/DolphinApplication.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/DolphinApplication.java @@ -6,6 +6,7 @@ import android.app.Application; import android.content.Context; import android.hardware.usb.UsbManager; +import org.dolphinemu.dolphinemu.utils.ActivityTracker; import org.dolphinemu.dolphinemu.utils.DirectoryInitialization; import org.dolphinemu.dolphinemu.utils.Java_GCAdapter; import org.dolphinemu.dolphinemu.utils.Java_WiimoteAdapter; @@ -20,6 +21,7 @@ public class DolphinApplication extends Application { super.onCreate(); application = this; + registerActivityLifecycleCallbacks(new ActivityTracker()); VolleyUtil.init(getApplicationContext()); System.loadLibrary("main"); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt new file mode 100644 index 0000000000..b3a6a5d91a --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt @@ -0,0 +1,41 @@ +package org.dolphinemu.dolphinemu.utils + +import android.app.Activity +import android.app.Application.ActivityLifecycleCallbacks +import android.os.Bundle + +class ActivityTracker : ActivityLifecycleCallbacks { + val resumedActivities = HashSet() + var backgroundExecutionAllowed = false + + override fun onActivityCreated(activity: Activity, bundle: Bundle?) {} + + override fun onActivityStarted(activity: Activity) {} + + override fun onActivityResumed(activity: Activity) { + resumedActivities.add(activity) + if (!backgroundExecutionAllowed && !resumedActivities.isEmpty()) { + backgroundExecutionAllowed = true + setBackgroundExecutionAllowedNative(true) + } + } + + override fun onActivityPaused(activity: Activity) { + resumedActivities.remove(activity) + if (backgroundExecutionAllowed && resumedActivities.isEmpty()) { + backgroundExecutionAllowed = false + setBackgroundExecutionAllowedNative(false) + } + } + + override fun onActivityStopped(activity: Activity) {} + + override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {} + + override fun onActivityDestroyed(activity: Activity) {} + + companion object { + @JvmStatic + external fun setBackgroundExecutionAllowedNative(allowed: Boolean) + } +} diff --git a/Source/Android/jni/ActivityTracker.cpp b/Source/Android/jni/ActivityTracker.cpp new file mode 100644 index 0000000000..b871890576 --- /dev/null +++ b/Source/Android/jni/ActivityTracker.cpp @@ -0,0 +1,21 @@ +// Copyright 2024 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "Common/Logging/Log.h" +#include "Core/AchievementManager.h" + +extern "C" { + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_utils_ActivityTracker_setBackgroundExecutionAllowedNative( + JNIEnv*, jclass, jboolean allowed) +{ + // This is called with allowed == false when the app goes into the background. + // We use this to stop continuously running background threads so we don't waste battery. + + INFO_LOG_FMT(CORE, "SetBackgroundExecutionAllowed {}", allowed); + AchievementManager::GetInstance().SetBackgroundExecutionAllowed(allowed); +} +} diff --git a/Source/Android/jni/CMakeLists.txt b/Source/Android/jni/CMakeLists.txt index ab2acc7063..7ddae94c07 100644 --- a/Source/Android/jni/CMakeLists.txt +++ b/Source/Android/jni/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(main SHARED + ActivityTracker.cpp Cheats/ARCheat.cpp Cheats/Cheats.h Cheats/GeckoCheat.cpp @@ -11,6 +12,7 @@ add_library(main SHARED GameList/GameFile.cpp GameList/GameFile.h GameList/GameFileCache.cpp + GpuDriver.cpp Host.cpp Host.h InfinityConfig.cpp @@ -32,7 +34,6 @@ add_library(main SHARED RiivolutionPatches.cpp SkylanderConfig.cpp WiiUtils.cpp - GpuDriver.cpp ) target_link_libraries(main diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index 4ef78bf322..7d7c0af374 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -25,6 +25,7 @@ #include "Core/Config/AchievementSettings.h" #include "Core/Core.h" #include "Core/HW/Memmap.h" +#include "Core/HW/VideoInterface.h" #include "Core/PowerPC/MMU.h" #include "Core/System.h" #include "DiscIO/Blob.h" @@ -47,7 +48,10 @@ void AchievementManager::Init() LoadDefaultBadges(); if (!m_client && Config::Get(Config::RA_ENABLED)) { - m_client = rc_client_create(MemoryVerifier, Request); + { + std::lock_guard lg{m_lock}; + m_client = rc_client_create(MemoryVerifier, Request); + } std::string host_url = Config::Get(Config::RA_HOST_URL); if (!host_url.empty()) rc_client_set_host(m_client, host_url.c_str()); @@ -159,6 +163,13 @@ bool AchievementManager::IsGameLoaded() const return game_info && game_info->id != 0; } +void AchievementManager::SetBackgroundExecutionAllowed(bool allowed) +{ + m_background_execution_allowed = allowed; + if (allowed && Core::GetState(*AchievementManager::GetInstance().m_system) == Core::State::Paused) + DoIdle(); +} + void AchievementManager::FetchPlayerBadge() { FetchBadge(&m_player_badge, RC_IMAGE_TYPE_USER, @@ -243,6 +254,54 @@ void AchievementManager::DoFrame() } } +bool AchievementManager::CanPause() +{ + u32 frames_to_next_pause = 0; + bool can_pause = rc_client_can_pause(m_client, &frames_to_next_pause); + if (!can_pause) + { + OSD::AddMessage("Cannot spam pausing in hardcore mode.", OSD::Duration::VERY_LONG, + OSD::Color::RED); + OSD::AddMessage( + fmt::format("Can pause in {} seconds.", + static_cast(frames_to_next_pause) / + Core::System::GetInstance().GetVideoInterface().GetTargetRefreshRate()), + OSD::Duration::VERY_LONG, OSD::Color::RED); + } + return can_pause; +} + +void AchievementManager::DoIdle() +{ + std::thread([this]() { + while (true) + { + Common::SleepCurrentThread(1000); + { + std::lock_guard lg{m_lock}; + if (!m_system || Core::GetState(*m_system) != Core::State::Paused) + return; + if (!m_background_execution_allowed) + return; + if (!m_client || !IsGameLoaded()) + return; + } + // rc_client_idle peeks at memory to recalculate rich presence and therefore + // needs to be on host or CPU thread to access memory. + Core::QueueHostJob([this](Core::System& system) { + std::lock_guard lg{m_lock}; + if (!m_system || Core::GetState(*m_system) != Core::State::Paused) + return; + if (!m_background_execution_allowed) + return; + if (!m_client || !IsGameLoaded()) + return; + rc_client_idle(m_client); + }); + } + }).detach(); +} + std::recursive_mutex& AchievementManager::GetLock() { return m_lock; @@ -441,8 +500,8 @@ void AchievementManager::CloseGame() void AchievementManager::Logout() { { - std::lock_guard lg{m_lock}; CloseGame(); + std::lock_guard lg{m_lock}; m_player_badge.width = 0; m_player_badge.height = 0; m_player_badge.data.clear(); @@ -459,6 +518,7 @@ void AchievementManager::Shutdown() { CloseGame(); m_queue.Shutdown(); + std::lock_guard lg{m_lock}; // DON'T log out - keep those credentials for next run. rc_client_destroy(m_client); m_client = nullptr; diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h index 6be2006c47..3996003040 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -96,12 +96,16 @@ public: bool HasAPIToken() const; void LoadGame(const std::string& file_path, const DiscIO::Volume* volume); bool IsGameLoaded() const; + void SetBackgroundExecutionAllowed(bool allowed); void FetchPlayerBadge(); void FetchGameBadges(); void DoFrame(); + bool CanPause(); + void DoIdle(); + std::recursive_mutex& GetLock(); void SetHardcoreMode(); bool IsHardcoreModeActive() const; @@ -193,6 +197,7 @@ private: Badge m_default_game_badge; Badge m_default_unlocked_badge; Badge m_default_locked_badge; + std::atomic_bool m_background_execution_allowed = true; Badge m_player_badge; Hash m_game_hash{}; u32 m_game_id = 0; @@ -239,6 +244,8 @@ public: constexpr void LoadGame(const std::string&, const DiscIO::Volume*) {} + constexpr void SetBackgroundExecutionAllowed(bool allowed) {} + constexpr void DoFrame() {} constexpr void CloseGame() {} diff --git a/Source/Core/Core/Core.cpp b/Source/Core/Core/Core.cpp index 8399d55cba..1a43f3189f 100644 --- a/Source/Core/Core/Core.cpp +++ b/Source/Core/Core/Core.cpp @@ -353,7 +353,7 @@ static void CPUSetInitialExecutionState(bool force_paused = false) // SetState must be called on the host thread, so we defer it for later. QueueHostJob([force_paused](Core::System& system) { bool paused = SConfig::GetInstance().bBootToPause || force_paused; - SetState(system, paused ? State::Paused : State::Running); + SetState(system, paused ? State::Paused : State::Running, true, true); Host_UpdateDisasmDialog(); Host_UpdateMainFrame(); Host_Message(HostMessageID::WMUserCreate); @@ -693,7 +693,8 @@ static void EmuThread(Core::System& system, std::unique_ptr boot // Set or get the running state -void SetState(Core::System& system, State state, bool report_state_change) +void SetState(Core::System& system, State state, bool report_state_change, + bool initial_execution_state) { // State cannot be controlled until the CPU Thread is operational if (s_state.load() != State::Running) @@ -702,11 +703,18 @@ void SetState(Core::System& system, State state, bool report_state_change) switch (state) { case State::Paused: +#ifdef USE_RETRO_ACHIEVEMENTS + if (!initial_execution_state && !AchievementManager::GetInstance().CanPause()) + return; +#endif // USE_RETRO_ACHIEVEMENTS // NOTE: GetState() will return State::Paused immediately, even before anything has // stopped (including the CPU). system.GetCPU().EnableStepping(true); // Break Wiimote::Pause(); ResetRumble(); +#ifdef USE_RETRO_ACHIEVEMENTS + AchievementManager::GetInstance().DoIdle(); +#endif // USE_RETRO_ACHIEVEMENTS break; case State::Running: { diff --git a/Source/Core/Core/Core.h b/Source/Core/Core/Core.h index e6c440566c..48043d4b10 100644 --- a/Source/Core/Core/Core.h +++ b/Source/Core/Core/Core.h @@ -143,7 +143,8 @@ bool IsHostThread(); bool WantsDeterminism(); // [NOT THREADSAFE] For use by Host only -void SetState(Core::System& system, State state, bool report_state_change = true); +void SetState(Core::System& system, State state, bool report_state_change = true, + bool initial_execution_state = false); State GetState(Core::System& system); void SaveScreenShot();