Browse Source

fix: persist paused image on resume, persist pause through lifecycle, add visual indicator of pause state, with resume on press

pull/3651/head
xXJSONDeruloXx 7 days ago
parent
commit
fd86f3009f
No known key found for this signature in database GPG Key ID: 629F3E618E280D7F
  1. 4
      src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
  2. 133
      src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
  3. 41
      src/android/app/src/main/jni/native.cpp
  4. 4
      src/android/app/src/main/res/drawable/circle_white.xml
  5. 20
      src/android/app/src/main/res/layout/fragment_emulation.xml

4
src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt

@ -152,6 +152,10 @@ object NativeLibrary {
external fun surfaceDestroyed()
external fun getAppletCaptureBuffer(): ByteArray
external fun getAppletCaptureWidth(): Int
external fun getAppletCaptureHeight(): Int
/**
* Unpauses emulation from a paused state.
*/

133
src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt

@ -15,6 +15,7 @@ import android.content.Intent
import android.content.IntentFilter
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri
import android.os.BatteryManager
import android.os.BatteryManager.*
@ -97,6 +98,7 @@ import org.yuzu.yuzu_emu.utils.collect
import org.yuzu.yuzu_emu.utils.CustomSettingsHandler
import java.io.ByteArrayOutputStream
import java.io.File
import java.nio.ByteBuffer
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.or
@ -141,6 +143,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private var wasInputOverlayAutoHidden = false
private var overlayTouchActive = false
private var pausedFrameBitmap: Bitmap? = null
var shouldUseCustom = false
private var isQuickSettingsMenuOpen = false
@ -703,6 +706,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
binding.inGameMenu.menu.findItem(R.id.menu_quick_settings)?.isVisible =
BooleanSetting.ENABLE_QUICK_SETTINGS.getBoolean()
binding.pausedIcon.setOnClickListener {
if (this::emulationState.isInitialized && emulationState.isPaused) {
resumeEmulationFromUi()
}
}
binding.inGameMenu.menu.findItem(R.id.menu_lock_drawer).apply {
val lockMode = IntSetting.LOCK_DRAWER.getInt()
val titleId = if (lockMode == DrawerLayout.LOCK_MODE_LOCKED_CLOSED) {
@ -728,11 +737,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
when (it.itemId) {
R.id.menu_pause_emulation -> {
if (emulationState.isPaused) {
emulationState.run(false, userInitiatedResume = true)
updatePauseMenuEntry(false)
resumeEmulationFromUi()
} else {
emulationState.pause(userInitiated = true)
emulationState.pause()
updatePauseMenuEntry(true)
capturePausedFrameFromCore()
updatePausedFrameVisibility()
}
binding.inGameMenu.requestFocus()
true
@ -826,6 +836,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
R.id.menu_exit -> {
clearPausedFrame()
emulationState.stop()
NativeConfig.reloadGlobalConfig()
emulationViewModel.setIsEmulationStopping(true)
@ -1197,6 +1208,74 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
}
private fun capturePausedFrameFromCore() {
lifecycleScope.launch(Dispatchers.Default) {
val frameData = NativeLibrary.getAppletCaptureBuffer()
val width = NativeLibrary.getAppletCaptureWidth()
val height = NativeLibrary.getAppletCaptureHeight()
if (frameData.isEmpty() || width <= 0 || height <= 0) {
Log.warning(
"[EmulationFragment] Paused frame capture returned empty/invalid data. " +
"size=${frameData.size}, width=$width, height=$height"
)
return@launch
}
val expectedSize = width * height * 4
if (frameData.size < expectedSize) {
Log.warning(
"[EmulationFragment] Paused frame buffer smaller than expected. " +
"size=${frameData.size}, expected=$expectedSize"
)
return@launch
}
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(frameData, 0, expectedSize))
withContext(Dispatchers.Main) {
pausedFrameBitmap?.recycle()
pausedFrameBitmap = bitmap
updatePausedFrameVisibility()
}
}
}
private fun updatePausedFrameVisibility() {
val b = _binding ?: return
val shouldShowPaused = this::emulationState.isInitialized && emulationState.isPaused
b.pausedIcon.setVisible(shouldShowPaused)
if (!shouldShowPaused) {
b.pausedFrameImage.setVisible(false)
b.pausedFrameImage.setImageDrawable(null)
return
}
val bitmap = pausedFrameBitmap ?: run {
b.pausedFrameImage.setVisible(false)
b.pausedFrameImage.setImageDrawable(null)
return
}
b.pausedFrameImage.setImageBitmap(bitmap)
b.pausedFrameImage.setVisible(true)
}
private fun resumeEmulationFromUi() {
clearPausedFrame()
emulationState.run(false, explicitResume = true)
updatePauseMenuEntry(false)
updatePausedFrameVisibility()
}
private fun clearPausedFrame() {
val b = _binding
b?.pausedFrameImage?.setVisible(false)
b?.pausedFrameImage?.setImageDrawable(null)
pausedFrameBitmap?.recycle()
pausedFrameBitmap = null
}
private fun handleLoadAmiiboSelection(): Boolean {
val binding = _binding ?: return true
@ -1292,7 +1371,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) {
emulationState.pause()
updatePauseMenuEntry(true)
capturePausedFrameFromCore()
}
updatePausedFrameVisibility()
}
super.onPause()
}
@ -1301,6 +1382,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
super.onDestroyView()
amiiboLoadJob?.cancel()
amiiboLoadJob = null
clearPausedFrame()
_binding?.surfaceInputOverlay?.touchEventListener = null
_binding = null
isAmiiboPickerOpen = false
@ -1321,6 +1403,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
b.inGameMenu.post {
if (!this::emulationState.isInitialized || _binding == null) return@post
updatePauseMenuEntry(emulationState.isPaused)
updatePausedFrameVisibility()
}
}
@ -1760,6 +1843,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
// Only update surface reference, don't trigger state changes
emulationState.updateSurfaceReference(holder.surface)
}
updatePausedFrameVisibility()
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
@ -2030,7 +2114,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private val emulationCanStart: () -> Boolean
) {
private var state: State
private var userPaused = false
private var surface: Surface? = null
lateinit var emulationThread: Thread
@ -2062,7 +2145,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
@Synchronized
fun pause(userInitiated: Boolean = false) {
fun pause() {
if (state != State.PAUSED) {
Log.debug("[EmulationFragment] Pausing emulation.")
@ -2070,11 +2153,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
NativeLibrary.playTimeManagerStop()
state = State.PAUSED
userPaused = userInitiated
} else {
if (userInitiated) {
userPaused = true
}
Log.warning("[EmulationFragment] Pause called while already paused.")
}
}
@ -2083,16 +2162,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
fun run(
isActivityRecreated: Boolean,
programIndex: Int = 0,
userInitiatedResume: Boolean = false
explicitResume: Boolean = false
) {
if (userInitiatedResume) {
userPaused = false
}
if (isActivityRecreated) {
if (NativeLibrary.isRunning()) {
state = State.PAUSED
userPaused = false
}
} else {
Log.debug("[EmulationFragment] activity resumed or fresh start")
@ -2100,7 +2174,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
// If the surface is set, run now. Otherwise, wait for it to get set.
if (surface != null) {
runWithValidSurface(programIndex)
runWithValidSurface(programIndex, explicitResume)
}
}
@ -2125,7 +2199,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
@Synchronized
fun updateSurface() {
if (surface != null) {
if (surface != null && state == State.RUNNING) {
NativeLibrary.surfaceChanged(surface)
}
}
@ -2141,35 +2215,40 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
@Synchronized
fun clearSurface() {
if (surface == null) {
Log.warning("[EmulationFragment] clearSurface called, but surface already null.")
Log.debug("[EmulationFragment] clearSurface called, but surface already null.")
} else {
NativeLibrary.surfaceDestroyed()
surface = null
Log.debug("[EmulationFragment] Surface destroyed.")
when (state) {
State.RUNNING -> {
state = State.PAUSED
userPaused = false
}
State.PAUSED -> Log.warning(
State.PAUSED -> Log.debug(
"[EmulationFragment] Surface cleared while emulation paused."
)
else -> Log.warning(
else -> Log.debug(
"[EmulationFragment] Surface cleared while emulation stopped."
)
}
}
}
private fun runWithValidSurface(programIndex: Int = 0) {
NativeLibrary.surfaceChanged(surface)
private fun runWithValidSurface(programIndex: Int = 0, explicitResume: Boolean = false) {
if (!emulationCanStart.invoke()) {
return
}
val currentSurface = surface
if (currentSurface == null || !currentSurface.isValid) {
Log.debug("[EmulationFragment] runWithValidSurface called with invalid surface.")
return
}
when (state) {
State.STOPPED -> {
NativeLibrary.surfaceChanged(currentSurface)
emulationThread = Thread({
Log.debug("[EmulationFragment] Starting emulation thread.")
NativeLibrary.run(gamePath, programIndex, true)
@ -2178,11 +2257,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
State.PAUSED -> {
if (userPaused) {
Log.debug("[EmulationFragment] Surface restored while user paused.")
if (!explicitResume) {
Log.debug(
"[EmulationFragment] Surface restored while emulation paused; " +
"deferring native surface update until explicit resume."
)
return
}
NativeLibrary.surfaceChanged(currentSurface)
Log.debug("[EmulationFragment] Resuming emulation.")
NativeLibrary.unpauseEmulation()
NativeLibrary.playTimeManagerStart()

41
src/android/app/src/main/jni/native.cpp

@ -89,6 +89,8 @@
#include "jni/native.h"
#include "video_core/renderer_base.h"
#include "video_core/renderer_vulkan/renderer_vulkan.h"
#include "video_core/capture.h"
#include "video_core/textures/decoders.h"
#include "video_core/vulkan_common/vulkan_instance.h"
#include "video_core/vulkan_common/vulkan_surface.h"
#include "video_core/shader_notify.h"
@ -780,9 +782,10 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceChanged(JNIEnv* env, jobject i
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceDestroyed(JNIEnv* env, jobject instance) {
ANativeWindow_release(EmulationSession::GetInstance().NativeWindow());
if (auto* native_window = EmulationSession::GetInstance().NativeWindow(); native_window) {
ANativeWindow_release(native_window);
}
EmulationSession::GetInstance().SetNativeWindow(nullptr);
EmulationSession::GetInstance().SurfaceChanged();
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, jobject instance,
@ -969,6 +972,40 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused(JNIEnv* env, jclass claz
return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused());
}
jbyteArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletCaptureBuffer(JNIEnv* env, jclass clazz) {
using namespace VideoCore::Capture;
if (!EmulationSession::GetInstance().IsRunning()) {
return env->NewByteArray(0);
}
const auto tiled = EmulationSession::GetInstance().System().GPU().GetAppletCaptureBuffer();
if (tiled.size() < TiledSize) {
return env->NewByteArray(0);
}
std::vector<u8> linear(LinearWidth * LinearHeight * BytesPerPixel);
Tegra::Texture::UnswizzleTexture(linear, tiled, BytesPerPixel, LinearWidth, LinearHeight,
LinearDepth, BlockHeight, BlockDepth);
auto buffer = env->NewByteArray(static_cast<jsize>(linear.size()));
if (!buffer) {
return env->NewByteArray(0);
}
env->SetByteArrayRegion(buffer, 0, static_cast<jsize>(linear.size()),
reinterpret_cast<const jbyte*>(linear.data()));
return buffer;
}
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletCaptureWidth(JNIEnv* env, jclass clazz) {
return static_cast<jint>(VideoCore::Capture::LinearWidth);
}
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletCaptureHeight(JNIEnv* env, jclass clazz) {
return static_cast<jint>(VideoCore::Capture::LinearHeight);
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass clazz,
jboolean reload) {
// Initialize the emulated system.

4
src/android/app/src/main/res/drawable/circle_white.xml

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="#E6FFFFFF" />
</shape>

20
src/android/app/src/main/res/layout/fragment_emulation.xml

@ -142,6 +142,26 @@
android:layout_height="match_parent"
android:fitsSystemWindows="false">
<ImageView
android:id="@+id/paused_frame_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@null"
android:scaleType="fitCenter"
android:visibility="gone" />
<ImageView
android:id="@+id/paused_icon"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center"
android:background="@drawable/circle_white"
android:contentDescription="@string/emulation_unpause"
android:padding="14dp"
android:src="@drawable/ic_pause"
android:visibility="gone"
app:tint="@android:color/black" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/show_stats_overlay_text"
style="@style/TextAppearance.Material3.BodySmall"

Loading…
Cancel
Save