summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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
26# OSX global filetypes 26# OSX global filetypes
27# Created by Finder or Spotlight in directories for various OS functionality (indexing, etc) 27# Created by Finder or Spotlight in directories for various OS functionality (indexing, etc)
28.DS_Store 28.DS_Store
29.DS_Store?
30._*
29.AppleDouble 31.AppleDouble
30.LSOverride 32.LSOverride
31.Spotlight-V100 33.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 {
9 id("org.jetbrains.kotlin.android") 9 id("org.jetbrains.kotlin.android")
10 id("kotlin-parcelize") 10 id("kotlin-parcelize")
11 kotlin("plugin.serialization") version "1.8.21" 11 kotlin("plugin.serialization") version "1.8.21"
12 id("androidx.navigation.safeargs.kotlin")
12} 13}
13 14
14/** 15/**
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
53 <activity 53 <activity
54 android:name="org.yuzu.yuzu_emu.activities.EmulationActivity" 54 android:name="org.yuzu.yuzu_emu.activities.EmulationActivity"
55 android:theme="@style/Theme.Yuzu.Main" 55 android:theme="@style/Theme.Yuzu.Main"
56 android:launchMode="singleTop" 56 android:supportsPictureInPicture="true"
57 android:screenOrientation="userLandscape" 57 android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
58 android:exported="true"> 58 android:exported="true">
59 59
60 <intent-filter> 60 <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 {
283 external fun isRunning(): Boolean 283 external fun isRunning(): Boolean
284 284
285 /** 285 /**
286 * Returns true if emulation is paused.
287 */
288 external fun isPaused(): Boolean
289
290 /**
286 * Returns the performance stats for the current game 291 * Returns the performance stats for the current game
287 */ 292 */
288 external fun getPerfStats(): DoubleArray 293 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 @@
4package org.yuzu.yuzu_emu.activities 4package org.yuzu.yuzu_emu.activities
5 5
6import android.app.Activity 6import android.app.Activity
7import android.app.PendingIntent
8import android.app.PictureInPictureParams
9import android.app.RemoteAction
10import android.content.BroadcastReceiver
7import android.content.Context 11import android.content.Context
8import android.content.Intent 12import android.content.Intent
13import android.content.IntentFilter
14import android.content.res.Configuration
9import android.graphics.Rect 15import android.graphics.Rect
16import android.graphics.drawable.Icon
10import android.hardware.Sensor 17import android.hardware.Sensor
11import android.hardware.SensorEvent 18import android.hardware.SensorEvent
12import android.hardware.SensorEventListener 19import android.hardware.SensorEventListener
13import android.hardware.SensorManager 20import android.hardware.SensorManager
21import android.os.Build
14import android.os.Bundle 22import android.os.Bundle
23import android.util.Rational
15import android.view.InputDevice 24import android.view.InputDevice
16import android.view.KeyEvent 25import android.view.KeyEvent
17import android.view.MotionEvent 26import android.view.MotionEvent
@@ -23,30 +32,27 @@ import androidx.appcompat.app.AppCompatActivity
23import androidx.core.view.WindowCompat 32import androidx.core.view.WindowCompat
24import androidx.core.view.WindowInsetsCompat 33import androidx.core.view.WindowInsetsCompat
25import androidx.core.view.WindowInsetsControllerCompat 34import androidx.core.view.WindowInsetsControllerCompat
26import androidx.lifecycle.Lifecycle 35import androidx.navigation.fragment.NavHostFragment
27import androidx.lifecycle.lifecycleScope
28import androidx.lifecycle.repeatOnLifecycle
29import androidx.window.layout.WindowInfoTracker
30import kotlinx.coroutines.Dispatchers
31import kotlinx.coroutines.launch
32import org.yuzu.yuzu_emu.NativeLibrary 36import org.yuzu.yuzu_emu.NativeLibrary
33import org.yuzu.yuzu_emu.R 37import org.yuzu.yuzu_emu.R
38import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding
39import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
40import org.yuzu.yuzu_emu.features.settings.model.IntSetting
34import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel 41import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
35import org.yuzu.yuzu_emu.fragments.EmulationFragment
36import org.yuzu.yuzu_emu.model.Game 42import org.yuzu.yuzu_emu.model.Game
37import org.yuzu.yuzu_emu.utils.ControllerMappingHelper 43import org.yuzu.yuzu_emu.utils.ControllerMappingHelper
38import org.yuzu.yuzu_emu.utils.ForegroundService 44import org.yuzu.yuzu_emu.utils.ForegroundService
39import org.yuzu.yuzu_emu.utils.InputHandler 45import org.yuzu.yuzu_emu.utils.InputHandler
40import org.yuzu.yuzu_emu.utils.NfcReader 46import org.yuzu.yuzu_emu.utils.NfcReader
41import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
42import org.yuzu.yuzu_emu.utils.ThemeHelper 47import org.yuzu.yuzu_emu.utils.ThemeHelper
43import kotlin.math.roundToInt 48import kotlin.math.roundToInt
44 49
45class EmulationActivity : AppCompatActivity(), SensorEventListener { 50class EmulationActivity : AppCompatActivity(), SensorEventListener {
51 private lateinit var binding: ActivityEmulationBinding
52
46 private var controllerMappingHelper: ControllerMappingHelper? = null 53 private var controllerMappingHelper: ControllerMappingHelper? = null
47 54
48 var isActivityRecreated = false 55 var isActivityRecreated = false
49 private var emulationFragment: EmulationFragment? = null
50 private lateinit var nfcReader: NfcReader 56 private lateinit var nfcReader: NfcReader
51 private lateinit var inputHandler: InputHandler 57 private lateinit var inputHandler: InputHandler
52 58
@@ -55,7 +61,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
55 private var motionTimestamp: Long = 0 61 private var motionTimestamp: Long = 0
56 private var flipMotionOrientation: Boolean = false 62 private var flipMotionOrientation: Boolean = false
57 63
58 private lateinit var game: Game 64 private val actionPause = "ACTION_EMULATOR_PAUSE"
65 private val actionPlay = "ACTION_EMULATOR_PLAY"
59 66
60 private val settingsViewModel: SettingsViewModel by viewModels() 67 private val settingsViewModel: SettingsViewModel by viewModels()
61 68
@@ -70,47 +77,31 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
70 settingsViewModel.settings.loadSettings() 77 settingsViewModel.settings.loadSettings()
71 78
72 super.onCreate(savedInstanceState) 79 super.onCreate(savedInstanceState)
73 if (savedInstanceState == null) { 80
74 // Get params we were passed 81 binding = ActivityEmulationBinding.inflate(layoutInflater)
75 game = intent.parcelable(EXTRA_SELECTED_GAME)!! 82 setContentView(binding.root)
76 isActivityRecreated = false 83
77 } else { 84 val navHostFragment =
78 isActivityRecreated = true 85 supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
79 restoreState(savedInstanceState) 86 val navController = navHostFragment.navController
80 } 87 navController
88 .setGraph(R.navigation.emulation_navigation, intent.extras)
89
90 isActivityRecreated = savedInstanceState != null
91
81 controllerMappingHelper = ControllerMappingHelper() 92 controllerMappingHelper = ControllerMappingHelper()
82 93
83 // Set these options now so that the SurfaceView the game renders into is the right size. 94 // Set these options now so that the SurfaceView the game renders into is the right size.
84 enableFullscreenImmersive() 95 enableFullscreenImmersive()
85 96
86 setContentView(R.layout.activity_emulation)
87 window.decorView.setBackgroundColor(getColor(android.R.color.black)) 97 window.decorView.setBackgroundColor(getColor(android.R.color.black))
88 98
89 // Find or create the EmulationFragment
90 emulationFragment =
91 supportFragmentManager.findFragmentById(R.id.frame_emulation_fragment) as EmulationFragment?
92 if (emulationFragment == null) {
93 emulationFragment = EmulationFragment.newInstance(game)
94 supportFragmentManager.beginTransaction()
95 .add(R.id.frame_emulation_fragment, emulationFragment!!)
96 .commit()
97 }
98 title = game.title
99
100 nfcReader = NfcReader(this) 99 nfcReader = NfcReader(this)
101 nfcReader.initialize() 100 nfcReader.initialize()
102 101
103 inputHandler = InputHandler() 102 inputHandler = InputHandler()
104 inputHandler.initialize() 103 inputHandler.initialize()
105 104
106 lifecycleScope.launch(Dispatchers.Main) {
107 lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
108 WindowInfoTracker.getOrCreate(this@EmulationActivity)
109 .windowLayoutInfo(this@EmulationActivity)
110 .collect { emulationFragment?.updateCurrentLayout(this@EmulationActivity, it) }
111 }
112 }
113
114 // Start a foreground service to prevent the app from getting killed in the background 105 // Start a foreground service to prevent the app from getting killed in the background
115 val startIntent = Intent(this, ForegroundService::class.java) 106 val startIntent = Intent(this, ForegroundService::class.java)
116 startForegroundService(startIntent) 107 startForegroundService(startIntent)
@@ -143,6 +134,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
143 super.onResume() 134 super.onResume()
144 nfcReader.startScanning() 135 nfcReader.startScanning()
145 startMotionSensorListener() 136 startMotionSensorListener()
137
138 buildPictureInPictureParams()
146 } 139 }
147 140
148 override fun onPause() { 141 override fun onPause() {
@@ -151,17 +144,22 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
151 stopMotionSensorListener() 144 stopMotionSensorListener()
152 } 145 }
153 146
147 override fun onUserLeaveHint() {
148 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
149 if (BooleanSetting.PICTURE_IN_PICTURE.boolean && !isInPictureInPictureMode) {
150 val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
151 .getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
152 enterPictureInPictureMode(pictureInPictureParamsBuilder.build())
153 }
154 }
155 }
156
154 override fun onNewIntent(intent: Intent) { 157 override fun onNewIntent(intent: Intent) {
155 super.onNewIntent(intent) 158 super.onNewIntent(intent)
156 setIntent(intent) 159 setIntent(intent)
157 nfcReader.onNewIntent(intent) 160 nfcReader.onNewIntent(intent)
158 } 161 }
159 162
160 override fun onSaveInstanceState(outState: Bundle) {
161 outState.putParcelable(EXTRA_SELECTED_GAME, game)
162 super.onSaveInstanceState(outState)
163 }
164
165 override fun dispatchKeyEvent(event: KeyEvent): Boolean { 163 override fun dispatchKeyEvent(event: KeyEvent): Boolean {
166 if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK && 164 if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
167 event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD 165 event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
@@ -248,10 +246,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
248 246
249 override fun onAccuracyChanged(sensor: Sensor, i: Int) {} 247 override fun onAccuracyChanged(sensor: Sensor, i: Int) {}
250 248
251 private fun restoreState(savedInstanceState: Bundle) {
252 game = savedInstanceState.parcelable(EXTRA_SELECTED_GAME)!!
253 }
254
255 private fun enableFullscreenImmersive() { 249 private fun enableFullscreenImmersive() {
256 WindowCompat.setDecorFitsSystemWindows(window, false) 250 WindowCompat.setDecorFitsSystemWindows(window, false)
257 251
@@ -262,6 +256,96 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
262 } 256 }
263 } 257 }
264 258
259 private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder(): PictureInPictureParams.Builder {
260 val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.int) {
261 0 -> Rational(16, 9)
262 1 -> Rational(4, 3)
263 2 -> Rational(21, 9)
264 3 -> Rational(16, 10)
265 else -> null // Best fit
266 }
267 return this.apply { aspectRatio?.let { setAspectRatio(it) } }
268 }
269
270 private fun PictureInPictureParams.Builder.getPictureInPictureActionsBuilder(): PictureInPictureParams.Builder {
271 val pictureInPictureActions: MutableList<RemoteAction> = mutableListOf()
272 val pendingFlags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
273
274 if (NativeLibrary.isPaused()) {
275 val playIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_play)
276 val playPendingIntent = PendingIntent.getBroadcast(
277 this@EmulationActivity,
278 R.drawable.ic_pip_play,
279 Intent(actionPlay),
280 pendingFlags
281 )
282 val playRemoteAction = RemoteAction(
283 playIcon,
284 getString(R.string.play),
285 getString(R.string.play),
286 playPendingIntent
287 )
288 pictureInPictureActions.add(playRemoteAction)
289 } else {
290 val pauseIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_pause)
291 val pausePendingIntent = PendingIntent.getBroadcast(
292 this@EmulationActivity,
293 R.drawable.ic_pip_pause,
294 Intent(actionPause),
295 pendingFlags
296 )
297 val pauseRemoteAction = RemoteAction(
298 pauseIcon,
299 getString(R.string.pause),
300 getString(R.string.pause),
301 pausePendingIntent
302 )
303 pictureInPictureActions.add(pauseRemoteAction)
304 }
305
306 return this.apply { setActions(pictureInPictureActions) }
307 }
308
309 fun buildPictureInPictureParams() {
310 val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
311 .getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
312 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
313 pictureInPictureParamsBuilder.setAutoEnterEnabled(BooleanSetting.PICTURE_IN_PICTURE.boolean)
314 }
315 setPictureInPictureParams(pictureInPictureParamsBuilder.build())
316 }
317
318 private var pictureInPictureReceiver = object : BroadcastReceiver() {
319 override fun onReceive(context: Context?, intent: Intent) {
320 if (intent.action == actionPlay) {
321 if (NativeLibrary.isPaused()) NativeLibrary.unPauseEmulation()
322 } else if (intent.action == actionPause) {
323 if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation()
324 }
325 buildPictureInPictureParams()
326 }
327 }
328
329 override fun onPictureInPictureModeChanged(
330 isInPictureInPictureMode: Boolean,
331 newConfig: Configuration
332 ) {
333 super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
334 if (isInPictureInPictureMode) {
335 IntentFilter().apply {
336 addAction(actionPause)
337 addAction(actionPlay)
338 }.also {
339 registerReceiver(pictureInPictureReceiver, it)
340 }
341 } else {
342 try {
343 unregisterReceiver(pictureInPictureReceiver)
344 } catch (ignored : Exception) {
345 }
346 }
347 }
348
265 private fun startMotionSensorListener() { 349 private fun startMotionSensorListener() {
266 val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager 350 val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
267 val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) 351 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
16import androidx.documentfile.provider.DocumentFile 16import androidx.documentfile.provider.DocumentFile
17import androidx.lifecycle.ViewModelProvider 17import androidx.lifecycle.ViewModelProvider
18import androidx.lifecycle.lifecycleScope 18import androidx.lifecycle.lifecycleScope
19import androidx.navigation.findNavController
19import androidx.preference.PreferenceManager 20import androidx.preference.PreferenceManager
20import androidx.recyclerview.widget.AsyncDifferConfig 21import androidx.recyclerview.widget.AsyncDifferConfig
21import androidx.recyclerview.widget.DiffUtil 22import androidx.recyclerview.widget.DiffUtil
@@ -23,6 +24,7 @@ import androidx.recyclerview.widget.ListAdapter
23import androidx.recyclerview.widget.RecyclerView 24import androidx.recyclerview.widget.RecyclerView
24import coil.load 25import coil.load
25import kotlinx.coroutines.launch 26import kotlinx.coroutines.launch
27import org.yuzu.yuzu_emu.HomeNavigationDirections
26import org.yuzu.yuzu_emu.NativeLibrary 28import org.yuzu.yuzu_emu.NativeLibrary
27import org.yuzu.yuzu_emu.R 29import org.yuzu.yuzu_emu.R
28import org.yuzu.yuzu_emu.YuzuApplication 30import org.yuzu.yuzu_emu.YuzuApplication
@@ -78,7 +80,8 @@ class GameAdapter(private val activity: AppCompatActivity) :
78 ) 80 )
79 .apply() 81 .apply()
80 82
81 EmulationActivity.launch(activity, holder.game) 83 val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game)
84 view.findNavController().navigate(action)
82 } 85 }
83 86
84 inner class GameViewHolder(val binding: CardGameBinding) : 87 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(
8 override val section: String, 8 override val section: String,
9 override val defaultValue: Boolean 9 override val defaultValue: Boolean
10) : AbstractBooleanSetting { 10) : AbstractBooleanSetting {
11 PICTURE_IN_PICTURE("picture_in_picture", Settings.SECTION_GENERAL, true),
11 USE_CUSTOM_RTC("custom_rtc_enabled", Settings.SECTION_SYSTEM, false); 12 USE_CUSTOM_RTC("custom_rtc_enabled", Settings.SECTION_SYSTEM, false);
12 13
13 override var boolean: Boolean = defaultValue 14 override var boolean: Boolean = defaultValue
@@ -27,6 +28,7 @@ enum class BooleanSetting(
27 28
28 companion object { 29 companion object {
29 private val NOT_RUNTIME_EDITABLE = listOf( 30 private val NOT_RUNTIME_EDITABLE = listOf(
31 PICTURE_IN_PICTURE,
30 USE_CUSTOM_RTC 32 USE_CUSTOM_RTC
31 ) 33 )
32 34
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(
93 Settings.SECTION_RENDERER, 93 Settings.SECTION_RENDERER,
94 0 94 0
95 ), 95 ),
96 RENDERER_SCREEN_LAYOUT(
97 "screen_layout",
98 Settings.SECTION_RENDERER,
99 Settings.LayoutOption_MobileLandscape
100 ),
96 RENDERER_ASPECT_RATIO( 101 RENDERER_ASPECT_RATIO(
97 "aspect_ratio", 102 "aspect_ratio",
98 Settings.SECTION_RENDERER, 103 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 {
133 const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter" 133 const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter"
134 const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable" 134 const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable"
135 const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics" 135 const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics"
136 const val PREF_MENU_SETTINGS_LANDSCAPE = "EmulationMenuSettings_LandscapeScreenLayout"
137 const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps" 136 const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps"
138 const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay" 137 const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay"
139 138
@@ -144,6 +143,10 @@ class Settings {
144 143
145 private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap() 144 private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap()
146 145
146 const val LayoutOption_Unspecified = 0
147 const val LayoutOption_MobilePortrait = 4
148 const val LayoutOption_MobileLandscape = 5
149
147 init { 150 init {
148 configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] = 151 configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] =
149 listOf( 152 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
16import androidx.core.view.WindowInsetsCompat 16import androidx.core.view.WindowInsetsCompat
17import android.view.ViewGroup.MarginLayoutParams 17import android.view.ViewGroup.MarginLayoutParams
18import androidx.activity.OnBackPressedCallback 18import androidx.activity.OnBackPressedCallback
19import androidx.activity.result.ActivityResultLauncher
19import androidx.core.view.updatePadding 20import androidx.core.view.updatePadding
20import com.google.android.material.color.MaterialColors 21import com.google.android.material.color.MaterialColors
21import org.yuzu.yuzu_emu.NativeLibrary 22import org.yuzu.yuzu_emu.NativeLibrary
@@ -239,5 +240,17 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
239 settings.putExtra(ARG_GAME_ID, gameId) 240 settings.putExtra(ARG_GAME_ID, gameId)
240 context.startActivity(settings) 241 context.startActivity(settings)
241 } 242 }
243
244 fun launch(
245 context: Context,
246 launcher: ActivityResultLauncher<Intent>,
247 menuTag: String?,
248 gameId: String?
249 ) {
250 val settings = Intent(context, SettingsActivity::class.java)
251 settings.putExtra(ARG_MENU_TAG, menuTag)
252 settings.putExtra(ARG_GAME_ID, gameId)
253 launcher.launch(settings)
254 }
242 } 255 }
243} 256}
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)
166 IntSetting.CPU_ACCURACY.defaultValue 166 IntSetting.CPU_ACCURACY.defaultValue
167 ) 167 )
168 ) 168 )
169 add(
170 SwitchSetting(
171 BooleanSetting.PICTURE_IN_PICTURE,
172 R.string.picture_in_picture,
173 R.string.picture_in_picture_description,
174 BooleanSetting.PICTURE_IN_PICTURE.key,
175 BooleanSetting.PICTURE_IN_PICTURE.defaultValue
176 )
177 )
169 } 178 }
170 } 179 }
171 180
@@ -285,6 +294,17 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
285 ) 294 )
286 add( 295 add(
287 SingleChoiceSetting( 296 SingleChoiceSetting(
297 IntSetting.RENDERER_SCREEN_LAYOUT,
298 R.string.renderer_screen_layout,
299 0,
300 R.array.rendererScreenLayoutNames,
301 R.array.rendererScreenLayoutValues,
302 IntSetting.RENDERER_SCREEN_LAYOUT.key,
303 IntSetting.RENDERER_SCREEN_LAYOUT.defaultValue
304 )
305 )
306 add(
307 SingleChoiceSetting(
288 IntSetting.RENDERER_ASPECT_RATIO, 308 IntSetting.RENDERER_ASPECT_RATIO,
289 R.string.renderer_aspect_ratio, 309 R.string.renderer_aspect_ratio,
290 0, 310 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
7import android.app.AlertDialog 7import android.app.AlertDialog
8import android.content.Context 8import android.content.Context
9import android.content.DialogInterface 9import android.content.DialogInterface
10import android.content.Intent
10import android.content.SharedPreferences 11import android.content.SharedPreferences
11import android.content.pm.ActivityInfo 12import android.content.pm.ActivityInfo
12import android.content.res.Resources 13import android.content.res.Configuration
13import android.graphics.Color 14import android.graphics.Color
14import android.os.Bundle 15import android.os.Bundle
15import android.os.Handler 16import android.os.Handler
16import android.os.Looper 17import android.os.Looper
17import android.util.Rational 18import android.util.Rational
18import android.util.TypedValue
19import android.view.* 19import android.view.*
20import android.widget.TextView 20import android.widget.TextView
21import androidx.activity.OnBackPressedCallback 21import androidx.activity.OnBackPressedCallback
22import androidx.activity.result.ActivityResultLauncher
23import androidx.activity.result.contract.ActivityResultContracts
22import androidx.appcompat.widget.PopupMenu 24import androidx.appcompat.widget.PopupMenu
23import androidx.core.content.res.ResourcesCompat 25import androidx.core.content.res.ResourcesCompat
24import androidx.core.graphics.Insets 26import androidx.core.graphics.Insets
25import androidx.core.view.ViewCompat 27import androidx.core.view.ViewCompat
26import androidx.core.view.WindowInsetsCompat 28import androidx.core.view.WindowInsetsCompat
27import androidx.core.view.updatePadding 29import androidx.core.view.isVisible
28import androidx.fragment.app.Fragment 30import androidx.fragment.app.Fragment
31import androidx.lifecycle.Lifecycle
32import androidx.lifecycle.lifecycleScope
33import androidx.lifecycle.repeatOnLifecycle
34import androidx.navigation.fragment.navArgs
29import androidx.preference.PreferenceManager 35import androidx.preference.PreferenceManager
30import androidx.window.layout.FoldingFeature 36import androidx.window.layout.FoldingFeature
37import androidx.window.layout.WindowInfoTracker
31import androidx.window.layout.WindowLayoutInfo 38import androidx.window.layout.WindowLayoutInfo
32import com.google.android.material.dialog.MaterialAlertDialogBuilder 39import com.google.android.material.dialog.MaterialAlertDialogBuilder
33import com.google.android.material.slider.Slider 40import com.google.android.material.slider.Slider
41import kotlinx.coroutines.Dispatchers
42import kotlinx.coroutines.launch
34import org.yuzu.yuzu_emu.NativeLibrary 43import org.yuzu.yuzu_emu.NativeLibrary
35import org.yuzu.yuzu_emu.R 44import org.yuzu.yuzu_emu.R
36import org.yuzu.yuzu_emu.YuzuApplication 45import org.yuzu.yuzu_emu.YuzuApplication
@@ -41,9 +50,8 @@ import org.yuzu.yuzu_emu.features.settings.model.IntSetting
41import org.yuzu.yuzu_emu.features.settings.model.Settings 50import org.yuzu.yuzu_emu.features.settings.model.Settings
42import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity 51import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
43import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile 52import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
44import org.yuzu.yuzu_emu.model.Game 53import org.yuzu.yuzu_emu.overlay.InputOverlay
45import org.yuzu.yuzu_emu.utils.* 54import org.yuzu.yuzu_emu.utils.*
46import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
47 55
48class EmulationFragment : Fragment(), SurfaceHolder.Callback { 56class EmulationFragment : Fragment(), SurfaceHolder.Callback {
49 private lateinit var preferences: SharedPreferences 57 private lateinit var preferences: SharedPreferences
@@ -54,13 +62,42 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
54 private var _binding: FragmentEmulationBinding? = null 62 private var _binding: FragmentEmulationBinding? = null
55 private val binding get() = _binding!! 63 private val binding get() = _binding!!
56 64
57 private lateinit var game: Game 65 val args by navArgs<EmulationFragmentArgs>()
66
67 private var isInFoldableLayout = false
68
69 private lateinit var onReturnFromSettings: ActivityResultLauncher<Intent>
58 70
59 override fun onAttach(context: Context) { 71 override fun onAttach(context: Context) {
60 super.onAttach(context) 72 super.onAttach(context)
61 if (context is EmulationActivity) { 73 if (context is EmulationActivity) {
62 emulationActivity = context 74 emulationActivity = context
63 NativeLibrary.setEmulationActivity(context) 75 NativeLibrary.setEmulationActivity(context)
76
77 lifecycleScope.launch(Dispatchers.Main) {
78 lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
79 WindowInfoTracker.getOrCreate(context)
80 .windowLayoutInfo(context)
81 .collect { updateFoldableLayout(context, it) }
82 }
83 }
84
85 onReturnFromSettings = context.activityResultRegistry.register(
86 "SettingsResult", ActivityResultContracts.StartActivityForResult()
87 ) {
88 binding.surfaceEmulation.setAspectRatio(
89 when (IntSetting.RENDERER_ASPECT_RATIO.int) {
90 0 -> Rational(16, 9)
91 1 -> Rational(4, 3)
92 2 -> Rational(21, 9)
93 3 -> Rational(16, 10)
94 4 -> null // Stretch
95 else -> Rational(16, 9)
96 }
97 )
98 emulationActivity?.buildPictureInPictureParams()
99 updateScreenLayout()
100 }
64 } else { 101 } else {
65 throw IllegalStateException("EmulationFragment must have EmulationActivity parent") 102 throw IllegalStateException("EmulationFragment must have EmulationActivity parent")
66 } 103 }
@@ -75,8 +112,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
75 // So this fragment doesn't restart on configuration changes; i.e. rotation. 112 // So this fragment doesn't restart on configuration changes; i.e. rotation.
76 retainInstance = true 113 retainInstance = true
77 preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) 114 preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
78 game = requireArguments().parcelable(EmulationActivity.EXTRA_SELECTED_GAME)!! 115 emulationState = EmulationState(args.game.path)
79 emulationState = EmulationState(game.path)
80 } 116 }
81 117
82 /** 118 /**
@@ -100,7 +136,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
100 updateShowFpsOverlay() 136 updateShowFpsOverlay()
101 137
102 binding.inGameMenu.getHeaderView(0).findViewById<TextView>(R.id.text_game_title).text = 138 binding.inGameMenu.getHeaderView(0).findViewById<TextView>(R.id.text_game_title).text =
103 game.title 139 args.game.title
104 binding.inGameMenu.setNavigationItemSelectedListener { 140 binding.inGameMenu.setNavigationItemSelectedListener {
105 when (it.itemId) { 141 when (it.itemId) {
106 R.id.menu_pause_emulation -> { 142 R.id.menu_pause_emulation -> {
@@ -125,7 +161,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
125 } 161 }
126 162
127 R.id.menu_settings -> { 163 R.id.menu_settings -> {
128 SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") 164 SettingsActivity.launch(
165 requireContext(),
166 onReturnFromSettings,
167 SettingsFile.FILE_NAME_CONFIG,
168 ""
169 )
129 true 170 true
130 } 171 }
131 172
@@ -153,6 +194,40 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
153 if (binding.drawerLayout.isOpen) binding.drawerLayout.close() else binding.drawerLayout.open() 194 if (binding.drawerLayout.isOpen) binding.drawerLayout.close() else binding.drawerLayout.open()
154 } 195 }
155 }) 196 })
197
198 viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
199 lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
200 WindowInfoTracker.getOrCreate(requireContext())
201 .windowLayoutInfo(requireActivity())
202 .collect { updateFoldableLayout(requireActivity() as EmulationActivity, it) }
203 }
204 }
205 }
206
207 override fun onConfigurationChanged(newConfig: Configuration) {
208 super.onConfigurationChanged(newConfig)
209 if (emulationActivity?.isInPictureInPictureMode == true) {
210 if (binding.drawerLayout.isOpen) {
211 binding.drawerLayout.close()
212 }
213 if (EmulationMenuSettings.showOverlay) {
214 binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = false }
215 }
216 } else {
217 if (EmulationMenuSettings.showOverlay) {
218 binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = true }
219 }
220 if (!isInFoldableLayout) {
221 if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
222 binding.surfaceInputOverlay.orientation = InputOverlay.PORTRAIT
223 } else {
224 binding.surfaceInputOverlay.orientation = InputOverlay.LANDSCAPE
225 }
226 }
227 if (!binding.surfaceInputOverlay.isInEditMode) {
228 refreshInputOverlay()
229 }
230 }
156 } 231 }
157 232
158 override fun onResume() { 233 override fun onResume() {
@@ -172,6 +247,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
172 } 247 }
173 ) 248 )
174 249
250 updateScreenLayout()
251
175 emulationState.run(emulationActivity!!.isActivityRecreated) 252 emulationState.run(emulationActivity!!.isActivityRecreated)
176 } 253 }
177 254
@@ -231,31 +308,50 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
231 } 308 }
232 } 309 }
233 310
234 private val Number.toPx get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics).toInt() 311 @SuppressLint("SourceLockedOrientationActivity")
312 private fun updateScreenLayout() {
313 emulationActivity?.let {
314 it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.int) {
315 Settings.LayoutOption_MobileLandscape -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
316 Settings.LayoutOption_MobilePortrait -> ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
317 Settings.LayoutOption_Unspecified -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
318 else -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
319 }
320 }
321 onConfigurationChanged(resources.configuration)
322 }
235 323
236 fun updateCurrentLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) { 324 private fun updateFoldableLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) {
237 val isFolding = (newLayoutInfo.displayFeatures.find { it is FoldingFeature } as? FoldingFeature)?.let { 325 val isFolding = (newLayoutInfo.displayFeatures.find { it is FoldingFeature } as? FoldingFeature)?.let {
238 if (it.isSeparating) { 326 if (it.isSeparating) {
239 emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED 327 emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
240 if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) { 328 if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) {
241 binding.surfaceEmulation.layoutParams.height = it.bounds.top 329 // Restrict emulation and overlays to the top of the screen
330 binding.emulationContainer.layoutParams.height = it.bounds.top
331 binding.overlayContainer.layoutParams.height = it.bounds.top
332 // Restrict input and menu drawer to the bottom of the screen
333 binding.inputContainer.layoutParams.height = it.bounds.bottom
242 binding.inGameMenu.layoutParams.height = it.bounds.bottom 334 binding.inGameMenu.layoutParams.height = it.bounds.bottom
243 binding.overlayContainer.layoutParams.height = it.bounds.bottom - 48.toPx 335
244 binding.overlayContainer.updatePadding(0, 0, 0, 24.toPx) 336 isInFoldableLayout = true
337 binding.surfaceInputOverlay.orientation = InputOverlay.FOLDABLE
338 refreshInputOverlay()
245 } 339 }
246 } 340 }
247 it.isSeparating 341 it.isSeparating
248 } ?: false 342 } ?: false
249 if (!isFolding) { 343 if (!isFolding) {
250 binding.surfaceEmulation.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT 344 binding.emulationContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
251 binding.inGameMenu.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT 345 binding.inputContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
252 binding.overlayContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT 346 binding.overlayContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
253 binding.overlayContainer.updatePadding(0, 0, 0, 0) 347 binding.inGameMenu.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
254 emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE 348 isInFoldableLayout = false
349 updateScreenLayout()
255 } 350 }
256 binding.surfaceInputOverlay.requestLayout() 351 binding.emulationContainer.requestLayout()
257 binding.inGameMenu.requestLayout() 352 binding.inputContainer.requestLayout()
258 binding.overlayContainer.requestLayout() 353 binding.overlayContainer.requestLayout()
354 binding.inGameMenu.requestLayout()
259 } 355 }
260 356
261 override fun surfaceCreated(holder: SurfaceHolder) { 357 override fun surfaceCreated(holder: SurfaceHolder) {
@@ -385,7 +481,19 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
385 popup.show() 481 popup.show()
386 } 482 }
387 483
484 @SuppressLint("SourceLockedOrientationActivity")
388 private fun startConfiguringControls() { 485 private fun startConfiguringControls() {
486 // Lock the current orientation to prevent editing inconsistencies
487 if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) {
488 emulationActivity?.let {
489 it.requestedOrientation =
490 if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
491 ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
492 } else {
493 ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
494 }
495 }
496 }
389 binding.doneControlConfig.visibility = View.VISIBLE 497 binding.doneControlConfig.visibility = View.VISIBLE
390 binding.surfaceInputOverlay.setIsInEditMode(true) 498 binding.surfaceInputOverlay.setIsInEditMode(true)
391 } 499 }
@@ -393,6 +501,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
393 private fun stopConfiguringControls() { 501 private fun stopConfiguringControls() {
394 binding.doneControlConfig.visibility = View.GONE 502 binding.doneControlConfig.visibility = View.GONE
395 binding.surfaceInputOverlay.setIsInEditMode(false) 503 binding.surfaceInputOverlay.setIsInEditMode(false)
504 // Unlock the orientation if it was locked for editing
505 if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) {
506 emulationActivity?.let {
507 it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
508 }
509 }
396 } 510 }
397 511
398 @SuppressLint("SetTextI18n") 512 @SuppressLint("SetTextI18n")
@@ -601,13 +715,5 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
601 715
602 companion object { 716 companion object {
603 private val perfStatsUpdateHandler = Handler(Looper.myLooper()!!) 717 private val perfStatsUpdateHandler = Handler(Looper.myLooper()!!)
604
605 fun newInstance(game: Game): EmulationFragment {
606 val args = Bundle()
607 args.putParcelable(EmulationActivity.EXTRA_SELECTED_GAME, game)
608 val fragment = EmulationFragment()
609 fragment.arguments = args
610 return fragment
611 }
612 } 718 }
613} 719}
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
51 51
52 private lateinit var windowInsets: WindowInsets 52 private lateinit var windowInsets: WindowInsets
53 53
54 var orientation = LANDSCAPE
55
54 override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { 56 override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
55 super.onLayout(changed, left, top, right, bottom) 57 super.onLayout(changed, left, top, right, bottom)
56 58
57 windowInsets = rootWindowInsets 59 windowInsets = rootWindowInsets
58 60
59 if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) { 61 if (!preferences.getBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", false)) {
60 defaultOverlay() 62 defaultOverlay()
61 } 63 }
62 64
@@ -233,10 +235,6 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
233 val fingerPositionX = event.getX(pointerIndex).toInt() 235 val fingerPositionX = event.getX(pointerIndex).toInt()
234 val fingerPositionY = event.getY(pointerIndex).toInt() 236 val fingerPositionY = event.getY(pointerIndex).toInt()
235 237
236 // TODO: Provide support for portrait layout
237 //val orientation =
238 // if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else ""
239
240 for (button in overlayButtons) { 238 for (button in overlayButtons) {
241 // Determine the button state to apply based on the MotionEvent action flag. 239 // Determine the button state to apply based on the MotionEvent action flag.
242 when (event.action and MotionEvent.ACTION_MASK) { 240 when (event.action and MotionEvent.ACTION_MASK) {
@@ -266,7 +264,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
266 buttonBeingConfigured!!.buttonId, 264 buttonBeingConfigured!!.buttonId,
267 buttonBeingConfigured!!.bounds.centerX(), 265 buttonBeingConfigured!!.bounds.centerX(),
268 buttonBeingConfigured!!.bounds.centerY(), 266 buttonBeingConfigured!!.bounds.centerY(),
269 "" 267 orientation
270 ) 268 )
271 buttonBeingConfigured = null 269 buttonBeingConfigured = null
272 } 270 }
@@ -299,7 +297,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
299 dpadBeingConfigured!!.upId, 297 dpadBeingConfigured!!.upId,
300 dpadBeingConfigured!!.bounds.centerX(), 298 dpadBeingConfigured!!.bounds.centerX(),
301 dpadBeingConfigured!!.bounds.centerY(), 299 dpadBeingConfigured!!.bounds.centerY(),
302 "" 300 orientation
303 ) 301 )
304 dpadBeingConfigured = null 302 dpadBeingConfigured = null
305 } 303 }
@@ -330,7 +328,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
330 joystickBeingConfigured!!.buttonId, 328 joystickBeingConfigured!!.buttonId,
331 joystickBeingConfigured!!.bounds.centerX(), 329 joystickBeingConfigured!!.bounds.centerX(),
332 joystickBeingConfigured!!.bounds.centerY(), 330 joystickBeingConfigured!!.bounds.centerY(),
333 "" 331 orientation
334 ) 332 )
335 joystickBeingConfigured = null 333 joystickBeingConfigured = null
336 } 334 }
@@ -533,8 +531,6 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
533 overlayButtons.clear() 531 overlayButtons.clear()
534 overlayDpads.clear() 532 overlayDpads.clear()
535 overlayJoysticks.clear() 533 overlayJoysticks.clear()
536 val orientation =
537 if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else ""
538 534
539 // Add all the enabled overlay items back to the HashSet. 535 // Add all the enabled overlay items back to the HashSet.
540 if (EmulationMenuSettings.showOverlay) { 536 if (EmulationMenuSettings.showOverlay) {
@@ -548,8 +544,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
548 val min = windowSize.first 544 val min = windowSize.first
549 val max = windowSize.second 545 val max = windowSize.second
550 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit() 546 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
551 .putFloat("$sharedPrefsId$orientation-X", (x - min.x).toFloat() / max.x) 547 .putFloat("$sharedPrefsId-X$orientation", (x - min.x).toFloat() / max.x)
552 .putFloat("$sharedPrefsId$orientation-Y", (y - min.y).toFloat() / max.y) 548 .putFloat("$sharedPrefsId-Y$orientation", (y - min.y).toFloat() / max.y)
553 .apply() 549 .apply()
554 } 550 }
555 551
@@ -558,145 +554,250 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
558 } 554 }
559 555
560 private fun defaultOverlay() { 556 private fun defaultOverlay() {
561 if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) { 557 if (!preferences.getBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", false)) {
562 defaultOverlayLandscape() 558 defaultOverlayByLayout(orientation)
563 } 559 }
564 560
565 resetButtonPlacement() 561 resetButtonPlacement()
566 preferences.edit() 562 preferences.edit()
567 .putBoolean(Settings.PREF_OVERLAY_INIT, true) 563 .putBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", true)
568 .apply() 564 .apply()
569 } 565 }
570 566
571 fun resetButtonPlacement() { 567 fun resetButtonPlacement() {
572 defaultOverlayLandscape() 568 defaultOverlayByLayout(orientation)
573 refreshControls() 569 refreshControls()
574 } 570 }
575 571
576 private fun defaultOverlayLandscape() { 572 private val landscapeResources = arrayOf(
573 R.integer.SWITCH_BUTTON_A_X,
574 R.integer.SWITCH_BUTTON_A_Y,
575 R.integer.SWITCH_BUTTON_B_X,
576 R.integer.SWITCH_BUTTON_B_Y,
577 R.integer.SWITCH_BUTTON_X_X,
578 R.integer.SWITCH_BUTTON_X_Y,
579 R.integer.SWITCH_BUTTON_Y_X,
580 R.integer.SWITCH_BUTTON_Y_Y,
581 R.integer.SWITCH_TRIGGER_ZL_X,
582 R.integer.SWITCH_TRIGGER_ZL_Y,
583 R.integer.SWITCH_TRIGGER_ZR_X,
584 R.integer.SWITCH_TRIGGER_ZR_Y,
585 R.integer.SWITCH_BUTTON_DPAD_X,
586 R.integer.SWITCH_BUTTON_DPAD_Y,
587 R.integer.SWITCH_TRIGGER_L_X,
588 R.integer.SWITCH_TRIGGER_L_Y,
589 R.integer.SWITCH_TRIGGER_R_X,
590 R.integer.SWITCH_TRIGGER_R_Y,
591 R.integer.SWITCH_BUTTON_PLUS_X,
592 R.integer.SWITCH_BUTTON_PLUS_Y,
593 R.integer.SWITCH_BUTTON_MINUS_X,
594 R.integer.SWITCH_BUTTON_MINUS_Y,
595 R.integer.SWITCH_BUTTON_HOME_X,
596 R.integer.SWITCH_BUTTON_HOME_Y,
597 R.integer.SWITCH_BUTTON_CAPTURE_X,
598 R.integer.SWITCH_BUTTON_CAPTURE_Y,
599 R.integer.SWITCH_STICK_R_X,
600 R.integer.SWITCH_STICK_R_Y,
601 R.integer.SWITCH_STICK_L_X,
602 R.integer.SWITCH_STICK_L_Y
603 )
604
605 private val portraitResources = arrayOf(
606 R.integer.SWITCH_BUTTON_A_X_PORTRAIT,
607 R.integer.SWITCH_BUTTON_A_Y_PORTRAIT,
608 R.integer.SWITCH_BUTTON_B_X_PORTRAIT,
609 R.integer.SWITCH_BUTTON_B_Y_PORTRAIT,
610 R.integer.SWITCH_BUTTON_X_X_PORTRAIT,
611 R.integer.SWITCH_BUTTON_X_Y_PORTRAIT,
612 R.integer.SWITCH_BUTTON_Y_X_PORTRAIT,
613 R.integer.SWITCH_BUTTON_Y_Y_PORTRAIT,
614 R.integer.SWITCH_TRIGGER_ZL_X_PORTRAIT,
615 R.integer.SWITCH_TRIGGER_ZL_Y_PORTRAIT,
616 R.integer.SWITCH_TRIGGER_ZR_X_PORTRAIT,
617 R.integer.SWITCH_TRIGGER_ZR_Y_PORTRAIT,
618 R.integer.SWITCH_BUTTON_DPAD_X_PORTRAIT,
619 R.integer.SWITCH_BUTTON_DPAD_Y_PORTRAIT,
620 R.integer.SWITCH_TRIGGER_L_X_PORTRAIT,
621 R.integer.SWITCH_TRIGGER_L_Y_PORTRAIT,
622 R.integer.SWITCH_TRIGGER_R_X_PORTRAIT,
623 R.integer.SWITCH_TRIGGER_R_Y_PORTRAIT,
624 R.integer.SWITCH_BUTTON_PLUS_X_PORTRAIT,
625 R.integer.SWITCH_BUTTON_PLUS_Y_PORTRAIT,
626 R.integer.SWITCH_BUTTON_MINUS_X_PORTRAIT,
627 R.integer.SWITCH_BUTTON_MINUS_Y_PORTRAIT,
628 R.integer.SWITCH_BUTTON_HOME_X_PORTRAIT,
629 R.integer.SWITCH_BUTTON_HOME_Y_PORTRAIT,
630 R.integer.SWITCH_BUTTON_CAPTURE_X_PORTRAIT,
631 R.integer.SWITCH_BUTTON_CAPTURE_Y_PORTRAIT,
632 R.integer.SWITCH_STICK_R_X_PORTRAIT,
633 R.integer.SWITCH_STICK_R_Y_PORTRAIT,
634 R.integer.SWITCH_STICK_L_X_PORTRAIT,
635 R.integer.SWITCH_STICK_L_Y_PORTRAIT
636 )
637
638 private val foldableResources = arrayOf(
639 R.integer.SWITCH_BUTTON_A_X_FOLDABLE,
640 R.integer.SWITCH_BUTTON_A_Y_FOLDABLE,
641 R.integer.SWITCH_BUTTON_B_X_FOLDABLE,
642 R.integer.SWITCH_BUTTON_B_Y_FOLDABLE,
643 R.integer.SWITCH_BUTTON_X_X_FOLDABLE,
644 R.integer.SWITCH_BUTTON_X_Y_FOLDABLE,
645 R.integer.SWITCH_BUTTON_Y_X_FOLDABLE,
646 R.integer.SWITCH_BUTTON_Y_Y_FOLDABLE,
647 R.integer.SWITCH_TRIGGER_ZL_X_FOLDABLE,
648 R.integer.SWITCH_TRIGGER_ZL_Y_FOLDABLE,
649 R.integer.SWITCH_TRIGGER_ZR_X_FOLDABLE,
650 R.integer.SWITCH_TRIGGER_ZR_Y_FOLDABLE,
651 R.integer.SWITCH_BUTTON_DPAD_X_FOLDABLE,
652 R.integer.SWITCH_BUTTON_DPAD_Y_FOLDABLE,
653 R.integer.SWITCH_TRIGGER_L_X_FOLDABLE,
654 R.integer.SWITCH_TRIGGER_L_Y_FOLDABLE,
655 R.integer.SWITCH_TRIGGER_R_X_FOLDABLE,
656 R.integer.SWITCH_TRIGGER_R_Y_FOLDABLE,
657 R.integer.SWITCH_BUTTON_PLUS_X_FOLDABLE,
658 R.integer.SWITCH_BUTTON_PLUS_Y_FOLDABLE,
659 R.integer.SWITCH_BUTTON_MINUS_X_FOLDABLE,
660 R.integer.SWITCH_BUTTON_MINUS_Y_FOLDABLE,
661 R.integer.SWITCH_BUTTON_HOME_X_FOLDABLE,
662 R.integer.SWITCH_BUTTON_HOME_Y_FOLDABLE,
663 R.integer.SWITCH_BUTTON_CAPTURE_X_FOLDABLE,
664 R.integer.SWITCH_BUTTON_CAPTURE_Y_FOLDABLE,
665 R.integer.SWITCH_STICK_R_X_FOLDABLE,
666 R.integer.SWITCH_STICK_R_Y_FOLDABLE,
667 R.integer.SWITCH_STICK_L_X_FOLDABLE,
668 R.integer.SWITCH_STICK_L_Y_FOLDABLE
669 )
670
671 private fun getResourceValue(orientation: String, position: Int) : Float {
672 return when (orientation) {
673 PORTRAIT -> resources.getInteger(portraitResources[position]).toFloat() / 1000
674 FOLDABLE -> resources.getInteger(foldableResources[position]).toFloat() / 1000
675 else -> resources.getInteger(landscapeResources[position]).toFloat() / 1000
676 }
677 }
678
679 private fun defaultOverlayByLayout(orientation: String) {
577 // Each value represents the position of the button in relation to the screen size without insets. 680 // Each value represents the position of the button in relation to the screen size without insets.
578 preferences.edit() 681 preferences.edit()
579 .putFloat( 682 .putFloat(
580 ButtonType.BUTTON_A.toString() + "-X", 683 ButtonType.BUTTON_A.toString() + "-X$orientation",
581 resources.getInteger(R.integer.SWITCH_BUTTON_A_X).toFloat() / 1000 684 getResourceValue(orientation, 0)
582 ) 685 )
583 .putFloat( 686 .putFloat(
584 ButtonType.BUTTON_A.toString() + "-Y", 687 ButtonType.BUTTON_A.toString() + "-Y$orientation",
585 resources.getInteger(R.integer.SWITCH_BUTTON_A_Y).toFloat() / 1000 688 getResourceValue(orientation, 1)
586 ) 689 )
587 .putFloat( 690 .putFloat(
588 ButtonType.BUTTON_B.toString() + "-X", 691 ButtonType.BUTTON_B.toString() + "-X$orientation",
589 resources.getInteger(R.integer.SWITCH_BUTTON_B_X).toFloat() / 1000 692 getResourceValue(orientation, 2)
590 ) 693 )
591 .putFloat( 694 .putFloat(
592 ButtonType.BUTTON_B.toString() + "-Y", 695 ButtonType.BUTTON_B.toString() + "-Y$orientation",
593 resources.getInteger(R.integer.SWITCH_BUTTON_B_Y).toFloat() / 1000 696 getResourceValue(orientation, 3)
594 ) 697 )
595 .putFloat( 698 .putFloat(
596 ButtonType.BUTTON_X.toString() + "-X", 699 ButtonType.BUTTON_X.toString() + "-X$orientation",
597 resources.getInteger(R.integer.SWITCH_BUTTON_X_X).toFloat() / 1000 700 getResourceValue(orientation, 4)
598 ) 701 )
599 .putFloat( 702 .putFloat(
600 ButtonType.BUTTON_X.toString() + "-Y", 703 ButtonType.BUTTON_X.toString() + "-Y$orientation",
601 resources.getInteger(R.integer.SWITCH_BUTTON_X_Y).toFloat() / 1000 704 getResourceValue(orientation, 5)
602 ) 705 )
603 .putFloat( 706 .putFloat(
604 ButtonType.BUTTON_Y.toString() + "-X", 707 ButtonType.BUTTON_Y.toString() + "-X$orientation",
605 resources.getInteger(R.integer.SWITCH_BUTTON_Y_X).toFloat() / 1000 708 getResourceValue(orientation, 6)
606 ) 709 )
607 .putFloat( 710 .putFloat(
608 ButtonType.BUTTON_Y.toString() + "-Y", 711 ButtonType.BUTTON_Y.toString() + "-Y$orientation",
609 resources.getInteger(R.integer.SWITCH_BUTTON_Y_Y).toFloat() / 1000 712 getResourceValue(orientation, 7)
610 ) 713 )
611 .putFloat( 714 .putFloat(
612 ButtonType.TRIGGER_ZL.toString() + "-X", 715 ButtonType.TRIGGER_ZL.toString() + "-X$orientation",
613 resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_X).toFloat() / 1000 716 getResourceValue(orientation, 8)
614 ) 717 )
615 .putFloat( 718 .putFloat(
616 ButtonType.TRIGGER_ZL.toString() + "-Y", 719 ButtonType.TRIGGER_ZL.toString() + "-Y$orientation",
617 resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_Y).toFloat() / 1000 720 getResourceValue(orientation, 9)
618 ) 721 )
619 .putFloat( 722 .putFloat(
620 ButtonType.TRIGGER_ZR.toString() + "-X", 723 ButtonType.TRIGGER_ZR.toString() + "-X$orientation",
621 resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_X).toFloat() / 1000 724 getResourceValue(orientation, 10)
622 ) 725 )
623 .putFloat( 726 .putFloat(
624 ButtonType.TRIGGER_ZR.toString() + "-Y", 727 ButtonType.TRIGGER_ZR.toString() + "-Y$orientation",
625 resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_Y).toFloat() / 1000 728 getResourceValue(orientation, 11)
626 ) 729 )
627 .putFloat( 730 .putFloat(
628 ButtonType.DPAD_UP.toString() + "-X", 731 ButtonType.DPAD_UP.toString() + "-X$orientation",
629 resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_X).toFloat() / 1000 732 getResourceValue(orientation, 12)
630 ) 733 )
631 .putFloat( 734 .putFloat(
632 ButtonType.DPAD_UP.toString() + "-Y", 735 ButtonType.DPAD_UP.toString() + "-Y$orientation",
633 resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_Y).toFloat() / 1000 736 getResourceValue(orientation, 13)
634 ) 737 )
635 .putFloat( 738 .putFloat(
636 ButtonType.TRIGGER_L.toString() + "-X", 739 ButtonType.TRIGGER_L.toString() + "-X$orientation",
637 resources.getInteger(R.integer.SWITCH_TRIGGER_L_X).toFloat() / 1000 740 getResourceValue(orientation, 14)
638 ) 741 )
639 .putFloat( 742 .putFloat(
640 ButtonType.TRIGGER_L.toString() + "-Y", 743 ButtonType.TRIGGER_L.toString() + "-Y$orientation",
641 resources.getInteger(R.integer.SWITCH_TRIGGER_L_Y).toFloat() / 1000 744 getResourceValue(orientation, 15)
642 ) 745 )
643 .putFloat( 746 .putFloat(
644 ButtonType.TRIGGER_R.toString() + "-X", 747 ButtonType.TRIGGER_R.toString() + "-X$orientation",
645 resources.getInteger(R.integer.SWITCH_TRIGGER_R_X).toFloat() / 1000 748 getResourceValue(orientation, 16)
646 ) 749 )
647 .putFloat( 750 .putFloat(
648 ButtonType.TRIGGER_R.toString() + "-Y", 751 ButtonType.TRIGGER_R.toString() + "-Y$orientation",
649 resources.getInteger(R.integer.SWITCH_TRIGGER_R_Y).toFloat() / 1000 752 getResourceValue(orientation, 17)
650 ) 753 )
651 .putFloat( 754 .putFloat(
652 ButtonType.BUTTON_PLUS.toString() + "-X", 755 ButtonType.BUTTON_PLUS.toString() + "-X$orientation",
653 resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_X).toFloat() / 1000 756 getResourceValue(orientation, 18)
654 ) 757 )
655 .putFloat( 758 .putFloat(
656 ButtonType.BUTTON_PLUS.toString() + "-Y", 759 ButtonType.BUTTON_PLUS.toString() + "-Y$orientation",
657 resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_Y).toFloat() / 1000 760 getResourceValue(orientation, 19)
658 ) 761 )
659 .putFloat( 762 .putFloat(
660 ButtonType.BUTTON_MINUS.toString() + "-X", 763 ButtonType.BUTTON_MINUS.toString() + "-X$orientation",
661 resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_X).toFloat() / 1000 764 getResourceValue(orientation, 20)
662 ) 765 )
663 .putFloat( 766 .putFloat(
664 ButtonType.BUTTON_MINUS.toString() + "-Y", 767 ButtonType.BUTTON_MINUS.toString() + "-Y$orientation",
665 resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_Y).toFloat() / 1000 768 getResourceValue(orientation, 21)
666 ) 769 )
667 .putFloat( 770 .putFloat(
668 ButtonType.BUTTON_HOME.toString() + "-X", 771 ButtonType.BUTTON_HOME.toString() + "-X$orientation",
669 resources.getInteger(R.integer.SWITCH_BUTTON_HOME_X).toFloat() / 1000 772 getResourceValue(orientation, 22)
670 ) 773 )
671 .putFloat( 774 .putFloat(
672 ButtonType.BUTTON_HOME.toString() + "-Y", 775 ButtonType.BUTTON_HOME.toString() + "-Y$orientation",
673 resources.getInteger(R.integer.SWITCH_BUTTON_HOME_Y).toFloat() / 1000 776 getResourceValue(orientation, 23)
674 ) 777 )
675 .putFloat( 778 .putFloat(
676 ButtonType.BUTTON_CAPTURE.toString() + "-X", 779 ButtonType.BUTTON_CAPTURE.toString() + "-X$orientation",
677 resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_X) 780 getResourceValue(orientation, 24)
678 .toFloat() / 1000
679 ) 781 )
680 .putFloat( 782 .putFloat(
681 ButtonType.BUTTON_CAPTURE.toString() + "-Y", 783 ButtonType.BUTTON_CAPTURE.toString() + "-Y$orientation",
682 resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_Y) 784 getResourceValue(orientation, 25)
683 .toFloat() / 1000
684 ) 785 )
685 .putFloat( 786 .putFloat(
686 ButtonType.STICK_R.toString() + "-X", 787 ButtonType.STICK_R.toString() + "-X$orientation",
687 resources.getInteger(R.integer.SWITCH_STICK_R_X).toFloat() / 1000 788 getResourceValue(orientation, 26)
688 ) 789 )
689 .putFloat( 790 .putFloat(
690 ButtonType.STICK_R.toString() + "-Y", 791 ButtonType.STICK_R.toString() + "-Y$orientation",
691 resources.getInteger(R.integer.SWITCH_STICK_R_Y).toFloat() / 1000 792 getResourceValue(orientation, 27)
692 ) 793 )
693 .putFloat( 794 .putFloat(
694 ButtonType.STICK_L.toString() + "-X", 795 ButtonType.STICK_L.toString() + "-X$orientation",
695 resources.getInteger(R.integer.SWITCH_STICK_L_X).toFloat() / 1000 796 getResourceValue(orientation, 28)
696 ) 797 )
697 .putFloat( 798 .putFloat(
698 ButtonType.STICK_L.toString() + "-Y", 799 ButtonType.STICK_L.toString() + "-Y$orientation",
699 resources.getInteger(R.integer.SWITCH_STICK_L_Y).toFloat() / 1000 800 getResourceValue(orientation, 29)
700 ) 801 )
701 .apply() 802 .apply()
702 } 803 }
@@ -709,6 +810,10 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
709 private val preferences: SharedPreferences = 810 private val preferences: SharedPreferences =
710 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) 811 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
711 812
813 const val LANDSCAPE = ""
814 const val PORTRAIT = "_Portrait"
815 const val FOLDABLE = "_Foldable"
816
712 /** 817 /**
713 * Resizes a [Bitmap] by a given scale factor 818 * Resizes a [Bitmap] by a given scale factor
714 * 819 *
@@ -754,9 +859,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
754 */ 859 */
755 private fun getSafeScreenSize(context: Context): Pair<Point, Point> { 860 private fun getSafeScreenSize(context: Context): Pair<Point, Point> {
756 // Get screen size 861 // Get screen size
757 val windowMetrics = 862 val windowMetrics = WindowMetricsCalculator.getOrCreate()
758 WindowMetricsCalculator.getOrCreate() 863 .computeCurrentWindowMetrics(context as Activity)
759 .computeCurrentWindowMetrics(context as Activity)
760 var maxY = windowMetrics.bounds.height().toFloat() 864 var maxY = windowMetrics.bounds.height().toFloat()
761 var maxX = windowMetrics.bounds.width().toFloat() 865 var maxX = windowMetrics.bounds.width().toFloat()
762 var minY = 0 866 var minY = 0
@@ -769,9 +873,9 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
769 val insets = context.windowManager.currentWindowMetrics.windowInsets.displayCutout 873 val insets = context.windowManager.currentWindowMetrics.windowInsets.displayCutout
770 if (insets != null) { 874 if (insets != null) {
771 if (insets.boundingRectTop.bottom != 0 && insets.boundingRectTop.bottom > maxY / 2) 875 if (insets.boundingRectTop.bottom != 0 && insets.boundingRectTop.bottom > maxY / 2)
772 insets.boundingRectTop.bottom.toFloat() else maxY 876 maxY = insets.boundingRectTop.bottom.toFloat()
773 if (insets.boundingRectRight.left != 0 && insets.boundingRectRight.left > maxX / 2) 877 if (insets.boundingRectRight.left != 0 && insets.boundingRectRight.left > maxX / 2)
774 insets.boundingRectRight.left.toFloat() else maxX 878 maxX = insets.boundingRectRight.left.toFloat()
775 879
776 minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left 880 minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left
777 minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom 881 minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom
@@ -878,8 +982,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
878 982
879 // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. 983 // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
880 // These were set in the input overlay configuration menu. 984 // These were set in the input overlay configuration menu.
881 val xKey = "$buttonId$orientation-X" 985 val xKey = "$buttonId-X$orientation"
882 val yKey = "$buttonId$orientation-Y" 986 val yKey = "$buttonId-Y$orientation"
883 val drawableXPercent = sPrefs.getFloat(xKey, 0f) 987 val drawableXPercent = sPrefs.getFloat(xKey, 0f)
884 val drawableYPercent = sPrefs.getFloat(yKey, 0f) 988 val drawableYPercent = sPrefs.getFloat(yKey, 0f)
885 val drawableX = (drawableXPercent * max.x + min.x).toInt() 989 val drawableX = (drawableXPercent * max.x + min.x).toInt()
@@ -959,8 +1063,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
959 1063
960 // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay. 1064 // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay.
961 // These were set in the input overlay configuration menu. 1065 // These were set in the input overlay configuration menu.
962 val drawableXPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-X", 0f) 1066 val drawableXPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}-X$orientation", 0f)
963 val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-Y", 0f) 1067 val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}-Y$orientation", 0f)
964 val drawableX = (drawableXPercent * max.x + min.x).toInt() 1068 val drawableX = (drawableXPercent * max.x + min.x).toInt()
965 val drawableY = (drawableYPercent * max.y + min.y).toInt() 1069 val drawableY = (drawableYPercent * max.y + min.y).toInt()
966 val width = overlayDrawable.width 1070 val width = overlayDrawable.width
@@ -1026,8 +1130,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
1026 1130
1027 // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. 1131 // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
1028 // These were set in the input overlay configuration menu. 1132 // These were set in the input overlay configuration menu.
1029 val drawableXPercent = sPrefs.getFloat("$button$orientation-X", 0f) 1133 val drawableXPercent = sPrefs.getFloat("$button-X$orientation", 0f)
1030 val drawableYPercent = sPrefs.getFloat("$button$orientation-Y", 0f) 1134 val drawableYPercent = sPrefs.getFloat("$button-Y$orientation", 0f)
1031 val drawableX = (drawableXPercent * max.x + min.x).toInt() 1135 val drawableX = (drawableXPercent * max.x + min.x).toInt()
1032 val drawableY = (drawableYPercent * max.y + min.y).toInt() 1136 val drawableY = (drawableYPercent * max.y + min.y).toInt()
1033 val outerScale = 1.66f 1137 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 {
11 private val preferences = 11 private val preferences =
12 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) 12 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
13 13
14 // These must match what is defined in src/core/settings.h
15 const val LayoutOption_Default = 0
16 const val LayoutOption_SingleScreen = 1
17 const val LayoutOption_LargeScreen = 2
18 const val LayoutOption_SideScreen = 3
19 const val LayoutOption_MobilePortrait = 4
20 const val LayoutOption_MobileLandscape = 5
21
22 var joystickRelCenter: Boolean 14 var joystickRelCenter: Boolean
23 get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, true) 15 get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, true)
24 set(value) { 16 set(value) {
@@ -41,16 +33,6 @@ object EmulationMenuSettings {
41 .apply() 33 .apply()
42 } 34 }
43 35
44 var landscapeScreenLayout: Int
45 get() = preferences.getInt(
46 Settings.PREF_MENU_SETTINGS_LANDSCAPE,
47 LayoutOption_MobileLandscape
48 )
49 set(value) {
50 preferences.edit()
51 .putInt(Settings.PREF_MENU_SETTINGS_LANDSCAPE, value)
52 .apply()
53 }
54 var showFps: Boolean 36 var showFps: Boolean
55 get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, false) 37 get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, false)
56 set(value) { 38 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:
202 return m_is_running; 202 return m_is_running;
203 } 203 }
204 204
205 bool IsPaused() const {
206 std::scoped_lock lock(m_mutex);
207 return m_is_running && m_is_paused;
208 }
209
205 const Core::PerfStatsResults& PerfStats() const { 210 const Core::PerfStatsResults& PerfStats() const {
206 std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex); 211 std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex);
207 return m_perf_stats; 212 return m_perf_stats;
@@ -287,11 +292,13 @@ public:
287 void PauseEmulation() { 292 void PauseEmulation() {
288 std::scoped_lock lock(m_mutex); 293 std::scoped_lock lock(m_mutex);
289 m_system.Pause(); 294 m_system.Pause();
295 m_is_paused = true;
290 } 296 }
291 297
292 void UnPauseEmulation() { 298 void UnPauseEmulation() {
293 std::scoped_lock lock(m_mutex); 299 std::scoped_lock lock(m_mutex);
294 m_system.Run(); 300 m_system.Run();
301 m_is_paused = false;
295 } 302 }
296 303
297 void HaltEmulation() { 304 void HaltEmulation() {
@@ -473,6 +480,7 @@ private:
473 std::shared_ptr<FileSys::VfsFilesystem> m_vfs; 480 std::shared_ptr<FileSys::VfsFilesystem> m_vfs;
474 Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized}; 481 Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
475 bool m_is_running{}; 482 bool m_is_running{};
483 bool m_is_paused{};
476 SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{}; 484 SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{};
477 std::unique_ptr<Service::Account::ProfileManager> m_profile_manager; 485 std::unique_ptr<Service::Account::ProfileManager> m_profile_manager;
478 486
@@ -583,6 +591,11 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isRunning([[maybe_unused]] JNIEnv
583 return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning()); 591 return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning());
584} 592}
585 593
594jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused([[maybe_unused]] JNIEnv* env,
595 [[maybe_unused]] jclass clazz) {
596 return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused());
597}
598
586jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly([[maybe_unused]] JNIEnv* env, 599jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly([[maybe_unused]] JNIEnv* env,
587 [[maybe_unused]] jclass clazz) { 600 [[maybe_unused]] jclass clazz) {
588 return EmulationSession::GetInstance().IsHandheldOnly(); 601 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 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportHeight="24"
5 android:viewportWidth="24">
6 <path
7 android:fillColor="@android:color/white"
8 android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z" />
9</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 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportHeight="24"
5 android:viewportWidth="24">
6 <path
7 android:fillColor="@android:color/white"
8 android:pathData="M8,5v14l11,-7z" />
9</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 @@
1<FrameLayout 1<androidx.fragment.app.FragmentContainerView
2 xmlns:android="http://schemas.android.com/apk/res/android" 2 xmlns:android="http://schemas.android.com/apk/res/android"
3 android:id="@+id/frame_content" 3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 android:id="@+id/fragment_container"
5 android:name="androidx.navigation.fragment.NavHostFragment"
4 android:layout_width="match_parent" 6 android:layout_width="match_parent"
5 android:layout_height="match_parent" 7 android:layout_height="match_parent"
6 android:keepScreenOn="true"> 8 android:keepScreenOn="true"
7 9 app:defaultNavHost="true" />
8 <FrameLayout
9 android:id="@+id/frame_emulation_fragment"
10 android:layout_width="match_parent"
11 android:layout_height="match_parent" />
12
13</FrameLayout>
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 @@
12 android:layout_width="match_parent" 12 android:layout_width="match_parent"
13 android:layout_height="match_parent"> 13 android:layout_height="match_parent">
14 14
15 <!-- This is what everything is rendered to during emulation --> 15 <FrameLayout
16 <org.yuzu.yuzu_emu.views.FixedRatioSurfaceView 16 android:id="@+id/emulation_container"
17 android:id="@+id/surface_emulation"
18 android:layout_width="match_parent" 17 android:layout_width="match_parent"
19 android:layout_height="match_parent" 18 android:layout_height="match_parent">
20 android:layout_gravity="center" 19
21 android:focusable="false" 20 <!-- This is what everything is rendered to during emulation -->
22 android:focusableInTouchMode="false" /> 21 <org.yuzu.yuzu_emu.views.FixedRatioSurfaceView
22 android:id="@+id/surface_emulation"
23 android:layout_width="match_parent"
24 android:layout_height="match_parent"
25 android:layout_gravity="center"
26 android:focusable="false"
27 android:focusableInTouchMode="false" />
28
29 </FrameLayout>
23 30
24 <FrameLayout 31 <FrameLayout
25 android:id="@+id/overlay_container" 32 android:id="@+id/input_container"
26 android:layout_width="match_parent" 33 android:layout_width="match_parent"
27 android:layout_height="match_parent" 34 android:layout_height="match_parent"
28 android:layout_gravity="bottom"> 35 android:layout_gravity="bottom">
29 36
30 <!-- This is the onscreen input overlay --> 37 <!-- This is the onscreen input overlay -->
31 <org.yuzu.yuzu_emu.overlay.InputOverlay 38 <org.yuzu.yuzu_emu.overlay.InputOverlay
32 android:id="@+id/surface_input_overlay" 39 android:id="@+id/surface_input_overlay"
40 android:layout_width="match_parent"
41 android:layout_height="match_parent"
42 android:layout_gravity="center"
43 android:focusable="true"
44 android:focusableInTouchMode="true" />
45
46 <Button
47 style="@style/Widget.Material3.Button.ElevatedButton"
48 android:id="@+id/done_control_config"
49 android:layout_width="wrap_content"
50 android:layout_height="wrap_content"
51 android:layout_gravity="center"
52 android:text="@string/emulation_done"
53 android:visibility="gone" />
54
55 </FrameLayout>
56
57 <FrameLayout
58 android:id="@+id/overlay_container"
33 android:layout_width="match_parent" 59 android:layout_width="match_parent"
34 android:layout_height="match_parent" 60 android:layout_height="match_parent">
35 android:focusable="true"
36 android:focusableInTouchMode="true" />
37 61
38 <TextView 62 <TextView
39 android:id="@+id/show_fps_text" 63 android:id="@+id/show_fps_text"
40 android:layout_width="wrap_content" 64 android:layout_width="wrap_content"
41 android:layout_height="wrap_content" 65 android:layout_height="wrap_content"
42 android:layout_gravity="left" 66 android:layout_gravity="left"
43 android:clickable="false" 67 android:clickable="false"
44 android:focusable="false" 68 android:focusable="false"
45 android:shadowColor="@android:color/black" 69 android:shadowColor="@android:color/black"
46 android:textColor="@android:color/white" 70 android:textColor="@android:color/white"
47 android:textSize="12sp" 71 android:textSize="12sp"
48 tools:ignore="RtlHardcoded" /> 72 tools:ignore="RtlHardcoded" />
49 73
50 <Button
51 style="@style/Widget.Material3.Button.ElevatedButton"
52 android:id="@+id/done_control_config"
53 android:layout_width="wrap_content"
54 android:layout_height="wrap_content"
55 android:layout_gravity="center"
56 android:text="@string/emulation_done"
57 android:visibility="gone" />
58 </FrameLayout> 74 </FrameLayout>
59 75
60 </androidx.coordinatorlayout.widget.CoordinatorLayout> 76 </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 @@
1<?xml version="1.0" encoding="utf-8"?>
2<navigation xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:id="@+id/emulation_navigation"
6 app:startDestination="@id/emulationFragment">
7
8 <fragment
9 android:id="@+id/emulationFragment"
10 android:name="org.yuzu.yuzu_emu.fragments.EmulationFragment"
11 android:label="fragment_emulation"
12 tools:layout="@layout/fragment_emulation" >
13 <argument
14 android:name="game"
15 app:argType="org.yuzu.yuzu_emu.model.Game" />
16 </fragment>
17
18</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 @@
56 android:name="org.yuzu.yuzu_emu.fragments.LicensesFragment" 56 android:name="org.yuzu.yuzu_emu.fragments.LicensesFragment"
57 android:label="LicensesFragment" /> 57 android:label="LicensesFragment" />
58 58
59 <activity
60 android:id="@+id/emulationActivity"
61 android:name="org.yuzu.yuzu_emu.activities.EmulationActivity"
62 android:label="EmulationActivity">
63 <argument
64 android:name="game"
65 app:argType="org.yuzu.yuzu_emu.model.Game" />
66 </activity>
67
68 <action
69 android:id="@+id/action_global_emulationActivity"
70 app:destination="@id/emulationActivity"
71 app:launchSingleTop="true" />
72
59</navigation> 73</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 @@
119 <item>3</item> 119 <item>3</item>
120 </integer-array> 120 </integer-array>
121 121
122 <string-array name="rendererScreenLayoutNames">
123 <item>@string/screen_layout_landscape</item>
124 <item>@string/screen_layout_portrait</item>
125 <item>@string/screen_layout_auto</item>
126 </string-array>
127
128 <integer-array name="rendererScreenLayoutValues">
129 <item>5</item>
130 <item>4</item>
131 <item>0</item>
132 </integer-array>
133
122 <string-array name="rendererAspectRatioNames"> 134 <string-array name="rendererAspectRatioNames">
123 <item>@string/ratio_default</item> 135 <item>@string/ratio_default</item>
124 <item>@string/ratio_force_four_three</item> 136 <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 @@
34 <integer name="SWITCH_BUTTON_DPAD_X">260</integer> 34 <integer name="SWITCH_BUTTON_DPAD_X">260</integer>
35 <integer name="SWITCH_BUTTON_DPAD_Y">790</integer> 35 <integer name="SWITCH_BUTTON_DPAD_Y">790</integer>
36 36
37 <!-- Default SWITCH portrait layout -->
38 <integer name="SWITCH_BUTTON_A_X_PORTRAIT">840</integer>
39 <integer name="SWITCH_BUTTON_A_Y_PORTRAIT">840</integer>
40 <integer name="SWITCH_BUTTON_B_X_PORTRAIT">740</integer>
41 <integer name="SWITCH_BUTTON_B_Y_PORTRAIT">880</integer>
42 <integer name="SWITCH_BUTTON_X_X_PORTRAIT">740</integer>
43 <integer name="SWITCH_BUTTON_X_Y_PORTRAIT">800</integer>
44 <integer name="SWITCH_BUTTON_Y_X_PORTRAIT">640</integer>
45 <integer name="SWITCH_BUTTON_Y_Y_PORTRAIT">840</integer>
46 <integer name="SWITCH_STICK_L_X_PORTRAIT">180</integer>
47 <integer name="SWITCH_STICK_L_Y_PORTRAIT">660</integer>
48 <integer name="SWITCH_STICK_R_X_PORTRAIT">820</integer>
49 <integer name="SWITCH_STICK_R_Y_PORTRAIT">660</integer>
50 <integer name="SWITCH_TRIGGER_L_X_PORTRAIT">140</integer>
51 <integer name="SWITCH_TRIGGER_L_Y_PORTRAIT">260</integer>
52 <integer name="SWITCH_TRIGGER_R_X_PORTRAIT">860</integer>
53 <integer name="SWITCH_TRIGGER_R_Y_PORTRAIT">260</integer>
54 <integer name="SWITCH_TRIGGER_ZL_X_PORTRAIT">140</integer>
55 <integer name="SWITCH_TRIGGER_ZL_Y_PORTRAIT">200</integer>
56 <integer name="SWITCH_TRIGGER_ZR_X_PORTRAIT">860</integer>
57 <integer name="SWITCH_TRIGGER_ZR_Y_PORTRAIT">200</integer>
58 <integer name="SWITCH_BUTTON_MINUS_X_PORTRAIT">440</integer>
59 <integer name="SWITCH_BUTTON_MINUS_Y_PORTRAIT">950</integer>
60 <integer name="SWITCH_BUTTON_PLUS_X_PORTRAIT">560</integer>
61 <integer name="SWITCH_BUTTON_PLUS_Y_PORTRAIT">950</integer>
62 <integer name="SWITCH_BUTTON_HOME_X_PORTRAIT">680</integer>
63 <integer name="SWITCH_BUTTON_HOME_Y_PORTRAIT">950</integer>
64 <integer name="SWITCH_BUTTON_CAPTURE_X_PORTRAIT">320</integer>
65 <integer name="SWITCH_BUTTON_CAPTURE_Y_PORTRAIT">950</integer>
66 <integer name="SWITCH_BUTTON_DPAD_X_PORTRAIT">240</integer>
67 <integer name="SWITCH_BUTTON_DPAD_Y_PORTRAIT">840</integer>
68
69 <!-- Default SWITCH foldable layout -->
70 <integer name="SWITCH_BUTTON_A_X_FOLDABLE">840</integer>
71 <integer name="SWITCH_BUTTON_A_Y_FOLDABLE">390</integer>
72 <integer name="SWITCH_BUTTON_B_X_FOLDABLE">740</integer>
73 <integer name="SWITCH_BUTTON_B_Y_FOLDABLE">430</integer>
74 <integer name="SWITCH_BUTTON_X_X_FOLDABLE">740</integer>
75 <integer name="SWITCH_BUTTON_X_Y_FOLDABLE">350</integer>
76 <integer name="SWITCH_BUTTON_Y_X_FOLDABLE">640</integer>
77 <integer name="SWITCH_BUTTON_Y_Y_FOLDABLE">390</integer>
78 <integer name="SWITCH_STICK_L_X_FOLDABLE">180</integer>
79 <integer name="SWITCH_STICK_L_Y_FOLDABLE">250</integer>
80 <integer name="SWITCH_STICK_R_X_FOLDABLE">820</integer>
81 <integer name="SWITCH_STICK_R_Y_FOLDABLE">250</integer>
82 <integer name="SWITCH_TRIGGER_L_X_FOLDABLE">140</integer>
83 <integer name="SWITCH_TRIGGER_L_Y_FOLDABLE">130</integer>
84 <integer name="SWITCH_TRIGGER_R_X_FOLDABLE">860</integer>
85 <integer name="SWITCH_TRIGGER_R_Y_FOLDABLE">130</integer>
86 <integer name="SWITCH_TRIGGER_ZL_X_FOLDABLE">140</integer>
87 <integer name="SWITCH_TRIGGER_ZL_Y_FOLDABLE">70</integer>
88 <integer name="SWITCH_TRIGGER_ZR_X_FOLDABLE">860</integer>
89 <integer name="SWITCH_TRIGGER_ZR_Y_FOLDABLE">70</integer>
90 <integer name="SWITCH_BUTTON_MINUS_X_FOLDABLE">440</integer>
91 <integer name="SWITCH_BUTTON_MINUS_Y_FOLDABLE">470</integer>
92 <integer name="SWITCH_BUTTON_PLUS_X_FOLDABLE">560</integer>
93 <integer name="SWITCH_BUTTON_PLUS_Y_FOLDABLE">470</integer>
94 <integer name="SWITCH_BUTTON_HOME_X_FOLDABLE">680</integer>
95 <integer name="SWITCH_BUTTON_HOME_Y_FOLDABLE">470</integer>
96 <integer name="SWITCH_BUTTON_CAPTURE_X_FOLDABLE">320</integer>
97 <integer name="SWITCH_BUTTON_CAPTURE_Y_FOLDABLE">470</integer>
98 <integer name="SWITCH_BUTTON_DPAD_X_FOLDABLE">240</integer>
99 <integer name="SWITCH_BUTTON_DPAD_Y_FOLDABLE">390</integer>
100
37</resources> 101</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 @@
162 <string name="renderer_accuracy">Accuracy level</string> 162 <string name="renderer_accuracy">Accuracy level</string>
163 <string name="renderer_resolution">Resolution (Handheld/Docked)</string> 163 <string name="renderer_resolution">Resolution (Handheld/Docked)</string>
164 <string name="renderer_vsync">VSync mode</string> 164 <string name="renderer_vsync">VSync mode</string>
165 <string name="renderer_screen_layout">Orientation</string>
165 <string name="renderer_aspect_ratio">Aspect ratio</string> 166 <string name="renderer_aspect_ratio">Aspect ratio</string>
166 <string name="renderer_scaling_filter">Window adapting filter</string> 167 <string name="renderer_scaling_filter">Window adapting filter</string>
167 <string name="renderer_anti_aliasing">Anti-aliasing method</string> 168 <string name="renderer_anti_aliasing">Anti-aliasing method</string>
@@ -326,6 +327,11 @@
326 <string name="anti_aliasing_fxaa">FXAA</string> 327 <string name="anti_aliasing_fxaa">FXAA</string>
327 <string name="anti_aliasing_smaa">SMAA</string> 328 <string name="anti_aliasing_smaa">SMAA</string>
328 329
330 <!-- Screen Layouts -->
331 <string name="screen_layout_landscape">Landscape</string>
332 <string name="screen_layout_portrait">Portrait</string>
333 <string name="screen_layout_auto">Auto</string>
334
329 <!-- Aspect Ratios --> 335 <!-- Aspect Ratios -->
330 <string name="ratio_default">Default (16:9)</string> 336 <string name="ratio_default">Default (16:9)</string>
331 <string name="ratio_force_four_three">Force 4:3</string> 337 <string name="ratio_force_four_three">Force 4:3</string>
@@ -364,6 +370,12 @@
364 <string name="use_black_backgrounds">Black backgrounds</string> 370 <string name="use_black_backgrounds">Black backgrounds</string>
365 <string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string> 371 <string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string>
366 372
373 <!-- Picture-In-Picture -->
374 <string name="picture_in_picture">Picture in Picture</string>
375 <string name="picture_in_picture_description">Minimize window when placed in the background</string>
376 <string name="pause">Pause</string>
377 <string name="play">Play</string>
378
367 <!-- Licenses screen strings --> 379 <!-- Licenses screen strings -->
368 <string name="licenses">Licenses</string> 380 <string name="licenses">Licenses</string>
369 <string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string> 381 <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 {
11tasks.register("clean").configure { 11tasks.register("clean").configure {
12 delete(rootProject.buildDir) 12 delete(rootProject.buildDir)
13} 13}
14
15buildscript {
16 repositories {
17 google()
18 }
19 dependencies {
20 classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.6.0")
21 }
22}
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;
25 25
26namespace { 26namespace {
27 27
28constexpr size_t MaxOpenFiles = 512;
29
28constexpr FS::FileAccessMode ModeFlagsToFileAccessMode(Mode mode) { 30constexpr FS::FileAccessMode ModeFlagsToFileAccessMode(Mode mode) {
29 switch (mode) { 31 switch (mode) {
30 case Mode::Read: 32 case Mode::Read:
@@ -73,28 +75,30 @@ VfsEntryType RealVfsFilesystem::GetEntryType(std::string_view path_) const {
73VirtualFile RealVfsFilesystem::OpenFile(std::string_view path_, Mode perms) { 75VirtualFile RealVfsFilesystem::OpenFile(std::string_view path_, Mode perms) {
74 const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault); 76 const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault);
75 77
76 if (const auto weak_iter = cache.find(path); weak_iter != cache.cend()) { 78 if (auto it = cache.find(path); it != cache.end()) {
77 const auto& weak = weak_iter->second; 79 if (auto file = it->second.lock(); file) {
78 80 return file;
79 if (!weak.expired()) {
80 return std::shared_ptr<RealVfsFile>(new RealVfsFile(*this, weak.lock(), path, perms));
81 } 81 }
82 } 82 }
83 83
84 auto backing = FS::FileOpen(path, ModeFlagsToFileAccessMode(perms), FS::FileType::BinaryFile); 84 if (!FS::Exists(path) || !FS::IsFile(path)) {
85
86 if (!backing) {
87 return nullptr; 85 return nullptr;
88 } 86 }
89 87
90 cache.insert_or_assign(path, std::move(backing)); 88 auto reference = std::make_unique<FileReference>();
89 this->InsertReferenceIntoList(*reference);
91 90
92 // Cannot use make_shared as RealVfsFile constructor is private 91 auto file =
93 return std::shared_ptr<RealVfsFile>(new RealVfsFile(*this, backing, path, perms)); 92 std::shared_ptr<RealVfsFile>(new RealVfsFile(*this, std::move(reference), path, perms));
93 cache[path] = file;
94
95 return file;
94} 96}
95 97
96VirtualFile RealVfsFilesystem::CreateFile(std::string_view path_, Mode perms) { 98VirtualFile RealVfsFilesystem::CreateFile(std::string_view path_, Mode perms) {
97 const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault); 99 const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault);
100 cache.erase(path);
101
98 // Current usages of CreateFile expect to delete the contents of an existing file. 102 // Current usages of CreateFile expect to delete the contents of an existing file.
99 if (FS::IsFile(path)) { 103 if (FS::IsFile(path)) {
100 FS::IOFile temp{path, FS::FileAccessMode::Write, FS::FileType::BinaryFile}; 104 FS::IOFile temp{path, FS::FileAccessMode::Write, FS::FileType::BinaryFile};
@@ -123,51 +127,22 @@ VirtualFile RealVfsFilesystem::CopyFile(std::string_view old_path_, std::string_
123VirtualFile RealVfsFilesystem::MoveFile(std::string_view old_path_, std::string_view new_path_) { 127VirtualFile RealVfsFilesystem::MoveFile(std::string_view old_path_, std::string_view new_path_) {
124 const auto old_path = FS::SanitizePath(old_path_, FS::DirectorySeparator::PlatformDefault); 128 const auto old_path = FS::SanitizePath(old_path_, FS::DirectorySeparator::PlatformDefault);
125 const auto new_path = FS::SanitizePath(new_path_, FS::DirectorySeparator::PlatformDefault); 129 const auto new_path = FS::SanitizePath(new_path_, FS::DirectorySeparator::PlatformDefault);
126 const auto cached_file_iter = cache.find(old_path); 130 cache.erase(old_path);
127 131 cache.erase(new_path);
128 if (cached_file_iter != cache.cend()) { 132 if (!FS::RenameFile(old_path, new_path)) {
129 auto file = cached_file_iter->second.lock();
130
131 if (!cached_file_iter->second.expired()) {
132 file->Close();
133 }
134
135 if (!FS::RenameFile(old_path, new_path)) {
136 return nullptr;
137 }
138
139 cache.erase(old_path);
140 file->Open(new_path, FS::FileAccessMode::Read, FS::FileType::BinaryFile);
141 if (file->IsOpen()) {
142 cache.insert_or_assign(new_path, std::move(file));
143 } else {
144 LOG_ERROR(Service_FS, "Failed to open path {} in order to re-cache it", new_path);
145 }
146 } else {
147 ASSERT(false);
148 return nullptr; 133 return nullptr;
149 } 134 }
150
151 return OpenFile(new_path, Mode::ReadWrite); 135 return OpenFile(new_path, Mode::ReadWrite);
152} 136}
153 137
154bool RealVfsFilesystem::DeleteFile(std::string_view path_) { 138bool RealVfsFilesystem::DeleteFile(std::string_view path_) {
155 const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault); 139 const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault);
156 const auto cached_iter = cache.find(path); 140 cache.erase(path);
157
158 if (cached_iter != cache.cend()) {
159 if (!cached_iter->second.expired()) {
160 cached_iter->second.lock()->Close();
161 }
162 cache.erase(path);
163 }
164
165 return FS::RemoveFile(path); 141 return FS::RemoveFile(path);
166} 142}
167 143
168VirtualDir RealVfsFilesystem::OpenDirectory(std::string_view path_, Mode perms) { 144VirtualDir RealVfsFilesystem::OpenDirectory(std::string_view path_, Mode perms) {
169 const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault); 145 const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault);
170 // Cannot use make_shared as RealVfsDirectory constructor is private
171 return std::shared_ptr<RealVfsDirectory>(new RealVfsDirectory(*this, path, perms)); 146 return std::shared_ptr<RealVfsDirectory>(new RealVfsDirectory(*this, path, perms));
172} 147}
173 148
@@ -176,7 +151,6 @@ VirtualDir RealVfsFilesystem::CreateDirectory(std::string_view path_, Mode perms
176 if (!FS::CreateDirs(path)) { 151 if (!FS::CreateDirs(path)) {
177 return nullptr; 152 return nullptr;
178 } 153 }
179 // Cannot use make_shared as RealVfsDirectory constructor is private
180 return std::shared_ptr<RealVfsDirectory>(new RealVfsDirectory(*this, path, perms)); 154 return std::shared_ptr<RealVfsDirectory>(new RealVfsDirectory(*this, path, perms));
181} 155}
182 156
@@ -194,73 +168,102 @@ VirtualDir RealVfsFilesystem::MoveDirectory(std::string_view old_path_,
194 if (!FS::RenameDir(old_path, new_path)) { 168 if (!FS::RenameDir(old_path, new_path)) {
195 return nullptr; 169 return nullptr;
196 } 170 }
171 return OpenDirectory(new_path, Mode::ReadWrite);
172}
197 173
198 for (auto& kv : cache) { 174bool RealVfsFilesystem::DeleteDirectory(std::string_view path_) {
199 // If the path in the cache doesn't start with old_path, then bail on this file. 175 const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault);
200 if (kv.first.rfind(old_path, 0) != 0) { 176 return FS::RemoveDirRecursively(path);
201 continue; 177}
202 }
203 178
204 const auto file_old_path = 179void RealVfsFilesystem::RefreshReference(const std::string& path, Mode perms,
205 FS::SanitizePath(kv.first, FS::DirectorySeparator::PlatformDefault); 180 FileReference& reference) {
206 auto file_new_path = FS::SanitizePath(new_path + '/' + kv.first.substr(old_path.size()), 181 // Temporarily remove from list.
207 FS::DirectorySeparator::PlatformDefault); 182 this->RemoveReferenceFromList(reference);
208 const auto& cached = cache[file_old_path];
209 183
210 if (cached.expired()) { 184 // Restore file if needed.
211 continue; 185 if (!reference.file) {
212 } 186 this->EvictSingleReference();
213 187
214 auto file = cached.lock(); 188 reference.file =
215 cache.erase(file_old_path); 189 FS::FileOpen(path, ModeFlagsToFileAccessMode(perms), FS::FileType::BinaryFile);
216 file->Open(file_new_path, FS::FileAccessMode::Read, FS::FileType::BinaryFile); 190 if (reference.file) {
217 if (file->IsOpen()) { 191 num_open_files++;
218 cache.insert_or_assign(std::move(file_new_path), std::move(file));
219 } else {
220 LOG_ERROR(Service_FS, "Failed to open path {} in order to re-cache it", file_new_path);
221 } 192 }
222 } 193 }
223 194
224 return OpenDirectory(new_path, Mode::ReadWrite); 195 // Reinsert into list.
196 this->InsertReferenceIntoList(reference);
225} 197}
226 198
227bool RealVfsFilesystem::DeleteDirectory(std::string_view path_) { 199void RealVfsFilesystem::DropReference(std::unique_ptr<FileReference>&& reference) {
228 const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault); 200 // Remove from list.
201 this->RemoveReferenceFromList(*reference);
229 202
230 for (auto& kv : cache) { 203 // Close the file.
231 // If the path in the cache doesn't start with path, then bail on this file. 204 if (reference->file) {
232 if (kv.first.rfind(path, 0) != 0) { 205 reference->file.reset();
233 continue; 206 num_open_files--;
234 } 207 }
208}
235 209
236 const auto& entry = cache[kv.first]; 210void RealVfsFilesystem::EvictSingleReference() {
237 if (!entry.expired()) { 211 if (num_open_files < MaxOpenFiles || open_references.empty()) {
238 entry.lock()->Close(); 212 return;
239 } 213 }
214
215 // Get and remove from list.
216 auto& reference = open_references.back();
217 this->RemoveReferenceFromList(reference);
240 218
241 cache.erase(kv.first); 219 // Close the file.
220 if (reference.file) {
221 reference.file.reset();
222 num_open_files--;
242 } 223 }
243 224
244 return FS::RemoveDirRecursively(path); 225 // Reinsert into closed list.
226 this->InsertReferenceIntoList(reference);
227}
228
229void RealVfsFilesystem::InsertReferenceIntoList(FileReference& reference) {
230 if (reference.file) {
231 open_references.push_front(reference);
232 } else {
233 closed_references.push_front(reference);
234 }
235}
236
237void RealVfsFilesystem::RemoveReferenceFromList(FileReference& reference) {
238 if (reference.file) {
239 open_references.erase(open_references.iterator_to(reference));
240 } else {
241 closed_references.erase(closed_references.iterator_to(reference));
242 }
245} 243}
246 244
247RealVfsFile::RealVfsFile(RealVfsFilesystem& base_, std::shared_ptr<FS::IOFile> backing_, 245RealVfsFile::RealVfsFile(RealVfsFilesystem& base_, std::unique_ptr<FileReference> reference_,
248 const std::string& path_, Mode perms_) 246 const std::string& path_, Mode perms_)
249 : base(base_), backing(std::move(backing_)), path(path_), parent_path(FS::GetParentPath(path_)), 247 : base(base_), reference(std::move(reference_)), path(path_),
250 path_components(FS::SplitPathComponents(path_)), perms(perms_) {} 248 parent_path(FS::GetParentPath(path_)), path_components(FS::SplitPathComponents(path_)),
249 perms(perms_) {}
251 250
252RealVfsFile::~RealVfsFile() = default; 251RealVfsFile::~RealVfsFile() {
252 base.DropReference(std::move(reference));
253}
253 254
254std::string RealVfsFile::GetName() const { 255std::string RealVfsFile::GetName() const {
255 return path_components.back(); 256 return path_components.back();
256} 257}
257 258
258std::size_t RealVfsFile::GetSize() const { 259std::size_t RealVfsFile::GetSize() const {
259 return backing->GetSize(); 260 base.RefreshReference(path, perms, *reference);
261 return reference->file ? reference->file->GetSize() : 0;
260} 262}
261 263
262bool RealVfsFile::Resize(std::size_t new_size) { 264bool RealVfsFile::Resize(std::size_t new_size) {
263 return backing->SetSize(new_size); 265 base.RefreshReference(path, perms, *reference);
266 return reference->file ? reference->file->SetSize(new_size) : false;
264} 267}
265 268
266VirtualDir RealVfsFile::GetContainingDirectory() const { 269VirtualDir RealVfsFile::GetContainingDirectory() const {
@@ -276,27 +279,25 @@ bool RealVfsFile::IsReadable() const {
276} 279}
277 280
278std::size_t RealVfsFile::Read(u8* data, std::size_t length, std::size_t offset) const { 281std::size_t RealVfsFile::Read(u8* data, std::size_t length, std::size_t offset) const {
279 if (!backing->Seek(static_cast<s64>(offset))) { 282 base.RefreshReference(path, perms, *reference);
283 if (!reference->file || !reference->file->Seek(static_cast<s64>(offset))) {
280 return 0; 284 return 0;
281 } 285 }
282 return backing->ReadSpan(std::span{data, length}); 286 return reference->file->ReadSpan(std::span{data, length});
283} 287}
284 288
285std::size_t RealVfsFile::Write(const u8* data, std::size_t length, std::size_t offset) { 289std::size_t RealVfsFile::Write(const u8* data, std::size_t length, std::size_t offset) {
286 if (!backing->Seek(static_cast<s64>(offset))) { 290 base.RefreshReference(path, perms, *reference);
291 if (!reference->file || !reference->file->Seek(static_cast<s64>(offset))) {
287 return 0; 292 return 0;
288 } 293 }
289 return backing->WriteSpan(std::span{data, length}); 294 return reference->file->WriteSpan(std::span{data, length});
290} 295}
291 296
292bool RealVfsFile::Rename(std::string_view name) { 297bool RealVfsFile::Rename(std::string_view name) {
293 return base.MoveFile(path, parent_path + '/' + std::string(name)) != nullptr; 298 return base.MoveFile(path, parent_path + '/' + std::string(name)) != nullptr;
294} 299}
295 300
296void RealVfsFile::Close() {
297 backing->Close();
298}
299
300// TODO(DarkLordZach): MSVC would not let me combine the following two functions using 'if 301// TODO(DarkLordZach): MSVC would not let me combine the following two functions using 'if
301// constexpr' because there is a compile error in the branch not used. 302// constexpr' because there is a compile error in the branch not used.
302 303
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 @@
3 3
4#pragma once 4#pragma once
5 5
6#include <map>
6#include <string_view> 7#include <string_view>
7#include <boost/container/flat_map.hpp> 8#include "common/intrusive_list.h"
8#include "core/file_sys/mode.h" 9#include "core/file_sys/mode.h"
9#include "core/file_sys/vfs.h" 10#include "core/file_sys/vfs.h"
10 11
@@ -14,6 +15,11 @@ class IOFile;
14 15
15namespace FileSys { 16namespace FileSys {
16 17
18struct FileReference : public Common::IntrusiveListBaseNode<FileReference> {
19 std::shared_ptr<Common::FS::IOFile> file{};
20};
21
22class RealVfsFile;
17class RealVfsFilesystem : public VfsFilesystem { 23class RealVfsFilesystem : public VfsFilesystem {
18public: 24public:
19 RealVfsFilesystem(); 25 RealVfsFilesystem();
@@ -35,7 +41,21 @@ public:
35 bool DeleteDirectory(std::string_view path) override; 41 bool DeleteDirectory(std::string_view path) override;
36 42
37private: 43private:
38 boost::container::flat_map<std::string, std::weak_ptr<Common::FS::IOFile>> cache; 44 using ReferenceListType = Common::IntrusiveListBaseTraits<FileReference>::ListType;
45 std::map<std::string, std::weak_ptr<VfsFile>, std::less<>> cache;
46 ReferenceListType open_references;
47 ReferenceListType closed_references;
48 size_t num_open_files{};
49
50private:
51 friend class RealVfsFile;
52 void RefreshReference(const std::string& path, Mode perms, FileReference& reference);
53 void DropReference(std::unique_ptr<FileReference>&& reference);
54 void EvictSingleReference();
55
56private:
57 void InsertReferenceIntoList(FileReference& reference);
58 void RemoveReferenceFromList(FileReference& reference);
39}; 59};
40 60
41// An implementation of VfsFile that represents a file on the user's computer. 61// An implementation of VfsFile that represents a file on the user's computer.
@@ -57,13 +77,11 @@ public:
57 bool Rename(std::string_view name) override; 77 bool Rename(std::string_view name) override;
58 78
59private: 79private:
60 RealVfsFile(RealVfsFilesystem& base, std::shared_ptr<Common::FS::IOFile> backing, 80 RealVfsFile(RealVfsFilesystem& base, std::unique_ptr<FileReference> reference,
61 const std::string& path, Mode perms = Mode::Read); 81 const std::string& path, Mode perms = Mode::Read);
62 82
63 void Close();
64
65 RealVfsFilesystem& base; 83 RealVfsFilesystem& base;
66 std::shared_ptr<Common::FS::IOFile> backing; 84 std::unique_ptr<FileReference> reference;
67 std::string path; 85 std::string path;
68 std::string parent_path; 86 std::string parent_path;
69 std::vector<std::string> path_components; 87 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() {
715 715
716template <class P> 716template <class P>
717void BufferCache<P>::BindHostVertexBuffers() { 717void BufferCache<P>::BindHostVertexBuffers() {
718 HostBindings host_bindings; 718 HostBindings<typename P::Buffer> host_bindings;
719 bool any_valid{false}; 719 bool any_valid{false};
720 auto& flags = maxwell3d->dirty.flags; 720 auto& flags = maxwell3d->dirty.flags;
721 for (u32 index = 0; index < NUM_VERTEX_BUFFERS; ++index) { 721 for (u32 index = 0; index < NUM_VERTEX_BUFFERS; ++index) {
@@ -741,7 +741,7 @@ void BufferCache<P>::BindHostVertexBuffers() {
741 const u32 stride = maxwell3d->regs.vertex_streams[index].stride; 741 const u32 stride = maxwell3d->regs.vertex_streams[index].stride;
742 const u32 offset = buffer.Offset(binding.cpu_addr); 742 const u32 offset = buffer.Offset(binding.cpu_addr);
743 743
744 host_bindings.buffers.push_back(reinterpret_cast<void*>(&buffer)); 744 host_bindings.buffers.push_back(&buffer);
745 host_bindings.offsets.push_back(offset); 745 host_bindings.offsets.push_back(offset);
746 host_bindings.sizes.push_back(binding.size); 746 host_bindings.sizes.push_back(binding.size);
747 host_bindings.strides.push_back(stride); 747 host_bindings.strides.push_back(stride);
@@ -900,7 +900,7 @@ void BufferCache<P>::BindHostTransformFeedbackBuffers() {
900 if (maxwell3d->regs.transform_feedback_enabled == 0) { 900 if (maxwell3d->regs.transform_feedback_enabled == 0) {
901 return; 901 return;
902 } 902 }
903 HostBindings host_bindings; 903 HostBindings<typename P::Buffer> host_bindings;
904 for (u32 index = 0; index < NUM_TRANSFORM_FEEDBACK_BUFFERS; ++index) { 904 for (u32 index = 0; index < NUM_TRANSFORM_FEEDBACK_BUFFERS; ++index) {
905 const Binding& binding = channel_state->transform_feedback_buffers[index]; 905 const Binding& binding = channel_state->transform_feedback_buffers[index];
906 if (maxwell3d->regs.transform_feedback.controls[index].varying_count == 0 && 906 if (maxwell3d->regs.transform_feedback.controls[index].varying_count == 0 &&
@@ -913,7 +913,7 @@ void BufferCache<P>::BindHostTransformFeedbackBuffers() {
913 SynchronizeBuffer(buffer, binding.cpu_addr, size); 913 SynchronizeBuffer(buffer, binding.cpu_addr, size);
914 914
915 const u32 offset = buffer.Offset(binding.cpu_addr); 915 const u32 offset = buffer.Offset(binding.cpu_addr);
916 host_bindings.buffers.push_back(reinterpret_cast<void*>(&buffer)); 916 host_bindings.buffers.push_back(&buffer);
917 host_bindings.offsets.push_back(offset); 917 host_bindings.offsets.push_back(offset);
918 host_bindings.sizes.push_back(binding.size); 918 host_bindings.sizes.push_back(binding.size);
919 } 919 }
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{
105 .buffer_id = NULL_BUFFER_ID, 105 .buffer_id = NULL_BUFFER_ID,
106}; 106};
107 107
108template <typename Buffer>
108struct HostBindings { 109struct HostBindings {
109 boost::container::small_vector<void*, NUM_VERTEX_BUFFERS> buffers; 110 boost::container::small_vector<Buffer*, NUM_VERTEX_BUFFERS> buffers;
110 boost::container::small_vector<u64, NUM_VERTEX_BUFFERS> offsets; 111 boost::container::small_vector<u64, NUM_VERTEX_BUFFERS> offsets;
111 boost::container::small_vector<u64, NUM_VERTEX_BUFFERS> sizes; 112 boost::container::small_vector<u64, NUM_VERTEX_BUFFERS> sizes;
112 boost::container::small_vector<u64, NUM_VERTEX_BUFFERS> strides; 113 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,
232 } 232 }
233} 233}
234 234
235void BufferCacheRuntime::BindVertexBuffers(VideoCommon::HostBindings& bindings) { 235void BufferCacheRuntime::BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bindings) {
236 for (u32 index = 0; index < bindings.buffers.size(); index++) { 236 for (u32 index = 0; index < bindings.buffers.size(); ++index) {
237 BindVertexBuffer( 237 BindVertexBuffer(bindings.min_index + index, *bindings.buffers[index],
238 bindings.min_index + index, *reinterpret_cast<Buffer*>(bindings.buffers[index]), 238 static_cast<u32>(bindings.offsets[index]),
239 static_cast<u32>(bindings.offsets[index]), static_cast<u32>(bindings.sizes[index]), 239 static_cast<u32>(bindings.sizes[index]),
240 static_cast<u32>(bindings.strides[index])); 240 static_cast<u32>(bindings.strides[index]));
241 } 241 }
242} 242}
243 243
@@ -329,10 +329,9 @@ void BufferCacheRuntime::BindTransformFeedbackBuffer(u32 index, Buffer& buffer,
329 static_cast<GLintptr>(offset), static_cast<GLsizeiptr>(size)); 329 static_cast<GLintptr>(offset), static_cast<GLsizeiptr>(size));
330} 330}
331 331
332void BufferCacheRuntime::BindTransformFeedbackBuffers(VideoCommon::HostBindings& bindings) { 332void BufferCacheRuntime::BindTransformFeedbackBuffers(VideoCommon::HostBindings<Buffer>& bindings) {
333 for (u32 index = 0; index < bindings.buffers.size(); index++) { 333 for (u32 index = 0; index < bindings.buffers.size(); ++index) {
334 glBindBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, index, 334 glBindBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, index, bindings.buffers[index]->Handle(),
335 reinterpret_cast<Buffer*>(bindings.buffers[index])->Handle(),
336 static_cast<GLintptr>(bindings.offsets[index]), 335 static_cast<GLintptr>(bindings.offsets[index]),
337 static_cast<GLsizeiptr>(bindings.sizes[index])); 336 static_cast<GLsizeiptr>(bindings.sizes[index]));
338 } 337 }
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:
87 void BindIndexBuffer(Buffer& buffer, u32 offset, u32 size); 87 void BindIndexBuffer(Buffer& buffer, u32 offset, u32 size);
88 88
89 void BindVertexBuffer(u32 index, Buffer& buffer, u32 offset, u32 size, u32 stride); 89 void BindVertexBuffer(u32 index, Buffer& buffer, u32 offset, u32 size, u32 stride);
90 void BindVertexBuffers(VideoCommon::HostBindings& bindings); 90
91 void BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bindings);
91 92
92 void BindUniformBuffer(size_t stage, u32 binding_index, Buffer& buffer, u32 offset, u32 size); 93 void BindUniformBuffer(size_t stage, u32 binding_index, Buffer& buffer, u32 offset, u32 size);
93 94
@@ -100,7 +101,8 @@ public:
100 bool is_written); 101 bool is_written);
101 102
102 void BindTransformFeedbackBuffer(u32 index, Buffer& buffer, u32 offset, u32 size); 103 void BindTransformFeedbackBuffer(u32 index, Buffer& buffer, u32 offset, u32 size);
103 void BindTransformFeedbackBuffers(VideoCommon::HostBindings& bindings); 104
105 void BindTransformFeedbackBuffers(VideoCommon::HostBindings<Buffer>& bindings);
104 106
105 void BindTextureBuffer(Buffer& buffer, u32 offset, u32 size, 107 void BindTextureBuffer(Buffer& buffer, u32 offset, u32 size,
106 VideoCore::Surface::PixelFormat format); 108 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
501 } 501 }
502} 502}
503 503
504void BufferCacheRuntime::BindVertexBuffers(VideoCommon::HostBindings& bindings) { 504void BufferCacheRuntime::BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bindings) {
505 boost::container::small_vector<VkBuffer, 32> buffer_handles; 505 boost::container::small_vector<VkBuffer, 32> buffer_handles;
506 for (u32 index = 0; index < bindings.buffers.size(); index++) { 506 for (u32 index = 0; index < bindings.buffers.size(); ++index) {
507 auto& buffer = *reinterpret_cast<Buffer*>(bindings.buffers[index]); 507 auto handle = bindings.buffers[index]->Handle();
508 auto handle = buffer.Handle();
509 if (handle == VK_NULL_HANDLE) { 508 if (handle == VK_NULL_HANDLE) {
510 bindings.offsets[index] = 0; 509 bindings.offsets[index] = 0;
511 bindings.sizes[index] = VK_WHOLE_SIZE; 510 bindings.sizes[index] = VK_WHOLE_SIZE;
@@ -521,16 +520,13 @@ void BufferCacheRuntime::BindVertexBuffers(VideoCommon::HostBindings& bindings)
521 buffer_handles = buffer_handles](vk::CommandBuffer cmdbuf) { 520 buffer_handles = buffer_handles](vk::CommandBuffer cmdbuf) {
522 cmdbuf.BindVertexBuffers2EXT( 521 cmdbuf.BindVertexBuffers2EXT(
523 bindings.min_index, bindings.max_index - bindings.min_index, buffer_handles.data(), 522 bindings.min_index, bindings.max_index - bindings.min_index, buffer_handles.data(),
524 reinterpret_cast<const VkDeviceSize*>(bindings.offsets.data()), 523 bindings.offsets.data(), bindings.sizes.data(), bindings.strides.data());
525 reinterpret_cast<const VkDeviceSize*>(bindings.sizes.data()),
526 reinterpret_cast<const VkDeviceSize*>(bindings.strides.data()));
527 }); 524 });
528 } else { 525 } else {
529 scheduler.Record([bindings = bindings, 526 scheduler.Record([bindings = bindings,
530 buffer_handles = buffer_handles](vk::CommandBuffer cmdbuf) { 527 buffer_handles = buffer_handles](vk::CommandBuffer cmdbuf) {
531 cmdbuf.BindVertexBuffers( 528 cmdbuf.BindVertexBuffers(bindings.min_index, bindings.max_index - bindings.min_index,
532 bindings.min_index, bindings.max_index - bindings.min_index, buffer_handles.data(), 529 buffer_handles.data(), bindings.offsets.data());
533 reinterpret_cast<const VkDeviceSize*>(bindings.offsets.data()));
534 }); 530 });
535 } 531 }
536} 532}
@@ -556,22 +552,20 @@ void BufferCacheRuntime::BindTransformFeedbackBuffer(u32 index, VkBuffer buffer,
556 }); 552 });
557} 553}
558 554
559void BufferCacheRuntime::BindTransformFeedbackBuffers(VideoCommon::HostBindings& bindings) { 555void BufferCacheRuntime::BindTransformFeedbackBuffers(VideoCommon::HostBindings<Buffer>& bindings) {
560 if (!device.IsExtTransformFeedbackSupported()) { 556 if (!device.IsExtTransformFeedbackSupported()) {
561 // Already logged in the rasterizer 557 // Already logged in the rasterizer
562 return; 558 return;
563 } 559 }
564 boost::container::small_vector<VkBuffer, 4> buffer_handles; 560 boost::container::small_vector<VkBuffer, 4> buffer_handles;
565 for (u32 index = 0; index < bindings.buffers.size(); index++) { 561 for (u32 index = 0; index < bindings.buffers.size(); ++index) {
566 auto& buffer = *reinterpret_cast<Buffer*>(bindings.buffers[index]); 562 buffer_handles.push_back(bindings.buffers[index]->Handle());
567 buffer_handles.push_back(buffer.Handle());
568 } 563 }
569 scheduler.Record( 564 scheduler.Record(
570 [bindings = bindings, buffer_handles = buffer_handles](vk::CommandBuffer cmdbuf) { 565 [bindings = bindings, buffer_handles = buffer_handles](vk::CommandBuffer cmdbuf) {
571 cmdbuf.BindTransformFeedbackBuffersEXT( 566 cmdbuf.BindTransformFeedbackBuffersEXT(0, static_cast<u32>(buffer_handles.size()),
572 0, static_cast<u32>(buffer_handles.size()), buffer_handles.data(), 567 buffer_handles.data(), bindings.offsets.data(),
573 reinterpret_cast<const VkDeviceSize*>(bindings.offsets.data()), 568 bindings.sizes.data());
574 reinterpret_cast<const VkDeviceSize*>(bindings.sizes.data()));
575 }); 569 });
576} 570}
577 571
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:
97 void BindQuadIndexBuffer(PrimitiveTopology topology, u32 first, u32 count); 97 void BindQuadIndexBuffer(PrimitiveTopology topology, u32 first, u32 count);
98 98
99 void BindVertexBuffer(u32 index, VkBuffer buffer, u32 offset, u32 size, u32 stride); 99 void BindVertexBuffer(u32 index, VkBuffer buffer, u32 offset, u32 size, u32 stride);
100 void BindVertexBuffers(VideoCommon::HostBindings& bindings); 100
101 void BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bindings);
101 102
102 void BindTransformFeedbackBuffer(u32 index, VkBuffer buffer, u32 offset, u32 size); 103 void BindTransformFeedbackBuffer(u32 index, VkBuffer buffer, u32 offset, u32 size);
103 void BindTransformFeedbackBuffers(VideoCommon::HostBindings& bindings); 104
105 void BindTransformFeedbackBuffers(VideoCommon::HostBindings<Buffer>& bindings);
104 106
105 std::span<u8> BindMappedUniformBuffer([[maybe_unused]] size_t stage, 107 std::span<u8> BindMappedUniformBuffer([[maybe_unused]] size_t stage,
106 [[maybe_unused]] u32 binding_index, u32 size) { 108 [[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 @@
1{ 1{
2 "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", 2 "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
3 "name": "yuzu", 3 "name": "yuzu",
4 "builtin-baseline": "656fcc6ab2b05c6d999b7eaca717027ac3738f71", 4 "builtin-baseline": "a487471068f4cb1cbb4eeb340763cdcc0a75fd68",
5 "version": "1.0", 5 "version": "1.0",
6 "dependencies": [ 6 "dependencies": [
7 "boost-algorithm", 7 "boost-algorithm",