From 7ea9cee4790f49d789d7d2e9ebe62e792de89f6d Mon Sep 17 00:00:00 2001 From: Ygor Dreyer Date: Mon, 12 Oct 2026 14:10:59 -0300 Subject: [PATCH] android: Fix black screen when relaunching a game from a shortcut (#2656) * android: Fix black screen when relaunching a game from a shortcut onNewIntent stopped emulation and then called setGraph with an equal graph, which keeps the old emulation fragment, so nothing restarted. A shortcut for the game that is already running now just brings it back. A shortcut for a different game replaces the emulation fragment so the new game boots. The old fragment no longer clears the activity registration the new one already set, and its stats overlay updater stops with its view instead of resetting the new game's stats. After HOME the second screen surface comes back after the main one, so the emulation thread waits briefly for it instead of crashing. Fixes #2329 * android: Update EmulationFragment.kt license header to GPLv3 --- .../citra_emu/activities/EmulationActivity.kt | 25 ++++++++++++++++++- .../citra_emu/fragments/EmulationFragment.kt | 10 +++++++- src/android/app/src/main/jni/native.cpp | 10 ++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt index d4066d182..5d009774e 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt @@ -27,6 +27,7 @@ import androidx.core.os.BundleCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat +import androidx.navigation.NavOptions import androidx.navigation.fragment.NavHostFragment import androidx.preference.PreferenceManager import org.citra.citra_emu.CitraApplication @@ -184,6 +185,11 @@ class EmulationActivity : AppCompatActivity() { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) + val launchUri = getLaunchUri(intent) + if (launchUri != null && launchUri != Uri.EMPTY && launchUri == getLaunchUri(this.intent)) { + Log.info("[EmulationActivity] Ignoring relaunch of the running game") + return + } setIntent(intent) NativeLibrary.stopEmulation() @@ -203,7 +209,24 @@ class EmulationActivity : AppCompatActivity() { val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment - navHostFragment.navController.setGraph(R.navigation.emulation_navigation, intent.extras) + navHostFragment.navController.navigate( + R.id.emulationFragment, + intent.extras, + NavOptions.Builder().setPopUpTo(R.id.emulationFragment, true).build() + ) + } + + private fun getLaunchUri(intent: Intent): Uri? = try { + val game = intent.extras?.let { extras -> + BundleCompat.getParcelable(extras, "game", Game::class.java) + } + if (game != null) { + game.launchIntent.data + } else { + intent.data ?: intent.getStringExtra("SelectedGame")?.toUri() + } + } catch (e: Exception) { + null } // On some devices, the system bars will not disappear on first boot or after some diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index 5401b9b43..ab7c7e2e3 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -564,8 +564,16 @@ class EmulationFragment : super.onPause() } + override fun onDestroyView() { + perfStatsUpdater?.let { perfStatsUpdateHandler.removeCallbacks(it) } + perfStatsUpdater = null + super.onDestroyView() + } + override fun onDetach() { - NativeLibrary.clearEmulationActivity() + if (!isRemoving) { + NativeLibrary.clearEmulationActivity() + } super.onDetach() } diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index b6fe5ddd5..8a38de52c 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -3,6 +3,7 @@ // Refer to the LICENSE.txt file included. #include +#include #include #include #include @@ -196,6 +197,12 @@ static bool CheckMicPermission() { IDCache::GetRequestMicPermission()); } +static void WaitForEmulationSurfaces(std::unique_lock& surface_lock) { + surface_cv.wait_for(surface_lock, std::chrono::seconds(2), + [] { return s_secondary_surface != nullptr; }); + surface_cv.wait(surface_lock, [] { return s_surface != nullptr; }); +} + static Core::System::ResultStatus RunCitra(const std::string& filepath) { // Citra core only supports a single running instance std::scoped_lock lock(running_mutex); @@ -221,6 +228,8 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { // s_surface/s_secondary_surface while they're still being used. std::unique_lock surface_lock(surface_mutex); + WaitForEmulationSurfaces(surface_lock); + // We also need to lock the surface mutex when System::Init() is called. // This is because save state saving and loading may call System::Init // on its own which does GPU reinitialization. @@ -467,6 +476,7 @@ void Java_org_citra_citra_1emu_NativeLibrary_secondarySurfaceChanged(JNIEnv* env if (!s_secondary_surface) { return; } + surface_cv.notify_all(); bool notify = false; if (secondary_window) {