mirror of https://git.h3cjp.net/H3cJP/yuzu.git
717 lines
27 KiB
Kotlin
717 lines
27 KiB
Kotlin
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
package org.yuzu.yuzu_emu.fragments
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.app.AlertDialog
|
|
import android.content.Context
|
|
import android.content.DialogInterface
|
|
import android.content.Intent
|
|
import android.content.SharedPreferences
|
|
import android.content.pm.ActivityInfo
|
|
import android.content.res.Configuration
|
|
import android.content.res.Resources
|
|
import android.graphics.Color
|
|
import android.os.Bundle
|
|
import android.os.Handler
|
|
import android.os.Looper
|
|
import android.util.Rational
|
|
import android.util.TypedValue
|
|
import android.view.*
|
|
import android.widget.TextView
|
|
import androidx.activity.OnBackPressedCallback
|
|
import androidx.activity.result.ActivityResultLauncher
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
import androidx.appcompat.widget.PopupMenu
|
|
import androidx.core.content.res.ResourcesCompat
|
|
import androidx.core.graphics.Insets
|
|
import androidx.core.view.ViewCompat
|
|
import androidx.core.view.WindowInsetsCompat
|
|
import androidx.core.view.isVisible
|
|
import androidx.core.view.updatePadding
|
|
import androidx.fragment.app.Fragment
|
|
import androidx.lifecycle.Lifecycle
|
|
import androidx.lifecycle.lifecycleScope
|
|
import androidx.lifecycle.repeatOnLifecycle
|
|
import androidx.preference.PreferenceManager
|
|
import androidx.window.layout.FoldingFeature
|
|
import androidx.window.layout.WindowInfoTracker
|
|
import androidx.window.layout.WindowLayoutInfo
|
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
import com.google.android.material.slider.Slider
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.launch
|
|
import org.yuzu.yuzu_emu.NativeLibrary
|
|
import org.yuzu.yuzu_emu.R
|
|
import org.yuzu.yuzu_emu.YuzuApplication
|
|
import org.yuzu.yuzu_emu.activities.EmulationActivity
|
|
import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
|
|
import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
|
|
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
|
|
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
|
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
|
|
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
|
|
import org.yuzu.yuzu_emu.overlay.InputOverlay
|
|
import org.yuzu.yuzu_emu.utils.*
|
|
|
|
class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|
private lateinit var preferences: SharedPreferences
|
|
private lateinit var emulationState: EmulationState
|
|
private var emulationActivity: EmulationActivity? = null
|
|
private var perfStatsUpdater: (() -> Unit)? = null
|
|
|
|
private var _binding: FragmentEmulationBinding? = null
|
|
private val binding get() = _binding!!
|
|
|
|
val args by navArgs<EmulationFragmentArgs>()
|
|
|
|
private var isInFoldableLayout = false
|
|
|
|
private lateinit var onReturnFromSettings: ActivityResultLauncher<Intent>
|
|
|
|
override fun onAttach(context: Context) {
|
|
super.onAttach(context)
|
|
if (context is EmulationActivity) {
|
|
emulationActivity = context
|
|
NativeLibrary.setEmulationActivity(context)
|
|
|
|
lifecycleScope.launch(Dispatchers.Main) {
|
|
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
WindowInfoTracker.getOrCreate(context)
|
|
.windowLayoutInfo(context)
|
|
.collect { updateFoldableLayout(context, it) }
|
|
}
|
|
}
|
|
|
|
onReturnFromSettings = context.activityResultRegistry.register(
|
|
"SettingsResult", ActivityResultContracts.StartActivityForResult()
|
|
) {
|
|
binding.surfaceEmulation.setAspectRatio(
|
|
when (IntSetting.RENDERER_ASPECT_RATIO.int) {
|
|
0 -> Rational(16, 9)
|
|
1 -> Rational(4, 3)
|
|
2 -> Rational(21, 9)
|
|
3 -> Rational(16, 10)
|
|
4 -> null // Stretch
|
|
else -> Rational(16, 9)
|
|
}
|
|
)
|
|
emulationActivity?.buildPictureInPictureParams()
|
|
updateScreenLayout()
|
|
}
|
|
} else {
|
|
throw IllegalStateException("EmulationFragment must have EmulationActivity parent")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize anything that doesn't depend on the layout / views in here.
|
|
*/
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
|
|
// So this fragment doesn't restart on configuration changes; i.e. rotation.
|
|
retainInstance = true
|
|
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
|
emulationState = EmulationState(args.game.path)
|
|
}
|
|
|
|
/**
|
|
* Initialize the UI and start emulation in here.
|
|
*/
|
|
override fun onCreateView(
|
|
inflater: LayoutInflater,
|
|
container: ViewGroup?,
|
|
savedInstanceState: Bundle?
|
|
): View {
|
|
_binding = FragmentEmulationBinding.inflate(layoutInflater)
|
|
return binding.root
|
|
}
|
|
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
binding.surfaceEmulation.holder.addCallback(this)
|
|
binding.showFpsText.setTextColor(Color.YELLOW)
|
|
binding.doneControlConfig.setOnClickListener { stopConfiguringControls() }
|
|
|
|
// Setup overlay.
|
|
updateShowFpsOverlay()
|
|
|
|
binding.inGameMenu.getHeaderView(0).findViewById<TextView>(R.id.text_game_title).text =
|
|
args.game.title
|
|
binding.inGameMenu.setNavigationItemSelectedListener {
|
|
when (it.itemId) {
|
|
R.id.menu_pause_emulation -> {
|
|
if (emulationState.isPaused) {
|
|
emulationState.run(false)
|
|
it.title = resources.getString(R.string.emulation_pause)
|
|
it.icon = ResourcesCompat.getDrawable(
|
|
resources,
|
|
R.drawable.ic_pause,
|
|
requireContext().theme
|
|
)
|
|
} else {
|
|
emulationState.pause()
|
|
it.title = resources.getString(R.string.emulation_unpause)
|
|
it.icon = ResourcesCompat.getDrawable(
|
|
resources,
|
|
R.drawable.ic_play,
|
|
requireContext().theme
|
|
)
|
|
}
|
|
true
|
|
}
|
|
|
|
R.id.menu_settings -> {
|
|
SettingsActivity.launch(
|
|
requireContext(),
|
|
onReturnFromSettings,
|
|
SettingsFile.FILE_NAME_CONFIG,
|
|
""
|
|
)
|
|
true
|
|
}
|
|
|
|
R.id.menu_overlay_controls -> {
|
|
showOverlayOptions()
|
|
true
|
|
}
|
|
|
|
R.id.menu_exit -> {
|
|
emulationState.stop()
|
|
requireActivity().finish()
|
|
true
|
|
}
|
|
|
|
else -> true
|
|
}
|
|
}
|
|
|
|
setInsets()
|
|
|
|
requireActivity().onBackPressedDispatcher.addCallback(
|
|
requireActivity(),
|
|
object : OnBackPressedCallback(true) {
|
|
override fun handleOnBackPressed() {
|
|
if (binding.drawerLayout.isOpen) binding.drawerLayout.close() else binding.drawerLayout.open()
|
|
}
|
|
})
|
|
|
|
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
|
|
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
WindowInfoTracker.getOrCreate(requireContext())
|
|
.windowLayoutInfo(requireActivity())
|
|
.collect { updateFoldableLayout(requireActivity() as EmulationActivity, it) }
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
super.onConfigurationChanged(newConfig)
|
|
if (emulationActivity?.isInPictureInPictureMode == true) {
|
|
if (binding.drawerLayout.isOpen) {
|
|
binding.drawerLayout.close()
|
|
}
|
|
if (EmulationMenuSettings.showOverlay) {
|
|
binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = false }
|
|
}
|
|
} else {
|
|
if (EmulationMenuSettings.showOverlay) {
|
|
binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = true }
|
|
}
|
|
if (!isInFoldableLayout) {
|
|
if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
|
binding.surfaceInputOverlay.orientation = InputOverlay.PORTRAIT
|
|
} else {
|
|
binding.surfaceInputOverlay.orientation = InputOverlay.LANDSCAPE
|
|
}
|
|
}
|
|
if (!binding.surfaceInputOverlay.isInEditMode) {
|
|
refreshInputOverlay()
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onResume() {
|
|
super.onResume()
|
|
if (!DirectoryInitialization.areDirectoriesReady) {
|
|
DirectoryInitialization.start(requireContext())
|
|
}
|
|
|
|
binding.surfaceEmulation.setAspectRatio(
|
|
when (IntSetting.RENDERER_ASPECT_RATIO.int) {
|
|
0 -> Rational(16, 9)
|
|
1 -> Rational(4, 3)
|
|
2 -> Rational(21, 9)
|
|
3 -> Rational(16, 10)
|
|
4 -> null // Stretch
|
|
else -> Rational(16, 9)
|
|
}
|
|
)
|
|
|
|
updateScreenLayout()
|
|
|
|
emulationState.run(emulationActivity!!.isActivityRecreated)
|
|
}
|
|
|
|
override fun onPause() {
|
|
if (emulationState.isRunning) {
|
|
emulationState.pause()
|
|
}
|
|
super.onPause()
|
|
}
|
|
|
|
override fun onDestroyView() {
|
|
super.onDestroyView()
|
|
_binding = null
|
|
}
|
|
|
|
override fun onDetach() {
|
|
NativeLibrary.clearEmulationActivity()
|
|
super.onDetach()
|
|
}
|
|
|
|
private fun refreshInputOverlay() {
|
|
binding.surfaceInputOverlay.refreshControls()
|
|
}
|
|
|
|
private fun resetInputOverlay() {
|
|
preferences.edit()
|
|
.remove(Settings.PREF_CONTROL_SCALE)
|
|
.remove(Settings.PREF_CONTROL_OPACITY)
|
|
.apply()
|
|
binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.resetButtonPlacement() }
|
|
}
|
|
|
|
private fun updateShowFpsOverlay() {
|
|
if (EmulationMenuSettings.showFps) {
|
|
val SYSTEM_FPS = 0
|
|
val FPS = 1
|
|
val FRAMETIME = 2
|
|
val SPEED = 3
|
|
perfStatsUpdater = {
|
|
val perfStats = NativeLibrary.getPerfStats()
|
|
if (perfStats[FPS] > 0 && _binding != null) {
|
|
binding.showFpsText.text = String.format("FPS: %.1f", perfStats[FPS])
|
|
}
|
|
|
|
if (!emulationState.isStopped) {
|
|
perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 100)
|
|
}
|
|
}
|
|
perfStatsUpdateHandler.post(perfStatsUpdater!!)
|
|
binding.showFpsText.text = resources.getString(R.string.emulation_game_loading)
|
|
binding.showFpsText.visibility = View.VISIBLE
|
|
} else {
|
|
if (perfStatsUpdater != null) {
|
|
perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!)
|
|
}
|
|
binding.showFpsText.visibility = View.GONE
|
|
}
|
|
}
|
|
|
|
@SuppressLint("SourceLockedOrientationActivity")
|
|
private fun updateScreenLayout() {
|
|
emulationActivity?.let {
|
|
it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.int) {
|
|
Settings.LayoutOption_MobileLandscape -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|
|
Settings.LayoutOption_MobilePortrait -> ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
|
|
Settings.LayoutOption_Unspecified -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
else -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|
|
}
|
|
}
|
|
onConfigurationChanged(resources.configuration)
|
|
}
|
|
|
|
private fun updateFoldableLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) {
|
|
val isFolding = (newLayoutInfo.displayFeatures.find { it is FoldingFeature } as? FoldingFeature)?.let {
|
|
if (it.isSeparating) {
|
|
emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) {
|
|
binding.emulationContainer.layoutParams.height = it.bounds.top
|
|
// Prevent touch regions from being displayed in the hinge
|
|
binding.overlayContainer.layoutParams.height = it.bounds.bottom
|
|
binding.inGameMenu.layoutParams.height = it.bounds.bottom
|
|
isInFoldableLayout = true
|
|
binding.surfaceInputOverlay.orientation = InputOverlay.FOLDABLE
|
|
refreshInputOverlay()
|
|
}
|
|
}
|
|
it.isSeparating
|
|
} ?: false
|
|
if (!isFolding) {
|
|
binding.emulationContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
|
binding.overlayContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
|
binding.inGameMenu.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
|
isInFoldableLayout = false
|
|
updateScreenLayout()
|
|
}
|
|
binding.emulationContainer.requestLayout()
|
|
binding.overlayContainer.requestLayout()
|
|
binding.inGameMenu.requestLayout()
|
|
}
|
|
|
|
override fun surfaceCreated(holder: SurfaceHolder) {
|
|
// We purposely don't do anything here.
|
|
// All work is done in surfaceChanged, which we are guaranteed to get even for surface creation.
|
|
}
|
|
|
|
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
|
Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height)
|
|
emulationState.newSurface(holder.surface)
|
|
}
|
|
|
|
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
|
emulationState.clearSurface()
|
|
}
|
|
|
|
private fun showOverlayOptions() {
|
|
val anchor = binding.inGameMenu.findViewById<View>(R.id.menu_overlay_controls)
|
|
val popup = PopupMenu(requireContext(), anchor)
|
|
|
|
popup.menuInflater.inflate(R.menu.menu_overlay_options, popup.menu)
|
|
|
|
popup.menu.apply {
|
|
findItem(R.id.menu_toggle_fps).isChecked = EmulationMenuSettings.showFps
|
|
findItem(R.id.menu_rel_stick_center).isChecked = EmulationMenuSettings.joystickRelCenter
|
|
findItem(R.id.menu_dpad_slide).isChecked = EmulationMenuSettings.dpadSlide
|
|
findItem(R.id.menu_show_overlay).isChecked = EmulationMenuSettings.showOverlay
|
|
findItem(R.id.menu_haptics).isChecked = EmulationMenuSettings.hapticFeedback
|
|
}
|
|
|
|
popup.setOnMenuItemClickListener {
|
|
when (it.itemId) {
|
|
R.id.menu_toggle_fps -> {
|
|
it.isChecked = !it.isChecked
|
|
EmulationMenuSettings.showFps = it.isChecked
|
|
updateShowFpsOverlay()
|
|
true
|
|
}
|
|
|
|
R.id.menu_edit_overlay -> {
|
|
binding.drawerLayout.close()
|
|
binding.surfaceInputOverlay.requestFocus()
|
|
startConfiguringControls()
|
|
true
|
|
}
|
|
|
|
R.id.menu_adjust_overlay -> {
|
|
adjustOverlay()
|
|
true
|
|
}
|
|
|
|
R.id.menu_toggle_controls -> {
|
|
val preferences =
|
|
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
|
val optionsArray = BooleanArray(15)
|
|
for (i in 0..14) {
|
|
optionsArray[i] = preferences.getBoolean("buttonToggle$i", i < 13)
|
|
}
|
|
|
|
val dialog = MaterialAlertDialogBuilder(requireContext())
|
|
.setTitle(R.string.emulation_toggle_controls)
|
|
.setMultiChoiceItems(
|
|
R.array.gamepadButtons,
|
|
optionsArray
|
|
) { _, indexSelected, isChecked ->
|
|
preferences.edit()
|
|
.putBoolean("buttonToggle$indexSelected", isChecked)
|
|
.apply()
|
|
}
|
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
refreshInputOverlay()
|
|
}
|
|
.setNegativeButton(android.R.string.cancel, null)
|
|
.setNeutralButton(R.string.emulation_toggle_all) { _, _ -> }
|
|
.show()
|
|
|
|
// Override normal behaviour so the dialog doesn't close
|
|
dialog.getButton(AlertDialog.BUTTON_NEUTRAL)
|
|
.setOnClickListener {
|
|
val isChecked = !optionsArray[0]
|
|
for (i in 0..14) {
|
|
optionsArray[i] = isChecked
|
|
dialog.listView.setItemChecked(i, isChecked)
|
|
preferences.edit()
|
|
.putBoolean("buttonToggle$i", isChecked)
|
|
.apply()
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
R.id.menu_show_overlay -> {
|
|
it.isChecked = !it.isChecked
|
|
EmulationMenuSettings.showOverlay = it.isChecked
|
|
refreshInputOverlay()
|
|
true
|
|
}
|
|
|
|
R.id.menu_rel_stick_center -> {
|
|
it.isChecked = !it.isChecked
|
|
EmulationMenuSettings.joystickRelCenter = it.isChecked
|
|
true
|
|
}
|
|
|
|
R.id.menu_dpad_slide -> {
|
|
it.isChecked = !it.isChecked
|
|
EmulationMenuSettings.dpadSlide = it.isChecked
|
|
true
|
|
}
|
|
|
|
R.id.menu_haptics -> {
|
|
it.isChecked = !it.isChecked
|
|
EmulationMenuSettings.hapticFeedback = it.isChecked
|
|
true
|
|
}
|
|
|
|
R.id.menu_reset_overlay -> {
|
|
binding.drawerLayout.close()
|
|
resetInputOverlay()
|
|
true
|
|
}
|
|
|
|
else -> true
|
|
}
|
|
}
|
|
|
|
popup.show()
|
|
}
|
|
|
|
@SuppressLint("SourceLockedOrientationActivity")
|
|
private fun startConfiguringControls() {
|
|
// Lock the current orientation to prevent editing inconsistencies
|
|
if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) {
|
|
emulationActivity?.let {
|
|
it.requestedOrientation =
|
|
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
|
ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
|
|
} else {
|
|
ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|
|
}
|
|
}
|
|
}
|
|
binding.doneControlConfig.visibility = View.VISIBLE
|
|
binding.surfaceInputOverlay.setIsInEditMode(true)
|
|
}
|
|
|
|
private fun stopConfiguringControls() {
|
|
binding.doneControlConfig.visibility = View.GONE
|
|
binding.surfaceInputOverlay.setIsInEditMode(false)
|
|
// Unlock the orientation if it was locked for editing
|
|
if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) {
|
|
emulationActivity?.let {
|
|
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
}
|
|
}
|
|
}
|
|
|
|
@SuppressLint("SetTextI18n")
|
|
private fun adjustOverlay() {
|
|
val adjustBinding = DialogOverlayAdjustBinding.inflate(layoutInflater)
|
|
adjustBinding.apply {
|
|
inputScaleSlider.apply {
|
|
valueTo = 150F
|
|
value = preferences.getInt(Settings.PREF_CONTROL_SCALE, 50).toFloat()
|
|
addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
|
|
inputScaleValue.text = "${value.toInt()}%"
|
|
setControlScale(value.toInt())
|
|
})
|
|
}
|
|
inputOpacitySlider.apply {
|
|
valueTo = 100F
|
|
value = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100).toFloat()
|
|
addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
|
|
inputOpacityValue.text = "${value.toInt()}%"
|
|
setControlOpacity(value.toInt())
|
|
})
|
|
}
|
|
inputScaleValue.text = "${inputScaleSlider.value.toInt()}%"
|
|
inputOpacityValue.text = "${inputOpacitySlider.value.toInt()}%"
|
|
}
|
|
|
|
MaterialAlertDialogBuilder(requireContext())
|
|
.setTitle(R.string.emulation_control_adjust)
|
|
.setView(adjustBinding.root)
|
|
.setPositiveButton(android.R.string.ok, null)
|
|
.setNeutralButton(R.string.slider_default) { _: DialogInterface?, _: Int ->
|
|
setControlScale(50)
|
|
setControlOpacity(100)
|
|
}
|
|
.show()
|
|
}
|
|
|
|
private fun setControlScale(scale: Int) {
|
|
preferences.edit()
|
|
.putInt(Settings.PREF_CONTROL_SCALE, scale)
|
|
.apply()
|
|
refreshInputOverlay()
|
|
}
|
|
|
|
private fun setControlOpacity(opacity: Int) {
|
|
preferences.edit()
|
|
.putInt(Settings.PREF_CONTROL_OPACITY, opacity)
|
|
.apply()
|
|
refreshInputOverlay()
|
|
}
|
|
|
|
private fun setInsets() {
|
|
ViewCompat.setOnApplyWindowInsetsListener(binding.inGameMenu) { v: View, windowInsets: WindowInsetsCompat ->
|
|
val cutInsets: Insets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
|
var left = 0
|
|
var right = 0
|
|
if (ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_LTR) {
|
|
left = cutInsets.left
|
|
} else {
|
|
right = cutInsets.right
|
|
}
|
|
|
|
v.setPadding(left, cutInsets.top, right, 0)
|
|
|
|
// Ensure FPS text doesn't get cut off by rounded display corners
|
|
val sidePadding = resources.getDimensionPixelSize(R.dimen.spacing_xtralarge)
|
|
if (cutInsets.left == 0) {
|
|
binding.showFpsText.setPadding(
|
|
sidePadding,
|
|
cutInsets.top,
|
|
cutInsets.right,
|
|
cutInsets.bottom
|
|
)
|
|
} else {
|
|
binding.showFpsText.setPadding(
|
|
cutInsets.left,
|
|
cutInsets.top,
|
|
cutInsets.right,
|
|
cutInsets.bottom
|
|
)
|
|
}
|
|
windowInsets
|
|
}
|
|
}
|
|
|
|
private class EmulationState(private val gamePath: String) {
|
|
private var state: State
|
|
private var surface: Surface? = null
|
|
private var runWhenSurfaceIsValid = false
|
|
|
|
init {
|
|
// Starting state is stopped.
|
|
state = State.STOPPED
|
|
}
|
|
|
|
@get:Synchronized
|
|
val isStopped: Boolean
|
|
get() = state == State.STOPPED
|
|
|
|
// Getters for the current state
|
|
@get:Synchronized
|
|
val isPaused: Boolean
|
|
get() = state == State.PAUSED
|
|
|
|
@get:Synchronized
|
|
val isRunning: Boolean
|
|
get() = state == State.RUNNING
|
|
|
|
@Synchronized
|
|
fun stop() {
|
|
if (state != State.STOPPED) {
|
|
Log.debug("[EmulationFragment] Stopping emulation.")
|
|
NativeLibrary.stopEmulation()
|
|
state = State.STOPPED
|
|
} else {
|
|
Log.warning("[EmulationFragment] Stop called while already stopped.")
|
|
}
|
|
}
|
|
|
|
// State changing methods
|
|
@Synchronized
|
|
fun pause() {
|
|
if (state != State.PAUSED) {
|
|
Log.debug("[EmulationFragment] Pausing emulation.")
|
|
|
|
NativeLibrary.pauseEmulation()
|
|
|
|
state = State.PAUSED
|
|
} else {
|
|
Log.warning("[EmulationFragment] Pause called while already paused.")
|
|
}
|
|
}
|
|
|
|
@Synchronized
|
|
fun run(isActivityRecreated: Boolean) {
|
|
if (isActivityRecreated) {
|
|
if (NativeLibrary.isRunning()) {
|
|
state = State.PAUSED
|
|
}
|
|
} else {
|
|
Log.debug("[EmulationFragment] activity resumed or fresh start")
|
|
}
|
|
|
|
// If the surface is set, run now. Otherwise, wait for it to get set.
|
|
if (surface != null) {
|
|
runWithValidSurface()
|
|
} else {
|
|
runWhenSurfaceIsValid = true
|
|
}
|
|
}
|
|
|
|
// Surface callbacks
|
|
@Synchronized
|
|
fun newSurface(surface: Surface?) {
|
|
this.surface = surface
|
|
if (runWhenSurfaceIsValid) {
|
|
runWithValidSurface()
|
|
}
|
|
}
|
|
|
|
@Synchronized
|
|
fun clearSurface() {
|
|
if (surface == null) {
|
|
Log.warning("[EmulationFragment] clearSurface called, but surface already null.")
|
|
} else {
|
|
surface = null
|
|
Log.debug("[EmulationFragment] Surface destroyed.")
|
|
when (state) {
|
|
State.RUNNING -> {
|
|
state = State.PAUSED
|
|
}
|
|
|
|
State.PAUSED -> Log.warning("[EmulationFragment] Surface cleared while emulation paused.")
|
|
else -> Log.warning("[EmulationFragment] Surface cleared while emulation stopped.")
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun runWithValidSurface() {
|
|
runWhenSurfaceIsValid = false
|
|
when (state) {
|
|
State.STOPPED -> {
|
|
NativeLibrary.surfaceChanged(surface)
|
|
val emulationThread = Thread({
|
|
Log.debug("[EmulationFragment] Starting emulation thread.")
|
|
NativeLibrary.run(gamePath)
|
|
}, "NativeEmulation")
|
|
emulationThread.start()
|
|
}
|
|
|
|
State.PAUSED -> {
|
|
Log.debug("[EmulationFragment] Resuming emulation.")
|
|
NativeLibrary.surfaceChanged(surface)
|
|
NativeLibrary.unPauseEmulation()
|
|
}
|
|
|
|
else -> Log.debug("[EmulationFragment] Bug, run called while already running.")
|
|
}
|
|
state = State.RUNNING
|
|
}
|
|
|
|
private enum class State {
|
|
STOPPED, RUNNING, PAUSED
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
private val perfStatsUpdateHandler = Handler(Looper.myLooper()!!)
|
|
}
|
|
}
|