From 5aa1b9553f874a180e6b8e5a2f4774bea32383ca Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 13 Sep 2020 14:37:39 +1000 Subject: [PATCH] Android: Fix emulation stopping on app switch and UI covering display --- .../app/src/cpp/android_host_interface.cpp | 115 ++++++++++++++---- android/app/src/cpp/android_host_interface.h | 7 +- .../duckstation/AndroidHostInterface.java | 8 ++ .../duckstation/EmulationActivity.java | 95 +++++++++++---- 4 files changed, 176 insertions(+), 49 deletions(-) diff --git a/android/app/src/cpp/android_host_interface.cpp b/android/app/src/cpp/android_host_interface.cpp index 36d07f637..4dce585b8 100644 --- a/android/app/src/cpp/android_host_interface.cpp +++ b/android/app/src/cpp/android_host_interface.cpp @@ -162,6 +162,11 @@ void AndroidHostInterface::UpdateInputMap() CommonHostInterface::UpdateInputMap(m_settings_interface); } +bool AndroidHostInterface::IsEmulationThreadPaused() const +{ + return System::IsValid() && System::IsPaused(); +} + bool AndroidHostInterface::StartEmulationThread(jobject emulation_activity, ANativeWindow* initial_surface, SystemBootParameters boot_params, bool resume_state) { @@ -176,11 +181,23 @@ bool AndroidHostInterface::StartEmulationThread(jobject emulation_activity, ANat return true; } +void AndroidHostInterface::PauseEmulationThread(bool paused) +{ + Assert(IsEmulationThreadRunning()); + RunOnEmulationThread([this, paused]() { + PauseSystem(paused); + }); +} + void AndroidHostInterface::StopEmulationThread() { Assert(IsEmulationThreadRunning()); Log_InfoPrint("Stopping emulation thread..."); - m_emulation_thread_stop_request.store(true); + { + std::unique_lock lock(m_mutex); + m_emulation_thread_stop_request.store(true); + m_sleep_cv.notify_one(); + } m_emulation_thread.join(); Log_InfoPrint("Emulation thread stopped"); } @@ -193,8 +210,9 @@ void AndroidHostInterface::RunOnEmulationThread(std::function function, return; } - m_callback_mutex.lock(); + m_mutex.lock(); m_callback_queue.push_back(std::move(function)); + m_sleep_cv.notify_one(); if (blocking) { @@ -204,12 +222,12 @@ void AndroidHostInterface::RunOnEmulationThread(std::function function, if (m_callback_queue.empty()) break; - m_callback_mutex.unlock(); - m_callback_mutex.lock(); + m_mutex.unlock(); + m_mutex.lock(); } } - m_callback_mutex.unlock(); + m_mutex.unlock(); } void AndroidHostInterface::EmulationThreadEntryPoint(jobject emulation_activity, ANativeWindow* initial_surface, @@ -254,22 +272,44 @@ void AndroidHostInterface::EmulationThreadEntryPoint(jobject emulation_activity, // System is ready to go. thread_env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_onEmulationStarted); - while (!m_emulation_thread_stop_request.load()) + EmulationThreadLoop(); + + thread_env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_onEmulationStopped); + PowerOffSystem(); + DestroyImGuiContext(); + thread_env->DeleteGlobalRef(m_emulation_activity_object); + m_emulation_activity_object = {}; + s_jvm->DetachCurrentThread(); +} + +void AndroidHostInterface::EmulationThreadLoop() +{ + for (;;) { // run any events - m_callback_mutex.lock(); - for (;;) { - if (m_callback_queue.empty()) - break; + std::unique_lock lock(m_mutex); + for (;;) { + while (!m_callback_queue.empty()) { + auto callback = std::move(m_callback_queue.front()); + m_callback_queue.pop_front(); + lock.unlock(); + callback(); + lock.lock(); + } - auto callback = std::move(m_callback_queue.front()); - m_callback_queue.pop_front(); - m_callback_mutex.unlock(); - callback(); - m_callback_mutex.lock(); + if (m_emulation_thread_stop_request.load()) + return; + + if (System::IsPaused()) { + // paused, wait for us to resume + m_sleep_cv.wait(lock); + } else { + // done with callbacks, run the frame + break; + } + } } - m_callback_mutex.unlock(); // simulate the system if not paused if (System::IsRunning()) @@ -291,13 +331,6 @@ void AndroidHostInterface::EmulationThreadEntryPoint(jobject emulation_activity, } } } - - thread_env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_onEmulationStopped); - PowerOffSystem(); - DestroyImGuiContext(); - thread_env->DeleteGlobalRef(m_emulation_activity_object); - m_emulation_activity_object = {}; - s_jvm->DetachCurrentThread(); } bool AndroidHostInterface::AcquireHostDisplay() @@ -373,7 +406,7 @@ void AndroidHostInterface::SurfaceChanged(ANativeWindow* surface, int format, in if (m_display) { WindowInfo wi; - wi.type = WindowInfo::Type::Android; + wi.type = surface ? WindowInfo::Type::Android : WindowInfo::Type::Surfaceless; wi.window_handle = surface; wi.surface_width = width; wi.surface_height = height; @@ -588,8 +621,8 @@ DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_stopEmulationThread, jobject o DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_surfaceChanged, jobject obj, jobject surface, jint format, jint width, jint height) { - ANativeWindow* native_surface = ANativeWindow_fromSurface(env, surface); - if (!native_surface) + ANativeWindow* native_surface = surface ? ANativeWindow_fromSurface(env, surface) : nullptr; + if (surface && !native_surface) Log_ErrorPrint("ANativeWindow_fromSurface() returned null"); AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); @@ -724,9 +757,39 @@ DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_saveState, jobject obj, jboole hi->RunOnEmulationThread([hi, global, slot]() { hi->SaveState(global, slot); }); } +DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_saveResumeState, jobject obj, jboolean wait_for_completion) +{ + AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); + hi->RunOnEmulationThread([hi]() { hi->SaveResumeSaveState(); }, wait_for_completion); +} + DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setDisplayAlignment, jobject obj, jint alignment) { AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); hi->RunOnEmulationThread([hi, alignment]() { hi->GetDisplay()->SetDisplayAlignment(static_cast(alignment)); }); } +DEFINE_JNI_ARGS_METHOD(bool, AndroidHostInterface_hasSurface, jobject obj) +{ + AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); + HostDisplay* display = hi->GetDisplay(); + if (display) + return display->HasRenderSurface(); + else + return false; +} + +DEFINE_JNI_ARGS_METHOD(bool, AndroidHostInterface_isEmulationThreadPaused, jobject obj) +{ + AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); + return hi->IsEmulationThreadPaused(); +} + +DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_pauseEmulationThread, jobject obj, jboolean paused) +{ + AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); + hi->PauseEmulationThread(paused); +} + + + diff --git a/android/app/src/cpp/android_host_interface.h b/android/app/src/cpp/android_host_interface.h index 5667631b4..4c8af0dad 100644 --- a/android/app/src/cpp/android_host_interface.h +++ b/android/app/src/cpp/android_host_interface.h @@ -4,6 +4,7 @@ #include "frontend-common/common_host_interface.h" #include #include +#include #include #include #include @@ -35,9 +36,11 @@ public: float GetFloatSettingValue(const char* section, const char* key, float default_value = 0.0f) override; bool IsEmulationThreadRunning() const { return m_emulation_thread.joinable(); } + bool IsEmulationThreadPaused() const; bool StartEmulationThread(jobject emulation_activity, ANativeWindow* initial_surface, SystemBootParameters boot_params, bool resume_state); void RunOnEmulationThread(std::function function, bool blocking = false); + void PauseEmulationThread(bool paused); void StopEmulationThread(); void SurfaceChanged(ANativeWindow* surface, int format, int width, int height); @@ -63,6 +66,7 @@ protected: private: void EmulationThreadEntryPoint(jobject emulation_activity, ANativeWindow* initial_surface, SystemBootParameters boot_params, bool resume_state); + void EmulationThreadLoop(); void CreateImGuiContext(); void DestroyImGuiContext(); @@ -74,7 +78,8 @@ private: ANativeWindow* m_surface = nullptr; - std::mutex m_callback_mutex; + std::mutex m_mutex; + std::condition_variable m_sleep_cv; std::deque> m_callback_queue; std::thread m_emulation_thread; diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java index f1376485d..d6a015a7b 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java @@ -34,8 +34,14 @@ public class AndroidHostInterface { public native boolean startEmulationThread(EmulationActivity emulationActivity, Surface surface, String filename, boolean resumeState, String state_filename); + public native boolean isEmulationThreadPaused(); + + public native void pauseEmulationThread(boolean paused); + public native void stopEmulationThread(); + public native boolean hasSurface(); + public native void surfaceChanged(Surface surface, int format, int width, int height); // TODO: Find a better place for this. @@ -59,6 +65,8 @@ public class AndroidHostInterface { public native void saveState(boolean global, int slot); + public native void saveResumeState(boolean waitForCompletion); + public native void applySettings(); public native void setDisplayAlignment(int alignment); diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java index 9dbb3f803..f8b2e2327 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java @@ -3,6 +3,7 @@ package com.github.stenzek.duckstation; import android.annotation.SuppressLint; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; @@ -12,6 +13,7 @@ import android.content.SharedPreferences; import android.content.res.Configuration; import android.os.Bundle; import android.os.Handler; +import android.util.AndroidException; import android.util.Log; import android.view.Menu; import android.view.SurfaceHolder; @@ -30,7 +32,10 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde /** * Settings interfaces. */ - SharedPreferences mPreferences; + private SharedPreferences mPreferences; + private boolean mWasDestroyed = false; + private boolean mWasPausedOnSurfaceLoss = false; + private boolean mApplySettingsOnSurfaceRestored = false; private boolean getBooleanSetting(String key, boolean defaultValue) { return mPreferences.getBoolean(key, defaultValue); @@ -85,7 +90,8 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde public void onEmulationStopped() { runOnUiThread(() -> { - finish(); + if (!mWasDestroyed) + finish(); }); } @@ -95,6 +101,16 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde }); } + private void applySettings() { + if (!AndroidHostInterface.getInstance().isEmulationThreadRunning()) + return; + + if (AndroidHostInterface.getInstance().hasSurface()) + AndroidHostInterface.getInstance().applySettings(); + else + mApplySettingsOnSurfaceRestored = true; + } + @Override public void surfaceCreated(SurfaceHolder holder) { } @@ -103,8 +119,19 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // Once we get a surface, we can boot. if (AndroidHostInterface.getInstance().isEmulationThreadRunning()) { + final boolean hadSurface = AndroidHostInterface.getInstance().hasSurface(); AndroidHostInterface.getInstance().surfaceChanged(holder.getSurface(), format, width, height); updateOrientation(); + + if (holder.getSurface() != null && !hadSurface && AndroidHostInterface.getInstance().isEmulationThreadPaused() && !mWasPausedOnSurfaceLoss) { + AndroidHostInterface.getInstance().pauseEmulationThread(false); + } + + if (mApplySettingsOnSurfaceRestored) { + AndroidHostInterface.getInstance().applySettings(); + mApplySettingsOnSurfaceRestored = false; + } + return; } @@ -121,14 +148,21 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde if (!AndroidHostInterface.getInstance().isEmulationThreadRunning()) return; - Log.i("EmulationActivity", "Stopping emulation thread"); - AndroidHostInterface.getInstance().stopEmulationThread(); + Log.i("EmulationActivity", "Surface destroyed"); + + // Save the resume state in case we never get back again... + AndroidHostInterface.getInstance().saveResumeState(true); + + mWasPausedOnSurfaceLoss = AndroidHostInterface.getInstance().isEmulationThreadPaused(); + AndroidHostInterface.getInstance().pauseEmulationThread(true); + AndroidHostInterface.getInstance().surfaceChanged(null, 0, 0, 0); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPreferences = PreferenceManager.getDefaultSharedPreferences(this); + Log.i("EmulationActivity", "OnCreate"); setContentView(R.layout.activity_emulation); ActionBar actionBar = getSupportActionBar(); @@ -167,10 +201,20 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde } @Override - protected void onStop() { - super.onStop(); + protected void onPostResume() { + super.onPostResume(); + if (!mSystemUIVisible) + hideSystemUI(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + Log.i("EmulationActivity", "OnStop"); + showSystemUI(false); if (AndroidHostInterface.getInstance().isEmulationThreadRunning()) { + mWasDestroyed = true; AndroidHostInterface.getInstance().stopEmulationThread(); } } @@ -185,6 +229,8 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde return true; } + private static final int REQUEST_CODE_SETTINGS = 0; + @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will @@ -195,7 +241,8 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { Intent intent = new Intent(this, SettingsActivity.class); - startActivity(intent); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivityForResult(intent, REQUEST_CODE_SETTINGS); return true; } else if (id == R.id.show_controller) { setTouchscreenControllerVisibility(!mTouchscreenControllerVisible); @@ -205,7 +252,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde boolean newSetting = !getBooleanSetting("Main/SpeedLimiterEnabled", true); setBooleanSetting("Main/SpeedLimiterEnabled", newSetting); item.setChecked(newSetting); - AndroidHostInterface.getInstance().applySettings(); + applySettings(); return true; } else if (id == R.id.reset) { AndroidHostInterface.getInstance().resetSystem(); @@ -221,6 +268,17 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde return super.onOptionsItemSelected(item); } + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == REQUEST_CODE_SETTINGS) { + // TODO: Sync any menu settings. + if (AndroidHostInterface.getInstance().isEmulationThreadRunning()) + applySettings(); + } + } + @Override public void onBackPressed() { if (mSystemUIVisible) { @@ -228,7 +286,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde return; } - showSystemUI(); + showSystemUI(true); } @Override @@ -308,24 +366,17 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde } @SuppressLint("InlinedApi") - private void showSystemUI() { + private void showSystemUI(boolean delay) { // Show the system bar - mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + mContentView.setSystemUiVisibility(0); mSystemUIVisible = true; // Schedule a runnable to display UI elements after a delay mSystemUIHideHandler.removeCallbacks(mHidePart2Runnable); - mSystemUIHideHandler.postDelayed(mShowPart2Runnable, UI_ANIMATION_DELAY); - } - - /** - * Schedules a call to hide() in delay milliseconds, canceling any - * previously scheduled calls. - */ - private void delayedHide(int delayMillis) { - mSystemUIHideHandler.removeCallbacks(mHideRunnable); - mSystemUIHideHandler.postDelayed(mHideRunnable, delayMillis); + if (delay) + mSystemUIHideHandler.postDelayed(mShowPart2Runnable, UI_ANIMATION_DELAY); + else + mShowPart2Runnable.run(); } /**