From 861510b1926f50d104466701c7b705fd24bb74f2 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Wed, 12 Aug 2020 23:20:04 +0800 Subject: [PATCH 1/6] core, video_core: Fixes to make savestates work 1. Acquire the context before initializing renderer when using async gpu 2. Do not try present when renderer is nullptr This has some potential race condition but is also what we do in qt 3. Synchronize before serializing video core (WaitForProcessing) For this, the GPU thread is changed to pop commands *after* processing. 4. Avoid waiting on future fences Such events can exist in core timing queue when deserializing. --- .../app/src/main/jni/emu_window/emu_window.cpp | 2 +- src/video_core/gpu.cpp | 6 ++++++ src/video_core/gpu.h | 2 ++ src/video_core/gpu_thread.cpp | 14 +++++++++++--- src/video_core/gpu_thread.h | 9 ++++++++- src/video_core/renderer_opengl/renderer_opengl.cpp | 4 ++++ src/video_core/video_core.cpp | 1 + 7 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/android/app/src/main/jni/emu_window/emu_window.cpp b/src/android/app/src/main/jni/emu_window/emu_window.cpp index 71a5ff633..118e33e4c 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.cpp +++ b/src/android/app/src/main/jni/emu_window/emu_window.cpp @@ -246,7 +246,7 @@ void EmuWindow_Android::TryPresenting() { } } eglSwapInterval(egl_display, Settings::values.use_vsync_new ? 1 : 0); - if (VideoCore::g_renderer->TryPresent()) { + if (VideoCore::g_renderer && VideoCore::g_renderer->TryPresent()) { eglSwapBuffers(egl_display, egl_surface); } } diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp index b75157b40..d5423c739 100644 --- a/src/video_core/gpu.cpp +++ b/src/video_core/gpu.cpp @@ -13,6 +13,8 @@ GPUBackend::GPUBackend(VideoCore::RendererBase& renderer) : renderer{renderer} { GPUBackend::~GPUBackend() = default; +void GPUBackend::WaitForProcessing() {} + GPUSerial::GPUSerial(Core::System& system, VideoCore::RendererBase& renderer) : GPUBackend(renderer), system{system} {} @@ -83,4 +85,8 @@ void GPUParallel::InvalidateRegion(VAddr addr, u64 size) { gpu_thread.InvalidateRegion(addr, size); } +void GPUParallel::WaitForProcessing() { + gpu_thread.WaitForProcessing(); +} + } // namespace VideoCore diff --git a/src/video_core/gpu.h b/src/video_core/gpu.h index d239704ba..b47023dc5 100644 --- a/src/video_core/gpu.h +++ b/src/video_core/gpu.h @@ -29,6 +29,7 @@ public: virtual void FlushRegion(VAddr addr, u64 size) = 0; virtual void FlushAndInvalidateRegion(VAddr addr, u64 size) = 0; virtual void InvalidateRegion(VAddr addr, u64 size) = 0; + virtual void WaitForProcessing(); protected: VideoCore::RendererBase& renderer; @@ -65,6 +66,7 @@ public: void FlushRegion(VAddr addr, u64 size) override; void FlushAndInvalidateRegion(VAddr addr, u64 size) override; void InvalidateRegion(VAddr addr, u64 size) override; + void WaitForProcessing() override; private: GPUThread::ThreadManager gpu_thread; diff --git a/src/video_core/gpu_thread.cpp b/src/video_core/gpu_thread.cpp index 5cc22433c..fc62e69ae 100644 --- a/src/video_core/gpu_thread.cpp +++ b/src/video_core/gpu_thread.cpp @@ -32,12 +32,12 @@ static void RunThread(VideoCore::RendererBase& renderer, SynchState& state, Core Frontend::ScopeAcquireContext acquire_context{renderer.GetRenderWindow()}; - CommandDataContainer next; while (state.is_running) { state.WaitForCommands(); - CommandDataContainer next; - while (state.queue.Pop(next)) { + while (!state.queue.Empty()) { + CommandDataContainer next = state.queue.Front(); + auto command = &next.data; auto fence = next.fence; if (const auto submit_list = std::get_if(command)) { @@ -62,6 +62,7 @@ static void RunThread(VideoCore::RendererBase& renderer, SynchState& state, Core UNREACHABLE(); } state.signaled_fence = next.fence; + state.queue.Pop(); } } } @@ -211,8 +212,15 @@ u64 ThreadManager::PushCommand(CommandData&& command_data) { return fence; } +void ThreadManager::WaitForProcessing() { + state.WaitForProcessing(); +} + MICROPROFILE_DEFINE(GPU_wait, "GPU", "Wait for the GPU", MP_RGB(128, 128, 192)); void SynchState::WaitForSynchronization(u64 fence) { + if (fence > last_fence) { // We don't want to wait infinitely + return; + } if (signaled_fence >= fence) { return; } diff --git a/src/video_core/gpu_thread.h b/src/video_core/gpu_thread.h index 27e33d7f8..ade649cdb 100644 --- a/src/video_core/gpu_thread.h +++ b/src/video_core/gpu_thread.h @@ -169,9 +169,14 @@ struct SynchState final { // commands_condition.wait(lock, [this] { return !queue.Empty(); }); } + void WaitForProcessing() { + while (!queue.Empty() && is_running) + ; + } + using CommandQueue = Common::SPSCQueue; CommandQueue queue; - u64 last_fence{}; + std::atomic last_fence{}; std::atomic signaled_fence{}; }; @@ -195,6 +200,8 @@ public: void InvalidateRegion(VAddr addr, u64 size); + void WaitForProcessing(); + private: void Synchronize(u64 fence, Settings::GpuTimingMode mode); diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp index 56718c54a..8c67376f8 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.cpp +++ b/src/video_core/renderer_opengl/renderer_opengl.cpp @@ -1193,6 +1193,10 @@ VideoCore::ResultStatus RendererOpenGL::Init() { } #endif + if (Settings::values.use_asynchronous_gpu_emulation) { + render_window.MakeCurrent(); + } + const char* gl_version{reinterpret_cast(glGetString(GL_VERSION))}; const char* gpu_vendor{reinterpret_cast(glGetString(GL_VENDOR))}; const char* gpu_model{reinterpret_cast(glGetString(GL_RENDERER))}; diff --git a/src/video_core/video_core.cpp b/src/video_core/video_core.cpp index 2e32953de..e0e234418 100644 --- a/src/video_core/video_core.cpp +++ b/src/video_core/video_core.cpp @@ -104,6 +104,7 @@ u16 GetResolutionScaleFactor() { template void serialize(Archive& ar, const unsigned int) { + g_gpu->WaitForProcessing(); ar& Pica::g_state; } From 1d1fd105e47acd9645659f697b8796c4b219c763 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Wed, 12 Aug 2020 23:48:57 +0800 Subject: [PATCH 2/6] android: Add savestates UI A simple menu with savestates info. --- .../org/citra/citra_emu/NativeLibrary.java | 15 +++++++ .../activities/EmulationActivity.java | 40 +++++++++++++++++ src/android/app/src/main/jni/id_cache.cpp | 8 ++++ src/android/app/src/main/jni/id_cache.h | 1 + src/android/app/src/main/jni/native.cpp | 44 +++++++++++++++++++ src/android/app/src/main/jni/native.h | 19 ++++++-- .../app/src/main/res/menu/menu_emulation.xml | 12 +++++ .../app/src/main/res/values/strings.xml | 4 ++ 8 files changed, 139 insertions(+), 4 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java index 9baeeb5e4..62685eb9f 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java @@ -16,6 +16,7 @@ import android.widget.EditText; import android.widget.FrameLayout; import android.widget.TextView; +import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; @@ -25,6 +26,7 @@ import org.citra.citra_emu.utils.Log; import org.citra.citra_emu.utils.PermissionsHandler; import java.lang.ref.WeakReference; +import java.util.Date; import static android.Manifest.permission.CAMERA; import static android.Manifest.permission.RECORD_AUDIO; @@ -487,6 +489,19 @@ public final class NativeLibrary { public static native void InstallCIAS(String[] path); + public static final int SAVESTATE_SLOT_COUNT = 10; + + public static final class SavestateInfo { + public int slot; + public Date time; + } + + @Nullable + public static native SavestateInfo[] GetSavestateInfo(); + + public static native void SaveState(int slot); + public static native void LoadState(int slot); + /** * Button type for use in onTouchEvent */ diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java index 4f058a76a..9837c2ec7 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java @@ -14,6 +14,7 @@ import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; +import android.view.SubMenu; import android.view.View; import android.widget.SeekBar; import android.widget.TextView; @@ -303,6 +304,45 @@ public final class EmulationActivity extends AppCompatActivity { return true; } + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + + final NativeLibrary.SavestateInfo[] savestates = NativeLibrary.GetSavestateInfo(); + if (savestates == null) { + menu.findItem(R.id.menu_emulation_save_state).setVisible(false); + menu.findItem(R.id.menu_emulation_load_state).setVisible(false); + return true; + } + menu.findItem(R.id.menu_emulation_save_state).setVisible(true); + menu.findItem(R.id.menu_emulation_load_state).setVisible(true); + + final SubMenu saveStateMenu = menu.findItem(R.id.menu_emulation_save_state).getSubMenu(); + final SubMenu loadStateMenu = menu.findItem(R.id.menu_emulation_load_state).getSubMenu(); + saveStateMenu.clear(); + loadStateMenu.clear(); + + // Update savestates information + for (int i = 0; i < NativeLibrary.SAVESTATE_SLOT_COUNT; ++i) { + final int slot = i + 1; + final String text = getString(R.string.emulation_empty_state_slot, slot); + saveStateMenu.add(text).setEnabled(true).setOnMenuItemClickListener((item) -> { + NativeLibrary.SaveState(slot); + return true; + }); + loadStateMenu.add(text).setEnabled(false).setOnMenuItemClickListener((item) -> { + NativeLibrary.LoadState(slot); + return true; + }); + } + for (final NativeLibrary.SavestateInfo info : savestates) { + final String text = getString(R.string.emulation_occupied_state_slot, info.slot, info.time); + saveStateMenu.getItem(info.slot - 1).setTitle(text); + loadStateMenu.getItem(info.slot - 1).setTitle(text).setEnabled(true); + } + return true; + } + @SuppressWarnings("WrongConstant") @Override public boolean onOptionsItemSelected(MenuItem item) { diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp index 6c5c31acf..b51b11846 100644 --- a/src/android/app/src/main/jni/id_cache.cpp +++ b/src/android/app/src/main/jni/id_cache.cpp @@ -19,6 +19,7 @@ static constexpr jint JNI_VERSION = JNI_VERSION_1_6; static JavaVM* s_java_vm; static jclass s_native_library_class; +static jclass s_savestate_info_class; static jmethodID s_display_alert_msg; static jmethodID s_display_alert_prompt; static jmethodID s_alert_prompt_button; @@ -53,6 +54,10 @@ jclass GetNativeLibraryClass() { return s_native_library_class; } +jclass GetSavestateInfoClass() { + return s_savestate_info_class; +} + jmethodID GetDisplayAlertMsg() { return s_display_alert_msg; } @@ -111,6 +116,8 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { // Initialize Java methods const jclass native_library_class = env->FindClass("org/citra/citra_emu/NativeLibrary"); s_native_library_class = reinterpret_cast(env->NewGlobalRef(native_library_class)); + s_savestate_info_class = reinterpret_cast( + env->NewGlobalRef(env->FindClass("org/citra/citra_emu/NativeLibrary$SavestateInfo"))); s_display_alert_msg = env->GetStaticMethodID(s_native_library_class, "displayAlertMsg", "(Ljava/lang/String;Ljava/lang/String;Z)Z"); s_display_alert_prompt = @@ -142,6 +149,7 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) { } env->DeleteGlobalRef(s_native_library_class); + env->DeleteGlobalRef(s_savestate_info_class); MiiSelector::CleanupJNI(env); SoftwareKeyboard::CleanupJNI(env); Camera::StillImage::CleanupJNI(env); diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h index 64c185e80..ee3fe6be8 100644 --- a/src/android/app/src/main/jni/id_cache.h +++ b/src/android/app/src/main/jni/id_cache.h @@ -12,6 +12,7 @@ namespace IDCache { JNIEnv* GetEnvForThread(); jclass GetNativeLibraryClass(); +jclass GetSavestateInfoClass(); jmethodID GetDisplayAlertMsg(); jmethodID GetDisplayAlertPrompt(); jmethodID GetAlertPromptButton(); diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 8262b9f38..7a6f89e90 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -23,6 +23,7 @@ #include "core/frontend/scope_acquire_context.h" #include "core/hle/service/am/am.h" #include "core/hle/service/nfc/nfc.h" +#include "core/savestate.h" #include "core/settings.h" #include "jni/applets/mii_selector.h" #include "jni/applets/swkbd.h" @@ -620,4 +621,47 @@ void Java_org_citra_citra_1emu_NativeLibrary_InstallCIAS(JNIEnv* env, [[maybe_un thread.join(); } +jobjectArray Java_org_citra_citra_1emu_NativeLibrary_GetSavestateInfo( + JNIEnv* env, [[maybe_unused]] jclass clazz) { + const jclass date_class = env->FindClass("java/util/Date"); + const auto date_constructor = env->GetMethodID(date_class, "", "(J)V"); + + const jclass savestate_info_class = IDCache::GetSavestateInfoClass(); + const auto slot_field = env->GetFieldID(savestate_info_class, "slot", "I"); + const auto date_field = env->GetFieldID(savestate_info_class, "time", "Ljava/util/Date;"); + + const Core::System& system{Core::System::GetInstance()}; + if (!system.IsPoweredOn()) { + return nullptr; + } + + u64 title_id; + if (system.GetAppLoader().ReadProgramId(title_id) != Loader::ResultStatus::Success) { + return nullptr; + } + + const auto savestates = Core::ListSaveStates(title_id); + const jobjectArray array = + env->NewObjectArray(static_cast(savestates.size()), savestate_info_class, nullptr); + for (std::size_t i = 0; i < savestates.size(); ++i) { + const jobject object = env->AllocObject(savestate_info_class); + env->SetIntField(object, slot_field, static_cast(savestates[i].slot)); + env->SetObjectField(object, date_field, + env->NewObject(date_class, date_constructor, + static_cast(savestates[i].time * 1000))); + + env->SetObjectArrayElement(array, i, object); + } + LOG_CRITICAL(Frontend, "Called"); + return array; +} + +void Java_org_citra_citra_1emu_NativeLibrary_SaveState(JNIEnv* env, jclass clazz, jint slot) { + Core::System::GetInstance().SendSignal(Core::System::Signal::Save, slot); +} + +void Java_org_citra_citra_1emu_NativeLibrary_LoadState(JNIEnv* env, jclass clazz, jint slot) { + Core::System::GetInstance().SendSignal(Core::System::Signal::Load, slot); +} + } // extern "C" diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h index 11f526215..f02b15b35 100644 --- a/src/android/app/src/main/jni/native.h +++ b/src/android/app/src/main/jni/native.h @@ -33,9 +33,9 @@ JNIEXPORT jboolean JNICALL Java_org_citra_citra_1emu_NativeLibrary_onGamePadAxis JNIEnv* env, jclass clazz, jstring j_device, jint axis_id, jfloat axis_val); JNIEXPORT jboolean JNICALL Java_org_citra_citra_1emu_NativeLibrary_onTouchEvent(JNIEnv* env, - jclass clazz, jfloat x, - jfloat y, - jboolean pressed); + jclass clazz, + jfloat x, jfloat y, + jboolean pressed); JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, jclass clazz, jfloat x, @@ -142,7 +142,18 @@ JNIEXPORT jboolean Java_org_citra_citra_1emu_NativeLibrary_LoadAmiibo(JNIEnv* en JNIEXPORT void Java_org_citra_citra_1emu_NativeLibrary_RemoveAmiibo(JNIEnv* env, jclass clazz); -JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_InstallCIAS(JNIEnv* env, jclass clazz, jobjectArray path); +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_InstallCIAS(JNIEnv* env, + jclass clazz, + jobjectArray path); + +JNIEXPORT jobjectArray JNICALL +Java_org_citra_citra_1emu_NativeLibrary_GetSavestateInfo(JNIEnv* env, jclass clazz); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SaveState(JNIEnv* env, jclass clazz, + jint slot); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_LoadState(JNIEnv* env, jclass clazz, + jint slot); #ifdef __cplusplus } diff --git a/src/android/app/src/main/res/menu/menu_emulation.xml b/src/android/app/src/main/res/menu/menu_emulation.xml index a3ecb80d7..ea3301d37 100644 --- a/src/android/app/src/main/res/menu/menu_emulation.xml +++ b/src/android/app/src/main/res/menu/menu_emulation.xml @@ -3,6 +3,18 @@ xmlns:app="http://schemas.android.com/apk/res-auto" tools:context="org.citra.citra_emu.activities.EmulationActivity"> + + + + + + + + diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 0e9799dfe..afe96d16e 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -143,6 +143,10 @@ Invalid ROM format + Save State + Load State + Slot %1$d + Slot %1$d - %2$tF %2$tR Show FPS Configure Controls Edit Layout From 62cadb96350fbc524c056ed66b7d827ce1e139e8 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Fri, 14 Aug 2020 23:25:26 +0800 Subject: [PATCH 3/6] android: Handle core errors The errors are handled in a similar manner to the Qt frontend: an AlertDialog will pop up, prompting the user to select 'Abort' or 'Continue'. Error messages are translatable as string values. --- .../org/citra/citra_emu/NativeLibrary.java | 78 +++++++++++++++++++ src/android/app/src/main/jni/id_cache.cpp | 16 ++++ src/android/app/src/main/jni/id_cache.h | 2 + src/android/app/src/main/jni/native.cpp | 38 ++++++++- .../app/src/main/res/values/strings.xml | 10 +++ 5 files changed, 143 insertions(+), 1 deletion(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java index 62685eb9f..01e5aca95 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java @@ -211,6 +211,84 @@ public final class NativeLibrary { */ public static native void SwapScreens(boolean swap_screens, int rotation); + public enum CoreError { + ErrorSystemFiles, + ErrorSavestate, + ErrorUnknown, + } + + private static boolean coreErrorAlertResult = false; + + /** + * Handles a core error. + * @return true: continue; false: abort + */ + public static boolean OnCoreError(CoreError error, String details) { + final EmulationActivity emulationActivity = sEmulationActivity.get(); + if (emulationActivity == null) { + Log.error("[NativeLibrary] EmulationActivity not present"); + return false; + } + + String title, message; + switch (error) { + case ErrorSystemFiles: { + title = emulationActivity.getString(R.string.system_archive_not_found); + message = emulationActivity.getString(R.string.system_archive_not_found_message, details.isEmpty() ? emulationActivity.getString(R.string.system_archive_general) : details); + break; + } + case ErrorSavestate: { + title = emulationActivity.getString(R.string.save_load_error); + message = details; + break; + } + case ErrorUnknown: { + title = emulationActivity.getString(R.string.fatal_error); + message = emulationActivity.getString(R.string.fatal_error_message); + break; + } + default: { + return true; + } + } + + // Create object used for waiting. + final Object lock = new Object(); + final AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity) + .setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.continue_button, (dialog, which) -> { + coreErrorAlertResult = true; + synchronized (lock) { + lock.notify(); + } + }) + .setNegativeButton(R.string.abort_button, (dialog, which) -> { + coreErrorAlertResult = false; + synchronized (lock) { + lock.notify(); + } + }).setOnDismissListener(dialog -> { + coreErrorAlertResult = true; + synchronized (lock) { + lock.notify(); + } + }); + + // Show the AlertDialog on the main thread. + emulationActivity.runOnUiThread(builder::show); + + // Wait for the lock to notify that it is complete. + synchronized (lock) { + try { + lock.wait(); + } catch (Exception ignored) { + } + } + + return coreErrorAlertResult; + } + public static boolean isPortraitMode() { return CitraApplication.getAppContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp index b51b11846..d716640b6 100644 --- a/src/android/app/src/main/jni/id_cache.cpp +++ b/src/android/app/src/main/jni/id_cache.cpp @@ -19,7 +19,9 @@ static constexpr jint JNI_VERSION = JNI_VERSION_1_6; static JavaVM* s_java_vm; static jclass s_native_library_class; +static jclass s_core_error_class; static jclass s_savestate_info_class; +static jmethodID s_on_core_error; static jmethodID s_display_alert_msg; static jmethodID s_display_alert_prompt; static jmethodID s_alert_prompt_button; @@ -54,10 +56,18 @@ jclass GetNativeLibraryClass() { return s_native_library_class; } +jclass GetCoreErrorClass() { + return s_core_error_class; +} + jclass GetSavestateInfoClass() { return s_savestate_info_class; } +jmethodID GetOnCoreError() { + return s_on_core_error; +} + jmethodID GetDisplayAlertMsg() { return s_display_alert_msg; } @@ -118,6 +128,11 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { s_native_library_class = reinterpret_cast(env->NewGlobalRef(native_library_class)); s_savestate_info_class = reinterpret_cast( env->NewGlobalRef(env->FindClass("org/citra/citra_emu/NativeLibrary$SavestateInfo"))); + s_core_error_class = reinterpret_cast( + env->NewGlobalRef(env->FindClass("org/citra/citra_emu/NativeLibrary$CoreError"))); + s_on_core_error = env->GetStaticMethodID( + s_native_library_class, "OnCoreError", + "(Lorg/citra/citra_emu/NativeLibrary$CoreError;Ljava/lang/String;)Z"); s_display_alert_msg = env->GetStaticMethodID(s_native_library_class, "displayAlertMsg", "(Ljava/lang/String;Ljava/lang/String;Z)Z"); s_display_alert_prompt = @@ -150,6 +165,7 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) { env->DeleteGlobalRef(s_native_library_class); env->DeleteGlobalRef(s_savestate_info_class); + env->DeleteGlobalRef(s_core_error_class); MiiSelector::CleanupJNI(env); SoftwareKeyboard::CleanupJNI(env); Camera::StillImage::CleanupJNI(env); diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h index ee3fe6be8..fc4987b46 100644 --- a/src/android/app/src/main/jni/id_cache.h +++ b/src/android/app/src/main/jni/id_cache.h @@ -12,7 +12,9 @@ namespace IDCache { JNIEnv* GetEnvForThread(); jclass GetNativeLibraryClass(); +jclass GetCoreErrorClass(); jclass GetSavestateInfoClass(); +jmethodID GetOnCoreError(); jmethodID GetDisplayAlertMsg(); jmethodID GetDisplayAlertPrompt(); jmethodID GetAlertPromptButton(); diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 7a6f89e90..62b57db10 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -96,6 +96,29 @@ static int AlertPromptButton() { IDCache::GetAlertPromptButton())); } +static jobject ToJavaCoreError(Core::System::ResultStatus result) { + static const std::map CoreErrorNameMap{ + {Core::System::ResultStatus::ErrorSystemFiles, "ErrorSystemFiles"}, + {Core::System::ResultStatus::ErrorSavestate, "ErrorSavestate"}, + {Core::System::ResultStatus::ErrorUnknown, "ErrorUnknown"}, + }; + + const auto name = CoreErrorNameMap.count(result) ? CoreErrorNameMap.at(result) : "ErrorUnknown"; + + JNIEnv* env = IDCache::GetEnvForThread(); + const jclass core_error_class = IDCache::GetCoreErrorClass(); + return env->GetStaticObjectField( + core_error_class, env->GetStaticFieldID(core_error_class, name, + "Lorg/citra/citra_emu/NativeLibrary$CoreError;")); +} + +static bool HandleCoreError(Core::System::ResultStatus result, const std::string& details) { + JNIEnv* env = IDCache::GetEnvForThread(); + return env->CallStaticBooleanMethod(IDCache::GetNativeLibraryClass(), IDCache::GetOnCoreError(), + ToJavaCoreError(result), + env->NewStringUTF(details.c_str())) != JNI_FALSE; +} + static Camera::NDK::Factory* g_ndk_factory{}; static void TryShutdown() { @@ -187,7 +210,20 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { // Start running emulation while (is_running) { if (!pause_emulation) { - system.RunLoop(); + const auto result = system.RunLoop(); + if (result == Core::System::ResultStatus::Success) { + continue; + } + if (result == Core::System::ResultStatus::ShutdownRequested) { + return result; // This also exits the emulation activity + } else { + InputManager::NDKMotionHandler()->DisableSensors(); + if (!HandleCoreError(result, system.GetStatusDetails())) { + // Frontend requests us to abort + return result; + } + InputManager::NDKMotionHandler()->EnableSensors(); + } } else { // Ensure no audio bleeds out while game is paused const float volume = Settings::values.volume; diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index afe96d16e..a661203d5 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -201,4 +201,14 @@ Microphone Citra needs to access your microphone to emulate the 3DS\'s microphone.\n\nAlternatively, you can also change \"Audio Input Device\" in Audio Settings. + + + Abort + Continue + System Archive Not Found + %s is missing. Please dump your system archives.\nContinuing emulation may result in crashes and bugs. + A system archive + Save/Load Error + Fatal Error + A fatal error occurred. Check the log for details.\nContinuing emulation may result in crashes and bugs. From b0d3867c59eb04741aee85176d596df44caadb7b Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 15 Aug 2020 10:57:33 +0800 Subject: [PATCH 4/6] fixup! android: Add savestates UI --- src/android/app/src/main/jni/native.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 62b57db10..278ea8439 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -688,7 +688,6 @@ jobjectArray Java_org_citra_citra_1emu_NativeLibrary_GetSavestateInfo( env->SetObjectArrayElement(array, i, object); } - LOG_CRITICAL(Frontend, "Called"); return array; } From f9ddef9cbb500850ac593bc6f6fe70ad1397eae6 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Mon, 31 Aug 2020 22:32:17 +0800 Subject: [PATCH 5/6] android: Use DialogFragment for the core error dialog Fixes a bug when changing orientation while the dialog is shown. --- .../org/citra/citra_emu/NativeLibrary.java | 89 +++++++++++++------ 1 file changed, 63 insertions(+), 26 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java index 01e5aca95..48555252e 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java @@ -6,8 +6,11 @@ package org.citra.citra_emu; +import android.app.Activity; +import android.app.Dialog; import android.content.pm.PackageManager; import android.content.res.Configuration; +import android.os.Bundle; import android.text.Html; import android.text.method.LinkMovementMethod; import android.view.Surface; @@ -16,17 +19,21 @@ import android.widget.EditText; import android.widget.FrameLayout; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; +import androidx.fragment.app.DialogFragment; import org.citra.citra_emu.activities.EmulationActivity; +import org.citra.citra_emu.applets.SoftwareKeyboard; import org.citra.citra_emu.utils.EmulationMenuSettings; import org.citra.citra_emu.utils.Log; import org.citra.citra_emu.utils.PermissionsHandler; import java.lang.ref.WeakReference; import java.util.Date; +import java.util.Objects; import static android.Manifest.permission.CAMERA; import static android.Manifest.permission.RECORD_AUDIO; @@ -218,6 +225,59 @@ public final class NativeLibrary { } private static boolean coreErrorAlertResult = false; + private static final Object coreErrorAlertLock = new Object(); + + public static class CoreErrorDialogFragment extends DialogFragment { + static CoreErrorDialogFragment newInstance(String title, String message) { + CoreErrorDialogFragment frag = new CoreErrorDialogFragment(); + Bundle args = new Bundle(); + args.putString("title", title); + args.putString("message", message); + frag.setArguments(args); + return frag; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity emulationActivity = Objects.requireNonNull(getActivity()); + + final String title = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("title")); + final String message = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("message")); + + return new AlertDialog.Builder(emulationActivity) + .setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.continue_button, (dialog, which) -> { + coreErrorAlertResult = true; + synchronized (coreErrorAlertLock) { + coreErrorAlertLock.notify(); + } + }) + .setNegativeButton(R.string.abort_button, (dialog, which) -> { + coreErrorAlertResult = false; + synchronized (coreErrorAlertLock) { + coreErrorAlertLock.notify(); + } + }).setOnDismissListener(dialog -> { + coreErrorAlertResult = true; + synchronized (coreErrorAlertLock) { + coreErrorAlertLock.notify(); + } + }).create(); + } + } + + private static void OnCoreErrorImpl(String title, String message) { + final EmulationActivity emulationActivity = sEmulationActivity.get(); + if (emulationActivity == null) { + Log.error("[NativeLibrary] EmulationActivity not present"); + return; + } + + CoreErrorDialogFragment fragment = CoreErrorDialogFragment.newInstance(title, message); + fragment.show(emulationActivity.getSupportFragmentManager(), "coreError"); + } /** * Handles a core error. @@ -252,36 +312,13 @@ public final class NativeLibrary { } } - // Create object used for waiting. - final Object lock = new Object(); - final AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity) - .setTitle(title) - .setMessage(message) - .setPositiveButton(R.string.continue_button, (dialog, which) -> { - coreErrorAlertResult = true; - synchronized (lock) { - lock.notify(); - } - }) - .setNegativeButton(R.string.abort_button, (dialog, which) -> { - coreErrorAlertResult = false; - synchronized (lock) { - lock.notify(); - } - }).setOnDismissListener(dialog -> { - coreErrorAlertResult = true; - synchronized (lock) { - lock.notify(); - } - }); - // Show the AlertDialog on the main thread. - emulationActivity.runOnUiThread(builder::show); + emulationActivity.runOnUiThread(() -> OnCoreErrorImpl(title, message)); // Wait for the lock to notify that it is complete. - synchronized (lock) { + synchronized (coreErrorAlertLock) { try { - lock.wait(); + coreErrorAlertLock.wait(); } catch (Exception ignored) { } } From 1bcef18b9ac4cbca0636f93c0f44471a426673b9 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 5 Sep 2020 23:13:12 +0800 Subject: [PATCH 6/6] android: Add a warning when saving state Display a warning when a user saves a state, until they explicitly choose to not see it anymore. --- .../activities/EmulationActivity.java | 23 +++++++++++++++++++ .../src/main/res/layout/dialog_checkbox.xml | 16 +++++++++++++ .../app/src/main/res/values/strings.xml | 4 ++++ 3 files changed, 43 insertions(+) create mode 100644 src/android/app/src/main/res/layout/dialog_checkbox.xml diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java index 9837c2ec7..b9b4c6b01 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java @@ -16,6 +16,7 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.view.SubMenu; import android.view.View; +import android.widget.CheckBox; import android.widget.SeekBar; import android.widget.TextView; @@ -26,6 +27,7 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.NotificationManagerCompat; import androidx.fragment.app.FragmentActivity; +import org.citra.citra_emu.CitraApplication; import org.citra.citra_emu.NativeLibrary; import org.citra.citra_emu.R; import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; @@ -304,6 +306,26 @@ public final class EmulationActivity extends AppCompatActivity { return true; } + private void DisplaySavestateWarning() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); + if (preferences.getBoolean("savestateWarningShown", false)) { + return; + } + + LayoutInflater inflater = mEmulationFragment.requireActivity().getLayoutInflater(); + View view = inflater.inflate(R.layout.dialog_checkbox, null); + CheckBox checkBox = view.findViewById(R.id.checkBox); + + new AlertDialog.Builder(this) + .setTitle(R.string.savestate_warning_title) + .setMessage(R.string.savestate_warning_message) + .setView(view) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + preferences.edit().putBoolean("savestateWarningShown", checkBox.isChecked()).apply(); + }) + .show(); + } + @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); @@ -327,6 +349,7 @@ public final class EmulationActivity extends AppCompatActivity { final int slot = i + 1; final String text = getString(R.string.emulation_empty_state_slot, slot); saveStateMenu.add(text).setEnabled(true).setOnMenuItemClickListener((item) -> { + DisplaySavestateWarning(); NativeLibrary.SaveState(slot); return true; }); diff --git a/src/android/app/src/main/res/layout/dialog_checkbox.xml b/src/android/app/src/main/res/layout/dialog_checkbox.xml new file mode 100644 index 000000000..c0f307117 --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_checkbox.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index a661203d5..dac2f6bae 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -181,6 +181,10 @@ Select This Directory No files were found or no game directory has been selected yet. + Do not show this again + Savestates + Warning: Savestates are NOT a replacement for in-game saves, and are not meant to be reliable.\n\nUse at your own risk! + Software Keyboard I Forgot