summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--.gitignore2
m---------externals/vcpkg0
-rw-r--r--src/android/app/build.gradle.kts1
-rw-r--r--src/android/app/src/main/AndroidManifest.xml4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt5
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt178
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt5
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt5
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt5
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt13
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt20
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt164
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt284
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.kt18
-rw-r--r--src/android/app/src/main/jni/native.cpp13
-rw-r--r--src/android/app/src/main/res/drawable/ic_pip_pause.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_pip_play.xml9
-rw-r--r--src/android/app/src/main/res/layout/activity_emulation.xml16
-rw-r--r--src/android/app/src/main/res/layout/fragment_emulation.xml82
-rw-r--r--src/android/app/src/main/res/navigation/emulation_navigation.xml18
-rw-r--r--src/android/app/src/main/res/navigation/home_navigation.xml14
-rw-r--r--src/android/app/src/main/res/values/arrays.xml12
-rw-r--r--src/android/app/src/main/res/values/integers.xml64
-rw-r--r--src/android/app/src/main/res/values/strings.xml12
-rw-r--r--src/android/build.gradle.kts9
-rw-r--r--src/core/file_sys/vfs_real.cpp187
-rw-r--r--src/core/file_sys/vfs_real.h30
-rw-r--r--src/video_core/buffer_cache/buffer_cache.h8
-rw-r--r--src/video_core/buffer_cache/buffer_cache_base.h3
-rw-r--r--src/video_core/renderer_opengl/gl_buffer_cache.cpp19
-rw-r--r--src/video_core/renderer_opengl/gl_buffer_cache.h6
-rw-r--r--src/video_core/renderer_vulkan/vk_buffer_cache.cpp30
-rw-r--r--src/video_core/renderer_vulkan/vk_buffer_cache.h6
-rw-r--r--vcpkg.json2
35 files changed, 887 insertions, 368 deletions
diff --git a/.gitignore b/.gitignore
index a5f7248c7..fbadb208b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,8 @@ CMakeSettings.json
# OSX global filetypes
# Created by Finder or Spotlight in directories for various OS functionality (indexing, etc)
.DS_Store
+.DS_Store?
+._*
.AppleDouble
.LSOverride
.Spotlight-V100
diff --git a/externals/vcpkg b/externals/vcpkg
-Subproject 656fcc6ab2b05c6d999b7eaca717027ac3738f7
+Subproject a487471068f4cb1cbb4eeb340763cdcc0a75fd6
diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts
index fe613d339..a637db78a 100644
--- a/src/android/app/build.gradle.kts
+++ b/src/android/app/build.gradle.kts
@@ -9,6 +9,7 @@ plugins {
id("org.jetbrains.kotlin.android")
id("kotlin-parcelize")
kotlin("plugin.serialization") version "1.8.21"
+ id("androidx.navigation.safeargs.kotlin")
}
/**
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index 55f62b4b9..a6f87fc2e 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -53,8 +53,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
<activity
android:name="org.yuzu.yuzu_emu.activities.EmulationActivity"
android:theme="@style/Theme.Yuzu.Main"
- android:launchMode="singleTop"
- android:screenOrientation="userLandscape"
+ android:supportsPictureInPicture="true"
+ android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
android:exported="true">
<intent-filter>
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
index 4be9ade14..22f0a2646 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
@@ -283,6 +283,11 @@ object NativeLibrary {
external fun isRunning(): Boolean
/**
+ * Returns true if emulation is paused.
+ */
+ external fun isPaused(): Boolean
+
+ /**
* Returns the performance stats for the current game
*/
external fun getPerfStats(): DoubleArray
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
index 20a0394f5..5ca519f0a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
@@ -4,14 +4,23 @@
package org.yuzu.yuzu_emu.activities
import android.app.Activity
+import android.app.PendingIntent
+import android.app.PictureInPictureParams
+import android.app.RemoteAction
+import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
+import android.content.IntentFilter
+import android.content.res.Configuration
import android.graphics.Rect
+import android.graphics.drawable.Icon
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
+import android.os.Build
import android.os.Bundle
+import android.util.Rational
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
@@ -23,30 +32,27 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import androidx.window.layout.WindowInfoTracker
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
+import androidx.navigation.fragment.NavHostFragment
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding
+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.SettingsViewModel
-import org.yuzu.yuzu_emu.fragments.EmulationFragment
import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.utils.ControllerMappingHelper
import org.yuzu.yuzu_emu.utils.ForegroundService
import org.yuzu.yuzu_emu.utils.InputHandler
import org.yuzu.yuzu_emu.utils.NfcReader
-import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
import org.yuzu.yuzu_emu.utils.ThemeHelper
import kotlin.math.roundToInt
class EmulationActivity : AppCompatActivity(), SensorEventListener {
+ private lateinit var binding: ActivityEmulationBinding
+
private var controllerMappingHelper: ControllerMappingHelper? = null
var isActivityRecreated = false
- private var emulationFragment: EmulationFragment? = null
private lateinit var nfcReader: NfcReader
private lateinit var inputHandler: InputHandler
@@ -55,7 +61,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
private var motionTimestamp: Long = 0
private var flipMotionOrientation: Boolean = false
- private lateinit var game: Game
+ private val actionPause = "ACTION_EMULATOR_PAUSE"
+ private val actionPlay = "ACTION_EMULATOR_PLAY"
private val settingsViewModel: SettingsViewModel by viewModels()
@@ -70,47 +77,31 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
settingsViewModel.settings.loadSettings()
super.onCreate(savedInstanceState)
- if (savedInstanceState == null) {
- // Get params we were passed
- game = intent.parcelable(EXTRA_SELECTED_GAME)!!
- isActivityRecreated = false
- } else {
- isActivityRecreated = true
- restoreState(savedInstanceState)
- }
+
+ binding = ActivityEmulationBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ val navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
+ val navController = navHostFragment.navController
+ navController
+ .setGraph(R.navigation.emulation_navigation, intent.extras)
+
+ isActivityRecreated = savedInstanceState != null
+
controllerMappingHelper = ControllerMappingHelper()
// Set these options now so that the SurfaceView the game renders into is the right size.
enableFullscreenImmersive()
- setContentView(R.layout.activity_emulation)
window.decorView.setBackgroundColor(getColor(android.R.color.black))
- // Find or create the EmulationFragment
- emulationFragment =
- supportFragmentManager.findFragmentById(R.id.frame_emulation_fragment) as EmulationFragment?
- if (emulationFragment == null) {
- emulationFragment = EmulationFragment.newInstance(game)
- supportFragmentManager.beginTransaction()
- .add(R.id.frame_emulation_fragment, emulationFragment!!)
- .commit()
- }
- title = game.title
-
nfcReader = NfcReader(this)
nfcReader.initialize()
inputHandler = InputHandler()
inputHandler.initialize()
- lifecycleScope.launch(Dispatchers.Main) {
- lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
- WindowInfoTracker.getOrCreate(this@EmulationActivity)
- .windowLayoutInfo(this@EmulationActivity)
- .collect { emulationFragment?.updateCurrentLayout(this@EmulationActivity, it) }
- }
- }
-
// Start a foreground service to prevent the app from getting killed in the background
val startIntent = Intent(this, ForegroundService::class.java)
startForegroundService(startIntent)
@@ -143,6 +134,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
super.onResume()
nfcReader.startScanning()
startMotionSensorListener()
+
+ buildPictureInPictureParams()
}
override fun onPause() {
@@ -151,17 +144,22 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
stopMotionSensorListener()
}
+ override fun onUserLeaveHint() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+ if (BooleanSetting.PICTURE_IN_PICTURE.boolean && !isInPictureInPictureMode) {
+ val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
+ .getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
+ enterPictureInPictureMode(pictureInPictureParamsBuilder.build())
+ }
+ }
+ }
+
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
nfcReader.onNewIntent(intent)
}
- override fun onSaveInstanceState(outState: Bundle) {
- outState.putParcelable(EXTRA_SELECTED_GAME, game)
- super.onSaveInstanceState(outState)
- }
-
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
@@ -248,10 +246,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
override fun onAccuracyChanged(sensor: Sensor, i: Int) {}
- private fun restoreState(savedInstanceState: Bundle) {
- game = savedInstanceState.parcelable(EXTRA_SELECTED_GAME)!!
- }
-
private fun enableFullscreenImmersive() {
WindowCompat.setDecorFitsSystemWindows(window, false)
@@ -262,6 +256,96 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
}
}
+ private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder(): PictureInPictureParams.Builder {
+ val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.int) {
+ 0 -> Rational(16, 9)
+ 1 -> Rational(4, 3)
+ 2 -> Rational(21, 9)
+ 3 -> Rational(16, 10)
+ else -> null // Best fit
+ }
+ return this.apply { aspectRatio?.let { setAspectRatio(it) } }
+ }
+
+ private fun PictureInPictureParams.Builder.getPictureInPictureActionsBuilder(): PictureInPictureParams.Builder {
+ val pictureInPictureActions: MutableList<RemoteAction> = mutableListOf()
+ val pendingFlags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+
+ if (NativeLibrary.isPaused()) {
+ val playIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_play)
+ val playPendingIntent = PendingIntent.getBroadcast(
+ this@EmulationActivity,
+ R.drawable.ic_pip_play,
+ Intent(actionPlay),
+ pendingFlags
+ )
+ val playRemoteAction = RemoteAction(
+ playIcon,
+ getString(R.string.play),
+ getString(R.string.play),
+ playPendingIntent
+ )
+ pictureInPictureActions.add(playRemoteAction)
+ } else {
+ val pauseIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_pause)
+ val pausePendingIntent = PendingIntent.getBroadcast(
+ this@EmulationActivity,
+ R.drawable.ic_pip_pause,
+ Intent(actionPause),
+ pendingFlags
+ )
+ val pauseRemoteAction = RemoteAction(
+ pauseIcon,
+ getString(R.string.pause),
+ getString(R.string.pause),
+ pausePendingIntent
+ )
+ pictureInPictureActions.add(pauseRemoteAction)
+ }
+
+ return this.apply { setActions(pictureInPictureActions) }
+ }
+
+ fun buildPictureInPictureParams() {
+ val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
+ .getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ pictureInPictureParamsBuilder.setAutoEnterEnabled(BooleanSetting.PICTURE_IN_PICTURE.boolean)
+ }
+ setPictureInPictureParams(pictureInPictureParamsBuilder.build())
+ }
+
+ private var pictureInPictureReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent) {
+ if (intent.action == actionPlay) {
+ if (NativeLibrary.isPaused()) NativeLibrary.unPauseEmulation()
+ } else if (intent.action == actionPause) {
+ if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation()
+ }
+ buildPictureInPictureParams()
+ }
+ }
+
+ override fun onPictureInPictureModeChanged(
+ isInPictureInPictureMode: Boolean,
+ newConfig: Configuration
+ ) {
+ super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
+ if (isInPictureInPictureMode) {
+ IntentFilter().apply {
+ addAction(actionPause)
+ addAction(actionPlay)
+ }.also {
+ registerReceiver(pictureInPictureReceiver, it)
+ }
+ } else {
+ try {
+ unregisterReceiver(pictureInPictureReceiver)
+ } catch (ignored : Exception) {
+ }
+ }
+ }
+
private fun startMotionSensorListener() {
val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
index 7f9e2e2d4..83d08841b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
@@ -16,6 +16,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
+import androidx.navigation.findNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
@@ -23,6 +24,7 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import coil.load
import kotlinx.coroutines.launch
+import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
@@ -78,7 +80,8 @@ class GameAdapter(private val activity: AppCompatActivity) :
)
.apply()
- EmulationActivity.launch(activity, holder.game)
+ val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game)
+ view.findNavController().navigate(action)
}
inner class GameViewHolder(val binding: CardGameBinding) :
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
index 3dfd66779..63b4df273 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
@@ -8,6 +8,7 @@ enum class BooleanSetting(
override val section: String,
override val defaultValue: Boolean
) : AbstractBooleanSetting {
+ PICTURE_IN_PICTURE("picture_in_picture", Settings.SECTION_GENERAL, true),
USE_CUSTOM_RTC("custom_rtc_enabled", Settings.SECTION_SYSTEM, false);
override var boolean: Boolean = defaultValue
@@ -27,6 +28,7 @@ enum class BooleanSetting(
companion object {
private val NOT_RUNTIME_EDITABLE = listOf(
+ PICTURE_IN_PICTURE,
USE_CUSTOM_RTC
)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
index fa84f94f5..4427a7d9d 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
@@ -93,6 +93,11 @@ enum class IntSetting(
Settings.SECTION_RENDERER,
0
),
+ RENDERER_SCREEN_LAYOUT(
+ "screen_layout",
+ Settings.SECTION_RENDERER,
+ Settings.LayoutOption_MobileLandscape
+ ),
RENDERER_ASPECT_RATIO(
"aspect_ratio",
Settings.SECTION_RENDERER,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
index 8df20b928..6bcb7bee0 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
@@ -133,7 +133,6 @@ class Settings {
const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter"
const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable"
const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics"
- const val PREF_MENU_SETTINGS_LANDSCAPE = "EmulationMenuSettings_LandscapeScreenLayout"
const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps"
const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay"
@@ -144,6 +143,10 @@ class Settings {
private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap()
+ const val LayoutOption_Unspecified = 0
+ const val LayoutOption_MobilePortrait = 4
+ const val LayoutOption_MobileLandscape = 5
+
init {
configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] =
listOf(
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
index 72e2cce2a..da7062b87 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
@@ -16,6 +16,7 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import android.view.ViewGroup.MarginLayoutParams
import androidx.activity.OnBackPressedCallback
+import androidx.activity.result.ActivityResultLauncher
import androidx.core.view.updatePadding
import com.google.android.material.color.MaterialColors
import org.yuzu.yuzu_emu.NativeLibrary
@@ -239,5 +240,17 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
settings.putExtra(ARG_GAME_ID, gameId)
context.startActivity(settings)
}
+
+ fun launch(
+ context: Context,
+ launcher: ActivityResultLauncher<Intent>,
+ menuTag: String?,
+ gameId: String?
+ ) {
+ val settings = Intent(context, SettingsActivity::class.java)
+ settings.putExtra(ARG_MENU_TAG, menuTag)
+ settings.putExtra(ARG_GAME_ID, gameId)
+ launcher.launch(settings)
+ }
}
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
index 1ceaa6fb4..b611389a1 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -166,6 +166,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
IntSetting.CPU_ACCURACY.defaultValue
)
)
+ add(
+ SwitchSetting(
+ BooleanSetting.PICTURE_IN_PICTURE,
+ R.string.picture_in_picture,
+ R.string.picture_in_picture_description,
+ BooleanSetting.PICTURE_IN_PICTURE.key,
+ BooleanSetting.PICTURE_IN_PICTURE.defaultValue
+ )
+ )
}
}
@@ -285,6 +294,17 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
)
add(
SingleChoiceSetting(
+ IntSetting.RENDERER_SCREEN_LAYOUT,
+ R.string.renderer_screen_layout,
+ 0,
+ R.array.rendererScreenLayoutNames,
+ R.array.rendererScreenLayoutValues,
+ IntSetting.RENDERER_SCREEN_LAYOUT.key,
+ IntSetting.RENDERER_SCREEN_LAYOUT.defaultValue
+ )
+ )
+ add(
+ SingleChoiceSetting(
IntSetting.RENDERER_ASPECT_RATIO,
R.string.renderer_aspect_ratio,
0,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
index 9523381cd..4b2305892 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
@@ -7,30 +7,39 @@ 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.Resources
+import android.content.res.Configuration
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.updatePadding
+import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.fragment.navArgs
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
@@ -41,9 +50,8 @@ 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.model.Game
+import org.yuzu.yuzu_emu.overlay.InputOverlay
import org.yuzu.yuzu_emu.utils.*
-import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private lateinit var preferences: SharedPreferences
@@ -54,13 +62,42 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private var _binding: FragmentEmulationBinding? = null
private val binding get() = _binding!!
- private lateinit var game: Game
+ 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")
}
@@ -75,8 +112,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
// So this fragment doesn't restart on configuration changes; i.e. rotation.
retainInstance = true
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
- game = requireArguments().parcelable(EmulationActivity.EXTRA_SELECTED_GAME)!!
- emulationState = EmulationState(game.path)
+ emulationState = EmulationState(args.game.path)
}
/**
@@ -100,7 +136,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
updateShowFpsOverlay()
binding.inGameMenu.getHeaderView(0).findViewById<TextView>(R.id.text_game_title).text =
- game.title
+ args.game.title
binding.inGameMenu.setNavigationItemSelectedListener {
when (it.itemId) {
R.id.menu_pause_emulation -> {
@@ -125,7 +161,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
R.id.menu_settings -> {
- SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "")
+ SettingsActivity.launch(
+ requireContext(),
+ onReturnFromSettings,
+ SettingsFile.FILE_NAME_CONFIG,
+ ""
+ )
true
}
@@ -153,6 +194,40 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
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() {
@@ -172,6 +247,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
)
+ updateScreenLayout()
+
emulationState.run(emulationActivity!!.isActivityRecreated)
}
@@ -231,31 +308,50 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
}
- private val Number.toPx get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics).toInt()
+ @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)
+ }
- fun updateCurrentLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) {
+ 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.surfaceEmulation.layoutParams.height = it.bounds.top
+ // Restrict emulation and overlays to the top of the screen
+ binding.emulationContainer.layoutParams.height = it.bounds.top
+ binding.overlayContainer.layoutParams.height = it.bounds.top
+ // Restrict input and menu drawer to the bottom of the screen
+ binding.inputContainer.layoutParams.height = it.bounds.bottom
binding.inGameMenu.layoutParams.height = it.bounds.bottom
- binding.overlayContainer.layoutParams.height = it.bounds.bottom - 48.toPx
- binding.overlayContainer.updatePadding(0, 0, 0, 24.toPx)
+
+ isInFoldableLayout = true
+ binding.surfaceInputOverlay.orientation = InputOverlay.FOLDABLE
+ refreshInputOverlay()
}
}
it.isSeparating
} ?: false
if (!isFolding) {
- binding.surfaceEmulation.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
- binding.inGameMenu.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
+ binding.emulationContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
+ binding.inputContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
binding.overlayContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
- binding.overlayContainer.updatePadding(0, 0, 0, 0)
- emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
+ binding.inGameMenu.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
+ isInFoldableLayout = false
+ updateScreenLayout()
}
- binding.surfaceInputOverlay.requestLayout()
- binding.inGameMenu.requestLayout()
+ binding.emulationContainer.requestLayout()
+ binding.inputContainer.requestLayout()
binding.overlayContainer.requestLayout()
+ binding.inGameMenu.requestLayout()
}
override fun surfaceCreated(holder: SurfaceHolder) {
@@ -385,7 +481,19 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
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)
}
@@ -393,6 +501,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
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")
@@ -601,13 +715,5 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
companion object {
private val perfStatsUpdateHandler = Handler(Looper.myLooper()!!)
-
- fun newInstance(game: Game): EmulationFragment {
- val args = Bundle()
- args.putParcelable(EmulationActivity.EXTRA_SELECTED_GAME, game)
- val fragment = EmulationFragment()
- fragment.arguments = args
- return fragment
- }
}
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
index aa424c768..d12d08e9f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
@@ -51,12 +51,14 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
private lateinit var windowInsets: WindowInsets
+ var orientation = LANDSCAPE
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
windowInsets = rootWindowInsets
- if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) {
+ if (!preferences.getBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", false)) {
defaultOverlay()
}
@@ -233,10 +235,6 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
val fingerPositionX = event.getX(pointerIndex).toInt()
val fingerPositionY = event.getY(pointerIndex).toInt()
- // TODO: Provide support for portrait layout
- //val orientation =
- // if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else ""
-
for (button in overlayButtons) {
// Determine the button state to apply based on the MotionEvent action flag.
when (event.action and MotionEvent.ACTION_MASK) {
@@ -266,7 +264,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
buttonBeingConfigured!!.buttonId,
buttonBeingConfigured!!.bounds.centerX(),
buttonBeingConfigured!!.bounds.centerY(),
- ""
+ orientation
)
buttonBeingConfigured = null
}
@@ -299,7 +297,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
dpadBeingConfigured!!.upId,
dpadBeingConfigured!!.bounds.centerX(),
dpadBeingConfigured!!.bounds.centerY(),
- ""
+ orientation
)
dpadBeingConfigured = null
}
@@ -330,7 +328,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
joystickBeingConfigured!!.buttonId,
joystickBeingConfigured!!.bounds.centerX(),
joystickBeingConfigured!!.bounds.centerY(),
- ""
+ orientation
)
joystickBeingConfigured = null
}
@@ -533,8 +531,6 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
overlayButtons.clear()
overlayDpads.clear()
overlayJoysticks.clear()
- val orientation =
- if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else ""
// Add all the enabled overlay items back to the HashSet.
if (EmulationMenuSettings.showOverlay) {
@@ -548,8 +544,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
val min = windowSize.first
val max = windowSize.second
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
- .putFloat("$sharedPrefsId$orientation-X", (x - min.x).toFloat() / max.x)
- .putFloat("$sharedPrefsId$orientation-Y", (y - min.y).toFloat() / max.y)
+ .putFloat("$sharedPrefsId-X$orientation", (x - min.x).toFloat() / max.x)
+ .putFloat("$sharedPrefsId-Y$orientation", (y - min.y).toFloat() / max.y)
.apply()
}
@@ -558,145 +554,250 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
}
private fun defaultOverlay() {
- if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) {
- defaultOverlayLandscape()
+ if (!preferences.getBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", false)) {
+ defaultOverlayByLayout(orientation)
}
resetButtonPlacement()
preferences.edit()
- .putBoolean(Settings.PREF_OVERLAY_INIT, true)
+ .putBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", true)
.apply()
}
fun resetButtonPlacement() {
- defaultOverlayLandscape()
+ defaultOverlayByLayout(orientation)
refreshControls()
}
- private fun defaultOverlayLandscape() {
+ private val landscapeResources = arrayOf(
+ R.integer.SWITCH_BUTTON_A_X,
+ R.integer.SWITCH_BUTTON_A_Y,
+ R.integer.SWITCH_BUTTON_B_X,
+ R.integer.SWITCH_BUTTON_B_Y,
+ R.integer.SWITCH_BUTTON_X_X,
+ R.integer.SWITCH_BUTTON_X_Y,
+ R.integer.SWITCH_BUTTON_Y_X,
+ R.integer.SWITCH_BUTTON_Y_Y,
+ R.integer.SWITCH_TRIGGER_ZL_X,
+ R.integer.SWITCH_TRIGGER_ZL_Y,
+ R.integer.SWITCH_TRIGGER_ZR_X,
+ R.integer.SWITCH_TRIGGER_ZR_Y,
+ R.integer.SWITCH_BUTTON_DPAD_X,
+ R.integer.SWITCH_BUTTON_DPAD_Y,
+ R.integer.SWITCH_TRIGGER_L_X,
+ R.integer.SWITCH_TRIGGER_L_Y,
+ R.integer.SWITCH_TRIGGER_R_X,
+ R.integer.SWITCH_TRIGGER_R_Y,
+ R.integer.SWITCH_BUTTON_PLUS_X,
+ R.integer.SWITCH_BUTTON_PLUS_Y,
+ R.integer.SWITCH_BUTTON_MINUS_X,
+ R.integer.SWITCH_BUTTON_MINUS_Y,
+ R.integer.SWITCH_BUTTON_HOME_X,
+ R.integer.SWITCH_BUTTON_HOME_Y,
+ R.integer.SWITCH_BUTTON_CAPTURE_X,
+ R.integer.SWITCH_BUTTON_CAPTURE_Y,
+ R.integer.SWITCH_STICK_R_X,
+ R.integer.SWITCH_STICK_R_Y,
+ R.integer.SWITCH_STICK_L_X,
+ R.integer.SWITCH_STICK_L_Y
+ )
+
+ private val portraitResources = arrayOf(
+ R.integer.SWITCH_BUTTON_A_X_PORTRAIT,
+ R.integer.SWITCH_BUTTON_A_Y_PORTRAIT,
+ R.integer.SWITCH_BUTTON_B_X_PORTRAIT,
+ R.integer.SWITCH_BUTTON_B_Y_PORTRAIT,
+ R.integer.SWITCH_BUTTON_X_X_PORTRAIT,
+ R.integer.SWITCH_BUTTON_X_Y_PORTRAIT,
+ R.integer.SWITCH_BUTTON_Y_X_PORTRAIT,
+ R.integer.SWITCH_BUTTON_Y_Y_PORTRAIT,
+ R.integer.SWITCH_TRIGGER_ZL_X_PORTRAIT,
+ R.integer.SWITCH_TRIGGER_ZL_Y_PORTRAIT,
+ R.integer.SWITCH_TRIGGER_ZR_X_PORTRAIT,
+ R.integer.SWITCH_TRIGGER_ZR_Y_PORTRAIT,
+ R.integer.SWITCH_BUTTON_DPAD_X_PORTRAIT,
+ R.integer.SWITCH_BUTTON_DPAD_Y_PORTRAIT,
+ R.integer.SWITCH_TRIGGER_L_X_PORTRAIT,
+ R.integer.SWITCH_TRIGGER_L_Y_PORTRAIT,
+ R.integer.SWITCH_TRIGGER_R_X_PORTRAIT,
+ R.integer.SWITCH_TRIGGER_R_Y_PORTRAIT,
+ R.integer.SWITCH_BUTTON_PLUS_X_PORTRAIT,
+ R.integer.SWITCH_BUTTON_PLUS_Y_PORTRAIT,
+ R.integer.SWITCH_BUTTON_MINUS_X_PORTRAIT,
+ R.integer.SWITCH_BUTTON_MINUS_Y_PORTRAIT,
+ R.integer.SWITCH_BUTTON_HOME_X_PORTRAIT,
+ R.integer.SWITCH_BUTTON_HOME_Y_PORTRAIT,
+ R.integer.SWITCH_BUTTON_CAPTURE_X_PORTRAIT,
+ R.integer.SWITCH_BUTTON_CAPTURE_Y_PORTRAIT,
+ R.integer.SWITCH_STICK_R_X_PORTRAIT,
+ R.integer.SWITCH_STICK_R_Y_PORTRAIT,
+ R.integer.SWITCH_STICK_L_X_PORTRAIT,
+ R.integer.SWITCH_STICK_L_Y_PORTRAIT
+ )
+
+ private val foldableResources = arrayOf(
+ R.integer.SWITCH_BUTTON_A_X_FOLDABLE,
+ R.integer.SWITCH_BUTTON_A_Y_FOLDABLE,
+ R.integer.SWITCH_BUTTON_B_X_FOLDABLE,
+ R.integer.SWITCH_BUTTON_B_Y_FOLDABLE,
+ R.integer.SWITCH_BUTTON_X_X_FOLDABLE,
+ R.integer.SWITCH_BUTTON_X_Y_FOLDABLE,
+ R.integer.SWITCH_BUTTON_Y_X_FOLDABLE,
+ R.integer.SWITCH_BUTTON_Y_Y_FOLDABLE,
+ R.integer.SWITCH_TRIGGER_ZL_X_FOLDABLE,
+ R.integer.SWITCH_TRIGGER_ZL_Y_FOLDABLE,
+ R.integer.SWITCH_TRIGGER_ZR_X_FOLDABLE,
+ R.integer.SWITCH_TRIGGER_ZR_Y_FOLDABLE,
+ R.integer.SWITCH_BUTTON_DPAD_X_FOLDABLE,
+ R.integer.SWITCH_BUTTON_DPAD_Y_FOLDABLE,
+ R.integer.SWITCH_TRIGGER_L_X_FOLDABLE,
+ R.integer.SWITCH_TRIGGER_L_Y_FOLDABLE,
+ R.integer.SWITCH_TRIGGER_R_X_FOLDABLE,
+ R.integer.SWITCH_TRIGGER_R_Y_FOLDABLE,
+ R.integer.SWITCH_BUTTON_PLUS_X_FOLDABLE,
+ R.integer.SWITCH_BUTTON_PLUS_Y_FOLDABLE,
+ R.integer.SWITCH_BUTTON_MINUS_X_FOLDABLE,
+ R.integer.SWITCH_BUTTON_MINUS_Y_FOLDABLE,
+ R.integer.SWITCH_BUTTON_HOME_X_FOLDABLE,
+ R.integer.SWITCH_BUTTON_HOME_Y_FOLDABLE,
+ R.integer.SWITCH_BUTTON_CAPTURE_X_FOLDABLE,
+ R.integer.SWITCH_BUTTON_CAPTURE_Y_FOLDABLE,
+ R.integer.SWITCH_STICK_R_X_FOLDABLE,
+ R.integer.SWITCH_STICK_R_Y_FOLDABLE,
+ R.integer.SWITCH_STICK_L_X_FOLDABLE,
+ R.integer.SWITCH_STICK_L_Y_FOLDABLE
+ )
+
+ private fun getResourceValue(orientation: String, position: Int) : Float {
+ return when (orientation) {
+ PORTRAIT -> resources.getInteger(portraitResources[position]).toFloat() / 1000
+ FOLDABLE -> resources.getInteger(foldableResources[position]).toFloat() / 1000
+ else -> resources.getInteger(landscapeResources[position]).toFloat() / 1000
+ }
+ }
+
+ private fun defaultOverlayByLayout(orientation: String) {
// Each value represents the position of the button in relation to the screen size without insets.
preferences.edit()
.putFloat(
- ButtonType.BUTTON_A.toString() + "-X",
- resources.getInteger(R.integer.SWITCH_BUTTON_A_X).toFloat() / 1000
+ ButtonType.BUTTON_A.toString() + "-X$orientation",
+ getResourceValue(orientation, 0)
)
.putFloat(
- ButtonType.BUTTON_A.toString() + "-Y",
- resources.getInteger(R.integer.SWITCH_BUTTON_A_Y).toFloat() / 1000
+ ButtonType.BUTTON_A.toString() + "-Y$orientation",
+ getResourceValue(orientation, 1)
)
.putFloat(
- ButtonType.BUTTON_B.toString() + "-X",
- resources.getInteger(R.integer.SWITCH_BUTTON_B_X).toFloat() / 1000
+ ButtonType.BUTTON_B.toString() + "-X$orientation",
+ getResourceValue(orientation, 2)
)
.putFloat(
- ButtonType.BUTTON_B.toString() + "-Y",
- resources.getInteger(R.integer.SWITCH_BUTTON_B_Y).toFloat() / 1000
+ ButtonType.BUTTON_B.toString() + "-Y$orientation",
+ getResourceValue(orientation, 3)
)
.putFloat(
- ButtonType.BUTTON_X.toString() + "-X",
- resources.getInteger(R.integer.SWITCH_BUTTON_X_X).toFloat() / 1000
+ ButtonType.BUTTON_X.toString() + "-X$orientation",
+ getResourceValue(orientation, 4)
)
.putFloat(
- ButtonType.BUTTON_X.toString() + "-Y",
- resources.getInteger(R.integer.SWITCH_BUTTON_X_Y).toFloat() / 1000
+ ButtonType.BUTTON_X.toString() + "-Y$orientation",
+ getResourceValue(orientation, 5)
)
.putFloat(
- ButtonType.BUTTON_Y.toString() + "-X",
- resources.getInteger(R.integer.SWITCH_BUTTON_Y_X).toFloat() / 1000
+ ButtonType.BUTTON_Y.toString() + "-X$orientation",
+ getResourceValue(orientation, 6)
)
.putFloat(
- ButtonType.BUTTON_Y.toString() + "-Y",
- resources.getInteger(R.integer.SWITCH_BUTTON_Y_Y).toFloat() / 1000
+ ButtonType.BUTTON_Y.toString() + "-Y$orientation",
+ getResourceValue(orientation, 7)
)
.putFloat(
- ButtonType.TRIGGER_ZL.toString() + "-X",
- resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_X).toFloat() / 1000
+ ButtonType.TRIGGER_ZL.toString() + "-X$orientation",
+ getResourceValue(orientation, 8)
)
.putFloat(
- ButtonType.TRIGGER_ZL.toString() + "-Y",
- resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_Y).toFloat() / 1000
+ ButtonType.TRIGGER_ZL.toString() + "-Y$orientation",
+ getResourceValue(orientation, 9)
)
.putFloat(
- ButtonType.TRIGGER_ZR.toString() + "-X",
- resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_X).toFloat() / 1000
+ ButtonType.TRIGGER_ZR.toString() + "-X$orientation",
+ getResourceValue(orientation, 10)
)
.putFloat(
- ButtonType.TRIGGER_ZR.toString() + "-Y",
- resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_Y).toFloat() / 1000
+ ButtonType.TRIGGER_ZR.toString() + "-Y$orientation",
+ getResourceValue(orientation, 11)
)
.putFloat(
- ButtonType.DPAD_UP.toString() + "-X",
- resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_X).toFloat() / 1000
+ ButtonType.DPAD_UP.toString() + "-X$orientation",
+ getResourceValue(orientation, 12)
)
.putFloat(
- ButtonType.DPAD_UP.toString() + "-Y",
- resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_Y).toFloat() / 1000
+ ButtonType.DPAD_UP.toString() + "-Y$orientation",
+ getResourceValue(orientation, 13)
)
.putFloat(
- ButtonType.TRIGGER_L.toString() + "-X",
- resources.getInteger(R.integer.SWITCH_TRIGGER_L_X).toFloat() / 1000
+ ButtonType.TRIGGER_L.toString() + "-X$orientation",
+ getResourceValue(orientation, 14)
)
.putFloat(
- ButtonType.TRIGGER_L.toString() + "-Y",
- resources.getInteger(R.integer.SWITCH_TRIGGER_L_Y).toFloat() / 1000
+ ButtonType.TRIGGER_L.toString() + "-Y$orientation",
+ getResourceValue(orientation, 15)
)
.putFloat(
- ButtonType.TRIGGER_R.toString() + "-X",
- resources.getInteger(R.integer.SWITCH_TRIGGER_R_X).toFloat() / 1000
+ ButtonType.TRIGGER_R.toString() + "-X$orientation",
+ getResourceValue(orientation, 16)
)
.putFloat(
- ButtonType.TRIGGER_R.toString() + "-Y",
- resources.getInteger(R.integer.SWITCH_TRIGGER_R_Y).toFloat() / 1000
+ ButtonType.TRIGGER_R.toString() + "-Y$orientation",
+ getResourceValue(orientation, 17)
)
.putFloat(
- ButtonType.BUTTON_PLUS.toString() + "-X",
- resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_X).toFloat() / 1000
+ ButtonType.BUTTON_PLUS.toString() + "-X$orientation",
+ getResourceValue(orientation, 18)
)
.putFloat(
- ButtonType.BUTTON_PLUS.toString() + "-Y",
- resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_Y).toFloat() / 1000
+ ButtonType.BUTTON_PLUS.toString() + "-Y$orientation",
+ getResourceValue(orientation, 19)
)
.putFloat(
- ButtonType.BUTTON_MINUS.toString() + "-X",
- resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_X).toFloat() / 1000
+ ButtonType.BUTTON_MINUS.toString() + "-X$orientation",
+ getResourceValue(orientation, 20)
)
.putFloat(
- ButtonType.BUTTON_MINUS.toString() + "-Y",
- resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_Y).toFloat() / 1000
+ ButtonType.BUTTON_MINUS.toString() + "-Y$orientation",
+ getResourceValue(orientation, 21)
)
.putFloat(
- ButtonType.BUTTON_HOME.toString() + "-X",
- resources.getInteger(R.integer.SWITCH_BUTTON_HOME_X).toFloat() / 1000
+ ButtonType.BUTTON_HOME.toString() + "-X$orientation",
+ getResourceValue(orientation, 22)
)
.putFloat(
- ButtonType.BUTTON_HOME.toString() + "-Y",
- resources.getInteger(R.integer.SWITCH_BUTTON_HOME_Y).toFloat() / 1000
+ ButtonType.BUTTON_HOME.toString() + "-Y$orientation",
+ getResourceValue(orientation, 23)
)
.putFloat(
- ButtonType.BUTTON_CAPTURE.toString() + "-X",
- resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_X)
- .toFloat() / 1000
+ ButtonType.BUTTON_CAPTURE.toString() + "-X$orientation",
+ getResourceValue(orientation, 24)
)
.putFloat(
- ButtonType.BUTTON_CAPTURE.toString() + "-Y",
- resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_Y)
- .toFloat() / 1000
+ ButtonType.BUTTON_CAPTURE.toString() + "-Y$orientation",
+ getResourceValue(orientation, 25)
)
.putFloat(
- ButtonType.STICK_R.toString() + "-X",
- resources.getInteger(R.integer.SWITCH_STICK_R_X).toFloat() / 1000
+ ButtonType.STICK_R.toString() + "-X$orientation",
+ getResourceValue(orientation, 26)
)
.putFloat(
- ButtonType.STICK_R.toString() + "-Y",
- resources.getInteger(R.integer.SWITCH_STICK_R_Y).toFloat() / 1000
+ ButtonType.STICK_R.toString() + "-Y$orientation",
+ getResourceValue(orientation, 27)
)
.putFloat(
- ButtonType.STICK_L.toString() + "-X",
- resources.getInteger(R.integer.SWITCH_STICK_L_X).toFloat() / 1000
+ ButtonType.STICK_L.toString() + "-X$orientation",
+ getResourceValue(orientation, 28)
)
.putFloat(
- ButtonType.STICK_L.toString() + "-Y",
- resources.getInteger(R.integer.SWITCH_STICK_L_Y).toFloat() / 1000
+ ButtonType.STICK_L.toString() + "-Y$orientation",
+ getResourceValue(orientation, 29)
)
.apply()
}
@@ -709,6 +810,10 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
private val preferences: SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+ const val LANDSCAPE = ""
+ const val PORTRAIT = "_Portrait"
+ const val FOLDABLE = "_Foldable"
+
/**
* Resizes a [Bitmap] by a given scale factor
*
@@ -754,9 +859,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
*/
private fun getSafeScreenSize(context: Context): Pair<Point, Point> {
// Get screen size
- val windowMetrics =
- WindowMetricsCalculator.getOrCreate()
- .computeCurrentWindowMetrics(context as Activity)
+ val windowMetrics = WindowMetricsCalculator.getOrCreate()
+ .computeCurrentWindowMetrics(context as Activity)
var maxY = windowMetrics.bounds.height().toFloat()
var maxX = windowMetrics.bounds.width().toFloat()
var minY = 0
@@ -769,9 +873,9 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
val insets = context.windowManager.currentWindowMetrics.windowInsets.displayCutout
if (insets != null) {
if (insets.boundingRectTop.bottom != 0 && insets.boundingRectTop.bottom > maxY / 2)
- insets.boundingRectTop.bottom.toFloat() else maxY
+ maxY = insets.boundingRectTop.bottom.toFloat()
if (insets.boundingRectRight.left != 0 && insets.boundingRectRight.left > maxX / 2)
- insets.boundingRectRight.left.toFloat() else maxX
+ maxX = insets.boundingRectRight.left.toFloat()
minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left
minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom
@@ -878,8 +982,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
// The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
// These were set in the input overlay configuration menu.
- val xKey = "$buttonId$orientation-X"
- val yKey = "$buttonId$orientation-Y"
+ val xKey = "$buttonId-X$orientation"
+ val yKey = "$buttonId-Y$orientation"
val drawableXPercent = sPrefs.getFloat(xKey, 0f)
val drawableYPercent = sPrefs.getFloat(yKey, 0f)
val drawableX = (drawableXPercent * max.x + min.x).toInt()
@@ -959,8 +1063,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
// The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay.
// These were set in the input overlay configuration menu.
- val drawableXPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-X", 0f)
- val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-Y", 0f)
+ val drawableXPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}-X$orientation", 0f)
+ val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}-Y$orientation", 0f)
val drawableX = (drawableXPercent * max.x + min.x).toInt()
val drawableY = (drawableYPercent * max.y + min.y).toInt()
val width = overlayDrawable.width
@@ -1026,8 +1130,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
// The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
// These were set in the input overlay configuration menu.
- val drawableXPercent = sPrefs.getFloat("$button$orientation-X", 0f)
- val drawableYPercent = sPrefs.getFloat("$button$orientation-Y", 0f)
+ val drawableXPercent = sPrefs.getFloat("$button-X$orientation", 0f)
+ val drawableYPercent = sPrefs.getFloat("$button-Y$orientation", 0f)
val drawableX = (drawableXPercent * max.x + min.x).toInt()
val drawableY = (drawableYPercent * max.y + min.y).toInt()
val outerScale = 1.66f
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.kt
index e1e7a59d7..7e8f058c1 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.kt
@@ -11,14 +11,6 @@ object EmulationMenuSettings {
private val preferences =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
- // These must match what is defined in src/core/settings.h
- const val LayoutOption_Default = 0
- const val LayoutOption_SingleScreen = 1
- const val LayoutOption_LargeScreen = 2
- const val LayoutOption_SideScreen = 3
- const val LayoutOption_MobilePortrait = 4
- const val LayoutOption_MobileLandscape = 5
-
var joystickRelCenter: Boolean
get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, true)
set(value) {
@@ -41,16 +33,6 @@ object EmulationMenuSettings {
.apply()
}
- var landscapeScreenLayout: Int
- get() = preferences.getInt(
- Settings.PREF_MENU_SETTINGS_LANDSCAPE,
- LayoutOption_MobileLandscape
- )
- set(value) {
- preferences.edit()
- .putInt(Settings.PREF_MENU_SETTINGS_LANDSCAPE, value)
- .apply()
- }
var showFps: Boolean
get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, false)
set(value) {
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index 4091c23d1..f9617202b 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -202,6 +202,11 @@ public:
return m_is_running;
}
+ bool IsPaused() const {
+ std::scoped_lock lock(m_mutex);
+ return m_is_running && m_is_paused;
+ }
+
const Core::PerfStatsResults& PerfStats() const {
std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex);
return m_perf_stats;
@@ -287,11 +292,13 @@ public:
void PauseEmulation() {
std::scoped_lock lock(m_mutex);
m_system.Pause();
+ m_is_paused = true;
}
void UnPauseEmulation() {
std::scoped_lock lock(m_mutex);
m_system.Run();
+ m_is_paused = false;
}
void HaltEmulation() {
@@ -473,6 +480,7 @@ private:
std::shared_ptr<FileSys::VfsFilesystem> m_vfs;
Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
bool m_is_running{};
+ bool m_is_paused{};
SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{};
std::unique_ptr<Service::Account::ProfileManager> m_profile_manager;
@@ -583,6 +591,11 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isRunning([[maybe_unused]] JNIEnv
return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning());
}
+jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz) {
+ return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused());
+}
+
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jclass clazz) {
return EmulationSession::GetInstance().IsHandheldOnly();
diff --git a/src/android/app/src/main/res/drawable/ic_pip_pause.xml b/src/android/app/src/main/res/drawable/ic_pip_pause.xml
new file mode 100644
index 000000000..4a7d4ea03
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_pip_pause.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z" />
+</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_pip_play.xml b/src/android/app/src/main/res/drawable/ic_pip_play.xml
new file mode 100644
index 000000000..2303a4623
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_pip_play.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M8,5v14l11,-7z" />
+</vector>
diff --git a/src/android/app/src/main/res/layout/activity_emulation.xml b/src/android/app/src/main/res/layout/activity_emulation.xml
index f6360a65b..139065d3d 100644
--- a/src/android/app/src/main/res/layout/activity_emulation.xml
+++ b/src/android/app/src/main/res/layout/activity_emulation.xml
@@ -1,13 +1,9 @@
-<FrameLayout
+<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/frame_content"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/fragment_container"
+ android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:keepScreenOn="true">
-
- <FrameLayout
- android:id="@+id/frame_emulation_fragment"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
-
-</FrameLayout>
+ android:keepScreenOn="true"
+ app:defaultNavHost="true" />
diff --git a/src/android/app/src/main/res/layout/fragment_emulation.xml b/src/android/app/src/main/res/layout/fragment_emulation.xml
index 09b789b6b..e54a10e8f 100644
--- a/src/android/app/src/main/res/layout/fragment_emulation.xml
+++ b/src/android/app/src/main/res/layout/fragment_emulation.xml
@@ -12,49 +12,65 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <!-- This is what everything is rendered to during emulation -->
- <org.yuzu.yuzu_emu.views.FixedRatioSurfaceView
- android:id="@+id/surface_emulation"
+ <FrameLayout
+ android:id="@+id/emulation_container"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_gravity="center"
- android:focusable="false"
- android:focusableInTouchMode="false" />
+ android:layout_height="match_parent">
+
+ <!-- This is what everything is rendered to during emulation -->
+ <org.yuzu.yuzu_emu.views.FixedRatioSurfaceView
+ android:id="@+id/surface_emulation"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:focusable="false"
+ android:focusableInTouchMode="false" />
+
+ </FrameLayout>
<FrameLayout
- android:id="@+id/overlay_container"
+ android:id="@+id/input_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom">
- <!-- This is the onscreen input overlay -->
- <org.yuzu.yuzu_emu.overlay.InputOverlay
- android:id="@+id/surface_input_overlay"
+ <!-- This is the onscreen input overlay -->
+ <org.yuzu.yuzu_emu.overlay.InputOverlay
+ android:id="@+id/surface_input_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:focusable="true"
+ android:focusableInTouchMode="true" />
+
+ <Button
+ style="@style/Widget.Material3.Button.ElevatedButton"
+ android:id="@+id/done_control_config"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:text="@string/emulation_done"
+ android:visibility="gone" />
+
+ </FrameLayout>
+
+ <FrameLayout
+ android:id="@+id/overlay_container"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:focusable="true"
- android:focusableInTouchMode="true" />
+ android:layout_height="match_parent">
- <TextView
- android:id="@+id/show_fps_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="left"
- android:clickable="false"
- android:focusable="false"
- android:shadowColor="@android:color/black"
- android:textColor="@android:color/white"
- android:textSize="12sp"
- tools:ignore="RtlHardcoded" />
+ <TextView
+ android:id="@+id/show_fps_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left"
+ android:clickable="false"
+ android:focusable="false"
+ android:shadowColor="@android:color/black"
+ android:textColor="@android:color/white"
+ android:textSize="12sp"
+ tools:ignore="RtlHardcoded" />
- <Button
- style="@style/Widget.Material3.Button.ElevatedButton"
- android:id="@+id/done_control_config"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:text="@string/emulation_done"
- android:visibility="gone" />
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/src/android/app/src/main/res/navigation/emulation_navigation.xml b/src/android/app/src/main/res/navigation/emulation_navigation.xml
new file mode 100644
index 000000000..8208f4c2c
--- /dev/null
+++ b/src/android/app/src/main/res/navigation/emulation_navigation.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/emulation_navigation"
+ app:startDestination="@id/emulationFragment">
+
+ <fragment
+ android:id="@+id/emulationFragment"
+ android:name="org.yuzu.yuzu_emu.fragments.EmulationFragment"
+ android:label="fragment_emulation"
+ tools:layout="@layout/fragment_emulation" >
+ <argument
+ android:name="game"
+ app:argType="org.yuzu.yuzu_emu.model.Game" />
+ </fragment>
+
+</navigation>
diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml
index 48072683e..fcebba726 100644
--- a/src/android/app/src/main/res/navigation/home_navigation.xml
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -56,4 +56,18 @@
android:name="org.yuzu.yuzu_emu.fragments.LicensesFragment"
android:label="LicensesFragment" />
+ <activity
+ android:id="@+id/emulationActivity"
+ android:name="org.yuzu.yuzu_emu.activities.EmulationActivity"
+ android:label="EmulationActivity">
+ <argument
+ android:name="game"
+ app:argType="org.yuzu.yuzu_emu.model.Game" />
+ </activity>
+
+ <action
+ android:id="@+id/action_global_emulationActivity"
+ app:destination="@id/emulationActivity"
+ app:launchSingleTop="true" />
+
</navigation>
diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml
index ea20cb17c..7f7b1938c 100644
--- a/src/android/app/src/main/res/values/arrays.xml
+++ b/src/android/app/src/main/res/values/arrays.xml
@@ -119,6 +119,18 @@
<item>3</item>
</integer-array>
+ <string-array name="rendererScreenLayoutNames">
+ <item>@string/screen_layout_landscape</item>
+ <item>@string/screen_layout_portrait</item>
+ <item>@string/screen_layout_auto</item>
+ </string-array>
+
+ <integer-array name="rendererScreenLayoutValues">
+ <item>5</item>
+ <item>4</item>
+ <item>0</item>
+ </integer-array>
+
<string-array name="rendererAspectRatioNames">
<item>@string/ratio_default</item>
<item>@string/ratio_force_four_three</item>
diff --git a/src/android/app/src/main/res/values/integers.xml b/src/android/app/src/main/res/values/integers.xml
index bc614b81d..2e93b408c 100644
--- a/src/android/app/src/main/res/values/integers.xml
+++ b/src/android/app/src/main/res/values/integers.xml
@@ -34,4 +34,68 @@
<integer name="SWITCH_BUTTON_DPAD_X">260</integer>
<integer name="SWITCH_BUTTON_DPAD_Y">790</integer>
+ <!-- Default SWITCH portrait layout -->
+ <integer name="SWITCH_BUTTON_A_X_PORTRAIT">840</integer>
+ <integer name="SWITCH_BUTTON_A_Y_PORTRAIT">840</integer>
+ <integer name="SWITCH_BUTTON_B_X_PORTRAIT">740</integer>
+ <integer name="SWITCH_BUTTON_B_Y_PORTRAIT">880</integer>
+ <integer name="SWITCH_BUTTON_X_X_PORTRAIT">740</integer>
+ <integer name="SWITCH_BUTTON_X_Y_PORTRAIT">800</integer>
+ <integer name="SWITCH_BUTTON_Y_X_PORTRAIT">640</integer>
+ <integer name="SWITCH_BUTTON_Y_Y_PORTRAIT">840</integer>
+ <integer name="SWITCH_STICK_L_X_PORTRAIT">180</integer>
+ <integer name="SWITCH_STICK_L_Y_PORTRAIT">660</integer>
+ <integer name="SWITCH_STICK_R_X_PORTRAIT">820</integer>
+ <integer name="SWITCH_STICK_R_Y_PORTRAIT">660</integer>
+ <integer name="SWITCH_TRIGGER_L_X_PORTRAIT">140</integer>
+ <integer name="SWITCH_TRIGGER_L_Y_PORTRAIT">260</integer>
+ <integer name="SWITCH_TRIGGER_R_X_PORTRAIT">860</integer>
+ <integer name="SWITCH_TRIGGER_R_Y_PORTRAIT">260</integer>
+ <integer name="SWITCH_TRIGGER_ZL_X_PORTRAIT">140</integer>
+ <integer name="SWITCH_TRIGGER_ZL_Y_PORTRAIT">200</integer>
+ <integer name="SWITCH_TRIGGER_ZR_X_PORTRAIT">860</integer>
+ <integer name="SWITCH_TRIGGER_ZR_Y_PORTRAIT">200</integer>
+ <integer name="SWITCH_BUTTON_MINUS_X_PORTRAIT">440</integer>
+ <integer name="SWITCH_BUTTON_MINUS_Y_PORTRAIT">950</integer>
+ <integer name="SWITCH_BUTTON_PLUS_X_PORTRAIT">560</integer>
+ <integer name="SWITCH_BUTTON_PLUS_Y_PORTRAIT">950</integer>
+ <integer name="SWITCH_BUTTON_HOME_X_PORTRAIT">680</integer>
+ <integer name="SWITCH_BUTTON_HOME_Y_PORTRAIT">950</integer>
+ <integer name="SWITCH_BUTTON_CAPTURE_X_PORTRAIT">320</integer>
+ <integer name="SWITCH_BUTTON_CAPTURE_Y_PORTRAIT">950</integer>
+ <integer name="SWITCH_BUTTON_DPAD_X_PORTRAIT">240</integer>
+ <integer name="SWITCH_BUTTON_DPAD_Y_PORTRAIT">840</integer>
+
+ <!-- Default SWITCH foldable layout -->
+ <integer name="SWITCH_BUTTON_A_X_FOLDABLE">840</integer>
+ <integer name="SWITCH_BUTTON_A_Y_FOLDABLE">390</integer>
+ <integer name="SWITCH_BUTTON_B_X_FOLDABLE">740</integer>
+ <integer name="SWITCH_BUTTON_B_Y_FOLDABLE">430</integer>
+ <integer name="SWITCH_BUTTON_X_X_FOLDABLE">740</integer>
+ <integer name="SWITCH_BUTTON_X_Y_FOLDABLE">350</integer>
+ <integer name="SWITCH_BUTTON_Y_X_FOLDABLE">640</integer>
+ <integer name="SWITCH_BUTTON_Y_Y_FOLDABLE">390</integer>
+ <integer name="SWITCH_STICK_L_X_FOLDABLE">180</integer>
+ <integer name="SWITCH_STICK_L_Y_FOLDABLE">250</integer>
+ <integer name="SWITCH_STICK_R_X_FOLDABLE">820</integer>
+ <integer name="SWITCH_STICK_R_Y_FOLDABLE">250</integer>
+ <integer name="SWITCH_TRIGGER_L_X_FOLDABLE">140</integer>
+ <integer name="SWITCH_TRIGGER_L_Y_FOLDABLE">130</integer>
+ <integer name="SWITCH_TRIGGER_R_X_FOLDABLE">860</integer>
+ <integer name="SWITCH_TRIGGER_R_Y_FOLDABLE">130</integer>
+ <integer name="SWITCH_TRIGGER_ZL_X_FOLDABLE">140</integer>
+ <integer name="SWITCH_TRIGGER_ZL_Y_FOLDABLE">70</integer>
+ <integer name="SWITCH_TRIGGER_ZR_X_FOLDABLE">860</integer>
+ <integer name="SWITCH_TRIGGER_ZR_Y_FOLDABLE">70</integer>
+ <integer name="SWITCH_BUTTON_MINUS_X_FOLDABLE">440</integer>
+ <integer name="SWITCH_BUTTON_MINUS_Y_FOLDABLE">470</integer>
+ <integer name="SWITCH_BUTTON_PLUS_X_FOLDABLE">560</integer>
+ <integer name="SWITCH_BUTTON_PLUS_Y_FOLDABLE">470</integer>
+ <integer name="SWITCH_BUTTON_HOME_X_FOLDABLE">680</integer>
+ <integer name="SWITCH_BUTTON_HOME_Y_FOLDABLE">470</integer>
+ <integer name="SWITCH_BUTTON_CAPTURE_X_FOLDABLE">320</integer>
+ <integer name="SWITCH_BUTTON_CAPTURE_Y_FOLDABLE">470</integer>
+ <integer name="SWITCH_BUTTON_DPAD_X_FOLDABLE">240</integer>
+ <integer name="SWITCH_BUTTON_DPAD_Y_FOLDABLE">390</integer>
+
</resources>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index c236811fa..b5bc249d4 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -162,6 +162,7 @@
<string name="renderer_accuracy">Accuracy level</string>
<string name="renderer_resolution">Resolution (Handheld/Docked)</string>
<string name="renderer_vsync">VSync mode</string>
+ <string name="renderer_screen_layout">Orientation</string>
<string name="renderer_aspect_ratio">Aspect ratio</string>
<string name="renderer_scaling_filter">Window adapting filter</string>
<string name="renderer_anti_aliasing">Anti-aliasing method</string>
@@ -326,6 +327,11 @@
<string name="anti_aliasing_fxaa">FXAA</string>
<string name="anti_aliasing_smaa">SMAA</string>
+ <!-- Screen Layouts -->
+ <string name="screen_layout_landscape">Landscape</string>
+ <string name="screen_layout_portrait">Portrait</string>
+ <string name="screen_layout_auto">Auto</string>
+
<!-- Aspect Ratios -->
<string name="ratio_default">Default (16:9)</string>
<string name="ratio_force_four_three">Force 4:3</string>
@@ -364,6 +370,12 @@
<string name="use_black_backgrounds">Black backgrounds</string>
<string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string>
+ <!-- Picture-In-Picture -->
+ <string name="picture_in_picture">Picture in Picture</string>
+ <string name="picture_in_picture_description">Minimize window when placed in the background</string>
+ <string name="pause">Pause</string>
+ <string name="play">Play</string>
+
<!-- Licenses screen strings -->
<string name="licenses">Licenses</string>
<string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string>
diff --git a/src/android/build.gradle.kts b/src/android/build.gradle.kts
index e19e8ce58..80f370c16 100644
--- a/src/android/build.gradle.kts
+++ b/src/android/build.gradle.kts
@@ -11,3 +11,12 @@ plugins {
tasks.register("clean").configure {
delete(rootProject.buildDir)
}
+
+buildscript {
+ repositories {
+ google()
+ }
+ dependencies {
+ classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.6.0")
+ }
+}
diff --git a/src/core/file_sys/vfs_real.cpp b/src/core/file_sys/vfs_real.cpp
index cc0076238..7a15d8438 100644
--- a/src/core/file_sys/vfs_real.cpp
+++ b/src/core/file_sys/vfs_real.cpp
@@ -25,6 +25,8 @@ namespace FS = Common::FS;
namespace {
+constexpr size_t MaxOpenFiles = 512;
+
constexpr FS::FileAccessMode ModeFlagsToFileAccessMode(Mode mode) {
switch (mode) {
case Mode::Read:
@@ -73,28 +75,30 @@ VfsEntryType RealVfsFilesystem::GetEntryType(std::string_view path_) const {
VirtualFile RealVfsFilesystem::OpenFile(std::string_view path_, Mode perms) {
const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault);
- if (const auto weak_iter = cache.find(path); weak_iter != cache.cend()) {
- const auto& weak = weak_iter->second;
-
- if (!weak.expired()) {
- return std::shared_ptr<RealVfsFile>(new RealVfsFile(*this, weak.lock(), path, perms));
+ if (auto it = cache.find(path); it != cache.end()) {
+ if (auto file = it->second.lock(); file) {
+ return file;
}
}
- auto backing = FS::FileOpen(path, ModeFlagsToFileAccessMode(perms), FS::FileType::BinaryFile);
-
- if (!backing) {
+ if (!FS::Exists(path) || !FS::IsFile(path)) {
return nullptr;
}
- cache.insert_or_assign(path, std::move(backing));
+ auto reference = std::make_unique<FileReference>();
+ this->InsertReferenceIntoList(*reference);
- // Cannot use make_shared as RealVfsFile constructor is private
- return std::shared_ptr<RealVfsFile>(new RealVfsFile(*this, backing, path, perms));
+ auto file =
+ std::shared_ptr<RealVfsFile>(new RealVfsFile(*this, std::move(reference), path, perms));
+ cache[path] = file;
+
+ return file;
}
VirtualFile RealVfsFilesystem::CreateFile(std::string_view path_, Mode perms) {
const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault);
+ cache.erase(path);
+
// Current usages of CreateFile expect to delete the contents of an existing file.
if (FS::IsFile(path)) {
FS::IOFile temp{path, FS::FileAccessMode::Write, FS::FileType::BinaryFile};
@@ -123,51 +127,22 @@ VirtualFile RealVfsFilesystem::CopyFile(std::string_view old_path_, std::string_
VirtualFile RealVfsFilesystem::MoveFile(std::string_view old_path_, std::string_view new_path_) {
const auto old_path = FS::SanitizePath(old_path_, FS::DirectorySeparator::PlatformDefault);
const auto new_path = FS::SanitizePath(new_path_, FS::DirectorySeparator::PlatformDefault);
- const auto cached_file_iter = cache.find(old_path);
-
- if (cached_file_iter != cache.cend()) {
- auto file = cached_file_iter->second.lock();
-
- if (!cached_file_iter->second.expired()) {
- file->Close();
- }
-
- if (!FS::RenameFile(old_path, new_path)) {
- return nullptr;
- }
-
- cache.erase(old_path);
- file->Open(new_path, FS::FileAccessMode::Read, FS::FileType::BinaryFile);
- if (file->IsOpen()) {
- cache.insert_or_assign(new_path, std::move(file));
- } else {
- LOG_ERROR(Service_FS, "Failed to open path {} in order to re-cache it", new_path);
- }
- } else {
- ASSERT(false);
+ cache.erase(old_path);
+ cache.erase(new_path);
+ if (!FS::RenameFile(old_path, new_path)) {
return nullptr;
}
-
return OpenFile(new_path, Mode::ReadWrite);
}
bool RealVfsFilesystem::DeleteFile(std::string_view path_) {
const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault);
- const auto cached_iter = cache.find(path);
-
- if (cached_iter != cache.cend()) {
- if (!cached_iter->second.expired()) {
- cached_iter->second.lock()->Close();
- }
- cache.erase(path);
- }
-
+ cache.erase(path);
return FS::RemoveFile(path);
}
VirtualDir RealVfsFilesystem::OpenDirectory(std::string_view path_, Mode perms) {
const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault);
- // Cannot use make_shared as RealVfsDirectory constructor is private
return std::shared_ptr<RealVfsDirectory>(new RealVfsDirectory(*this, path, perms));
}
@@ -176,7 +151,6 @@ VirtualDir RealVfsFilesystem::CreateDirectory(std::string_view path_, Mode perms
if (!FS::CreateDirs(path)) {
return nullptr;
}
- // Cannot use make_shared as RealVfsDirectory constructor is private
return std::shared_ptr<RealVfsDirectory>(new RealVfsDirectory(*this, path, perms));
}
@@ -194,73 +168,102 @@ VirtualDir RealVfsFilesystem::MoveDirectory(std::string_view old_path_,
if (!FS::RenameDir(old_path, new_path)) {
return nullptr;
}
+ return OpenDirectory(new_path, Mode::ReadWrite);
+}
- for (auto& kv : cache) {
- // If the path in the cache doesn't start with old_path, then bail on this file.
- if (kv.first.rfind(old_path, 0) != 0) {
- continue;
- }
+bool RealVfsFilesystem::DeleteDirectory(std::string_view path_) {
+ const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault);
+ return FS::RemoveDirRecursively(path);
+}
- const auto file_old_path =
- FS::SanitizePath(kv.first, FS::DirectorySeparator::PlatformDefault);
- auto file_new_path = FS::SanitizePath(new_path + '/' + kv.first.substr(old_path.size()),
- FS::DirectorySeparator::PlatformDefault);
- const auto& cached = cache[file_old_path];
+void RealVfsFilesystem::RefreshReference(const std::string& path, Mode perms,
+ FileReference& reference) {
+ // Temporarily remove from list.
+ this->RemoveReferenceFromList(reference);
- if (cached.expired()) {
- continue;
- }
+ // Restore file if needed.
+ if (!reference.file) {
+ this->EvictSingleReference();
- auto file = cached.lock();
- cache.erase(file_old_path);
- file->Open(file_new_path, FS::FileAccessMode::Read, FS::FileType::BinaryFile);
- if (file->IsOpen()) {
- cache.insert_or_assign(std::move(file_new_path), std::move(file));
- } else {
- LOG_ERROR(Service_FS, "Failed to open path {} in order to re-cache it", file_new_path);
+ reference.file =
+ FS::FileOpen(path, ModeFlagsToFileAccessMode(perms), FS::FileType::BinaryFile);
+ if (reference.file) {
+ num_open_files++;
}
}
- return OpenDirectory(new_path, Mode::ReadWrite);
+ // Reinsert into list.
+ this->InsertReferenceIntoList(reference);
}
-bool RealVfsFilesystem::DeleteDirectory(std::string_view path_) {
- const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault);
+void RealVfsFilesystem::DropReference(std::unique_ptr<FileReference>&& reference) {
+ // Remove from list.
+ this->RemoveReferenceFromList(*reference);
- for (auto& kv : cache) {
- // If the path in the cache doesn't start with path, then bail on this file.
- if (kv.first.rfind(path, 0) != 0) {
- continue;
- }
+ // Close the file.
+ if (reference->file) {
+ reference->file.reset();
+ num_open_files--;
+ }
+}
- const auto& entry = cache[kv.first];
- if (!entry.expired()) {
- entry.lock()->Close();
- }
+void RealVfsFilesystem::EvictSingleReference() {
+ if (num_open_files < MaxOpenFiles || open_references.empty()) {
+ return;
+ }
+
+ // Get and remove from list.
+ auto& reference = open_references.back();
+ this->RemoveReferenceFromList(reference);
- cache.erase(kv.first);
+ // Close the file.
+ if (reference.file) {
+ reference.file.reset();
+ num_open_files--;
}
- return FS::RemoveDirRecursively(path);
+ // Reinsert into closed list.
+ this->InsertReferenceIntoList(reference);
+}
+
+void RealVfsFilesystem::InsertReferenceIntoList(FileReference& reference) {
+ if (reference.file) {
+ open_references.push_front(reference);
+ } else {
+ closed_references.push_front(reference);
+ }
+}
+
+void RealVfsFilesystem::RemoveReferenceFromList(FileReference& reference) {
+ if (reference.file) {
+ open_references.erase(open_references.iterator_to(reference));
+ } else {
+ closed_references.erase(closed_references.iterator_to(reference));
+ }
}
-RealVfsFile::RealVfsFile(RealVfsFilesystem& base_, std::shared_ptr<FS::IOFile> backing_,
+RealVfsFile::RealVfsFile(RealVfsFilesystem& base_, std::unique_ptr<FileReference> reference_,
const std::string& path_, Mode perms_)
- : base(base_), backing(std::move(backing_)), path(path_), parent_path(FS::GetParentPath(path_)),
- path_components(FS::SplitPathComponents(path_)), perms(perms_) {}
+ : base(base_), reference(std::move(reference_)), path(path_),
+ parent_path(FS::GetParentPath(path_)), path_components(FS::SplitPathComponents(path_)),
+ perms(perms_) {}
-RealVfsFile::~RealVfsFile() = default;
+RealVfsFile::~RealVfsFile() {
+ base.DropReference(std::move(reference));
+}
std::string RealVfsFile::GetName() const {
return path_components.back();
}
std::size_t RealVfsFile::GetSize() const {
- return backing->GetSize();
+ base.RefreshReference(path, perms, *reference);
+ return reference->file ? reference->file->GetSize() : 0;
}
bool RealVfsFile::Resize(std::size_t new_size) {
- return backing->SetSize(new_size);
+ base.RefreshReference(path, perms, *reference);
+ return reference->file ? reference->file->SetSize(new_size) : false;
}
VirtualDir RealVfsFile::GetContainingDirectory() const {
@@ -276,27 +279,25 @@ bool RealVfsFile::IsReadable() const {
}
std::size_t RealVfsFile::Read(u8* data, std::size_t length, std::size_t offset) const {
- if (!backing->Seek(static_cast<s64>(offset))) {
+ base.RefreshReference(path, perms, *reference);
+ if (!reference->file || !reference->file->Seek(static_cast<s64>(offset))) {
return 0;
}
- return backing->ReadSpan(std::span{data, length});
+ return reference->file->ReadSpan(std::span{data, length});
}
std::size_t RealVfsFile::Write(const u8* data, std::size_t length, std::size_t offset) {
- if (!backing->Seek(static_cast<s64>(offset))) {
+ base.RefreshReference(path, perms, *reference);
+ if (!reference->file || !reference->file->Seek(static_cast<s64>(offset))) {
return 0;
}
- return backing->WriteSpan(std::span{data, length});
+ return reference->file->WriteSpan(std::span{data, length});
}
bool RealVfsFile::Rename(std::string_view name) {
return base.MoveFile(path, parent_path + '/' + std::string(name)) != nullptr;
}
-void RealVfsFile::Close() {
- backing->Close();
-}
-
// TODO(DarkLordZach): MSVC would not let me combine the following two functions using 'if
// constexpr' because there is a compile error in the branch not used.
diff --git a/src/core/file_sys/vfs_real.h b/src/core/file_sys/vfs_real.h
index b92c84316..d8c900e33 100644
--- a/src/core/file_sys/vfs_real.h
+++ b/src/core/file_sys/vfs_real.h
@@ -3,8 +3,9 @@
#pragma once
+#include <map>
#include <string_view>
-#include <boost/container/flat_map.hpp>
+#include "common/intrusive_list.h"
#include "core/file_sys/mode.h"
#include "core/file_sys/vfs.h"
@@ -14,6 +15,11 @@ class IOFile;
namespace FileSys {
+struct FileReference : public Common::IntrusiveListBaseNode<FileReference> {
+ std::shared_ptr<Common::FS::IOFile> file{};
+};
+
+class RealVfsFile;
class RealVfsFilesystem : public VfsFilesystem {
public:
RealVfsFilesystem();
@@ -35,7 +41,21 @@ public:
bool DeleteDirectory(std::string_view path) override;
private:
- boost::container::flat_map<std::string, std::weak_ptr<Common::FS::IOFile>> cache;
+ using ReferenceListType = Common::IntrusiveListBaseTraits<FileReference>::ListType;
+ std::map<std::string, std::weak_ptr<VfsFile>, std::less<>> cache;
+ ReferenceListType open_references;
+ ReferenceListType closed_references;
+ size_t num_open_files{};
+
+private:
+ friend class RealVfsFile;
+ void RefreshReference(const std::string& path, Mode perms, FileReference& reference);
+ void DropReference(std::unique_ptr<FileReference>&& reference);
+ void EvictSingleReference();
+
+private:
+ void InsertReferenceIntoList(FileReference& reference);
+ void RemoveReferenceFromList(FileReference& reference);
};
// An implementation of VfsFile that represents a file on the user's computer.
@@ -57,13 +77,11 @@ public:
bool Rename(std::string_view name) override;
private:
- RealVfsFile(RealVfsFilesystem& base, std::shared_ptr<Common::FS::IOFile> backing,
+ RealVfsFile(RealVfsFilesystem& base, std::unique_ptr<FileReference> reference,
const std::string& path, Mode perms = Mode::Read);
- void Close();
-
RealVfsFilesystem& base;
- std::shared_ptr<Common::FS::IOFile> backing;
+ std::unique_ptr<FileReference> reference;
std::string path;
std::string parent_path;
std::vector<std::string> path_components;
diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h
index 251a4a880..9bafd8cc0 100644
--- a/src/video_core/buffer_cache/buffer_cache.h
+++ b/src/video_core/buffer_cache/buffer_cache.h
@@ -715,7 +715,7 @@ void BufferCache<P>::BindHostIndexBuffer() {
template <class P>
void BufferCache<P>::BindHostVertexBuffers() {
- HostBindings host_bindings;
+ HostBindings<typename P::Buffer> host_bindings;
bool any_valid{false};
auto& flags = maxwell3d->dirty.flags;
for (u32 index = 0; index < NUM_VERTEX_BUFFERS; ++index) {
@@ -741,7 +741,7 @@ void BufferCache<P>::BindHostVertexBuffers() {
const u32 stride = maxwell3d->regs.vertex_streams[index].stride;
const u32 offset = buffer.Offset(binding.cpu_addr);
- host_bindings.buffers.push_back(reinterpret_cast<void*>(&buffer));
+ host_bindings.buffers.push_back(&buffer);
host_bindings.offsets.push_back(offset);
host_bindings.sizes.push_back(binding.size);
host_bindings.strides.push_back(stride);
@@ -900,7 +900,7 @@ void BufferCache<P>::BindHostTransformFeedbackBuffers() {
if (maxwell3d->regs.transform_feedback_enabled == 0) {
return;
}
- HostBindings host_bindings;
+ HostBindings<typename P::Buffer> host_bindings;
for (u32 index = 0; index < NUM_TRANSFORM_FEEDBACK_BUFFERS; ++index) {
const Binding& binding = channel_state->transform_feedback_buffers[index];
if (maxwell3d->regs.transform_feedback.controls[index].varying_count == 0 &&
@@ -913,7 +913,7 @@ void BufferCache<P>::BindHostTransformFeedbackBuffers() {
SynchronizeBuffer(buffer, binding.cpu_addr, size);
const u32 offset = buffer.Offset(binding.cpu_addr);
- host_bindings.buffers.push_back(reinterpret_cast<void*>(&buffer));
+ host_bindings.buffers.push_back(&buffer);
host_bindings.offsets.push_back(offset);
host_bindings.sizes.push_back(binding.size);
}
diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h
index cf359e241..63a120f7a 100644
--- a/src/video_core/buffer_cache/buffer_cache_base.h
+++ b/src/video_core/buffer_cache/buffer_cache_base.h
@@ -105,8 +105,9 @@ static constexpr Binding NULL_BINDING{
.buffer_id = NULL_BUFFER_ID,
};
+template <typename Buffer>
struct HostBindings {
- boost::container::small_vector<void*, NUM_VERTEX_BUFFERS> buffers;
+ boost::container::small_vector<Buffer*, NUM_VERTEX_BUFFERS> buffers;
boost::container::small_vector<u64, NUM_VERTEX_BUFFERS> offsets;
boost::container::small_vector<u64, NUM_VERTEX_BUFFERS> sizes;
boost::container::small_vector<u64, NUM_VERTEX_BUFFERS> strides;
diff --git a/src/video_core/renderer_opengl/gl_buffer_cache.cpp b/src/video_core/renderer_opengl/gl_buffer_cache.cpp
index 0cc546a3a..38d553d3c 100644
--- a/src/video_core/renderer_opengl/gl_buffer_cache.cpp
+++ b/src/video_core/renderer_opengl/gl_buffer_cache.cpp
@@ -232,12 +232,12 @@ void BufferCacheRuntime::BindVertexBuffer(u32 index, Buffer& buffer, u32 offset,
}
}
-void BufferCacheRuntime::BindVertexBuffers(VideoCommon::HostBindings& bindings) {
- for (u32 index = 0; index < bindings.buffers.size(); index++) {
- BindVertexBuffer(
- bindings.min_index + index, *reinterpret_cast<Buffer*>(bindings.buffers[index]),
- static_cast<u32>(bindings.offsets[index]), static_cast<u32>(bindings.sizes[index]),
- static_cast<u32>(bindings.strides[index]));
+void BufferCacheRuntime::BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bindings) {
+ for (u32 index = 0; index < bindings.buffers.size(); ++index) {
+ BindVertexBuffer(bindings.min_index + index, *bindings.buffers[index],
+ static_cast<u32>(bindings.offsets[index]),
+ static_cast<u32>(bindings.sizes[index]),
+ static_cast<u32>(bindings.strides[index]));
}
}
@@ -329,10 +329,9 @@ void BufferCacheRuntime::BindTransformFeedbackBuffer(u32 index, Buffer& buffer,
static_cast<GLintptr>(offset), static_cast<GLsizeiptr>(size));
}
-void BufferCacheRuntime::BindTransformFeedbackBuffers(VideoCommon::HostBindings& bindings) {
- for (u32 index = 0; index < bindings.buffers.size(); index++) {
- glBindBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, index,
- reinterpret_cast<Buffer*>(bindings.buffers[index])->Handle(),
+void BufferCacheRuntime::BindTransformFeedbackBuffers(VideoCommon::HostBindings<Buffer>& bindings) {
+ for (u32 index = 0; index < bindings.buffers.size(); ++index) {
+ glBindBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, index, bindings.buffers[index]->Handle(),
static_cast<GLintptr>(bindings.offsets[index]),
static_cast<GLsizeiptr>(bindings.sizes[index]));
}
diff --git a/src/video_core/renderer_opengl/gl_buffer_cache.h b/src/video_core/renderer_opengl/gl_buffer_cache.h
index e4e000284..41b746f3b 100644
--- a/src/video_core/renderer_opengl/gl_buffer_cache.h
+++ b/src/video_core/renderer_opengl/gl_buffer_cache.h
@@ -87,7 +87,8 @@ public:
void BindIndexBuffer(Buffer& buffer, u32 offset, u32 size);
void BindVertexBuffer(u32 index, Buffer& buffer, u32 offset, u32 size, u32 stride);
- void BindVertexBuffers(VideoCommon::HostBindings& bindings);
+
+ void BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bindings);
void BindUniformBuffer(size_t stage, u32 binding_index, Buffer& buffer, u32 offset, u32 size);
@@ -100,7 +101,8 @@ public:
bool is_written);
void BindTransformFeedbackBuffer(u32 index, Buffer& buffer, u32 offset, u32 size);
- void BindTransformFeedbackBuffers(VideoCommon::HostBindings& bindings);
+
+ void BindTransformFeedbackBuffers(VideoCommon::HostBindings<Buffer>& bindings);
void BindTextureBuffer(Buffer& buffer, u32 offset, u32 size,
VideoCore::Surface::PixelFormat format);
diff --git a/src/video_core/renderer_vulkan/vk_buffer_cache.cpp b/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
index d72d99899..8c33722d3 100644
--- a/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
@@ -501,11 +501,10 @@ void BufferCacheRuntime::BindVertexBuffer(u32 index, VkBuffer buffer, u32 offset
}
}
-void BufferCacheRuntime::BindVertexBuffers(VideoCommon::HostBindings& bindings) {
+void BufferCacheRuntime::BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bindings) {
boost::container::small_vector<VkBuffer, 32> buffer_handles;
- for (u32 index = 0; index < bindings.buffers.size(); index++) {
- auto& buffer = *reinterpret_cast<Buffer*>(bindings.buffers[index]);
- auto handle = buffer.Handle();
+ for (u32 index = 0; index < bindings.buffers.size(); ++index) {
+ auto handle = bindings.buffers[index]->Handle();
if (handle == VK_NULL_HANDLE) {
bindings.offsets[index] = 0;
bindings.sizes[index] = VK_WHOLE_SIZE;
@@ -521,16 +520,13 @@ void BufferCacheRuntime::BindVertexBuffers(VideoCommon::HostBindings& bindings)
buffer_handles = buffer_handles](vk::CommandBuffer cmdbuf) {
cmdbuf.BindVertexBuffers2EXT(
bindings.min_index, bindings.max_index - bindings.min_index, buffer_handles.data(),
- reinterpret_cast<const VkDeviceSize*>(bindings.offsets.data()),
- reinterpret_cast<const VkDeviceSize*>(bindings.sizes.data()),
- reinterpret_cast<const VkDeviceSize*>(bindings.strides.data()));
+ bindings.offsets.data(), bindings.sizes.data(), bindings.strides.data());
});
} else {
scheduler.Record([bindings = bindings,
buffer_handles = buffer_handles](vk::CommandBuffer cmdbuf) {
- cmdbuf.BindVertexBuffers(
- bindings.min_index, bindings.max_index - bindings.min_index, buffer_handles.data(),
- reinterpret_cast<const VkDeviceSize*>(bindings.offsets.data()));
+ cmdbuf.BindVertexBuffers(bindings.min_index, bindings.max_index - bindings.min_index,
+ buffer_handles.data(), bindings.offsets.data());
});
}
}
@@ -556,22 +552,20 @@ void BufferCacheRuntime::BindTransformFeedbackBuffer(u32 index, VkBuffer buffer,
});
}
-void BufferCacheRuntime::BindTransformFeedbackBuffers(VideoCommon::HostBindings& bindings) {
+void BufferCacheRuntime::BindTransformFeedbackBuffers(VideoCommon::HostBindings<Buffer>& bindings) {
if (!device.IsExtTransformFeedbackSupported()) {
// Already logged in the rasterizer
return;
}
boost::container::small_vector<VkBuffer, 4> buffer_handles;
- for (u32 index = 0; index < bindings.buffers.size(); index++) {
- auto& buffer = *reinterpret_cast<Buffer*>(bindings.buffers[index]);
- buffer_handles.push_back(buffer.Handle());
+ for (u32 index = 0; index < bindings.buffers.size(); ++index) {
+ buffer_handles.push_back(bindings.buffers[index]->Handle());
}
scheduler.Record(
[bindings = bindings, buffer_handles = buffer_handles](vk::CommandBuffer cmdbuf) {
- cmdbuf.BindTransformFeedbackBuffersEXT(
- 0, static_cast<u32>(buffer_handles.size()), buffer_handles.data(),
- reinterpret_cast<const VkDeviceSize*>(bindings.offsets.data()),
- reinterpret_cast<const VkDeviceSize*>(bindings.sizes.data()));
+ cmdbuf.BindTransformFeedbackBuffersEXT(0, static_cast<u32>(buffer_handles.size()),
+ buffer_handles.data(), bindings.offsets.data(),
+ bindings.sizes.data());
});
}
diff --git a/src/video_core/renderer_vulkan/vk_buffer_cache.h b/src/video_core/renderer_vulkan/vk_buffer_cache.h
index 92d3e9f32..cdeef8846 100644
--- a/src/video_core/renderer_vulkan/vk_buffer_cache.h
+++ b/src/video_core/renderer_vulkan/vk_buffer_cache.h
@@ -97,10 +97,12 @@ public:
void BindQuadIndexBuffer(PrimitiveTopology topology, u32 first, u32 count);
void BindVertexBuffer(u32 index, VkBuffer buffer, u32 offset, u32 size, u32 stride);
- void BindVertexBuffers(VideoCommon::HostBindings& bindings);
+
+ void BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bindings);
void BindTransformFeedbackBuffer(u32 index, VkBuffer buffer, u32 offset, u32 size);
- void BindTransformFeedbackBuffers(VideoCommon::HostBindings& bindings);
+
+ void BindTransformFeedbackBuffers(VideoCommon::HostBindings<Buffer>& bindings);
std::span<u8> BindMappedUniformBuffer([[maybe_unused]] size_t stage,
[[maybe_unused]] u32 binding_index, u32 size) {
diff --git a/vcpkg.json b/vcpkg.json
index 26f545c6c..2fa2c80be 100644
--- a/vcpkg.json
+++ b/vcpkg.json
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
"name": "yuzu",
- "builtin-baseline": "656fcc6ab2b05c6d999b7eaca717027ac3738f71",
+ "builtin-baseline": "a487471068f4cb1cbb4eeb340763cdcc0a75fd68",
"version": "1.0",
"dependencies": [
"boost-algorithm",