Merge pull request #11405 from t895/emulation-loading

android: Emulation loading UI and fixes
This commit is contained in:
Charles Lombardo 2023-08-30 16:24:46 -04:00 committed by GitHub
commit a2f0caefd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 392 additions and 236 deletions

View file

@ -22,9 +22,7 @@ import org.yuzu.yuzu_emu.utils.FileUtil.exists
import org.yuzu.yuzu_emu.utils.FileUtil.getFileSize import org.yuzu.yuzu_emu.utils.FileUtil.getFileSize
import org.yuzu.yuzu_emu.utils.FileUtil.isDirectory import org.yuzu.yuzu_emu.utils.FileUtil.isDirectory
import org.yuzu.yuzu_emu.utils.FileUtil.openContentUri import org.yuzu.yuzu_emu.utils.FileUtil.openContentUri
import org.yuzu.yuzu_emu.utils.Log.error import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.Log.verbose
import org.yuzu.yuzu_emu.utils.Log.warning
import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
/** /**
@ -465,7 +463,7 @@ object NativeLibrary {
val emulationActivity = sEmulationActivity.get() val emulationActivity = sEmulationActivity.get()
if (emulationActivity == null) { if (emulationActivity == null) {
warning("[NativeLibrary] EmulationActivity is null, can't exit.") Log.warning("[NativeLibrary] EmulationActivity is null, can't exit.")
return return
} }
@ -490,15 +488,27 @@ object NativeLibrary {
} }
fun setEmulationActivity(emulationActivity: EmulationActivity?) { fun setEmulationActivity(emulationActivity: EmulationActivity?) {
verbose("[NativeLibrary] Registering EmulationActivity.") Log.verbose("[NativeLibrary] Registering EmulationActivity.")
sEmulationActivity = WeakReference(emulationActivity) sEmulationActivity = WeakReference(emulationActivity)
} }
fun clearEmulationActivity() { fun clearEmulationActivity() {
verbose("[NativeLibrary] Unregistering EmulationActivity.") Log.verbose("[NativeLibrary] Unregistering EmulationActivity.")
sEmulationActivity.clear() sEmulationActivity.clear()
} }
@Keep
@JvmStatic
fun onEmulationStarted() {
sEmulationActivity.get()!!.onEmulationStarted()
}
@Keep
@JvmStatic
fun onEmulationStopped(status: Int) {
sEmulationActivity.get()!!.onEmulationStopped(status)
}
/** /**
* Logs the Yuzu version, Android version and, CPU. * Logs the Yuzu version, Android version and, CPU.
*/ */

View file

@ -28,6 +28,7 @@ import android.view.Surface
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
@ -41,6 +42,7 @@ import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting 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.model.Settings
import org.yuzu.yuzu_emu.model.EmulationViewModel
import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.utils.ControllerMappingHelper import org.yuzu.yuzu_emu.utils.ControllerMappingHelper
import org.yuzu.yuzu_emu.utils.ForegroundService import org.yuzu.yuzu_emu.utils.ForegroundService
@ -70,8 +72,11 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
private val actionMute = "ACTION_EMULATOR_MUTE" private val actionMute = "ACTION_EMULATOR_MUTE"
private val actionUnmute = "ACTION_EMULATOR_UNMUTE" private val actionUnmute = "ACTION_EMULATOR_UNMUTE"
private val emulationViewModel: EmulationViewModel by viewModels()
override fun onDestroy() { override fun onDestroy() {
stopForegroundService(this) stopForegroundService(this)
emulationViewModel.clear()
super.onDestroy() super.onDestroy()
} }
@ -416,6 +421,16 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
} }
} }
fun onEmulationStarted() {
emulationViewModel.setEmulationStarted(true)
}
fun onEmulationStopped(status: Int) {
if (status == 0) {
finish()
}
}
private fun startMotionSensorListener() { private fun startMotionSensorListener() {
val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)

View file

@ -3,8 +3,6 @@
package org.yuzu.yuzu_emu.adapters package org.yuzu.yuzu_emu.adapters
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.text.TextUtils import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
@ -15,23 +13,20 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.load
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder
import org.yuzu.yuzu_emu.databinding.CardGameBinding import org.yuzu.yuzu_emu.databinding.CardGameBinding
import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.utils.GameIconUtils
class GameAdapter(private val activity: AppCompatActivity) : class GameAdapter(private val activity: AppCompatActivity) :
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()), ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
@ -98,12 +93,7 @@ class GameAdapter(private val activity: AppCompatActivity) :
this.game = game this.game = game
binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
activity.lifecycleScope.launch { GameIconUtils.loadGameIcon(game, binding.imageGameScreen)
val bitmap = decodeGameIcon(game.path)
binding.imageGameScreen.load(bitmap) {
error(R.drawable.default_icon)
}
}
binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ") binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ")
@ -126,14 +116,4 @@ class GameAdapter(private val activity: AppCompatActivity) :
return oldItem == newItem return oldItem == newItem
} }
} }
private fun decodeGameIcon(uri: String): Bitmap? {
val data = NativeLibrary.getIcon(uri)
return BitmapFactory.decodeByteArray(
data,
0,
data.size,
BitmapFactory.Options()
)
}
} }

View file

@ -4,43 +4,43 @@
package org.yuzu.yuzu_emu.disk_shader_cache package org.yuzu.yuzu_emu.disk_shader_cache
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.lifecycle.ViewModelProvider
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.disk_shader_cache.ui.ShaderProgressDialogFragment import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.model.EmulationViewModel
import org.yuzu.yuzu_emu.utils.Log
@Keep @Keep
object DiskShaderCacheProgress { object DiskShaderCacheProgress {
val finishLock = Object() private lateinit var emulationViewModel: EmulationViewModel
private lateinit var fragment: ShaderProgressDialogFragment
private fun prepareDialog() { private fun prepareViewModel() {
val emulationActivity = NativeLibrary.sEmulationActivity.get()!! emulationViewModel =
emulationActivity.runOnUiThread { ViewModelProvider(
fragment = ShaderProgressDialogFragment.newInstance( NativeLibrary.sEmulationActivity.get() as EmulationActivity
emulationActivity.getString(R.string.loading), )[EmulationViewModel::class.java]
emulationActivity.getString(R.string.preparing_shaders)
)
fragment.show(
emulationActivity.supportFragmentManager,
ShaderProgressDialogFragment.TAG
)
}
synchronized(finishLock) { finishLock.wait() }
} }
@JvmStatic @JvmStatic
fun loadProgress(stage: Int, progress: Int, max: Int) { fun loadProgress(stage: Int, progress: Int, max: Int) {
val emulationActivity = NativeLibrary.sEmulationActivity.get() val emulationActivity = NativeLibrary.sEmulationActivity.get()
?: error("[DiskShaderCacheProgress] EmulationActivity not present") if (emulationActivity == null) {
Log.error("[DiskShaderCacheProgress] EmulationActivity not present")
return
}
when (LoadCallbackStage.values()[stage]) { emulationActivity.runOnUiThread {
LoadCallbackStage.Prepare -> prepareDialog() when (LoadCallbackStage.values()[stage]) {
LoadCallbackStage.Build -> fragment.onUpdateProgress( LoadCallbackStage.Prepare -> prepareViewModel()
emulationActivity.getString(R.string.building_shaders), LoadCallbackStage.Build -> emulationViewModel.updateProgress(
progress, emulationActivity.getString(R.string.building_shaders),
max progress,
) max
LoadCallbackStage.Complete -> fragment.dismiss() )
LoadCallbackStage.Complete -> {}
}
} }
} }

View file

@ -1,31 +0,0 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.disk_shader_cache
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class ShaderProgressViewModel : ViewModel() {
private val _progress = MutableLiveData(0)
val progress: LiveData<Int> get() = _progress
private val _max = MutableLiveData(0)
val max: LiveData<Int> get() = _max
private val _message = MutableLiveData("")
val message: LiveData<String> get() = _message
fun setProgress(progress: Int) {
_progress.postValue(progress)
}
fun setMax(max: Int) {
_max.postValue(max)
}
fun setMessage(msg: String) {
_message.postValue(msg)
}
}

View file

@ -1,103 +0,0 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.disk_shader_cache.ui
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
import org.yuzu.yuzu_emu.disk_shader_cache.DiskShaderCacheProgress
import org.yuzu.yuzu_emu.disk_shader_cache.ShaderProgressViewModel
class ShaderProgressDialogFragment : DialogFragment() {
private var _binding: DialogProgressBarBinding? = null
private val binding get() = _binding!!
private lateinit var alertDialog: AlertDialog
private lateinit var shaderProgressViewModel: ShaderProgressViewModel
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
_binding = DialogProgressBarBinding.inflate(layoutInflater)
shaderProgressViewModel =
ViewModelProvider(requireActivity())[ShaderProgressViewModel::class.java]
val title = requireArguments().getString(TITLE)
val message = requireArguments().getString(MESSAGE)
isCancelable = false
alertDialog = MaterialAlertDialogBuilder(requireActivity())
.setView(binding.root)
.setTitle(title)
.setMessage(message)
.create()
return alertDialog
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
shaderProgressViewModel.progress.observe(viewLifecycleOwner) { progress ->
binding.progressBar.progress = progress
setUpdateText()
}
shaderProgressViewModel.max.observe(viewLifecycleOwner) { max ->
binding.progressBar.max = max
setUpdateText()
}
shaderProgressViewModel.message.observe(viewLifecycleOwner) { msg ->
alertDialog.setMessage(msg)
}
synchronized(DiskShaderCacheProgress.finishLock) {
DiskShaderCacheProgress.finishLock.notifyAll()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
fun onUpdateProgress(msg: String, progress: Int, max: Int) {
shaderProgressViewModel.setProgress(progress)
shaderProgressViewModel.setMax(max)
shaderProgressViewModel.setMessage(msg)
}
private fun setUpdateText() {
binding.progressText.text = String.format(
"%d/%d",
shaderProgressViewModel.progress.value,
shaderProgressViewModel.max.value
)
}
companion object {
const val TAG = "ProgressDialogFragment"
const val TITLE = "title"
const val MESSAGE = "message"
fun newInstance(title: String, message: String): ShaderProgressDialogFragment {
val frag = ShaderProgressDialogFragment()
val args = Bundle()
args.putString(TITLE, title)
args.putString(MESSAGE, message)
frag.arguments = args
return frag
}
}
}

View file

@ -24,8 +24,9 @@ import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
@ -50,6 +51,7 @@ 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.model.Settings
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.model.EmulationViewModel
import org.yuzu.yuzu_emu.overlay.InputOverlay import org.yuzu.yuzu_emu.overlay.InputOverlay
import org.yuzu.yuzu_emu.utils.* import org.yuzu.yuzu_emu.utils.*
@ -66,6 +68,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private lateinit var game: Game private lateinit var game: Game
private val emulationViewModel: EmulationViewModel by activityViewModels()
private var isInFoldableLayout = false private var isInFoldableLayout = false
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
@ -130,9 +134,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
binding.showFpsText.setTextColor(Color.YELLOW) binding.showFpsText.setTextColor(Color.YELLOW)
binding.doneControlConfig.setOnClickListener { stopConfiguringControls() } binding.doneControlConfig.setOnClickListener { stopConfiguringControls() }
// Setup overlay. binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
updateShowFpsOverlay()
binding.inGameMenu.getHeaderView(0).findViewById<TextView>(R.id.text_game_title).text = binding.inGameMenu.getHeaderView(0).findViewById<TextView>(R.id.text_game_title).text =
game.title game.title
binding.inGameMenu.setNavigationItemSelectedListener { binding.inGameMenu.setNavigationItemSelectedListener {
@ -174,7 +176,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
R.id.menu_exit -> { R.id.menu_exit -> {
emulationState.stop() emulationState.stop()
requireActivity().finish() emulationViewModel.setIsEmulationStopping(true)
binding.drawerLayout.close()
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
true true
} }
@ -188,6 +192,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
requireActivity(), requireActivity(),
object : OnBackPressedCallback(true) { object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
if (!NativeLibrary.isRunning()) {
return
}
if (binding.drawerLayout.isOpen) { if (binding.drawerLayout.isOpen) {
binding.drawerLayout.close() binding.drawerLayout.close()
} else { } else {
@ -204,6 +212,54 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
.collect { updateFoldableLayout(requireActivity() as EmulationActivity, it) } .collect { updateFoldableLayout(requireActivity() as EmulationActivity, it) }
} }
} }
GameIconUtils.loadGameIcon(game, binding.loadingImage)
binding.loadingTitle.text = game.title
binding.loadingTitle.isSelected = true
binding.loadingText.isSelected = true
emulationViewModel.shaderProgress.observe(viewLifecycleOwner) {
if (it > 0 && it != emulationViewModel.totalShaders.value!!) {
binding.loadingProgressIndicator.isIndeterminate = false
if (it < binding.loadingProgressIndicator.max) {
binding.loadingProgressIndicator.progress = it
}
}
if (it == emulationViewModel.totalShaders.value!!) {
binding.loadingText.setText(R.string.loading)
binding.loadingProgressIndicator.isIndeterminate = true
}
}
emulationViewModel.totalShaders.observe(viewLifecycleOwner) {
binding.loadingProgressIndicator.max = it
}
emulationViewModel.shaderMessage.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.loadingText.text = it
}
}
emulationViewModel.emulationStarted.observe(viewLifecycleOwner) { started ->
if (started) {
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
ViewUtils.showView(binding.surfaceInputOverlay)
ViewUtils.hideView(binding.loadingIndicator)
// Setup overlay
updateShowFpsOverlay()
}
}
emulationViewModel.isEmulationStopping.observe(viewLifecycleOwner) {
if (it) {
binding.loadingText.setText(R.string.shutting_down)
ViewUtils.showView(binding.loadingIndicator)
ViewUtils.hideView(binding.inputContainer)
ViewUtils.hideView(binding.showFpsText)
}
}
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
@ -213,11 +269,21 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
binding.drawerLayout.close() binding.drawerLayout.close()
} }
if (EmulationMenuSettings.showOverlay) { if (EmulationMenuSettings.showOverlay) {
binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = false } binding.surfaceInputOverlay.post {
binding.surfaceInputOverlay.visibility = View.VISIBLE
}
} }
} else { } else {
if (EmulationMenuSettings.showOverlay) { if (EmulationMenuSettings.showOverlay &&
binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = true } emulationViewModel.emulationStarted.value == true
) {
binding.surfaceInputOverlay.post {
binding.surfaceInputOverlay.visibility = View.VISIBLE
}
} else {
binding.surfaceInputOverlay.post {
binding.surfaceInputOverlay.visibility = View.INVISIBLE
}
} }
if (!isInFoldableLayout) { if (!isInFoldableLayout) {
if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
@ -226,9 +292,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
binding.surfaceInputOverlay.layout = InputOverlay.LANDSCAPE binding.surfaceInputOverlay.layout = InputOverlay.LANDSCAPE
} }
} }
if (!binding.surfaceInputOverlay.isInEditMode) {
refreshInputOverlay()
}
} }
} }
@ -260,10 +323,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
super.onDetach() super.onDetach()
} }
private fun refreshInputOverlay() {
binding.surfaceInputOverlay.refreshControls()
}
private fun resetInputOverlay() { private fun resetInputOverlay() {
preferences.edit() preferences.edit()
.remove(Settings.PREF_CONTROL_SCALE) .remove(Settings.PREF_CONTROL_SCALE)
@ -281,17 +340,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
val FRAMETIME = 2 val FRAMETIME = 2
val SPEED = 3 val SPEED = 3
perfStatsUpdater = { perfStatsUpdater = {
val perfStats = NativeLibrary.getPerfStats() if (emulationViewModel.emulationStarted.value == true) {
if (perfStats[FPS] > 0 && _binding != null) { val perfStats = NativeLibrary.getPerfStats()
binding.showFpsText.text = String.format("FPS: %.1f", perfStats[FPS]) if (perfStats[FPS] > 0 && _binding != null) {
} binding.showFpsText.text = String.format("FPS: %.1f", perfStats[FPS])
}
if (!emulationState.isStopped) {
perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 100) perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 100)
} }
} }
perfStatsUpdateHandler.post(perfStatsUpdater!!) perfStatsUpdateHandler.post(perfStatsUpdater!!)
binding.showFpsText.text = resources.getString(R.string.emulation_game_loading)
binding.showFpsText.visibility = View.VISIBLE binding.showFpsText.visibility = View.VISIBLE
} else { } else {
if (perfStatsUpdater != null) { if (perfStatsUpdater != null) {
@ -349,7 +406,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
isInFoldableLayout = true isInFoldableLayout = true
binding.surfaceInputOverlay.layout = InputOverlay.FOLDABLE binding.surfaceInputOverlay.layout = InputOverlay.FOLDABLE
refreshInputOverlay()
} }
} }
it.isSeparating it.isSeparating
@ -437,7 +493,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
.apply() .apply()
} }
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
refreshInputOverlay() binding.surfaceInputOverlay.refreshControls()
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(R.string.emulation_toggle_all) { _, _ -> } .setNeutralButton(R.string.emulation_toggle_all) { _, _ -> }
@ -461,7 +517,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
R.id.menu_show_overlay -> { R.id.menu_show_overlay -> {
it.isChecked = !it.isChecked it.isChecked = !it.isChecked
EmulationMenuSettings.showOverlay = it.isChecked EmulationMenuSettings.showOverlay = it.isChecked
refreshInputOverlay() binding.surfaceInputOverlay.refreshControls()
true true
} }
@ -567,14 +623,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
preferences.edit() preferences.edit()
.putInt(Settings.PREF_CONTROL_SCALE, scale) .putInt(Settings.PREF_CONTROL_SCALE, scale)
.apply() .apply()
refreshInputOverlay() binding.surfaceInputOverlay.refreshControls()
} }
private fun setControlOpacity(opacity: Int) { private fun setControlOpacity(opacity: Int) {
preferences.edit() preferences.edit()
.putInt(Settings.PREF_CONTROL_OPACITY, opacity) .putInt(Settings.PREF_CONTROL_OPACITY, opacity)
.apply() .apply()
refreshInputOverlay() binding.surfaceInputOverlay.refreshControls()
} }
private fun setInsets() { private fun setInsets() {

View file

@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.model
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class EmulationViewModel : ViewModel() {
private val _emulationStarted = MutableLiveData(false)
val emulationStarted: LiveData<Boolean> get() = _emulationStarted
private val _isEmulationStopping = MutableLiveData(false)
val isEmulationStopping: LiveData<Boolean> get() = _isEmulationStopping
private val _shaderProgress = MutableLiveData(0)
val shaderProgress: LiveData<Int> get() = _shaderProgress
private val _totalShaders = MutableLiveData(0)
val totalShaders: LiveData<Int> get() = _totalShaders
private val _shaderMessage = MutableLiveData("")
val shaderMessage: LiveData<String> get() = _shaderMessage
fun setEmulationStarted(started: Boolean) {
_emulationStarted.postValue(started)
}
fun setIsEmulationStopping(value: Boolean) {
_isEmulationStopping.value = value
}
fun setShaderProgress(progress: Int) {
_shaderProgress.value = progress
}
fun setTotalShaders(max: Int) {
_totalShaders.value = max
}
fun setShaderMessage(msg: String) {
_shaderMessage.value = msg
}
fun updateProgress(msg: String, progress: Int, max: Int) {
setShaderMessage(msg)
setShaderProgress(progress)
setTotalShaders(max)
}
fun clear() {
_emulationStarted.value = false
_isEmulationStopping.value = false
_shaderProgress.value = 0
_totalShaders.value = 0
_shaderMessage.value = ""
}
}

View file

@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.utils
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.widget.ImageView
import androidx.core.graphics.drawable.toDrawable
import coil.ImageLoader
import coil.decode.DataSource
import coil.fetch.DrawableResult
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.key.Keyer
import coil.memory.MemoryCache
import coil.request.ImageRequest
import coil.request.Options
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.model.Game
class GameIconFetcher(
private val game: Game,
private val options: Options
) : Fetcher {
override suspend fun fetch(): FetchResult {
return DrawableResult(
drawable = decodeGameIcon(game.path)!!.toDrawable(options.context.resources),
isSampled = false,
dataSource = DataSource.DISK
)
}
private fun decodeGameIcon(uri: String): Bitmap? {
val data = NativeLibrary.getIcon(uri)
return BitmapFactory.decodeByteArray(
data,
0,
data.size,
BitmapFactory.Options()
)
}
class Factory : Fetcher.Factory<Game> {
override fun create(data: Game, options: Options, imageLoader: ImageLoader): Fetcher =
GameIconFetcher(data, options)
}
}
class GameIconKeyer : Keyer<Game> {
override fun key(data: Game, options: Options): String = data.path
}
object GameIconUtils {
private val imageLoader = ImageLoader.Builder(YuzuApplication.appContext)
.components {
add(GameIconKeyer())
add(GameIconFetcher.Factory())
}
.memoryCache {
MemoryCache.Builder(YuzuApplication.appContext)
.maxSizePercent(0.25)
.build()
}
.build()
fun loadGameIcon(game: Game, imageView: ImageView) {
val request = ImageRequest.Builder(YuzuApplication.appContext)
.data(game)
.target(imageView)
.error(R.drawable.default_icon)
.build()
imageLoader.enqueue(request)
}
}

View file

@ -15,6 +15,8 @@ static jclass s_disk_cache_progress_class;
static jclass s_load_callback_stage_class; static jclass s_load_callback_stage_class;
static jmethodID s_exit_emulation_activity; static jmethodID s_exit_emulation_activity;
static jmethodID s_disk_cache_load_progress; static jmethodID s_disk_cache_load_progress;
static jmethodID s_on_emulation_started;
static jmethodID s_on_emulation_stopped;
static constexpr jint JNI_VERSION = JNI_VERSION_1_6; static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
@ -59,6 +61,14 @@ jmethodID GetDiskCacheLoadProgress() {
return s_disk_cache_load_progress; return s_disk_cache_load_progress;
} }
jmethodID GetOnEmulationStarted() {
return s_on_emulation_started;
}
jmethodID GetOnEmulationStopped() {
return s_on_emulation_stopped;
}
} // namespace IDCache } // namespace IDCache
#ifdef __cplusplus #ifdef __cplusplus
@ -85,6 +95,10 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V"); env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V");
s_disk_cache_load_progress = s_disk_cache_load_progress =
env->GetStaticMethodID(s_disk_cache_progress_class, "loadProgress", "(III)V"); env->GetStaticMethodID(s_disk_cache_progress_class, "loadProgress", "(III)V");
s_on_emulation_started =
env->GetStaticMethodID(s_native_library_class, "onEmulationStarted", "()V");
s_on_emulation_stopped =
env->GetStaticMethodID(s_native_library_class, "onEmulationStopped", "(I)V");
// Initialize Android Storage // Initialize Android Storage
Common::FS::Android::RegisterCallbacks(env, s_native_library_class); Common::FS::Android::RegisterCallbacks(env, s_native_library_class);

View file

@ -15,5 +15,7 @@ jclass GetDiskCacheProgressClass();
jclass GetDiskCacheLoadCallbackStageClass(); jclass GetDiskCacheLoadCallbackStageClass();
jmethodID GetExitEmulationActivity(); jmethodID GetExitEmulationActivity();
jmethodID GetDiskCacheLoadProgress(); jmethodID GetDiskCacheLoadProgress();
jmethodID GetOnEmulationStarted();
jmethodID GetOnEmulationStopped();
} // namespace IDCache } // namespace IDCache

View file

@ -203,12 +203,10 @@ public:
} }
bool IsRunning() const { bool IsRunning() const {
std::scoped_lock lock(m_mutex);
return m_is_running; return m_is_running;
} }
bool IsPaused() const { bool IsPaused() const {
std::scoped_lock lock(m_mutex);
return m_is_running && m_is_paused; return m_is_running && m_is_paused;
} }
@ -335,6 +333,8 @@ public:
// Tear down the render window. // Tear down the render window.
m_window.reset(); m_window.reset();
OnEmulationStopped(m_load_result);
} }
void PauseEmulation() { void PauseEmulation() {
@ -376,6 +376,8 @@ public:
m_system.InitializeDebugger(); m_system.InitializeDebugger();
} }
OnEmulationStarted();
while (true) { while (true) {
{ {
[[maybe_unused]] std::unique_lock lock(m_mutex); [[maybe_unused]] std::unique_lock lock(m_mutex);
@ -511,6 +513,18 @@ private:
static_cast<jint>(progress), static_cast<jint>(max)); static_cast<jint>(progress), static_cast<jint>(max));
} }
static void OnEmulationStarted() {
JNIEnv* env = IDCache::GetEnvForThread();
env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
IDCache::GetOnEmulationStarted());
}
static void OnEmulationStopped(Core::SystemResultStatus result) {
JNIEnv* env = IDCache::GetEnvForThread();
env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
IDCache::GetOnEmulationStopped(), static_cast<jint>(result));
}
private: private:
static EmulationSession s_instance; static EmulationSession s_instance;
@ -528,8 +542,8 @@ private:
Core::PerfStatsResults m_perf_stats{}; Core::PerfStatsResults m_perf_stats{};
std::shared_ptr<FileSys::VfsFilesystem> m_vfs; std::shared_ptr<FileSys::VfsFilesystem> m_vfs;
Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized}; Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
bool m_is_running{}; std::atomic<bool> m_is_running = false;
bool m_is_paused{}; std::atomic<bool> m_is_paused = false;
SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{}; SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{};
std::unique_ptr<Service::Account::ProfileManager> m_profile_manager; std::unique_ptr<Service::Account::ProfileManager> m_profile_manager;
std::unique_ptr<FileSys::ManualContentProvider> m_manual_provider; std::unique_ptr<FileSys::ManualContentProvider> m_manual_provider;

View file

@ -26,6 +26,81 @@
android:focusable="false" android:focusable="false"
android:focusableInTouchMode="false" /> android:focusableInTouchMode="false" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/loading_indicator"
style="?attr/materialCardViewOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:focusable="false">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/loading_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal">
<ImageView
android:id="@+id/loading_image"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:adjustViewBounds="true"
app:layout_constraintBottom_toBottomOf="@+id/linearLayout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/linearLayout"
tools:src="@drawable/default_icon" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="24dp"
android:paddingVertical="36dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/loading_image"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/loading_title"
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:requiresFadingEdge="horizontal"
android:singleLine="true"
android:textAlignment="viewStart"
tools:text="@string/games" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/loading_text"
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:requiresFadingEdge="horizontal"
android:singleLine="true"
android:text="@string/loading"
android:textAlignment="viewStart" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/loading_progress_indicator"
android:layout_width="192dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:indeterminate="true"
app:trackCornerRadius="8dp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</FrameLayout> </FrameLayout>
<FrameLayout <FrameLayout
@ -41,11 +116,12 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center" android:layout_gravity="center"
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" /> android:focusableInTouchMode="true"
android:visibility="invisible" />
<Button <Button
style="@style/Widget.Material3.Button.ElevatedButton"
android:id="@+id/done_control_config" android:id="@+id/done_control_config"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
@ -81,6 +157,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="start|bottom" android:layout_gravity="start|bottom"
app:headerLayout="@layout/header_in_game" app:headerLayout="@layout/header_in_game"
app:menu="@menu/menu_in_game" /> app:menu="@menu/menu_in_game"
tools:visibility="gone" />
</androidx.drawerlayout.widget.DrawerLayout> </androidx.drawerlayout.widget.DrawerLayout>

View file

@ -209,7 +209,6 @@
<string name="emulation_pause">Emulation pausieren</string> <string name="emulation_pause">Emulation pausieren</string>
<string name="emulation_unpause">Emulation fortsetzen</string> <string name="emulation_unpause">Emulation fortsetzen</string>
<string name="emulation_input_overlay">Overlay-Optionen</string> <string name="emulation_input_overlay">Overlay-Optionen</string>
<string name="emulation_game_loading">Spiel lädt…</string>
<string name="load_settings">Lädt Einstellungen...</string> <string name="load_settings">Lädt Einstellungen...</string>

View file

@ -213,7 +213,6 @@
<string name="emulation_pause">Pausar Emulación</string> <string name="emulation_pause">Pausar Emulación</string>
<string name="emulation_unpause">Reanudar Emulación</string> <string name="emulation_unpause">Reanudar Emulación</string>
<string name="emulation_input_overlay">Opciones de pantalla </string> <string name="emulation_input_overlay">Opciones de pantalla </string>
<string name="emulation_game_loading">Cargando juego...</string>
<string name="load_settings">Cargando configuración...</string> <string name="load_settings">Cargando configuración...</string>

View file

@ -213,7 +213,6 @@
<string name="emulation_pause">Mettre en pause l\'émulation</string> <string name="emulation_pause">Mettre en pause l\'émulation</string>
<string name="emulation_unpause">Reprendre l\'émulation</string> <string name="emulation_unpause">Reprendre l\'émulation</string>
<string name="emulation_input_overlay">Options de l\'overlay</string> <string name="emulation_input_overlay">Options de l\'overlay</string>
<string name="emulation_game_loading">Chargement du jeu...</string>
<string name="load_settings">Chargement des paramètres…</string> <string name="load_settings">Chargement des paramètres…</string>

View file

@ -213,7 +213,6 @@
<string name="emulation_pause">Metti in pausa l\'emulazione</string> <string name="emulation_pause">Metti in pausa l\'emulazione</string>
<string name="emulation_unpause">Riprendi Emulazione</string> <string name="emulation_unpause">Riprendi Emulazione</string>
<string name="emulation_input_overlay">Impostazioni Overlay</string> <string name="emulation_input_overlay">Impostazioni Overlay</string>
<string name="emulation_game_loading">Caricamento del gioco...</string>
<string name="load_settings">Caricamento delle impostazioni...</string> <string name="load_settings">Caricamento delle impostazioni...</string>

View file

@ -211,7 +211,6 @@
<string name="emulation_pause">エミュレーションを一時停止</string> <string name="emulation_pause">エミュレーションを一時停止</string>
<string name="emulation_unpause">エミュレーションを再開</string> <string name="emulation_unpause">エミュレーションを再開</string>
<string name="emulation_input_overlay">オーバーレイオプション</string> <string name="emulation_input_overlay">オーバーレイオプション</string>
<string name="emulation_game_loading">ロード中…</string>
<string name="load_settings">設定をロード中…</string> <string name="load_settings">設定をロード中…</string>

View file

@ -213,7 +213,6 @@
<string name="emulation_pause">에뮬레이션 일시 중지</string> <string name="emulation_pause">에뮬레이션 일시 중지</string>
<string name="emulation_unpause">에뮬레이션 일시 중지 해제</string> <string name="emulation_unpause">에뮬레이션 일시 중지 해제</string>
<string name="emulation_input_overlay">오버레이 옵션</string> <string name="emulation_input_overlay">오버레이 옵션</string>
<string name="emulation_game_loading">게임 불러오기 중...</string>
<string name="load_settings">설정 불러오기 중...</string> <string name="load_settings">설정 불러오기 중...</string>

View file

@ -213,7 +213,6 @@
<string name="emulation_pause">Pause Emulering</string> <string name="emulation_pause">Pause Emulering</string>
<string name="emulation_unpause">Opphev pausing av emulering</string> <string name="emulation_unpause">Opphev pausing av emulering</string>
<string name="emulation_input_overlay">Alternativer for overlegg</string> <string name="emulation_input_overlay">Alternativer for overlegg</string>
<string name="emulation_game_loading">Spillet lastes inn...</string>
<string name="load_settings">Laster inn innstillinger...</string> <string name="load_settings">Laster inn innstillinger...</string>

View file

@ -213,7 +213,6 @@
<string name="emulation_pause">Wstrzymaj emulację</string> <string name="emulation_pause">Wstrzymaj emulację</string>
<string name="emulation_unpause">Wznów emulację</string> <string name="emulation_unpause">Wznów emulację</string>
<string name="emulation_input_overlay">Opcje nakładki</string> <string name="emulation_input_overlay">Opcje nakładki</string>
<string name="emulation_game_loading">Wczytywanie gry...</string>
<string name="load_settings">Wczytywanie ustawień...</string> <string name="load_settings">Wczytywanie ustawień...</string>

View file

@ -213,7 +213,6 @@
<string name="emulation_pause">Pausa emulação</string> <string name="emulation_pause">Pausa emulação</string>
<string name="emulation_unpause">Retomar emulação</string> <string name="emulation_unpause">Retomar emulação</string>
<string name="emulation_input_overlay">Opções de sobreposição </string> <string name="emulation_input_overlay">Opções de sobreposição </string>
<string name="emulation_game_loading">Jogo a carregar...</string>
<string name="load_settings">Configurações a carregar...</string> <string name="load_settings">Configurações a carregar...</string>

View file

@ -213,7 +213,6 @@
<string name="emulation_pause">Pausa emulação</string> <string name="emulation_pause">Pausa emulação</string>
<string name="emulation_unpause">Retomar emulação</string> <string name="emulation_unpause">Retomar emulação</string>
<string name="emulation_input_overlay">Opções de sobreposição </string> <string name="emulation_input_overlay">Opções de sobreposição </string>
<string name="emulation_game_loading">Jogo a carregar...</string>
<string name="load_settings">Configurações a carregar...</string> <string name="load_settings">Configurações a carregar...</string>

View file

@ -213,7 +213,6 @@
<string name="emulation_pause">Пауза эмуляции</string> <string name="emulation_pause">Пауза эмуляции</string>
<string name="emulation_unpause">Возобновление эмуляции</string> <string name="emulation_unpause">Возобновление эмуляции</string>
<string name="emulation_input_overlay">Настройки оверлея</string> <string name="emulation_input_overlay">Настройки оверлея</string>
<string name="emulation_game_loading">Загрузка игры...</string>
<string name="load_settings">Загрузка настроек...</string> <string name="load_settings">Загрузка настроек...</string>

View file

@ -213,7 +213,6 @@
<string name="emulation_pause">Пауза емуляції</string> <string name="emulation_pause">Пауза емуляції</string>
<string name="emulation_unpause">Відновлення емуляції</string> <string name="emulation_unpause">Відновлення емуляції</string>
<string name="emulation_input_overlay">Налаштування оверлея</string> <string name="emulation_input_overlay">Налаштування оверлея</string>
<string name="emulation_game_loading">Завантаження гри...</string>
<string name="load_settings">Завантаження налаштувань...</string> <string name="load_settings">Завантаження налаштувань...</string>

View file

@ -213,7 +213,6 @@
<string name="emulation_pause">暂停模拟</string> <string name="emulation_pause">暂停模拟</string>
<string name="emulation_unpause">继续模拟</string> <string name="emulation_unpause">继续模拟</string>
<string name="emulation_input_overlay">虚拟按键选项</string> <string name="emulation_input_overlay">虚拟按键选项</string>
<string name="emulation_game_loading">载入游戏中…</string>
<string name="load_settings">正在载入设定…</string> <string name="load_settings">正在载入设定…</string>

View file

@ -213,7 +213,6 @@
<string name="emulation_pause">暫停模擬</string> <string name="emulation_pause">暫停模擬</string>
<string name="emulation_unpause">取消暫停模擬</string> <string name="emulation_unpause">取消暫停模擬</string>
<string name="emulation_input_overlay">覆疊選項</string> <string name="emulation_input_overlay">覆疊選項</string>
<string name="emulation_game_loading">遊戲正在載入…</string>
<string name="load_settings">正在載入設定…</string> <string name="load_settings">正在載入設定…</string>

View file

@ -204,6 +204,7 @@
<string name="error_saving">Error saving %1$s.ini: %2$s</string> <string name="error_saving">Error saving %1$s.ini: %2$s</string>
<string name="unimplemented_menu">Unimplemented Menu</string> <string name="unimplemented_menu">Unimplemented Menu</string>
<string name="loading">Loading…</string> <string name="loading">Loading…</string>
<string name="shutting_down">Shutting down…</string>
<string name="reset_setting_confirmation">Do you want to reset this setting back to its default value?</string> <string name="reset_setting_confirmation">Do you want to reset this setting back to its default value?</string>
<string name="reset_to_default">Reset to default</string> <string name="reset_to_default">Reset to default</string>
<string name="reset_all_settings">Reset all settings?</string> <string name="reset_all_settings">Reset all settings?</string>
@ -262,7 +263,6 @@
<string name="emulation_pause">Pause emulation</string> <string name="emulation_pause">Pause emulation</string>
<string name="emulation_unpause">Unpause emulation</string> <string name="emulation_unpause">Unpause emulation</string>
<string name="emulation_input_overlay">Overlay options</string> <string name="emulation_input_overlay">Overlay options</string>
<string name="emulation_game_loading">Game loading…</string>
<string name="load_settings">Loading settings…</string> <string name="load_settings">Loading settings…</string>