Android: Fix emulation stopping on app switch and UI covering display

This commit is contained in:
Connor McLaughlin 2020-09-13 14:37:39 +10:00
parent c360b41a79
commit 5aa1b9553f
4 changed files with 176 additions and 49 deletions

View file

@ -162,6 +162,11 @@ void AndroidHostInterface::UpdateInputMap()
CommonHostInterface::UpdateInputMap(m_settings_interface); CommonHostInterface::UpdateInputMap(m_settings_interface);
} }
bool AndroidHostInterface::IsEmulationThreadPaused() const
{
return System::IsValid() && System::IsPaused();
}
bool AndroidHostInterface::StartEmulationThread(jobject emulation_activity, ANativeWindow* initial_surface, bool AndroidHostInterface::StartEmulationThread(jobject emulation_activity, ANativeWindow* initial_surface,
SystemBootParameters boot_params, bool resume_state) SystemBootParameters boot_params, bool resume_state)
{ {
@ -176,11 +181,23 @@ bool AndroidHostInterface::StartEmulationThread(jobject emulation_activity, ANat
return true; return true;
} }
void AndroidHostInterface::PauseEmulationThread(bool paused)
{
Assert(IsEmulationThreadRunning());
RunOnEmulationThread([this, paused]() {
PauseSystem(paused);
});
}
void AndroidHostInterface::StopEmulationThread() void AndroidHostInterface::StopEmulationThread()
{ {
Assert(IsEmulationThreadRunning()); Assert(IsEmulationThreadRunning());
Log_InfoPrint("Stopping emulation thread..."); Log_InfoPrint("Stopping emulation thread...");
{
std::unique_lock<std::mutex> lock(m_mutex);
m_emulation_thread_stop_request.store(true); m_emulation_thread_stop_request.store(true);
m_sleep_cv.notify_one();
}
m_emulation_thread.join(); m_emulation_thread.join();
Log_InfoPrint("Emulation thread stopped"); Log_InfoPrint("Emulation thread stopped");
} }
@ -193,8 +210,9 @@ void AndroidHostInterface::RunOnEmulationThread(std::function<void()> function,
return; return;
} }
m_callback_mutex.lock(); m_mutex.lock();
m_callback_queue.push_back(std::move(function)); m_callback_queue.push_back(std::move(function));
m_sleep_cv.notify_one();
if (blocking) if (blocking)
{ {
@ -204,12 +222,12 @@ void AndroidHostInterface::RunOnEmulationThread(std::function<void()> function,
if (m_callback_queue.empty()) if (m_callback_queue.empty())
break; break;
m_callback_mutex.unlock(); m_mutex.unlock();
m_callback_mutex.lock(); m_mutex.lock();
} }
} }
m_callback_mutex.unlock(); m_mutex.unlock();
} }
void AndroidHostInterface::EmulationThreadEntryPoint(jobject emulation_activity, ANativeWindow* initial_surface, void AndroidHostInterface::EmulationThreadEntryPoint(jobject emulation_activity, ANativeWindow* initial_surface,
@ -254,22 +272,44 @@ void AndroidHostInterface::EmulationThreadEntryPoint(jobject emulation_activity,
// System is ready to go. // System is ready to go.
thread_env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_onEmulationStarted); thread_env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_onEmulationStarted);
while (!m_emulation_thread_stop_request.load()) EmulationThreadLoop();
{
// run any events thread_env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_onEmulationStopped);
m_callback_mutex.lock(); PowerOffSystem();
DestroyImGuiContext();
thread_env->DeleteGlobalRef(m_emulation_activity_object);
m_emulation_activity_object = {};
s_jvm->DetachCurrentThread();
}
void AndroidHostInterface::EmulationThreadLoop()
{
for (;;) for (;;)
{ {
if (m_callback_queue.empty()) // run any events
break; {
std::unique_lock<std::mutex> lock(m_mutex);
for (;;) {
while (!m_callback_queue.empty()) {
auto callback = std::move(m_callback_queue.front()); auto callback = std::move(m_callback_queue.front());
m_callback_queue.pop_front(); m_callback_queue.pop_front();
m_callback_mutex.unlock(); lock.unlock();
callback(); callback();
m_callback_mutex.lock(); lock.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 // simulate the system if not paused
if (System::IsRunning()) 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() bool AndroidHostInterface::AcquireHostDisplay()
@ -373,7 +406,7 @@ void AndroidHostInterface::SurfaceChanged(ANativeWindow* surface, int format, in
if (m_display) if (m_display)
{ {
WindowInfo wi; WindowInfo wi;
wi.type = WindowInfo::Type::Android; wi.type = surface ? WindowInfo::Type::Android : WindowInfo::Type::Surfaceless;
wi.window_handle = surface; wi.window_handle = surface;
wi.surface_width = width; wi.surface_width = width;
wi.surface_height = height; 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, DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_surfaceChanged, jobject obj, jobject surface, jint format, jint width,
jint height) jint height)
{ {
ANativeWindow* native_surface = ANativeWindow_fromSurface(env, surface); ANativeWindow* native_surface = surface ? ANativeWindow_fromSurface(env, surface) : nullptr;
if (!native_surface) if (surface && !native_surface)
Log_ErrorPrint("ANativeWindow_fromSurface() returned null"); Log_ErrorPrint("ANativeWindow_fromSurface() returned null");
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); 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); }); 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) DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setDisplayAlignment, jobject obj, jint alignment)
{ {
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
hi->RunOnEmulationThread([hi, alignment]() { hi->GetDisplay()->SetDisplayAlignment(static_cast<HostDisplay::Alignment>(alignment)); }); hi->RunOnEmulationThread([hi, alignment]() { hi->GetDisplay()->SetDisplayAlignment(static_cast<HostDisplay::Alignment>(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);
}

View file

@ -4,6 +4,7 @@
#include "frontend-common/common_host_interface.h" #include "frontend-common/common_host_interface.h"
#include <array> #include <array>
#include <atomic> #include <atomic>
#include <condition_variable>
#include <functional> #include <functional>
#include <jni.h> #include <jni.h>
#include <memory> #include <memory>
@ -35,9 +36,11 @@ public:
float GetFloatSettingValue(const char* section, const char* key, float default_value = 0.0f) override; float GetFloatSettingValue(const char* section, const char* key, float default_value = 0.0f) override;
bool IsEmulationThreadRunning() const { return m_emulation_thread.joinable(); } bool IsEmulationThreadRunning() const { return m_emulation_thread.joinable(); }
bool IsEmulationThreadPaused() const;
bool StartEmulationThread(jobject emulation_activity, ANativeWindow* initial_surface, bool StartEmulationThread(jobject emulation_activity, ANativeWindow* initial_surface,
SystemBootParameters boot_params, bool resume_state); SystemBootParameters boot_params, bool resume_state);
void RunOnEmulationThread(std::function<void()> function, bool blocking = false); void RunOnEmulationThread(std::function<void()> function, bool blocking = false);
void PauseEmulationThread(bool paused);
void StopEmulationThread(); void StopEmulationThread();
void SurfaceChanged(ANativeWindow* surface, int format, int width, int height); void SurfaceChanged(ANativeWindow* surface, int format, int width, int height);
@ -63,6 +66,7 @@ protected:
private: private:
void EmulationThreadEntryPoint(jobject emulation_activity, ANativeWindow* initial_surface, void EmulationThreadEntryPoint(jobject emulation_activity, ANativeWindow* initial_surface,
SystemBootParameters boot_params, bool resume_state); SystemBootParameters boot_params, bool resume_state);
void EmulationThreadLoop();
void CreateImGuiContext(); void CreateImGuiContext();
void DestroyImGuiContext(); void DestroyImGuiContext();
@ -74,7 +78,8 @@ private:
ANativeWindow* m_surface = nullptr; ANativeWindow* m_surface = nullptr;
std::mutex m_callback_mutex; std::mutex m_mutex;
std::condition_variable m_sleep_cv;
std::deque<std::function<void()>> m_callback_queue; std::deque<std::function<void()>> m_callback_queue;
std::thread m_emulation_thread; std::thread m_emulation_thread;

View file

@ -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 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 void stopEmulationThread();
public native boolean hasSurface();
public native void surfaceChanged(Surface surface, int format, int width, int height); public native void surfaceChanged(Surface surface, int format, int width, int height);
// TODO: Find a better place for this. // 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 saveState(boolean global, int slot);
public native void saveResumeState(boolean waitForCompletion);
public native void applySettings(); public native void applySettings();
public native void setDisplayAlignment(int alignment); public native void setDisplayAlignment(int alignment);

View file

@ -3,6 +3,7 @@ package com.github.stenzek.duckstation;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
@ -12,6 +13,7 @@ import android.content.SharedPreferences;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.util.AndroidException;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
@ -30,7 +32,10 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
/** /**
* Settings interfaces. * 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) { private boolean getBooleanSetting(String key, boolean defaultValue) {
return mPreferences.getBoolean(key, defaultValue); return mPreferences.getBoolean(key, defaultValue);
@ -85,6 +90,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
public void onEmulationStopped() { public void onEmulationStopped() {
runOnUiThread(() -> { runOnUiThread(() -> {
if (!mWasDestroyed)
finish(); 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 @Override
public void surfaceCreated(SurfaceHolder holder) { 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) { public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// Once we get a surface, we can boot. // Once we get a surface, we can boot.
if (AndroidHostInterface.getInstance().isEmulationThreadRunning()) { if (AndroidHostInterface.getInstance().isEmulationThreadRunning()) {
final boolean hadSurface = AndroidHostInterface.getInstance().hasSurface();
AndroidHostInterface.getInstance().surfaceChanged(holder.getSurface(), format, width, height); AndroidHostInterface.getInstance().surfaceChanged(holder.getSurface(), format, width, height);
updateOrientation(); updateOrientation();
if (holder.getSurface() != null && !hadSurface && AndroidHostInterface.getInstance().isEmulationThreadPaused() && !mWasPausedOnSurfaceLoss) {
AndroidHostInterface.getInstance().pauseEmulationThread(false);
}
if (mApplySettingsOnSurfaceRestored) {
AndroidHostInterface.getInstance().applySettings();
mApplySettingsOnSurfaceRestored = false;
}
return; return;
} }
@ -121,14 +148,21 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
if (!AndroidHostInterface.getInstance().isEmulationThreadRunning()) if (!AndroidHostInterface.getInstance().isEmulationThreadRunning())
return; return;
Log.i("EmulationActivity", "Stopping emulation thread"); Log.i("EmulationActivity", "Surface destroyed");
AndroidHostInterface.getInstance().stopEmulationThread();
// 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 @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
mPreferences = PreferenceManager.getDefaultSharedPreferences(this); mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
Log.i("EmulationActivity", "OnCreate");
setContentView(R.layout.activity_emulation); setContentView(R.layout.activity_emulation);
ActionBar actionBar = getSupportActionBar(); ActionBar actionBar = getSupportActionBar();
@ -167,10 +201,20 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
} }
@Override @Override
protected void onStop() { protected void onPostResume() {
super.onStop(); super.onPostResume();
if (!mSystemUIVisible)
hideSystemUI();
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.i("EmulationActivity", "OnStop");
showSystemUI(false);
if (AndroidHostInterface.getInstance().isEmulationThreadRunning()) { if (AndroidHostInterface.getInstance().isEmulationThreadRunning()) {
mWasDestroyed = true;
AndroidHostInterface.getInstance().stopEmulationThread(); AndroidHostInterface.getInstance().stopEmulationThread();
} }
} }
@ -185,6 +229,8 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
return true; return true;
} }
private static final int REQUEST_CODE_SETTINGS = 0;
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will // Handle action bar item clicks here. The action bar will
@ -195,7 +241,8 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
//noinspection SimplifiableIfStatement //noinspection SimplifiableIfStatement
if (id == R.id.action_settings) { if (id == R.id.action_settings) {
Intent intent = new Intent(this, SettingsActivity.class); Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResult(intent, REQUEST_CODE_SETTINGS);
return true; return true;
} else if (id == R.id.show_controller) { } else if (id == R.id.show_controller) {
setTouchscreenControllerVisibility(!mTouchscreenControllerVisible); setTouchscreenControllerVisibility(!mTouchscreenControllerVisible);
@ -205,7 +252,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
boolean newSetting = !getBooleanSetting("Main/SpeedLimiterEnabled", true); boolean newSetting = !getBooleanSetting("Main/SpeedLimiterEnabled", true);
setBooleanSetting("Main/SpeedLimiterEnabled", newSetting); setBooleanSetting("Main/SpeedLimiterEnabled", newSetting);
item.setChecked(newSetting); item.setChecked(newSetting);
AndroidHostInterface.getInstance().applySettings(); applySettings();
return true; return true;
} else if (id == R.id.reset) { } else if (id == R.id.reset) {
AndroidHostInterface.getInstance().resetSystem(); AndroidHostInterface.getInstance().resetSystem();
@ -221,6 +268,17 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
return super.onOptionsItemSelected(item); 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 @Override
public void onBackPressed() { public void onBackPressed() {
if (mSystemUIVisible) { if (mSystemUIVisible) {
@ -228,7 +286,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
return; return;
} }
showSystemUI(); showSystemUI(true);
} }
@Override @Override
@ -308,24 +366,17 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
} }
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
private void showSystemUI() { private void showSystemUI(boolean delay) {
// Show the system bar // Show the system bar
mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN mContentView.setSystemUiVisibility(0);
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
mSystemUIVisible = true; mSystemUIVisible = true;
// Schedule a runnable to display UI elements after a delay // Schedule a runnable to display UI elements after a delay
mSystemUIHideHandler.removeCallbacks(mHidePart2Runnable); mSystemUIHideHandler.removeCallbacks(mHidePart2Runnable);
if (delay)
mSystemUIHideHandler.postDelayed(mShowPart2Runnable, UI_ANIMATION_DELAY); mSystemUIHideHandler.postDelayed(mShowPart2Runnable, UI_ANIMATION_DELAY);
} else
mShowPart2Runnable.run();
/**
* 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);
} }
/** /**