From 3ae90631bd183f3374f6276fafc427c91797bb4d Mon Sep 17 00:00:00 2001 From: NullVector Date: Mon, 5 Oct 2026 22:36:31 +1030 Subject: [PATCH] Android: when the surface is lost, pause the game and allow coming back to it (#358) --- src/android/app/src/main/AndroidManifest.xml | 3 + .../src/main/java/org/nxemu/NativeLibrary.kt | 7 + .../nxemu/ui/emulation/EmulationActivity.kt | 198 +++++++++++++++--- .../java/org/nxemu/ui/main/MainActivity.kt | 63 ++++++ .../src/main/jni/android_render_window.cpp | 12 ++ .../app/src/main/jni/android_render_window.h | 2 + .../app/src/main/jni/emulation_session.cpp | 99 ++++++++- .../app/src/main/jni/emulation_session.h | 8 + src/android/app/src/main/jni/native.cpp | 74 ++++--- src/android/app/src/main/jni/native_input.cpp | 6 + .../src/main/res/drawable/circle_white.xml | 5 + .../app/src/main/res/drawable/ic_play.xml | 10 + .../main/res/layout/activity_emulation.xml | 36 ++++ .../app/src/main/res/values/strings.xml | 2 + src/nxemu-module-spec/base.h | 2 +- src/nxemu-module-spec/video.h | 2 + src/nxemu-video/render_window.cpp | 5 + src/nxemu-video/render_window.h | 2 + src/nxemu-video/video_manager.cpp | 35 ++++ src/nxemu-video/video_manager.h | 2 + .../renderer_vulkan/vk_present_manager.cpp | 37 ++-- .../renderer_vulkan/vk_present_manager.h | 1 + 22 files changed, 522 insertions(+), 89 deletions(-) create mode 100644 src/android/app/src/main/res/drawable/circle_white.xml create mode 100644 src/android/app/src/main/res/drawable/ic_play.xml diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index 716df5e8..ad740f54 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -14,8 +14,10 @@ android:theme="@style/Theme.NxEmu.Splash"> @@ -34,6 +36,7 @@ android:name="org.nxemu.ui.emulation.EmulationActivity" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|smallestScreenSize|density" android:exported="false" + android:launchMode="singleTop" android:screenOrientation="sensorLandscape" android:theme="@style/Theme.NxEmu" /> (R.id.emulation_surface).holder.surface + if (surface != null && surface.isValid) { + unpauseIfBackgrounded() + } + updatePauseLabel() + if (NativeLibrary.isPaused() && pausedFrame == null) { + capturePausedFrame() + } InputHandler.updateControllerData() if (::surfaceInputOverlay.isInitialized) { updateInputOverlayLayout() @@ -150,6 +184,22 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback { } } + override fun onPause() { + releaseDrawerStick() + val leftApp = leavingApp + if (!NativeLibrary.isPaused()) { + NativeLibrary.pauseEmulation() + if (!leftApp) { + resumeAfterBackground.set(true) + } + } + if (leftApp) { + capturePausedFrame() + } + leavingApp = false + super.onPause() + } + override fun dispatchKeyEvent(event: KeyEvent): Boolean { if (event.keyCode == KeyEvent.KEYCODE_BACK) { return super.dispatchKeyEvent(event) @@ -195,6 +245,7 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback { override fun onDrawerOpened(drawerView: View) { drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) + updatePauseLabel() drawerMenuItems().first().requestFocus() } @@ -206,14 +257,84 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback { } } }) + pauseItem.setOnClickListener { togglePause() } lockDrawerItem.setOnClickListener { toggleDrawerLock() } findViewById(R.id.menu_exit).setOnClickListener { drawerLayout.closeDrawers() + explicitExit = true + activeGamePath = null + clearPausedFrame() finish() } + updatePauseLabel() updateLockDrawerLabel() } + private fun togglePause() { + if (NativeLibrary.isPaused()) { + NativeLibrary.unpauseEmulation() + clearPausedFrame() + } else { + NativeLibrary.pauseEmulation() + capturePausedFrame() + } + updatePauseLabel() + } + + private fun updatePauseLabel() { + val paused = NativeLibrary.isPaused() + pauseItem.text = getString( + if (paused) R.string.emulation_unpause else R.string.emulation_pause, + ) + if (::pausedIcon.isInitialized) { + pausedIcon.visibility = if (paused) View.VISIBLE else View.GONE + } + if (::pausedFrameImage.isInitialized) { + val frame = if (paused) pausedFrame else null + pausedFrameImage.setImageBitmap(frame) + pausedFrameImage.visibility = if (frame != null) View.VISIBLE else View.GONE + } + } + + private fun capturePausedFrame() { + if (!captureInFlight.compareAndSet(false, true)) { + return + } + executor.execute { + try { + val width = NativeLibrary.getAppletCaptureWidth() + val height = NativeLibrary.getAppletCaptureHeight() + val frame = NativeLibrary.getAppletCaptureBuffer() + val expected = width * height * 4 + if (width <= 0 || height <= 0 || frame.size < expected) { + return@execute + } + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(frame, 0, expected)) + val previous = pausedFrame + pausedFrame = bitmap + Handler(Looper.getMainLooper()).post { + frameListener?.invoke() + if (previous != null && previous !== pausedFrame) { + previous.recycle() + } + } + } finally { + captureInFlight.set(false) + } + } + } + + private fun clearPausedFrame() { + val previous = pausedFrame + pausedFrame = null + if (::pausedFrameImage.isInitialized) { + pausedFrameImage.setImageDrawable(null) + pausedFrameImage.visibility = View.GONE + } + previous?.recycle() + } + private fun releaseOverlayTouches() { val event = MotionEvent.obtain( SystemClock.uptimeMillis(), @@ -253,6 +374,7 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback { private fun drawerMenuItems(): List { return listOf( + pauseItem, lockDrawerItem, findViewById(R.id.menu_exit), ) @@ -468,45 +590,43 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback { } } - override fun surfaceCreated(holder: SurfaceHolder) { - if (!nativeSurfaceSessionOpen.compareAndSet(false, true)) { - Log.w(TAG, "Ignoring extra surfaceCreated") + override fun surfaceCreated(holder: SurfaceHolder) = Unit + + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + if (width <= 0 || height <= 0) { return } val path = intent.getStringExtra(EXTRA_GAME_PATH) if (path.isNullOrEmpty()) { Log.e(TAG, "Missing EXTRA_GAME_PATH") - nativeSurfaceSessionOpen.set(false) finish() return } - val ratio = resources.displayMetrics.density val surface = holder.surface + val ratio = resources.displayMetrics.density executor.execute { try { - if (!NativeLibrary.emulationSurfaceReady(surface, ratio, path)) { - Log.e(TAG, "emulationSurfaceReady returned false") - nativeSurfaceSessionOpen.set(false) - runOnUiThread { finish() } + if (!nativeSurfaceSessionOpen.get()) { + if (!NativeLibrary.emulationSurfaceReady(surface, ratio, path)) { + Log.e(TAG, "emulationSurfaceReady returned false") + runOnUiThread { finish() } + } else { + nativeSurfaceSessionOpen.set(true) + activeGamePath = path + InputHandler.updateControllerData() + unpauseIfBackgrounded() + runOnUiThread { updatePauseLabel() } + } + } else { + NativeLibrary.surfaceChanged(surface) + unpauseIfBackgrounded() + runOnUiThread { updatePauseLabel() } } - } catch (e: Throwable) { - Log.e(TAG, "emulationSurfaceReady failed", e) - nativeSurfaceSessionOpen.set(false) - runOnUiThread { finish() } - } - } - } - - override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { - if (!nativeSurfaceSessionOpen.get() || width <= 0 || height <= 0) { - return - } - val surface = holder.surface - executor.execute { - try { - NativeLibrary.surfaceChanged(surface) } catch (e: Throwable) { Log.e(TAG, "surfaceChanged failed", e) + if (!nativeSurfaceSessionOpen.get()) { + runOnUiThread { finish() } + } } } } @@ -514,27 +634,25 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback { override fun surfaceDestroyed(holder: SurfaceHolder) { try { executor.submit { - try { - NativeLibrary.emulationSurfaceDestroyed() - } finally { - nativeSurfaceSessionOpen.set(false) - } + NativeLibrary.emulationSurfaceDestroyed() }.get(120, TimeUnit.SECONDS) } catch (e: Exception) { Log.e(TAG, "emulationSurfaceDestroyed failed", e) - nativeSurfaceSessionOpen.set(false) } } override fun onDestroy() { + if (frameListener === pausedFrameListener) { + frameListener = null + } NativeLibrary.removeSettingChangedListener(settingChangedListener) stopPerfOverlay() stopAnimatedDrawables() - if (nativeSurfaceSessionOpen.get()) { + if (explicitExit && nativeSurfaceSessionOpen.get()) { try { executor.submit { try { - NativeLibrary.emulationSurfaceDestroyed() + NativeLibrary.emulationStopped() } finally { nativeSurfaceSessionOpen.set(false) } @@ -555,7 +673,21 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback { super.onDestroy() } + private fun unpauseIfBackgrounded() { + if (resumeAfterBackground.compareAndSet(true, false)) { + NativeLibrary.unpauseEmulation() + runOnUiThread { + clearPausedFrame() + updatePauseLabel() + } + } + } + companion object { + private val resumeAfterBackground = AtomicBoolean(false) + var activeGamePath: String? = null + var pausedFrame: Bitmap? = null + var frameListener: (() -> Unit)? = null const val EXTRA_GAME_PATH = "org.nxemu.EXTRA_GAME_PATH" private const val TAG = "NxEmu-Emulation" private const val DRAWER_REPEAT_MS = 220L diff --git a/src/android/app/src/main/java/org/nxemu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/nxemu/ui/main/MainActivity.kt index 141185a7..1aa19db9 100644 --- a/src/android/app/src/main/java/org/nxemu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/nxemu/ui/main/MainActivity.kt @@ -1,5 +1,6 @@ package org.nxemu.ui.main +import android.content.Context import android.content.Intent import android.hardware.input.InputManager import android.net.Uri @@ -20,7 +21,10 @@ import org.json.JSONArray import org.json.JSONObject import org.nxemu.NXUISetting import org.nxemu.NativeLibrary +import org.nxemu.features.input.NativeInput +import org.nxemu.features.input.model.NativeButton import org.nxemu.ui.emulation.EmulationActivity +import org.nxemu.ui.settings.ControllerSettings import org.nxemu.ui.settings.InputMappingSession import org.nxemu.ui.settings.OverlayLayoutActivity import org.nxemu.utils.InputHandler @@ -143,15 +147,62 @@ open class MainActivity : ComponentActivity() { }, ) NativeLibrary.addSettingChangedListener(settingChangedForwarder) + bootstrapControllerOnFirstRun() val page = if (this is SettingsActivity) "settings.html" else "index.html" webView.loadUrl("file:///android_asset/$page") setContentView(webView) ThemeHelper.applySystemBars(this) } + private fun bootstrapControllerOnFirstRun() { + if (InputHandler.getDevices().isEmpty()) { + return + } + if (NativeInput.getButtonParam(0, NativeButton.A).get("engine", "") != "keyboard") { + return + } + InputHandler.updateControllerData() + ControllerSettings.autoMap(this, playerIndex = 0, index = 0) + if (NativeInput.getButtonParam(0, NativeButton.A).get("engine", "") == "keyboard") { + return + } + NativeLibrary.setSettingBool(NXUISetting.ShowInputOverlay, false) + NativeLibrary.saveSettings() + } + override fun onResume() { super.onResume() emulationLaunchPending = false + InputHandler.updateControllerData() + inputManager()?.registerInputDeviceListener(inputDeviceListener, null) + refreshConnectedControllers() + resumeRunningGame() + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + resumeRunningGame() + } + + private fun resumeRunningGame() { + if (this is SettingsActivity) { + return + } + val path = EmulationActivity.activeGamePath ?: return + startActivity( + Intent(this, EmulationActivity::class.java).apply { + putExtra(EmulationActivity.EXTRA_GAME_PATH, path) + addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) + } + ) + } + + override fun onPause() { + inputMapping.dismiss() + inputManager()?.unregisterInputDeviceListener(inputDeviceListener) + releaseBrowserStick() + super.onPause() } override fun dispatchKeyEvent(event: KeyEvent): Boolean { @@ -260,6 +311,14 @@ open class MainActivity : ComponentActivity() { } webView.evaluateJavascript("if(typeof moveGameSelection==='function'){$moves}", null) } + private fun releaseBrowserStick() { + stickX = 0 + stickY = 0 + if (::webView.isInitialized) { + webView.removeCallbacks(stickRepeat) + } + } + fun beginControllerMap(playerIndex: Int, mapId: String, title: String, filterIndex: Int) { runOnUiThread { inputMapping.begin(playerIndex, mapId, title, filterIndex) @@ -280,6 +339,10 @@ open class MainActivity : ComponentActivity() { } } + private fun inputManager(): InputManager? { + return getSystemService(Context.INPUT_SERVICE) as? InputManager + } + private fun refreshConnectedControllers() { if (!::webView.isInitialized || isDestroyed) { return diff --git a/src/android/app/src/main/jni/android_render_window.cpp b/src/android/app/src/main/jni/android_render_window.cpp index e5b0fdc7..76b2b5fa 100644 --- a/src/android/app/src/main/jni/android_render_window.cpp +++ b/src/android/app/src/main/jni/android_render_window.cpp @@ -10,6 +10,7 @@ AndroidRenderWindow::~AndroidRenderWindow() void AndroidRenderWindow::AttachSurface(ANativeWindow * native_window, float pixel_ratio) { std::lock_guard lock(m_mutex); + m_present = native_window != nullptr; m_pixel_ratio = pixel_ratio > 0.f ? pixel_ratio : 1.f; if (m_native_window == native_window) { @@ -23,9 +24,16 @@ void AndroidRenderWindow::AttachSurface(ANativeWindow * native_window, float pix m_native_window = native_window; } +void AndroidRenderWindow::SuppressPresentation() +{ + std::lock_guard lock(m_mutex); + m_present = false; +} + void AndroidRenderWindow::ClearSurface() { std::lock_guard lock(m_mutex); + m_present = false; if (m_native_window != nullptr) { ANativeWindow_release(m_native_window); @@ -37,6 +45,10 @@ void AndroidRenderWindow::ClearSurface() void * AndroidRenderWindow::RenderSurface() const { std::lock_guard lock(m_mutex); + if (!m_present) + { + return nullptr; + } return m_native_window; } diff --git a/src/android/app/src/main/jni/android_render_window.h b/src/android/app/src/main/jni/android_render_window.h index 616b0656..c780146a 100644 --- a/src/android/app/src/main/jni/android_render_window.h +++ b/src/android/app/src/main/jni/android_render_window.h @@ -12,6 +12,7 @@ public: ~AndroidRenderWindow(); void AttachSurface(ANativeWindow * native_window, float pixel_ratio); + void SuppressPresentation(); void ClearSurface(); void * RenderSurface() const override; @@ -21,4 +22,5 @@ private: mutable std::mutex m_mutex; ANativeWindow * m_native_window = nullptr; float m_pixel_ratio = 1.0f; + bool m_present = false; }; diff --git a/src/android/app/src/main/jni/emulation_session.cpp b/src/android/app/src/main/jni/emulation_session.cpp index 1d93056f..28e93bdb 100644 --- a/src/android/app/src/main/jni/emulation_session.cpp +++ b/src/android/app/src/main/jni/emulation_session.cpp @@ -118,7 +118,6 @@ bool EmulationSession::Run(ANativeWindow * native_window, float pixel_ratio, con if (native_window == nullptr) { - __android_log_print(ANDROID_LOG_ERROR, kLogTag, "Run: no native window"); RestoreStubModulesLocked(); return false; } @@ -131,7 +130,6 @@ bool EmulationSession::Run(ANativeWindow * native_window, float pixel_ratio, con if (!m_system_modules->IsValid()) { - __android_log_print(ANDROID_LOG_ERROR, kLogTag, "Run: SystemModules invalid after Setup"); RestoreStubModulesLocked(); return false; } @@ -147,20 +145,61 @@ bool EmulationSession::Run(ANativeWindow * native_window, float pixel_ratio, con ISystemloader & loader = m_system_modules->Modules().Systemloader(); if (!loader.LoadRom(rom_path.c_str(), 0, -1, ApplicationLaunchType::FrontendInitiated)) { - __android_log_print(ANDROID_LOG_ERROR, kLogTag, "Run: LoadRom failed"); RestoreStubModulesLocked(); return false; } - - __android_log_print(ANDROID_LOG_INFO, kLogTag, "Run: LoadRom ok"); return true; } +void EmulationSession::Stop() +{ + std::lock_guard lock(m_mutex); + SettingsStore::GetInstance().SetBool(NXCoreSetting::EmulationRunning, false); + RestoreStubModulesLocked(); +} + +void EmulationSession::SetPaused(bool paused) +{ + std::lock_guard lock(m_mutex); + if (!m_system_modules || !m_system_modules->IsValid()) + { + return; + } + IOperatingSystem & os = m_system_modules->Modules().OperatingSystem(); + if (!os.IsPoweredOn()) + { + return; + } + os.SetEmulationPaused(paused); +} + +bool EmulationSession::IsPaused() +{ + std::lock_guard lock(m_mutex); + if (!m_system_modules || !m_system_modules->IsValid()) + { + return false; + } + return m_system_modules->Modules().OperatingSystem().IsEmulationPaused(); +} + +void EmulationSession::SetNativeWindow(ANativeWindow * native_window) +{ + std::lock_guard lock(m_mutex); + m_render_window.AttachSurface(native_window, m_pixel_ratio); + m_native_window = native_window; +} + void EmulationSession::SurfaceDestroyed() { std::lock_guard lock(m_mutex); - SettingsStore::GetInstance().SetBool(NXCoreSetting::EmulationRunning, false); - RestoreStubModulesLocked(); + const bool keep_running = m_system_modules && m_system_modules->IsValid() && + SettingsStore::GetInstance().GetBool(NXCoreSetting::EmulationRunning); + m_render_window.SuppressPresentation(); + if (keep_running) + { + m_system_modules->Modules().Video().NotifyWindowChanged(); + } } void EmulationSession::SurfaceChanged() @@ -174,15 +213,57 @@ void EmulationSession::SurfaceChanged() { return; } + IVideo & video = m_system_modules->Modules().Video(); + video.NotifyWindowChanged(); const int native_w = ANativeWindow_getWidth(m_native_window); const int native_h = ANativeWindow_getHeight(m_native_window); if (native_w > 0 && native_h > 0) { - m_system_modules->Modules().Video().UpdateFramebufferLayout(static_cast(native_w), - static_cast(native_h)); + video.UpdateFramebufferLayout(static_cast(native_w), static_cast(native_h)); } } +bool EmulationSession::ReadAppletCapture(std::vector & out, uint32_t & width, uint32_t & height) +{ + IVideo * video = nullptr; + { + std::lock_guard lock(m_mutex); + if (!m_system_modules || !m_system_modules->IsValid()) + { + return false; + } + video = &m_system_modules->Modules().Video(); + } + uint32_t capture_width = 0; + uint32_t capture_height = 0; + const uint32_t size = video->GetLinearAppletCaptureBuffer(nullptr, 0, &capture_width, &capture_height); + if (size == 0 || capture_width == 0 || capture_height == 0) + { + return false; + } + out.resize(size); + video->GetLinearAppletCaptureBuffer(out.data(), size, &capture_width, &capture_height); + width = capture_width; + height = capture_height; + return width * height * 4 <= out.size(); +} + +void EmulationSession::AppletCaptureSize(uint32_t & width, uint32_t & height) +{ + width = 0; + height = 0; + IVideo * video = nullptr; + { + std::lock_guard lock(m_mutex); + if (!m_system_modules || !m_system_modules->IsValid()) + { + return; + } + video = &m_system_modules->Modules().Video(); + } + video->GetLinearAppletCaptureBuffer(nullptr, 0, &width, &height); +} + ANativeWindow * EmulationSession::NativeWindow() const { std::lock_guard lock(m_mutex); diff --git a/src/android/app/src/main/jni/emulation_session.h b/src/android/app/src/main/jni/emulation_session.h index fab42bbc..6a6721e6 100644 --- a/src/android/app/src/main/jni/emulation_session.h +++ b/src/android/app/src/main/jni/emulation_session.h @@ -1,9 +1,11 @@ #pragma once #include +#include #include #include #include +#include #include #include @@ -31,8 +33,14 @@ public: int GetStyleIndex(int player_index); bool Run(ANativeWindow * native_window, float pixel_ratio, const std::string & rom_path); + void Stop(); + void SetPaused(bool paused); + bool IsPaused(); + void SetNativeWindow(ANativeWindow * native_window); void SurfaceDestroyed(); void SurfaceChanged(); + bool ReadAppletCapture(std::vector & out, uint32_t & width, uint32_t & height); + void AppletCaptureSize(uint32_t & width, uint32_t & height); ANativeWindow * NativeWindow() const; float PixelRatio() const; diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 9d7985a9..bf41600e 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -3,7 +3,9 @@ #include #include #include +#include #include +#include #include #include @@ -162,6 +164,12 @@ Java_org_nxemu_NativeLibrary_emulationSurfaceDestroyed(JNIEnv * /*env*/, jclass EmulationSession::GetInstance().SurfaceDestroyed(); } +extern "C" JNIEXPORT void JNICALL +Java_org_nxemu_NativeLibrary_emulationStopped(JNIEnv * /*env*/, jclass /*clazz*/) +{ + EmulationSession::GetInstance().Stop(); +} + extern "C" JNIEXPORT void JNICALL Java_org_nxemu_NativeLibrary_surfaceChanged(JNIEnv * env, jclass /*clazz*/, jobject jsurface) { @@ -176,13 +184,7 @@ Java_org_nxemu_NativeLibrary_surfaceChanged(JNIEnv * env, jclass /*clazz*/, jobj return; } EmulationSession & session = EmulationSession::GetInstance(); - if (session.NativeWindow() == nw) - { - ANativeWindow_release(nw); - session.SurfaceChanged(); - return; - } - ANativeWindow_release(nw); + session.SetNativeWindow(nw); session.SurfaceChanged(); } @@ -215,40 +217,56 @@ Java_org_nxemu_NativeLibrary_getAppVersion(JNIEnv * env, jclass /*clazz*/) } extern "C" JNIEXPORT void JNICALL -Java_org_nxemu_features_input_NativeInput_onTouchPressed(JNIEnv * /*env*/, jobject /*obj*/, - jint /*fingerId*/, jfloat /*x*/, jfloat /*y*/) +Java_org_nxemu_NativeLibrary_pauseEmulation(JNIEnv * /*env*/, jclass /*clazz*/) { + EmulationSession::GetInstance().SetPaused(true); } extern "C" JNIEXPORT void JNICALL -Java_org_nxemu_features_input_NativeInput_onTouchMoved(JNIEnv * /*env*/, jobject /*obj*/, - jint /*fingerId*/, jfloat /*x*/, jfloat /*y*/) +Java_org_nxemu_NativeLibrary_unpauseEmulation(JNIEnv * /*env*/, jclass /*clazz*/) { + EmulationSession::GetInstance().SetPaused(false); } -extern "C" JNIEXPORT void JNICALL -Java_org_nxemu_features_input_NativeInput_onTouchReleased(JNIEnv * /*env*/, jobject /*obj*/, - jint /*fingerId*/) +extern "C" JNIEXPORT jboolean JNICALL +Java_org_nxemu_NativeLibrary_isPaused(JNIEnv * /*env*/, jclass /*clazz*/) { + return EmulationSession::GetInstance().IsPaused() ? JNI_TRUE : JNI_FALSE; } -extern "C" JNIEXPORT void JNICALL -Java_org_nxemu_features_input_NativeInput_onOverlayButtonEventImpl(JNIEnv * /*env*/, jobject /*obj*/, - jint port, jint buttonId, jint action) +extern "C" JNIEXPORT jbyteArray JNICALL +Java_org_nxemu_NativeLibrary_getAppletCaptureBuffer(JNIEnv * env, jclass /*clazz*/) { - EmulationSession::GetInstance().SetOverlayButton(port, buttonId, action == 1); -} - -extern "C" JNIEXPORT void JNICALL -Java_org_nxemu_features_input_NativeInput_onOverlayJoystickEventImpl(JNIEnv * /*env*/, jobject /*obj*/, - jint port, jint stickId, jfloat x, jfloat y) -{ - EmulationSession::GetInstance().SetOverlayJoystick(port, stickId, x, y); + std::vector bytes; + uint32_t width = 0; + uint32_t height = 0; + if (!EmulationSession::GetInstance().ReadAppletCapture(bytes, width, height) || bytes.empty()) + { + return env->NewByteArray(0); + } + jbyteArray buffer = env->NewByteArray(static_cast(bytes.size())); + if (buffer == nullptr) + { + return env->NewByteArray(0); + } + env->SetByteArrayRegion(buffer, 0, static_cast(bytes.size()), reinterpret_cast(bytes.data())); + return buffer; } extern "C" JNIEXPORT jint JNICALL -Java_org_nxemu_features_input_NativeInput_getStyleIndexImpl(JNIEnv * /*env*/, jobject /*obj*/, - jint playerIndex) +Java_org_nxemu_NativeLibrary_getAppletCaptureWidth(JNIEnv * /*env*/, jclass /*clazz*/) { - return EmulationSession::GetInstance().GetStyleIndex(playerIndex); + uint32_t width = 0; + uint32_t height = 0; + EmulationSession::GetInstance().AppletCaptureSize(width, height); + return static_cast(width); +} + +extern "C" JNIEXPORT jint JNICALL +Java_org_nxemu_NativeLibrary_getAppletCaptureHeight(JNIEnv * /*env*/, jclass /*clazz*/) +{ + uint32_t width = 0; + uint32_t height = 0; + EmulationSession::GetInstance().AppletCaptureSize(width, height); + return static_cast(height); } diff --git a/src/android/app/src/main/jni/native_input.cpp b/src/android/app/src/main/jni/native_input.cpp index 99da7f9a..9dbdce2c 100644 --- a/src/android/app/src/main/jni/native_input.cpp +++ b/src/android/app/src/main/jni/native_input.cpp @@ -450,6 +450,12 @@ JNIEXPORT jintArray JNICALL Java_org_nxemu_features_input_NativeInput_getSupport return result; } +JNIEXPORT jint JNICALL Java_org_nxemu_features_input_NativeInput_getStyleIndexImpl( + JNIEnv * /*env*/, jobject /*obj*/, jint j_player_index) +{ + return EmulationSession::GetInstance().GetStyleIndex(j_player_index); +} + JNIEXPORT void JNICALL Java_org_nxemu_features_input_NativeInput_setStyleIndexImpl(JNIEnv * /*env*/, jobject /*obj*/, jint j_player_index, jint j_style_index) { SystemModules * modules = EmulationSession::GetInstance().Modules(); diff --git a/src/android/app/src/main/res/drawable/circle_white.xml b/src/android/app/src/main/res/drawable/circle_white.xml new file mode 100644 index 00000000..28fca316 --- /dev/null +++ b/src/android/app/src/main/res/drawable/circle_white.xml @@ -0,0 +1,5 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/ic_play.xml b/src/android/app/src/main/res/drawable/ic_play.xml new file mode 100644 index 00000000..63c50ae7 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_play.xml @@ -0,0 +1,10 @@ + + + + diff --git a/src/android/app/src/main/res/layout/activity_emulation.xml b/src/android/app/src/main/res/layout/activity_emulation.xml index a438e2c2..4809eb05 100644 --- a/src/android/app/src/main/res/layout/activity_emulation.xml +++ b/src/android/app/src/main/res/layout/activity_emulation.xml @@ -143,6 +143,27 @@ android:textSize="12sp" android:visibility="gone" /> + + + + + + + Press a button or move a trigger/stick Done Reset overlay + Pause emulation + Unpause emulation Exit emulation Lock drawer Unlock drawer diff --git a/src/nxemu-module-spec/base.h b/src/nxemu-module-spec/base.h index 0b33dbc1..63f954e2 100644 --- a/src/nxemu-module-spec/base.h +++ b/src/nxemu-module-spec/base.h @@ -26,7 +26,7 @@ enum { MODULE_LOADER_SPECS_VERSION = 0x0130, - MODULE_VIDEO_SPECS_VERSION = 0x0121, + MODULE_VIDEO_SPECS_VERSION = 0x0122, MODULE_CPU_SPECS_VERSION = 0x011d, MODULE_OPERATING_SYSTEM_SPECS_VERSION = 0x0124, }; diff --git a/src/nxemu-module-spec/video.h b/src/nxemu-module-spec/video.h index 1fbcf162..5f717207 100644 --- a/src/nxemu-module-spec/video.h +++ b/src/nxemu-module-spec/video.h @@ -196,6 +196,8 @@ nxinterface IVideo virtual void ClearCdmaInstance(uint32_t id) = 0; virtual uint32_t GetAppletCaptureBuffer(uint8_t * out, uint32_t out_size) = 0; virtual void InvalidateMemory(const uint8_t * pointer, uint64_t size) = 0; + virtual void NotifyWindowChanged() = 0; + virtual uint32_t GetLinearAppletCaptureBuffer(uint8_t * out, uint32_t out_size, uint32_t * out_width, uint32_t * out_height) = 0; }; EXPORT IVideo * CALL CreateVideo(IRenderWindow & renderWindow, ISystemModules & modules); diff --git a/src/nxemu-video/render_window.cpp b/src/nxemu-video/render_window.cpp index 93d9a239..f8314440 100644 --- a/src/nxemu-video/render_window.cpp +++ b/src/nxemu-video/render_window.cpp @@ -323,6 +323,11 @@ RenderWindow::RenderWindow(IRenderWindow & renderWindow) : UpdateCurrentFramebufferLayout(640, 480); } +void RenderWindow::RefreshRenderSurface() +{ + window_info.render_surface = m_renderWindow.RenderSurface(); +} + void RenderWindow::OnFrameDisplayed() { if (!m_firstFrame) diff --git a/src/nxemu-video/render_window.h b/src/nxemu-video/render_window.h index f8905d74..b6c878df 100644 --- a/src/nxemu-video/render_window.h +++ b/src/nxemu-video/render_window.h @@ -11,6 +11,8 @@ class RenderWindow : public: RenderWindow(IRenderWindow & renderWindow); + void RefreshRenderSurface(); + // EmuWindow void OnFrameDisplayed(); std::unique_ptr CreateSharedContext() const; diff --git a/src/nxemu-video/video_manager.cpp b/src/nxemu-video/video_manager.cpp index 00803159..645d8768 100644 --- a/src/nxemu-video/video_manager.cpp +++ b/src/nxemu-video/video_manager.cpp @@ -11,6 +11,7 @@ #include "yuzu_video_core/rasterizer_interface.h" #include #include "yuzu_video_core/shader_notify.h" +#include "yuzu_video_core/textures/decoders.h" #include "yuzu_common/polyfill_thread.h" #include "yuzu_common/settings.h" #include "nxemu-video/video_settings.h" @@ -290,9 +291,43 @@ void VideoManager::RequestComposite(VideoFramebufferConfig * layers, uint32_t la void VideoManager::UpdateFramebufferLayout(uint32_t width, uint32_t height) { + impl->m_emuWindow->RefreshRenderSurface(); impl->m_emuWindow->UpdateCurrentFramebufferLayout(width, height); } +uint32_t VideoManager::GetLinearAppletCaptureBuffer(uint8_t * out, uint32_t out_size, uint32_t * out_width, uint32_t * out_height) +{ + using namespace VideoCore::Capture; + constexpr uint32_t linear_size = LinearWidth * LinearHeight * BytesPerPixel; + if (out_width != nullptr) + { + *out_width = LinearWidth; + } + if (out_height != nullptr) + { + *out_height = LinearHeight; + } + if (out == nullptr || out_size < linear_size) + { + return linear_size; + } + + std::vector tiled(TiledSize); + GetAppletCaptureBuffer(tiled.data(), static_cast(tiled.size())); + std::vector linear(linear_size); + Tegra::Texture::UnswizzleTexture(linear, tiled, BytesPerPixel, LinearWidth, LinearHeight, LinearDepth, BlockHeight, BlockDepth); + std::memcpy(out, linear.data(), linear_size); + return linear_size; +} + +void VideoManager::NotifyWindowChanged() +{ + if (impl->m_emuWindow) + { + impl->m_emuWindow->RefreshRenderSurface(); + } +} + IChannelState * VideoManager::AllocateChannel() { return std::make_unique(*impl->m_gpuCore, impl->m_memoryManagerRegistry, std::move(impl->m_gpuCore->AllocateChannel())).release(); diff --git a/src/nxemu-video/video_manager.h b/src/nxemu-video/video_manager.h index 265693da..9ebdd5bd 100644 --- a/src/nxemu-video/video_manager.h +++ b/src/nxemu-video/video_manager.h @@ -51,6 +51,8 @@ public: void ClearCdmaInstance(uint32_t id) override; uint32_t GetAppletCaptureBuffer(uint8_t * out, uint32_t out_size) override; void InvalidateMemory(const uint8_t * pointer, uint64_t size) override; + void NotifyWindowChanged() override; + uint32_t GetLinearAppletCaptureBuffer(uint8_t * out, uint32_t out_size, uint32_t * out_width, uint32_t * out_height) override; private: VideoManager() = delete; diff --git a/src/yuzu_video_core/renderer_vulkan/vk_present_manager.cpp b/src/yuzu_video_core/renderer_vulkan/vk_present_manager.cpp index f8db4930..a0f59db6 100644 --- a/src/yuzu_video_core/renderer_vulkan/vk_present_manager.cpp +++ b/src/yuzu_video_core/renderer_vulkan/vk_present_manager.cpp @@ -99,7 +99,8 @@ PresentManager::PresentManager(const vk::Instance& instance_, memory_allocator{memory_allocator_}, scheduler{scheduler_}, swapchain{swapchain_}, surface{surface_}, blit_supported{CanBlitToSwapchain(device.GetPhysical(), swapchain.GetImageViewFormat())}, - use_present_thread{videoSettings.async_presentation} { + use_present_thread{videoSettings.async_presentation}, + presented_window{render_window.GetWindowInfo().render_surface} { SetImageCount(); auto& dld = device.GetLogical(); @@ -296,25 +297,25 @@ void PresentManager::SetImageCount() { } void PresentManager::CopyToSwapchain(Frame* frame) { - bool requires_recreation = false; + const void* window = render_window.GetWindowInfo().render_surface; + if (window == nullptr) { + presented_window = nullptr; + return; + } - while (true) { - try { - // Recreate surface and swapchain if needed. - if (requires_recreation) { - surface = CreateSurface(instance, render_window.GetWindowInfo()); - RecreateSwapchain(frame); - } - - // Draw to swapchain. - return CopyToSwapchainImpl(frame); - } catch (const vk::Exception& except) { - if (except.GetResult() != VK_ERROR_SURFACE_LOST_KHR) { - throw; - } - - requires_recreation = true; + try { + if (window != presented_window) { + surface = CreateSurface(instance, render_window.GetWindowInfo()); + presented_window = window; + RecreateSwapchain(frame); } + CopyToSwapchainImpl(frame); + } catch (const vk::Exception& except) { + if (except.GetResult() != VK_ERROR_SURFACE_LOST_KHR && + except.GetResult() != VK_ERROR_INITIALIZATION_FAILED) { + throw; + } + presented_window = nullptr; } } diff --git a/src/yuzu_video_core/renderer_vulkan/vk_present_manager.h b/src/yuzu_video_core/renderer_vulkan/vk_present_manager.h index 512baf09..728cf5ea 100644 --- a/src/yuzu_video_core/renderer_vulkan/vk_present_manager.h +++ b/src/yuzu_video_core/renderer_vulkan/vk_present_manager.h @@ -86,6 +86,7 @@ private: bool blit_supported; bool use_present_thread; std::size_t image_count{}; + const void * presented_window = nullptr; }; } // namespace Vulkan