|
|
|
@ -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() |
|
|
|
|