early-access version 3680

This commit is contained in:
pineappleEA 2023-06-16 03:30:14 +02:00
parent 399502a03e
commit 22fb9ebe53
34 changed files with 840 additions and 241 deletions

View File

@ -1,7 +1,7 @@
yuzu emulator early access yuzu emulator early access
============= =============
This is the source code for early-access 3678. This is the source code for early-access 3680.
## Legal Notice ## Legal Notice

View File

@ -53,7 +53,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
<activity <activity
android:name="org.yuzu.yuzu_emu.activities.EmulationActivity" android:name="org.yuzu.yuzu_emu.activities.EmulationActivity"
android:theme="@style/Theme.Yuzu.Main" android:theme="@style/Theme.Yuzu.Main"
android:screenOrientation="userLandscape" android:supportsPictureInPicture="true"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>

View File

@ -282,6 +282,11 @@ object NativeLibrary {
*/ */
external fun isRunning(): Boolean external fun isRunning(): Boolean
/**
* Returns true if emulation is paused.
*/
external fun isPaused(): Boolean
/** /**
* Returns the performance stats for the current game * Returns the performance stats for the current game
*/ */

View File

@ -4,14 +4,23 @@
package org.yuzu.yuzu_emu.activities package org.yuzu.yuzu_emu.activities
import android.app.Activity import android.app.Activity
import android.app.PendingIntent
import android.app.PictureInPictureParams
import android.app.RemoteAction
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.Icon
import android.hardware.Sensor import android.hardware.Sensor
import android.hardware.SensorEvent import android.hardware.SensorEvent
import android.hardware.SensorEventListener import android.hardware.SensorEventListener
import android.hardware.SensorManager import android.hardware.SensorManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Rational
import android.view.InputDevice import android.view.InputDevice
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
@ -27,6 +36,8 @@ import androidx.navigation.fragment.NavHostFragment
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.utils.ControllerMappingHelper import org.yuzu.yuzu_emu.utils.ControllerMappingHelper
@ -50,6 +61,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
private var motionTimestamp: Long = 0 private var motionTimestamp: Long = 0
private var flipMotionOrientation: Boolean = false private var flipMotionOrientation: Boolean = false
private val actionPause = "ACTION_EMULATOR_PAUSE"
private val actionPlay = "ACTION_EMULATOR_PLAY"
private val settingsViewModel: SettingsViewModel by viewModels() private val settingsViewModel: SettingsViewModel by viewModels()
override fun onDestroy() { override fun onDestroy() {
@ -120,6 +134,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
super.onResume() super.onResume()
nfcReader.startScanning() nfcReader.startScanning()
startMotionSensorListener() startMotionSensorListener()
buildPictureInPictureParams()
} }
override fun onPause() { override fun onPause() {
@ -128,6 +144,16 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
stopMotionSensorListener() stopMotionSensorListener()
} }
override fun onUserLeaveHint() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
if (BooleanSetting.PICTURE_IN_PICTURE.boolean && !isInPictureInPictureMode) {
val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
.getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
enterPictureInPictureMode(pictureInPictureParamsBuilder.build())
}
}
}
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
setIntent(intent) setIntent(intent)
@ -230,6 +256,96 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
} }
} }
private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder(): PictureInPictureParams.Builder {
val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.int) {
0 -> Rational(16, 9)
1 -> Rational(4, 3)
2 -> Rational(21, 9)
3 -> Rational(16, 10)
else -> null // Best fit
}
return this.apply { aspectRatio?.let { setAspectRatio(it) } }
}
private fun PictureInPictureParams.Builder.getPictureInPictureActionsBuilder(): PictureInPictureParams.Builder {
val pictureInPictureActions: MutableList<RemoteAction> = mutableListOf()
val pendingFlags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
if (NativeLibrary.isPaused()) {
val playIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_play)
val playPendingIntent = PendingIntent.getBroadcast(
this@EmulationActivity,
R.drawable.ic_pip_play,
Intent(actionPlay),
pendingFlags
)
val playRemoteAction = RemoteAction(
playIcon,
getString(R.string.play),
getString(R.string.play),
playPendingIntent
)
pictureInPictureActions.add(playRemoteAction)
} else {
val pauseIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_pause)
val pausePendingIntent = PendingIntent.getBroadcast(
this@EmulationActivity,
R.drawable.ic_pip_pause,
Intent(actionPause),
pendingFlags
)
val pauseRemoteAction = RemoteAction(
pauseIcon,
getString(R.string.pause),
getString(R.string.pause),
pausePendingIntent
)
pictureInPictureActions.add(pauseRemoteAction)
}
return this.apply { setActions(pictureInPictureActions) }
}
fun buildPictureInPictureParams() {
val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
.getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
pictureInPictureParamsBuilder.setAutoEnterEnabled(BooleanSetting.PICTURE_IN_PICTURE.boolean)
}
setPictureInPictureParams(pictureInPictureParamsBuilder.build())
}
private var pictureInPictureReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
if (intent.action == actionPlay) {
if (NativeLibrary.isPaused()) NativeLibrary.unPauseEmulation()
} else if (intent.action == actionPause) {
if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation()
}
buildPictureInPictureParams()
}
}
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration
) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
if (isInPictureInPictureMode) {
IntentFilter().apply {
addAction(actionPause)
addAction(actionPlay)
}.also {
registerReceiver(pictureInPictureReceiver, it)
}
} else {
try {
unregisterReceiver(pictureInPictureReceiver)
} catch (ignored : Exception) {
}
}
}
private fun startMotionSensorListener() { private fun startMotionSensorListener() {
val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)

View File

@ -8,6 +8,7 @@ enum class BooleanSetting(
override val section: String, override val section: String,
override val defaultValue: Boolean override val defaultValue: Boolean
) : AbstractBooleanSetting { ) : AbstractBooleanSetting {
PICTURE_IN_PICTURE("picture_in_picture", Settings.SECTION_GENERAL, true),
USE_CUSTOM_RTC("custom_rtc_enabled", Settings.SECTION_SYSTEM, false); USE_CUSTOM_RTC("custom_rtc_enabled", Settings.SECTION_SYSTEM, false);
override var boolean: Boolean = defaultValue override var boolean: Boolean = defaultValue
@ -27,6 +28,7 @@ enum class BooleanSetting(
companion object { companion object {
private val NOT_RUNTIME_EDITABLE = listOf( private val NOT_RUNTIME_EDITABLE = listOf(
PICTURE_IN_PICTURE,
USE_CUSTOM_RTC USE_CUSTOM_RTC
) )

View File

@ -93,6 +93,11 @@ enum class IntSetting(
Settings.SECTION_RENDERER, Settings.SECTION_RENDERER,
0 0
), ),
RENDERER_SCREEN_LAYOUT(
"screen_layout",
Settings.SECTION_RENDERER,
Settings.LayoutOption_MobileLandscape
),
RENDERER_ASPECT_RATIO( RENDERER_ASPECT_RATIO(
"aspect_ratio", "aspect_ratio",
Settings.SECTION_RENDERER, Settings.SECTION_RENDERER,

View File

@ -133,7 +133,6 @@ class Settings {
const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter" const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter"
const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable" const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable"
const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics" const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics"
const val PREF_MENU_SETTINGS_LANDSCAPE = "EmulationMenuSettings_LandscapeScreenLayout"
const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps" const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps"
const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay" const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay"
@ -144,6 +143,10 @@ class Settings {
private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap() private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap()
const val LayoutOption_Unspecified = 0
const val LayoutOption_MobilePortrait = 4
const val LayoutOption_MobileLandscape = 5
init { init {
configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] = configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] =
listOf( listOf(

View File

@ -16,6 +16,7 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import android.view.ViewGroup.MarginLayoutParams import android.view.ViewGroup.MarginLayoutParams
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
@ -239,5 +240,17 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
settings.putExtra(ARG_GAME_ID, gameId) settings.putExtra(ARG_GAME_ID, gameId)
context.startActivity(settings) context.startActivity(settings)
} }
fun launch(
context: Context,
launcher: ActivityResultLauncher<Intent>,
menuTag: String?,
gameId: String?
) {
val settings = Intent(context, SettingsActivity::class.java)
settings.putExtra(ARG_MENU_TAG, menuTag)
settings.putExtra(ARG_GAME_ID, gameId)
launcher.launch(settings)
}
} }
} }

View File

@ -166,6 +166,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
IntSetting.CPU_ACCURACY.defaultValue IntSetting.CPU_ACCURACY.defaultValue
) )
) )
add(
SwitchSetting(
BooleanSetting.PICTURE_IN_PICTURE,
R.string.picture_in_picture,
R.string.picture_in_picture_description,
BooleanSetting.PICTURE_IN_PICTURE.key,
BooleanSetting.PICTURE_IN_PICTURE.defaultValue
)
)
} }
} }
@ -283,6 +292,17 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
IntSetting.RENDERER_ANTI_ALIASING.defaultValue IntSetting.RENDERER_ANTI_ALIASING.defaultValue
) )
) )
add(
SingleChoiceSetting(
IntSetting.RENDERER_SCREEN_LAYOUT,
R.string.renderer_screen_layout,
0,
R.array.rendererScreenLayoutNames,
R.array.rendererScreenLayoutValues,
IntSetting.RENDERER_SCREEN_LAYOUT.key,
IntSetting.RENDERER_SCREEN_LAYOUT.defaultValue
)
)
add( add(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.RENDERER_ASPECT_RATIO, IntSetting.RENDERER_ASPECT_RATIO,

View File

@ -7,24 +7,26 @@ import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.res.Resources import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Rational import android.util.Rational
import android.util.TypedValue
import android.view.* import android.view.*
import android.widget.TextView import android.widget.TextView
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -48,6 +50,7 @@ import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.overlay.InputOverlay
import org.yuzu.yuzu_emu.utils.* import org.yuzu.yuzu_emu.utils.*
class EmulationFragment : Fragment(), SurfaceHolder.Callback { class EmulationFragment : Fragment(), SurfaceHolder.Callback {
@ -61,11 +64,40 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
val args by navArgs<EmulationFragmentArgs>() val args by navArgs<EmulationFragmentArgs>()
private var isInFoldableLayout = false
private lateinit var onReturnFromSettings: ActivityResultLauncher<Intent>
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
if (context is EmulationActivity) { if (context is EmulationActivity) {
emulationActivity = context emulationActivity = context
NativeLibrary.setEmulationActivity(context) NativeLibrary.setEmulationActivity(context)
lifecycleScope.launch(Dispatchers.Main) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
WindowInfoTracker.getOrCreate(context)
.windowLayoutInfo(context)
.collect { updateFoldableLayout(context, it) }
}
}
onReturnFromSettings = context.activityResultRegistry.register(
"SettingsResult", ActivityResultContracts.StartActivityForResult()
) {
binding.surfaceEmulation.setAspectRatio(
when (IntSetting.RENDERER_ASPECT_RATIO.int) {
0 -> Rational(16, 9)
1 -> Rational(4, 3)
2 -> Rational(21, 9)
3 -> Rational(16, 10)
4 -> null // Stretch
else -> Rational(16, 9)
}
)
emulationActivity?.buildPictureInPictureParams()
updateScreenLayout()
}
} else { } else {
throw IllegalStateException("EmulationFragment must have EmulationActivity parent") throw IllegalStateException("EmulationFragment must have EmulationActivity parent")
} }
@ -129,7 +161,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
R.id.menu_settings -> { R.id.menu_settings -> {
SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") SettingsActivity.launch(
requireContext(),
onReturnFromSettings,
SettingsFile.FILE_NAME_CONFIG,
""
)
true true
} }
@ -162,7 +199,33 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
WindowInfoTracker.getOrCreate(requireContext()) WindowInfoTracker.getOrCreate(requireContext())
.windowLayoutInfo(requireActivity()) .windowLayoutInfo(requireActivity())
.collect { updateCurrentLayout(requireActivity() as EmulationActivity, it) } .collect { updateFoldableLayout(requireActivity() as EmulationActivity, it) }
}
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if (emulationActivity?.isInPictureInPictureMode == true) {
if (binding.drawerLayout.isOpen) {
binding.drawerLayout.close()
}
if (EmulationMenuSettings.showOverlay) {
binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = false }
}
} else {
if (EmulationMenuSettings.showOverlay) {
binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = true }
}
if (!isInFoldableLayout) {
if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
binding.surfaceInputOverlay.orientation = InputOverlay.PORTRAIT
} else {
binding.surfaceInputOverlay.orientation = InputOverlay.LANDSCAPE
}
}
if (!binding.surfaceInputOverlay.isInEditMode) {
refreshInputOverlay()
} }
} }
} }
@ -184,6 +247,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
) )
updateScreenLayout()
emulationState.run(emulationActivity!!.isActivityRecreated) emulationState.run(emulationActivity!!.isActivityRecreated)
} }
@ -243,31 +308,50 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
} }
private val Number.toPx get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics).toInt() @SuppressLint("SourceLockedOrientationActivity")
private fun updateScreenLayout() {
emulationActivity?.let {
it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.int) {
Settings.LayoutOption_MobileLandscape -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
Settings.LayoutOption_MobilePortrait -> ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
Settings.LayoutOption_Unspecified -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
else -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
}
}
onConfigurationChanged(resources.configuration)
}
fun updateCurrentLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) { private fun updateFoldableLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) {
val isFolding = (newLayoutInfo.displayFeatures.find { it is FoldingFeature } as? FoldingFeature)?.let { val isFolding = (newLayoutInfo.displayFeatures.find { it is FoldingFeature } as? FoldingFeature)?.let {
if (it.isSeparating) { if (it.isSeparating) {
emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) { if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) {
binding.surfaceEmulation.layoutParams.height = it.bounds.top // Restrict emulation and overlays to the top of the screen
binding.emulationContainer.layoutParams.height = it.bounds.top
binding.overlayContainer.layoutParams.height = it.bounds.top
// Restrict input and menu drawer to the bottom of the screen
binding.inputContainer.layoutParams.height = it.bounds.bottom
binding.inGameMenu.layoutParams.height = it.bounds.bottom binding.inGameMenu.layoutParams.height = it.bounds.bottom
binding.overlayContainer.layoutParams.height = it.bounds.bottom - 48.toPx
binding.overlayContainer.updatePadding(0, 0, 0, 24.toPx) isInFoldableLayout = true
binding.surfaceInputOverlay.orientation = InputOverlay.FOLDABLE
refreshInputOverlay()
} }
} }
it.isSeparating it.isSeparating
} ?: false } ?: false
if (!isFolding) { if (!isFolding) {
binding.surfaceEmulation.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT binding.emulationContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
binding.inGameMenu.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT binding.inputContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
binding.overlayContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT binding.overlayContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
binding.overlayContainer.updatePadding(0, 0, 0, 0) binding.inGameMenu.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE isInFoldableLayout = false
updateScreenLayout()
} }
binding.surfaceInputOverlay.requestLayout() binding.emulationContainer.requestLayout()
binding.inGameMenu.requestLayout() binding.inputContainer.requestLayout()
binding.overlayContainer.requestLayout() binding.overlayContainer.requestLayout()
binding.inGameMenu.requestLayout()
} }
override fun surfaceCreated(holder: SurfaceHolder) { override fun surfaceCreated(holder: SurfaceHolder) {
@ -397,7 +481,19 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
popup.show() popup.show()
} }
@SuppressLint("SourceLockedOrientationActivity")
private fun startConfiguringControls() { private fun startConfiguringControls() {
// Lock the current orientation to prevent editing inconsistencies
if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) {
emulationActivity?.let {
it.requestedOrientation =
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
} else {
ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
}
}
}
binding.doneControlConfig.visibility = View.VISIBLE binding.doneControlConfig.visibility = View.VISIBLE
binding.surfaceInputOverlay.setIsInEditMode(true) binding.surfaceInputOverlay.setIsInEditMode(true)
} }
@ -405,6 +501,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private fun stopConfiguringControls() { private fun stopConfiguringControls() {
binding.doneControlConfig.visibility = View.GONE binding.doneControlConfig.visibility = View.GONE
binding.surfaceInputOverlay.setIsInEditMode(false) binding.surfaceInputOverlay.setIsInEditMode(false)
// Unlock the orientation if it was locked for editing
if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) {
emulationActivity?.let {
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
}
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")

View File

@ -51,12 +51,14 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
private lateinit var windowInsets: WindowInsets private lateinit var windowInsets: WindowInsets
var orientation = LANDSCAPE
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom) super.onLayout(changed, left, top, right, bottom)
windowInsets = rootWindowInsets windowInsets = rootWindowInsets
if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) { if (!preferences.getBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", false)) {
defaultOverlay() defaultOverlay()
} }
@ -233,10 +235,6 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
val fingerPositionX = event.getX(pointerIndex).toInt() val fingerPositionX = event.getX(pointerIndex).toInt()
val fingerPositionY = event.getY(pointerIndex).toInt() val fingerPositionY = event.getY(pointerIndex).toInt()
// TODO: Provide support for portrait layout
//val orientation =
// if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else ""
for (button in overlayButtons) { for (button in overlayButtons) {
// Determine the button state to apply based on the MotionEvent action flag. // Determine the button state to apply based on the MotionEvent action flag.
when (event.action and MotionEvent.ACTION_MASK) { when (event.action and MotionEvent.ACTION_MASK) {
@ -266,7 +264,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
buttonBeingConfigured!!.buttonId, buttonBeingConfigured!!.buttonId,
buttonBeingConfigured!!.bounds.centerX(), buttonBeingConfigured!!.bounds.centerX(),
buttonBeingConfigured!!.bounds.centerY(), buttonBeingConfigured!!.bounds.centerY(),
"" orientation
) )
buttonBeingConfigured = null buttonBeingConfigured = null
} }
@ -299,7 +297,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
dpadBeingConfigured!!.upId, dpadBeingConfigured!!.upId,
dpadBeingConfigured!!.bounds.centerX(), dpadBeingConfigured!!.bounds.centerX(),
dpadBeingConfigured!!.bounds.centerY(), dpadBeingConfigured!!.bounds.centerY(),
"" orientation
) )
dpadBeingConfigured = null dpadBeingConfigured = null
} }
@ -330,7 +328,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
joystickBeingConfigured!!.buttonId, joystickBeingConfigured!!.buttonId,
joystickBeingConfigured!!.bounds.centerX(), joystickBeingConfigured!!.bounds.centerX(),
joystickBeingConfigured!!.bounds.centerY(), joystickBeingConfigured!!.bounds.centerY(),
"" orientation
) )
joystickBeingConfigured = null joystickBeingConfigured = null
} }
@ -533,8 +531,6 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
overlayButtons.clear() overlayButtons.clear()
overlayDpads.clear() overlayDpads.clear()
overlayJoysticks.clear() overlayJoysticks.clear()
val orientation =
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else ""
// Add all the enabled overlay items back to the HashSet. // Add all the enabled overlay items back to the HashSet.
if (EmulationMenuSettings.showOverlay) { if (EmulationMenuSettings.showOverlay) {
@ -548,8 +544,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
val min = windowSize.first val min = windowSize.first
val max = windowSize.second val max = windowSize.second
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit() PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
.putFloat("$sharedPrefsId$orientation-X", (x - min.x).toFloat() / max.x) .putFloat("$sharedPrefsId-X$orientation", (x - min.x).toFloat() / max.x)
.putFloat("$sharedPrefsId$orientation-Y", (y - min.y).toFloat() / max.y) .putFloat("$sharedPrefsId-Y$orientation", (y - min.y).toFloat() / max.y)
.apply() .apply()
} }
@ -558,145 +554,250 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
} }
private fun defaultOverlay() { private fun defaultOverlay() {
if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) { if (!preferences.getBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", false)) {
defaultOverlayLandscape() defaultOverlayByLayout(orientation)
} }
resetButtonPlacement() resetButtonPlacement()
preferences.edit() preferences.edit()
.putBoolean(Settings.PREF_OVERLAY_INIT, true) .putBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", true)
.apply() .apply()
} }
fun resetButtonPlacement() { fun resetButtonPlacement() {
defaultOverlayLandscape() defaultOverlayByLayout(orientation)
refreshControls() refreshControls()
} }
private fun defaultOverlayLandscape() { private val landscapeResources = arrayOf(
R.integer.SWITCH_BUTTON_A_X,
R.integer.SWITCH_BUTTON_A_Y,
R.integer.SWITCH_BUTTON_B_X,
R.integer.SWITCH_BUTTON_B_Y,
R.integer.SWITCH_BUTTON_X_X,
R.integer.SWITCH_BUTTON_X_Y,
R.integer.SWITCH_BUTTON_Y_X,
R.integer.SWITCH_BUTTON_Y_Y,
R.integer.SWITCH_TRIGGER_ZL_X,
R.integer.SWITCH_TRIGGER_ZL_Y,
R.integer.SWITCH_TRIGGER_ZR_X,
R.integer.SWITCH_TRIGGER_ZR_Y,
R.integer.SWITCH_BUTTON_DPAD_X,
R.integer.SWITCH_BUTTON_DPAD_Y,
R.integer.SWITCH_TRIGGER_L_X,
R.integer.SWITCH_TRIGGER_L_Y,
R.integer.SWITCH_TRIGGER_R_X,
R.integer.SWITCH_TRIGGER_R_Y,
R.integer.SWITCH_BUTTON_PLUS_X,
R.integer.SWITCH_BUTTON_PLUS_Y,
R.integer.SWITCH_BUTTON_MINUS_X,
R.integer.SWITCH_BUTTON_MINUS_Y,
R.integer.SWITCH_BUTTON_HOME_X,
R.integer.SWITCH_BUTTON_HOME_Y,
R.integer.SWITCH_BUTTON_CAPTURE_X,
R.integer.SWITCH_BUTTON_CAPTURE_Y,
R.integer.SWITCH_STICK_R_X,
R.integer.SWITCH_STICK_R_Y,
R.integer.SWITCH_STICK_L_X,
R.integer.SWITCH_STICK_L_Y
)
private val portraitResources = arrayOf(
R.integer.SWITCH_BUTTON_A_X_PORTRAIT,
R.integer.SWITCH_BUTTON_A_Y_PORTRAIT,
R.integer.SWITCH_BUTTON_B_X_PORTRAIT,
R.integer.SWITCH_BUTTON_B_Y_PORTRAIT,
R.integer.SWITCH_BUTTON_X_X_PORTRAIT,
R.integer.SWITCH_BUTTON_X_Y_PORTRAIT,
R.integer.SWITCH_BUTTON_Y_X_PORTRAIT,
R.integer.SWITCH_BUTTON_Y_Y_PORTRAIT,
R.integer.SWITCH_TRIGGER_ZL_X_PORTRAIT,
R.integer.SWITCH_TRIGGER_ZL_Y_PORTRAIT,
R.integer.SWITCH_TRIGGER_ZR_X_PORTRAIT,
R.integer.SWITCH_TRIGGER_ZR_Y_PORTRAIT,
R.integer.SWITCH_BUTTON_DPAD_X_PORTRAIT,
R.integer.SWITCH_BUTTON_DPAD_Y_PORTRAIT,
R.integer.SWITCH_TRIGGER_L_X_PORTRAIT,
R.integer.SWITCH_TRIGGER_L_Y_PORTRAIT,
R.integer.SWITCH_TRIGGER_R_X_PORTRAIT,
R.integer.SWITCH_TRIGGER_R_Y_PORTRAIT,
R.integer.SWITCH_BUTTON_PLUS_X_PORTRAIT,
R.integer.SWITCH_BUTTON_PLUS_Y_PORTRAIT,
R.integer.SWITCH_BUTTON_MINUS_X_PORTRAIT,
R.integer.SWITCH_BUTTON_MINUS_Y_PORTRAIT,
R.integer.SWITCH_BUTTON_HOME_X_PORTRAIT,
R.integer.SWITCH_BUTTON_HOME_Y_PORTRAIT,
R.integer.SWITCH_BUTTON_CAPTURE_X_PORTRAIT,
R.integer.SWITCH_BUTTON_CAPTURE_Y_PORTRAIT,
R.integer.SWITCH_STICK_R_X_PORTRAIT,
R.integer.SWITCH_STICK_R_Y_PORTRAIT,
R.integer.SWITCH_STICK_L_X_PORTRAIT,
R.integer.SWITCH_STICK_L_Y_PORTRAIT
)
private val foldableResources = arrayOf(
R.integer.SWITCH_BUTTON_A_X_FOLDABLE,
R.integer.SWITCH_BUTTON_A_Y_FOLDABLE,
R.integer.SWITCH_BUTTON_B_X_FOLDABLE,
R.integer.SWITCH_BUTTON_B_Y_FOLDABLE,
R.integer.SWITCH_BUTTON_X_X_FOLDABLE,
R.integer.SWITCH_BUTTON_X_Y_FOLDABLE,
R.integer.SWITCH_BUTTON_Y_X_FOLDABLE,
R.integer.SWITCH_BUTTON_Y_Y_FOLDABLE,
R.integer.SWITCH_TRIGGER_ZL_X_FOLDABLE,
R.integer.SWITCH_TRIGGER_ZL_Y_FOLDABLE,
R.integer.SWITCH_TRIGGER_ZR_X_FOLDABLE,
R.integer.SWITCH_TRIGGER_ZR_Y_FOLDABLE,
R.integer.SWITCH_BUTTON_DPAD_X_FOLDABLE,
R.integer.SWITCH_BUTTON_DPAD_Y_FOLDABLE,
R.integer.SWITCH_TRIGGER_L_X_FOLDABLE,
R.integer.SWITCH_TRIGGER_L_Y_FOLDABLE,
R.integer.SWITCH_TRIGGER_R_X_FOLDABLE,
R.integer.SWITCH_TRIGGER_R_Y_FOLDABLE,
R.integer.SWITCH_BUTTON_PLUS_X_FOLDABLE,
R.integer.SWITCH_BUTTON_PLUS_Y_FOLDABLE,
R.integer.SWITCH_BUTTON_MINUS_X_FOLDABLE,
R.integer.SWITCH_BUTTON_MINUS_Y_FOLDABLE,
R.integer.SWITCH_BUTTON_HOME_X_FOLDABLE,
R.integer.SWITCH_BUTTON_HOME_Y_FOLDABLE,
R.integer.SWITCH_BUTTON_CAPTURE_X_FOLDABLE,
R.integer.SWITCH_BUTTON_CAPTURE_Y_FOLDABLE,
R.integer.SWITCH_STICK_R_X_FOLDABLE,
R.integer.SWITCH_STICK_R_Y_FOLDABLE,
R.integer.SWITCH_STICK_L_X_FOLDABLE,
R.integer.SWITCH_STICK_L_Y_FOLDABLE
)
private fun getResourceValue(orientation: String, position: Int) : Float {
return when (orientation) {
PORTRAIT -> resources.getInteger(portraitResources[position]).toFloat() / 1000
FOLDABLE -> resources.getInteger(foldableResources[position]).toFloat() / 1000
else -> resources.getInteger(landscapeResources[position]).toFloat() / 1000
}
}
private fun defaultOverlayByLayout(orientation: String) {
// Each value represents the position of the button in relation to the screen size without insets. // Each value represents the position of the button in relation to the screen size without insets.
preferences.edit() preferences.edit()
.putFloat( .putFloat(
ButtonType.BUTTON_A.toString() + "-X", ButtonType.BUTTON_A.toString() + "-X$orientation",
resources.getInteger(R.integer.SWITCH_BUTTON_A_X).toFloat() / 1000 getResourceValue(orientation, 0)
) )
.putFloat( .putFloat(
ButtonType.BUTTON_A.toString() + "-Y", ButtonType.BUTTON_A.toString() + "-Y$orientation",
resources.getInteger(R.integer.SWITCH_BUTTON_A_Y).toFloat() / 1000 getResourceValue(orientation, 1)
) )
.putFloat( .putFloat(
ButtonType.BUTTON_B.toString() + "-X", ButtonType.BUTTON_B.toString() + "-X$orientation",
resources.getInteger(R.integer.SWITCH_BUTTON_B_X).toFloat() / 1000 getResourceValue(orientation, 2)
) )
.putFloat( .putFloat(
ButtonType.BUTTON_B.toString() + "-Y", ButtonType.BUTTON_B.toString() + "-Y$orientation",
resources.getInteger(R.integer.SWITCH_BUTTON_B_Y).toFloat() / 1000 getResourceValue(orientation, 3)
) )
.putFloat( .putFloat(
ButtonType.BUTTON_X.toString() + "-X", ButtonType.BUTTON_X.toString() + "-X$orientation",
resources.getInteger(R.integer.SWITCH_BUTTON_X_X).toFloat() / 1000 getResourceValue(orientation, 4)
) )
.putFloat( .putFloat(
ButtonType.BUTTON_X.toString() + "-Y", ButtonType.BUTTON_X.toString() + "-Y$orientation",
resources.getInteger(R.integer.SWITCH_BUTTON_X_Y).toFloat() / 1000 getResourceValue(orientation, 5)
) )
.putFloat( .putFloat(
ButtonType.BUTTON_Y.toString() + "-X", ButtonType.BUTTON_Y.toString() + "-X$orientation",
resources.getInteger(R.integer.SWITCH_BUTTON_Y_X).toFloat() / 1000 getResourceValue(orientation, 6)
) )
.putFloat( .putFloat(
ButtonType.BUTTON_Y.toString() + "-Y", ButtonType.BUTTON_Y.toString() + "-Y$orientation",
resources.getInteger(R.integer.SWITCH_BUTTON_Y_Y).toFloat() / 1000 getResourceValue(orientation, 7)
) )
.putFloat( .putFloat(
ButtonType.TRIGGER_ZL.toString() + "-X", ButtonType.TRIGGER_ZL.toString() + "-X$orientation",
resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_X).toFloat() / 1000 getResourceValue(orientation, 8)
) )
.putFloat( .putFloat(
ButtonType.TRIGGER_ZL.toString() + "-Y", ButtonType.TRIGGER_ZL.toString() + "-Y$orientation",
resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_Y).toFloat() / 1000 getResourceValue(orientation, 9)
) )
.putFloat( .putFloat(
ButtonType.TRIGGER_ZR.toString() + "-X", ButtonType.TRIGGER_ZR.toString() + "-X$orientation",
resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_X).toFloat() / 1000 getResourceValue(orientation, 10)
) )
.putFloat( .putFloat(
ButtonType.TRIGGER_ZR.toString() + "-Y", ButtonType.TRIGGER_ZR.toString() + "-Y$orientation",
resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_Y).toFloat() / 1000 getResourceValue(orientation, 11)
) )
.putFloat( .putFloat(
ButtonType.DPAD_UP.toString() + "-X", ButtonType.DPAD_UP.toString() + "-X$orientation",
resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_X).toFloat() / 1000 getResourceValue(orientation, 12)
) )
.putFloat( .putFloat(
ButtonType.DPAD_UP.toString() + "-Y", ButtonType.DPAD_UP.toString() + "-Y$orientation",
resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_Y).toFloat() / 1000 getResourceValue(orientation, 13)
) )
.putFloat( .putFloat(
ButtonType.TRIGGER_L.toString() + "-X", ButtonType.TRIGGER_L.toString() + "-X$orientation",
resources.getInteger(R.integer.SWITCH_TRIGGER_L_X).toFloat() / 1000 getResourceValue(orientation, 14)
) )
.putFloat( .putFloat(
ButtonType.TRIGGER_L.toString() + "-Y", ButtonType.TRIGGER_L.toString() + "-Y$orientation",
resources.getInteger(R.integer.SWITCH_TRIGGER_L_Y).toFloat() / 1000 getResourceValue(orientation, 15)
) )
.putFloat( .putFloat(
ButtonType.TRIGGER_R.toString() + "-X", ButtonType.TRIGGER_R.toString() + "-X$orientation",
resources.getInteger(R.integer.SWITCH_TRIGGER_R_X).toFloat() / 1000 getResourceValue(orientation, 16)
) )
.putFloat( .putFloat(
ButtonType.TRIGGER_R.toString() + "-Y", ButtonType.TRIGGER_R.toString() + "-Y$orientation",
resources.getInteger(R.integer.SWITCH_TRIGGER_R_Y).toFloat() / 1000 getResourceValue(orientation, 17)
) )
.putFloat( .putFloat(
ButtonType.BUTTON_PLUS.toString() + "-X", ButtonType.BUTTON_PLUS.toString() + "-X$orientation",
resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_X).toFloat() / 1000 getResourceValue(orientation, 18)
) )
.putFloat( .putFloat(
ButtonType.BUTTON_PLUS.toString() + "-Y", ButtonType.BUTTON_PLUS.toString() + "-Y$orientation",
resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_Y).toFloat() / 1000 getResourceValue(orientation, 19)
) )
.putFloat( .putFloat(
ButtonType.BUTTON_MINUS.toString() + "-X", ButtonType.BUTTON_MINUS.toString() + "-X$orientation",
resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_X).toFloat() / 1000 getResourceValue(orientation, 20)
) )
.putFloat( .putFloat(
ButtonType.BUTTON_MINUS.toString() + "-Y", ButtonType.BUTTON_MINUS.toString() + "-Y$orientation",
resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_Y).toFloat() / 1000 getResourceValue(orientation, 21)
) )
.putFloat( .putFloat(
ButtonType.BUTTON_HOME.toString() + "-X", ButtonType.BUTTON_HOME.toString() + "-X$orientation",
resources.getInteger(R.integer.SWITCH_BUTTON_HOME_X).toFloat() / 1000 getResourceValue(orientation, 22)
) )
.putFloat( .putFloat(
ButtonType.BUTTON_HOME.toString() + "-Y", ButtonType.BUTTON_HOME.toString() + "-Y$orientation",
resources.getInteger(R.integer.SWITCH_BUTTON_HOME_Y).toFloat() / 1000 getResourceValue(orientation, 23)
) )
.putFloat( .putFloat(
ButtonType.BUTTON_CAPTURE.toString() + "-X", ButtonType.BUTTON_CAPTURE.toString() + "-X$orientation",
resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_X) getResourceValue(orientation, 24)
.toFloat() / 1000
) )
.putFloat( .putFloat(
ButtonType.BUTTON_CAPTURE.toString() + "-Y", ButtonType.BUTTON_CAPTURE.toString() + "-Y$orientation",
resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_Y) getResourceValue(orientation, 25)
.toFloat() / 1000
) )
.putFloat( .putFloat(
ButtonType.STICK_R.toString() + "-X", ButtonType.STICK_R.toString() + "-X$orientation",
resources.getInteger(R.integer.SWITCH_STICK_R_X).toFloat() / 1000 getResourceValue(orientation, 26)
) )
.putFloat( .putFloat(
ButtonType.STICK_R.toString() + "-Y", ButtonType.STICK_R.toString() + "-Y$orientation",
resources.getInteger(R.integer.SWITCH_STICK_R_Y).toFloat() / 1000 getResourceValue(orientation, 27)
) )
.putFloat( .putFloat(
ButtonType.STICK_L.toString() + "-X", ButtonType.STICK_L.toString() + "-X$orientation",
resources.getInteger(R.integer.SWITCH_STICK_L_X).toFloat() / 1000 getResourceValue(orientation, 28)
) )
.putFloat( .putFloat(
ButtonType.STICK_L.toString() + "-Y", ButtonType.STICK_L.toString() + "-Y$orientation",
resources.getInteger(R.integer.SWITCH_STICK_L_Y).toFloat() / 1000 getResourceValue(orientation, 29)
) )
.apply() .apply()
} }
@ -709,6 +810,10 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
private val preferences: SharedPreferences = private val preferences: SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
const val LANDSCAPE = ""
const val PORTRAIT = "_Portrait"
const val FOLDABLE = "_Foldable"
/** /**
* Resizes a [Bitmap] by a given scale factor * Resizes a [Bitmap] by a given scale factor
* *
@ -754,8 +859,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
*/ */
private fun getSafeScreenSize(context: Context): Pair<Point, Point> { private fun getSafeScreenSize(context: Context): Pair<Point, Point> {
// Get screen size // Get screen size
val windowMetrics = val windowMetrics = WindowMetricsCalculator.getOrCreate()
WindowMetricsCalculator.getOrCreate()
.computeCurrentWindowMetrics(context as Activity) .computeCurrentWindowMetrics(context as Activity)
var maxY = windowMetrics.bounds.height().toFloat() var maxY = windowMetrics.bounds.height().toFloat()
var maxX = windowMetrics.bounds.width().toFloat() var maxX = windowMetrics.bounds.width().toFloat()
@ -769,9 +873,9 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
val insets = context.windowManager.currentWindowMetrics.windowInsets.displayCutout val insets = context.windowManager.currentWindowMetrics.windowInsets.displayCutout
if (insets != null) { if (insets != null) {
if (insets.boundingRectTop.bottom != 0 && insets.boundingRectTop.bottom > maxY / 2) if (insets.boundingRectTop.bottom != 0 && insets.boundingRectTop.bottom > maxY / 2)
insets.boundingRectTop.bottom.toFloat() else maxY maxY = insets.boundingRectTop.bottom.toFloat()
if (insets.boundingRectRight.left != 0 && insets.boundingRectRight.left > maxX / 2) if (insets.boundingRectRight.left != 0 && insets.boundingRectRight.left > maxX / 2)
insets.boundingRectRight.left.toFloat() else maxX maxX = insets.boundingRectRight.left.toFloat()
minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left
minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom
@ -878,8 +982,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
// The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
// These were set in the input overlay configuration menu. // These were set in the input overlay configuration menu.
val xKey = "$buttonId$orientation-X" val xKey = "$buttonId-X$orientation"
val yKey = "$buttonId$orientation-Y" val yKey = "$buttonId-Y$orientation"
val drawableXPercent = sPrefs.getFloat(xKey, 0f) val drawableXPercent = sPrefs.getFloat(xKey, 0f)
val drawableYPercent = sPrefs.getFloat(yKey, 0f) val drawableYPercent = sPrefs.getFloat(yKey, 0f)
val drawableX = (drawableXPercent * max.x + min.x).toInt() val drawableX = (drawableXPercent * max.x + min.x).toInt()
@ -959,8 +1063,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
// The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay. // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay.
// These were set in the input overlay configuration menu. // These were set in the input overlay configuration menu.
val drawableXPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-X", 0f) val drawableXPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}-X$orientation", 0f)
val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-Y", 0f) val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}-Y$orientation", 0f)
val drawableX = (drawableXPercent * max.x + min.x).toInt() val drawableX = (drawableXPercent * max.x + min.x).toInt()
val drawableY = (drawableYPercent * max.y + min.y).toInt() val drawableY = (drawableYPercent * max.y + min.y).toInt()
val width = overlayDrawable.width val width = overlayDrawable.width
@ -1026,8 +1130,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
// The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
// These were set in the input overlay configuration menu. // These were set in the input overlay configuration menu.
val drawableXPercent = sPrefs.getFloat("$button$orientation-X", 0f) val drawableXPercent = sPrefs.getFloat("$button-X$orientation", 0f)
val drawableYPercent = sPrefs.getFloat("$button$orientation-Y", 0f) val drawableYPercent = sPrefs.getFloat("$button-Y$orientation", 0f)
val drawableX = (drawableXPercent * max.x + min.x).toInt() val drawableX = (drawableXPercent * max.x + min.x).toInt()
val drawableY = (drawableYPercent * max.y + min.y).toInt() val drawableY = (drawableYPercent * max.y + min.y).toInt()
val outerScale = 1.66f val outerScale = 1.66f

View File

@ -11,14 +11,6 @@ object EmulationMenuSettings {
private val preferences = private val preferences =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
// These must match what is defined in src/core/settings.h
const val LayoutOption_Default = 0
const val LayoutOption_SingleScreen = 1
const val LayoutOption_LargeScreen = 2
const val LayoutOption_SideScreen = 3
const val LayoutOption_MobilePortrait = 4
const val LayoutOption_MobileLandscape = 5
var joystickRelCenter: Boolean var joystickRelCenter: Boolean
get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, true) get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, true)
set(value) { set(value) {
@ -41,16 +33,6 @@ object EmulationMenuSettings {
.apply() .apply()
} }
var landscapeScreenLayout: Int
get() = preferences.getInt(
Settings.PREF_MENU_SETTINGS_LANDSCAPE,
LayoutOption_MobileLandscape
)
set(value) {
preferences.edit()
.putInt(Settings.PREF_MENU_SETTINGS_LANDSCAPE, value)
.apply()
}
var showFps: Boolean var showFps: Boolean
get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, false) get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, false)
set(value) { set(value) {

View File

@ -202,6 +202,11 @@ public:
return m_is_running; return m_is_running;
} }
bool IsPaused() const {
std::scoped_lock lock(m_mutex);
return m_is_running && m_is_paused;
}
const Core::PerfStatsResults& PerfStats() const { const Core::PerfStatsResults& PerfStats() const {
std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex); std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex);
return m_perf_stats; return m_perf_stats;
@ -287,11 +292,13 @@ public:
void PauseEmulation() { void PauseEmulation() {
std::scoped_lock lock(m_mutex); std::scoped_lock lock(m_mutex);
m_system.Pause(); m_system.Pause();
m_is_paused = true;
} }
void UnPauseEmulation() { void UnPauseEmulation() {
std::scoped_lock lock(m_mutex); std::scoped_lock lock(m_mutex);
m_system.Run(); m_system.Run();
m_is_paused = false;
} }
void HaltEmulation() { void HaltEmulation() {
@ -473,6 +480,7 @@ private:
std::shared_ptr<FileSys::VfsFilesystem> m_vfs; std::shared_ptr<FileSys::VfsFilesystem> m_vfs;
Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized}; Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
bool m_is_running{}; bool m_is_running{};
bool m_is_paused{};
SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{}; SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{};
std::unique_ptr<Service::Account::ProfileManager> m_profile_manager; std::unique_ptr<Service::Account::ProfileManager> m_profile_manager;
@ -583,6 +591,11 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isRunning([[maybe_unused]] JNIEnv
return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning()); return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning());
} }
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jclass clazz) {
return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused());
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly([[maybe_unused]] JNIEnv* env, jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jclass clazz) { [[maybe_unused]] jclass clazz) {
return EmulationSession::GetInstance().IsHandheldOnly(); return EmulationSession::GetInstance().IsHandheldOnly();

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="@android:color/white"
android:pathData="M8,5v14l11,-7z" />
</vector>

View File

@ -12,6 +12,11 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<FrameLayout
android:id="@+id/emulation_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- This is what everything is rendered to during emulation --> <!-- This is what everything is rendered to during emulation -->
<org.yuzu.yuzu_emu.views.FixedRatioSurfaceView <org.yuzu.yuzu_emu.views.FixedRatioSurfaceView
android:id="@+id/surface_emulation" android:id="@+id/surface_emulation"
@ -21,8 +26,10 @@
android:focusable="false" android:focusable="false"
android:focusableInTouchMode="false" /> android:focusableInTouchMode="false" />
</FrameLayout>
<FrameLayout <FrameLayout
android:id="@+id/overlay_container" android:id="@+id/input_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="bottom"> android:layout_gravity="bottom">
@ -32,9 +39,26 @@
android:id="@+id/surface_input_overlay" android:id="@+id/surface_input_overlay"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center"
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" /> android:focusableInTouchMode="true" />
<Button
style="@style/Widget.Material3.Button.ElevatedButton"
android:id="@+id/done_control_config"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/emulation_done"
android:visibility="gone" />
</FrameLayout>
<FrameLayout
android:id="@+id/overlay_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView <TextView
android:id="@+id/show_fps_text" android:id="@+id/show_fps_text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -47,14 +71,6 @@
android:textSize="12sp" android:textSize="12sp"
tools:ignore="RtlHardcoded" /> tools:ignore="RtlHardcoded" />
<Button
style="@style/Widget.Material3.Button.ElevatedButton"
android:id="@+id/done_control_config"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/emulation_done"
android:visibility="gone" />
</FrameLayout> </FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -119,6 +119,18 @@
<item>3</item> <item>3</item>
</integer-array> </integer-array>
<string-array name="rendererScreenLayoutNames">
<item>@string/screen_layout_landscape</item>
<item>@string/screen_layout_portrait</item>
<item>@string/screen_layout_auto</item>
</string-array>
<integer-array name="rendererScreenLayoutValues">
<item>5</item>
<item>4</item>
<item>0</item>
</integer-array>
<string-array name="rendererAspectRatioNames"> <string-array name="rendererAspectRatioNames">
<item>@string/ratio_default</item> <item>@string/ratio_default</item>
<item>@string/ratio_force_four_three</item> <item>@string/ratio_force_four_three</item>

View File

@ -34,4 +34,68 @@
<integer name="SWITCH_BUTTON_DPAD_X">260</integer> <integer name="SWITCH_BUTTON_DPAD_X">260</integer>
<integer name="SWITCH_BUTTON_DPAD_Y">790</integer> <integer name="SWITCH_BUTTON_DPAD_Y">790</integer>
<!-- Default SWITCH portrait layout -->
<integer name="SWITCH_BUTTON_A_X_PORTRAIT">840</integer>
<integer name="SWITCH_BUTTON_A_Y_PORTRAIT">840</integer>
<integer name="SWITCH_BUTTON_B_X_PORTRAIT">740</integer>
<integer name="SWITCH_BUTTON_B_Y_PORTRAIT">880</integer>
<integer name="SWITCH_BUTTON_X_X_PORTRAIT">740</integer>
<integer name="SWITCH_BUTTON_X_Y_PORTRAIT">800</integer>
<integer name="SWITCH_BUTTON_Y_X_PORTRAIT">640</integer>
<integer name="SWITCH_BUTTON_Y_Y_PORTRAIT">840</integer>
<integer name="SWITCH_STICK_L_X_PORTRAIT">180</integer>
<integer name="SWITCH_STICK_L_Y_PORTRAIT">660</integer>
<integer name="SWITCH_STICK_R_X_PORTRAIT">820</integer>
<integer name="SWITCH_STICK_R_Y_PORTRAIT">660</integer>
<integer name="SWITCH_TRIGGER_L_X_PORTRAIT">140</integer>
<integer name="SWITCH_TRIGGER_L_Y_PORTRAIT">260</integer>
<integer name="SWITCH_TRIGGER_R_X_PORTRAIT">860</integer>
<integer name="SWITCH_TRIGGER_R_Y_PORTRAIT">260</integer>
<integer name="SWITCH_TRIGGER_ZL_X_PORTRAIT">140</integer>
<integer name="SWITCH_TRIGGER_ZL_Y_PORTRAIT">200</integer>
<integer name="SWITCH_TRIGGER_ZR_X_PORTRAIT">860</integer>
<integer name="SWITCH_TRIGGER_ZR_Y_PORTRAIT">200</integer>
<integer name="SWITCH_BUTTON_MINUS_X_PORTRAIT">440</integer>
<integer name="SWITCH_BUTTON_MINUS_Y_PORTRAIT">950</integer>
<integer name="SWITCH_BUTTON_PLUS_X_PORTRAIT">560</integer>
<integer name="SWITCH_BUTTON_PLUS_Y_PORTRAIT">950</integer>
<integer name="SWITCH_BUTTON_HOME_X_PORTRAIT">680</integer>
<integer name="SWITCH_BUTTON_HOME_Y_PORTRAIT">950</integer>
<integer name="SWITCH_BUTTON_CAPTURE_X_PORTRAIT">320</integer>
<integer name="SWITCH_BUTTON_CAPTURE_Y_PORTRAIT">950</integer>
<integer name="SWITCH_BUTTON_DPAD_X_PORTRAIT">240</integer>
<integer name="SWITCH_BUTTON_DPAD_Y_PORTRAIT">840</integer>
<!-- Default SWITCH foldable layout -->
<integer name="SWITCH_BUTTON_A_X_FOLDABLE">840</integer>
<integer name="SWITCH_BUTTON_A_Y_FOLDABLE">390</integer>
<integer name="SWITCH_BUTTON_B_X_FOLDABLE">740</integer>
<integer name="SWITCH_BUTTON_B_Y_FOLDABLE">430</integer>
<integer name="SWITCH_BUTTON_X_X_FOLDABLE">740</integer>
<integer name="SWITCH_BUTTON_X_Y_FOLDABLE">350</integer>
<integer name="SWITCH_BUTTON_Y_X_FOLDABLE">640</integer>
<integer name="SWITCH_BUTTON_Y_Y_FOLDABLE">390</integer>
<integer name="SWITCH_STICK_L_X_FOLDABLE">180</integer>
<integer name="SWITCH_STICK_L_Y_FOLDABLE">250</integer>
<integer name="SWITCH_STICK_R_X_FOLDABLE">820</integer>
<integer name="SWITCH_STICK_R_Y_FOLDABLE">250</integer>
<integer name="SWITCH_TRIGGER_L_X_FOLDABLE">140</integer>
<integer name="SWITCH_TRIGGER_L_Y_FOLDABLE">130</integer>
<integer name="SWITCH_TRIGGER_R_X_FOLDABLE">860</integer>
<integer name="SWITCH_TRIGGER_R_Y_FOLDABLE">130</integer>
<integer name="SWITCH_TRIGGER_ZL_X_FOLDABLE">140</integer>
<integer name="SWITCH_TRIGGER_ZL_Y_FOLDABLE">70</integer>
<integer name="SWITCH_TRIGGER_ZR_X_FOLDABLE">860</integer>
<integer name="SWITCH_TRIGGER_ZR_Y_FOLDABLE">70</integer>
<integer name="SWITCH_BUTTON_MINUS_X_FOLDABLE">440</integer>
<integer name="SWITCH_BUTTON_MINUS_Y_FOLDABLE">470</integer>
<integer name="SWITCH_BUTTON_PLUS_X_FOLDABLE">560</integer>
<integer name="SWITCH_BUTTON_PLUS_Y_FOLDABLE">470</integer>
<integer name="SWITCH_BUTTON_HOME_X_FOLDABLE">680</integer>
<integer name="SWITCH_BUTTON_HOME_Y_FOLDABLE">470</integer>
<integer name="SWITCH_BUTTON_CAPTURE_X_FOLDABLE">320</integer>
<integer name="SWITCH_BUTTON_CAPTURE_Y_FOLDABLE">470</integer>
<integer name="SWITCH_BUTTON_DPAD_X_FOLDABLE">240</integer>
<integer name="SWITCH_BUTTON_DPAD_Y_FOLDABLE">390</integer>
</resources> </resources>

View File

@ -162,6 +162,7 @@
<string name="renderer_accuracy">Accuracy level</string> <string name="renderer_accuracy">Accuracy level</string>
<string name="renderer_resolution">Resolution (Handheld/Docked)</string> <string name="renderer_resolution">Resolution (Handheld/Docked)</string>
<string name="renderer_vsync">VSync mode</string> <string name="renderer_vsync">VSync mode</string>
<string name="renderer_screen_layout">Orientation</string>
<string name="renderer_aspect_ratio">Aspect ratio</string> <string name="renderer_aspect_ratio">Aspect ratio</string>
<string name="renderer_scaling_filter">Window adapting filter</string> <string name="renderer_scaling_filter">Window adapting filter</string>
<string name="renderer_anti_aliasing">Anti-aliasing method</string> <string name="renderer_anti_aliasing">Anti-aliasing method</string>
@ -326,6 +327,11 @@
<string name="anti_aliasing_fxaa">FXAA</string> <string name="anti_aliasing_fxaa">FXAA</string>
<string name="anti_aliasing_smaa">SMAA</string> <string name="anti_aliasing_smaa">SMAA</string>
<!-- Screen Layouts -->
<string name="screen_layout_landscape">Landscape</string>
<string name="screen_layout_portrait">Portrait</string>
<string name="screen_layout_auto">Auto</string>
<!-- Aspect Ratios --> <!-- Aspect Ratios -->
<string name="ratio_default">Default (16:9)</string> <string name="ratio_default">Default (16:9)</string>
<string name="ratio_force_four_three">Force 4:3</string> <string name="ratio_force_four_three">Force 4:3</string>
@ -364,6 +370,12 @@
<string name="use_black_backgrounds">Black backgrounds</string> <string name="use_black_backgrounds">Black backgrounds</string>
<string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string> <string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string>
<!-- Picture-In-Picture -->
<string name="picture_in_picture">Picture in Picture</string>
<string name="picture_in_picture_description">Minimize window when placed in the background</string>
<string name="pause">Pause</string>
<string name="play">Play</string>
<!-- Licenses screen strings --> <!-- Licenses screen strings -->
<string name="licenses">Licenses</string> <string name="licenses">Licenses</string>
<string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string> <string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string>

View File

@ -142,9 +142,13 @@ void NfcInterface::AttachAvailabilityChangeEvent(HLERequestContext& ctx) {
void NfcInterface::StartDetection(HLERequestContext& ctx) { void NfcInterface::StartDetection(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx}; IPC::RequestParser rp{ctx};
const auto device_handle{rp.Pop<u64>()}; const auto device_handle{rp.Pop<u64>()};
const auto tag_protocol{rp.PopEnum<NfcProtocol>()}; auto tag_protocol{NfcProtocol::All};
LOG_INFO(Service_NFC, "called, device_handle={}, nfp_protocol={}", device_handle, tag_protocol);
if (backend_type == BackendType::Nfc) {
tag_protocol = rp.PopEnum<NfcProtocol>();
}
LOG_INFO(Service_NFC, "called, device_handle={}, nfp_protocol={}", device_handle, tag_protocol);
auto result = GetManager()->StartDetection(device_handle, tag_protocol); auto result = GetManager()->StartDetection(device_handle, tag_protocol);
result = TranslateResultToServiceError(result); result = TranslateResultToServiceError(result);

View File

@ -59,14 +59,11 @@ enum class PackedTagType : u8 {
}; };
// This is nn::nfc::NfcProtocol // This is nn::nfc::NfcProtocol
// Verify this enum. It might be completely wrong default protocol is 0x48
enum class NfcProtocol : u32 { enum class NfcProtocol : u32 {
None, None,
TypeA = 1U << 0, // ISO14443A TypeA = 1U << 0, // ISO14443A
TypeB = 1U << 1, // ISO14443B TypeB = 1U << 1, // ISO14443B
TypeF = 1U << 2, // Sony FeliCa TypeF = 1U << 2, // Sony FeliCa
Unknown1 = 1U << 3,
Unknown2 = 1U << 5,
All = 0xFFFFFFFFU, All = 0xFFFFFFFFU,
}; };

View File

@ -275,9 +275,9 @@ GraphicsPipeline::GraphicsPipeline(const Device& device, TextureCache& texture_c
template <typename Spec> template <typename Spec>
void GraphicsPipeline::ConfigureImpl(bool is_indexed) { void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
std::array<VideoCommon::ImageViewInOut, MAX_TEXTURES + MAX_IMAGES> views; std::array<VideoCommon::ImageViewInOut, MAX_TEXTURES + MAX_IMAGES> views;
std::array<GLuint, MAX_TEXTURES> samplers; std::array<const Sampler*, MAX_TEXTURES> samplers;
size_t views_index{}; size_t views_index{};
GLsizei sampler_binding{}; size_t samplers_index{};
texture_cache.SynchronizeGraphicsDescriptors(); texture_cache.SynchronizeGraphicsDescriptors();
@ -337,7 +337,6 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
for (u32 index = 0; index < desc.count; ++index) { for (u32 index = 0; index < desc.count; ++index) {
const auto handle{read_handle(desc, index)}; const auto handle{read_handle(desc, index)};
views[views_index++] = {handle.first}; views[views_index++] = {handle.first};
samplers[sampler_binding++] = 0;
} }
} }
} }
@ -352,7 +351,7 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
views[views_index++] = {handle.first}; views[views_index++] = {handle.first};
Sampler* const sampler{texture_cache.GetGraphicsSampler(handle.second)}; Sampler* const sampler{texture_cache.GetGraphicsSampler(handle.second)};
samplers[sampler_binding++] = sampler->Handle(); samplers[samplers_index++] = sampler;
} }
} }
if constexpr (Spec::has_images) { if constexpr (Spec::has_images) {
@ -445,10 +444,13 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
program_manager.BindSourcePrograms(source_programs); program_manager.BindSourcePrograms(source_programs);
} }
const VideoCommon::ImageViewInOut* views_it{views.data()}; const VideoCommon::ImageViewInOut* views_it{views.data()};
const Sampler** samplers_it{samplers.data()};
GLsizei texture_binding = 0; GLsizei texture_binding = 0;
GLsizei image_binding = 0; GLsizei image_binding = 0;
GLsizei sampler_binding{};
std::array<GLuint, MAX_TEXTURES> textures; std::array<GLuint, MAX_TEXTURES> textures;
std::array<GLuint, MAX_IMAGES> images; std::array<GLuint, MAX_IMAGES> images;
std::array<GLuint, MAX_TEXTURES> gl_samplers;
const auto prepare_stage{[&](size_t stage) { const auto prepare_stage{[&](size_t stage) {
buffer_cache.runtime.SetImagePointers(&textures[texture_binding], &images[image_binding]); buffer_cache.runtime.SetImagePointers(&textures[texture_binding], &images[image_binding]);
buffer_cache.BindHostStageBuffers(stage); buffer_cache.BindHostStageBuffers(stage);
@ -465,6 +467,13 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
u32 stage_image_binding{}; u32 stage_image_binding{};
const auto& info{stage_infos[stage]}; const auto& info{stage_infos[stage]};
if constexpr (Spec::has_texture_buffers) {
for (const auto& desc : info.texture_buffer_descriptors) {
for (u32 index = 0; index < desc.count; ++index) {
gl_samplers[sampler_binding++] = 0;
}
}
}
for (const auto& desc : info.texture_descriptors) { for (const auto& desc : info.texture_descriptors) {
for (u32 index = 0; index < desc.count; ++index) { for (u32 index = 0; index < desc.count; ++index) {
ImageView& image_view{texture_cache.GetImageView((views_it++)->id)}; ImageView& image_view{texture_cache.GetImageView((views_it++)->id)};
@ -474,6 +483,12 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
} }
++texture_binding; ++texture_binding;
++stage_texture_binding; ++stage_texture_binding;
const Sampler& sampler{**(samplers_it++)};
const bool use_fallback_sampler{sampler.HasAddedAnisotropy() &&
!image_view.SupportsAnisotropy()};
gl_samplers[sampler_binding++] =
use_fallback_sampler ? sampler.HandleWithDefaultAnisotropy() : sampler.Handle();
} }
} }
for (const auto& desc : info.image_descriptors) { for (const auto& desc : info.image_descriptors) {
@ -534,7 +549,7 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
if (texture_binding != 0) { if (texture_binding != 0) {
ASSERT(texture_binding == sampler_binding); ASSERT(texture_binding == sampler_binding);
glBindTextures(0, texture_binding, textures.data()); glBindTextures(0, texture_binding, textures.data());
glBindSamplers(0, sampler_binding, samplers.data()); glBindSamplers(0, sampler_binding, gl_samplers.data());
} }
if (image_binding != 0) { if (image_binding != 0) {
glBindImageTextures(0, image_binding, images.data()); glBindImageTextures(0, image_binding, images.data());

View File

@ -16,9 +16,9 @@ struct ShaderPools {
inst.ReleaseContents(); inst.ReleaseContents();
} }
Shader::ObjectPool<Shader::IR::Inst> inst; Shader::ObjectPool<Shader::IR::Inst> inst{8192};
Shader::ObjectPool<Shader::IR::Block> block; Shader::ObjectPool<Shader::IR::Block> block{32};
Shader::ObjectPool<Shader::Maxwell::Flow::Block> flow_block; Shader::ObjectPool<Shader::Maxwell::Flow::Block> flow_block{32};
}; };
struct Context { struct Context {

View File

@ -1268,8 +1268,12 @@ Sampler::Sampler(TextureCacheRuntime& runtime, const TSCEntry& config) {
UNIMPLEMENTED_IF(config.cubemap_anisotropy != 1); UNIMPLEMENTED_IF(config.cubemap_anisotropy != 1);
sampler.Create(); const f32 max_anisotropy = std::clamp(config.MaxAnisotropy(), 1.0f, 16.0f);
const GLuint handle = sampler.handle;
const auto create_sampler = [&](const f32 anisotropy) {
OGLSampler new_sampler;
new_sampler.Create();
const GLuint handle = new_sampler.handle;
glSamplerParameteri(handle, GL_TEXTURE_WRAP_S, MaxwellToGL::WrapMode(config.wrap_u)); glSamplerParameteri(handle, GL_TEXTURE_WRAP_S, MaxwellToGL::WrapMode(config.wrap_u));
glSamplerParameteri(handle, GL_TEXTURE_WRAP_T, MaxwellToGL::WrapMode(config.wrap_v)); glSamplerParameteri(handle, GL_TEXTURE_WRAP_T, MaxwellToGL::WrapMode(config.wrap_v));
glSamplerParameteri(handle, GL_TEXTURE_WRAP_R, MaxwellToGL::WrapMode(config.wrap_p)); glSamplerParameteri(handle, GL_TEXTURE_WRAP_R, MaxwellToGL::WrapMode(config.wrap_p));
@ -1283,8 +1287,7 @@ Sampler::Sampler(TextureCacheRuntime& runtime, const TSCEntry& config) {
glSamplerParameterfv(handle, GL_TEXTURE_BORDER_COLOR, config.BorderColor().data()); glSamplerParameterfv(handle, GL_TEXTURE_BORDER_COLOR, config.BorderColor().data());
if (GLAD_GL_ARB_texture_filter_anisotropic || GLAD_GL_EXT_texture_filter_anisotropic) { if (GLAD_GL_ARB_texture_filter_anisotropic || GLAD_GL_EXT_texture_filter_anisotropic) {
const f32 max_anisotropy = std::clamp(config.MaxAnisotropy(), 1.0f, 16.0f); glSamplerParameterf(handle, GL_TEXTURE_MAX_ANISOTROPY, anisotropy);
glSamplerParameterf(handle, GL_TEXTURE_MAX_ANISOTROPY, max_anisotropy);
} else { } else {
LOG_WARNING(Render_OpenGL, "GL_ARB_texture_filter_anisotropic is required"); LOG_WARNING(Render_OpenGL, "GL_ARB_texture_filter_anisotropic is required");
} }
@ -1299,6 +1302,15 @@ Sampler::Sampler(TextureCacheRuntime& runtime, const TSCEntry& config) {
// We default to false because it's more common // We default to false because it's more common
LOG_WARNING(Render_OpenGL, "GL_ARB_seamless_cubemap_per_texture is required"); LOG_WARNING(Render_OpenGL, "GL_ARB_seamless_cubemap_per_texture is required");
} }
return new_sampler;
};
sampler = create_sampler(max_anisotropy);
const f32 max_anisotropy_default = static_cast<f32>(1U << config.max_anisotropy);
if (max_anisotropy > max_anisotropy_default) {
sampler_default_anisotropy = create_sampler(max_anisotropy_default);
}
} }
Framebuffer::Framebuffer(TextureCacheRuntime& runtime, std::span<ImageView*, NUM_RT> color_buffers, Framebuffer::Framebuffer(TextureCacheRuntime& runtime, std::span<ImageView*, NUM_RT> color_buffers,

View File

@ -309,12 +309,21 @@ class Sampler {
public: public:
explicit Sampler(TextureCacheRuntime&, const Tegra::Texture::TSCEntry&); explicit Sampler(TextureCacheRuntime&, const Tegra::Texture::TSCEntry&);
GLuint Handle() const noexcept { [[nodiscard]] GLuint Handle() const noexcept {
return sampler.handle; return sampler.handle;
} }
[[nodiscard]] GLuint HandleWithDefaultAnisotropy() const noexcept {
return sampler_default_anisotropy.handle;
}
[[nodiscard]] bool HasAddedAnisotropy() const noexcept {
return static_cast<bool>(sampler_default_anisotropy.handle);
}
private: private:
OGLSampler sampler; OGLSampler sampler;
OGLSampler sampler_default_anisotropy;
}; };
class Framebuffer { class Framebuffer {

View File

@ -178,7 +178,7 @@ public:
inline void PushImageDescriptors(TextureCache& texture_cache, inline void PushImageDescriptors(TextureCache& texture_cache,
GuestDescriptorQueue& guest_descriptor_queue, GuestDescriptorQueue& guest_descriptor_queue,
const Shader::Info& info, RescalingPushConstant& rescaling, const Shader::Info& info, RescalingPushConstant& rescaling,
const VkSampler*& samplers, const Sampler**& samplers,
const VideoCommon::ImageViewInOut*& views) { const VideoCommon::ImageViewInOut*& views) {
const u32 num_texture_buffers = Shader::NumDescriptors(info.texture_buffer_descriptors); const u32 num_texture_buffers = Shader::NumDescriptors(info.texture_buffer_descriptors);
const u32 num_image_buffers = Shader::NumDescriptors(info.image_buffer_descriptors); const u32 num_image_buffers = Shader::NumDescriptors(info.image_buffer_descriptors);
@ -187,10 +187,14 @@ inline void PushImageDescriptors(TextureCache& texture_cache,
for (const auto& desc : info.texture_descriptors) { for (const auto& desc : info.texture_descriptors) {
for (u32 index = 0; index < desc.count; ++index) { for (u32 index = 0; index < desc.count; ++index) {
const VideoCommon::ImageViewId image_view_id{(views++)->id}; const VideoCommon::ImageViewId image_view_id{(views++)->id};
const VkSampler sampler{*(samplers++)};
ImageView& image_view{texture_cache.GetImageView(image_view_id)}; ImageView& image_view{texture_cache.GetImageView(image_view_id)};
const VkImageView vk_image_view{image_view.Handle(desc.type)}; const VkImageView vk_image_view{image_view.Handle(desc.type)};
guest_descriptor_queue.AddSampledImage(vk_image_view, sampler); const Sampler& sampler{**(samplers++)};
const bool use_fallback_sampler{sampler.HasAddedAnisotropy() &&
!image_view.SupportsAnisotropy()};
const VkSampler vk_sampler{use_fallback_sampler ? sampler.HandleWithDefaultAnisotropy()
: sampler.Handle()};
guest_descriptor_queue.AddSampledImage(vk_image_view, vk_sampler);
rescaling.PushTexture(texture_cache.IsRescaling(image_view)); rescaling.PushTexture(texture_cache.IsRescaling(image_view));
} }
} }

View File

@ -115,7 +115,7 @@ void ComputePipeline::Configure(Tegra::Engines::KeplerCompute& kepler_compute,
static constexpr size_t max_elements = 64; static constexpr size_t max_elements = 64;
boost::container::static_vector<VideoCommon::ImageViewInOut, max_elements> views; boost::container::static_vector<VideoCommon::ImageViewInOut, max_elements> views;
boost::container::static_vector<VkSampler, max_elements> samplers; boost::container::static_vector<const Sampler*, max_elements> samplers;
const auto& qmd{kepler_compute.launch_description}; const auto& qmd{kepler_compute.launch_description};
const auto& cbufs{qmd.const_buffer_config}; const auto& cbufs{qmd.const_buffer_config};
@ -161,7 +161,7 @@ void ComputePipeline::Configure(Tegra::Engines::KeplerCompute& kepler_compute,
views.push_back({handle.first}); views.push_back({handle.first});
Sampler* const sampler = texture_cache.GetComputeSampler(handle.second); Sampler* const sampler = texture_cache.GetComputeSampler(handle.second);
samplers.push_back(sampler->Handle()); samplers.push_back(sampler);
} }
} }
for (const auto& desc : info.image_descriptors) { for (const auto& desc : info.image_descriptors) {
@ -192,7 +192,7 @@ void ComputePipeline::Configure(Tegra::Engines::KeplerCompute& kepler_compute,
buffer_cache.BindHostComputeBuffers(); buffer_cache.BindHostComputeBuffers();
RescalingPushConstant rescaling; RescalingPushConstant rescaling;
const VkSampler* samplers_it{samplers.data()}; const Sampler** samplers_it{samplers.data()};
const VideoCommon::ImageViewInOut* views_it{views.data()}; const VideoCommon::ImageViewInOut* views_it{views.data()};
PushImageDescriptors(texture_cache, guest_descriptor_queue, info, rescaling, samplers_it, PushImageDescriptors(texture_cache, guest_descriptor_queue, info, rescaling, samplers_it,
views_it); views_it);

View File

@ -298,7 +298,7 @@ void GraphicsPipeline::AddTransition(GraphicsPipeline* transition) {
template <typename Spec> template <typename Spec>
void GraphicsPipeline::ConfigureImpl(bool is_indexed) { void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
std::array<VideoCommon::ImageViewInOut, MAX_IMAGE_ELEMENTS> views; std::array<VideoCommon::ImageViewInOut, MAX_IMAGE_ELEMENTS> views;
std::array<VkSampler, MAX_IMAGE_ELEMENTS> samplers; std::array<const Sampler*, MAX_IMAGE_ELEMENTS> samplers;
size_t sampler_index{}; size_t sampler_index{};
size_t view_index{}; size_t view_index{};
@ -368,7 +368,7 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
views[view_index++] = {handle.first}; views[view_index++] = {handle.first};
Sampler* const sampler{texture_cache.GetGraphicsSampler(handle.second)}; Sampler* const sampler{texture_cache.GetGraphicsSampler(handle.second)};
samplers[sampler_index++] = sampler->Handle(); samplers[sampler_index++] = sampler;
} }
} }
if constexpr (Spec::has_images) { if constexpr (Spec::has_images) {
@ -453,7 +453,7 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
RescalingPushConstant rescaling; RescalingPushConstant rescaling;
RenderAreaPushConstant render_area; RenderAreaPushConstant render_area;
const VkSampler* samplers_it{samplers.data()}; const Sampler** samplers_it{samplers.data()};
const VideoCommon::ImageViewInOut* views_it{views.data()}; const VideoCommon::ImageViewInOut* views_it{views.data()};
const auto prepare_stage{[&](size_t stage) LAMBDA_FORCEINLINE { const auto prepare_stage{[&](size_t stage) LAMBDA_FORCEINLINE {
buffer_cache.BindHostStageBuffers(stage); buffer_cache.BindHostStageBuffers(stage);

View File

@ -92,9 +92,9 @@ struct ShaderPools {
inst.ReleaseContents(); inst.ReleaseContents();
} }
Shader::ObjectPool<Shader::IR::Inst> inst; Shader::ObjectPool<Shader::IR::Inst> inst{8192};
Shader::ObjectPool<Shader::IR::Block> block; Shader::ObjectPool<Shader::IR::Block> block{32};
Shader::ObjectPool<Shader::Maxwell::Flow::Block> flow_block; Shader::ObjectPool<Shader::Maxwell::Flow::Block> flow_block{32};
}; };
class PipelineCache : public VideoCommon::ShaderCache { class PipelineCache : public VideoCommon::ShaderCache {

View File

@ -1803,7 +1803,8 @@ Sampler::Sampler(TextureCacheRuntime& runtime, const Tegra::Texture::TSCEntry& t
// Some games have samplers with garbage. Sanitize them here. // Some games have samplers with garbage. Sanitize them here.
const f32 max_anisotropy = std::clamp(tsc.MaxAnisotropy(), 1.0f, 16.0f); const f32 max_anisotropy = std::clamp(tsc.MaxAnisotropy(), 1.0f, 16.0f);
sampler = device.GetLogical().CreateSampler(VkSamplerCreateInfo{ const auto create_sampler = [&](const f32 anisotropy) {
return device.GetLogical().CreateSampler(VkSamplerCreateInfo{
.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
.pNext = pnext, .pNext = pnext,
.flags = 0, .flags = 0,
@ -1814,8 +1815,8 @@ Sampler::Sampler(TextureCacheRuntime& runtime, const Tegra::Texture::TSCEntry& t
.addressModeV = MaxwellToVK::Sampler::WrapMode(device, tsc.wrap_v, tsc.mag_filter), .addressModeV = MaxwellToVK::Sampler::WrapMode(device, tsc.wrap_v, tsc.mag_filter),
.addressModeW = MaxwellToVK::Sampler::WrapMode(device, tsc.wrap_p, tsc.mag_filter), .addressModeW = MaxwellToVK::Sampler::WrapMode(device, tsc.wrap_p, tsc.mag_filter),
.mipLodBias = tsc.LodBias(), .mipLodBias = tsc.LodBias(),
.anisotropyEnable = static_cast<VkBool32>(max_anisotropy > 1.0f ? VK_TRUE : VK_FALSE), .anisotropyEnable = static_cast<VkBool32>(anisotropy > 1.0f ? VK_TRUE : VK_FALSE),
.maxAnisotropy = max_anisotropy, .maxAnisotropy = anisotropy,
.compareEnable = tsc.depth_compare_enabled, .compareEnable = tsc.depth_compare_enabled,
.compareOp = MaxwellToVK::Sampler::DepthCompareFunction(tsc.depth_compare_func), .compareOp = MaxwellToVK::Sampler::DepthCompareFunction(tsc.depth_compare_func),
.minLod = tsc.mipmap_filter == TextureMipmapFilter::None ? 0.0f : tsc.MinLod(), .minLod = tsc.mipmap_filter == TextureMipmapFilter::None ? 0.0f : tsc.MinLod(),
@ -1824,6 +1825,14 @@ Sampler::Sampler(TextureCacheRuntime& runtime, const Tegra::Texture::TSCEntry& t
arbitrary_borders ? VK_BORDER_COLOR_FLOAT_CUSTOM_EXT : ConvertBorderColor(color), arbitrary_borders ? VK_BORDER_COLOR_FLOAT_CUSTOM_EXT : ConvertBorderColor(color),
.unnormalizedCoordinates = VK_FALSE, .unnormalizedCoordinates = VK_FALSE,
}); });
};
sampler = create_sampler(max_anisotropy);
const f32 max_anisotropy_default = static_cast<f32>(1U << tsc.max_anisotropy);
if (max_anisotropy > max_anisotropy_default) {
sampler_default_anisotropy = create_sampler(max_anisotropy_default);
}
} }
Framebuffer::Framebuffer(TextureCacheRuntime& runtime, std::span<ImageView*, NUM_RT> color_buffers, Framebuffer::Framebuffer(TextureCacheRuntime& runtime, std::span<ImageView*, NUM_RT> color_buffers,

View File

@ -279,8 +279,17 @@ public:
return *sampler; return *sampler;
} }
[[nodiscard]] VkSampler HandleWithDefaultAnisotropy() const noexcept {
return *sampler_default_anisotropy;
}
[[nodiscard]] bool HasAddedAnisotropy() const noexcept {
return static_cast<bool>(sampler_default_anisotropy);
}
private: private:
vk::Sampler sampler; vk::Sampler sampler;
vk::Sampler sampler_default_anisotropy;
}; };
class Framebuffer { class Framebuffer {

View File

@ -45,4 +45,52 @@ ImageViewBase::ImageViewBase(const ImageInfo& info, const ImageViewInfo& view_in
ImageViewBase::ImageViewBase(const NullImageViewParams&) : image_id{NULL_IMAGE_ID} {} ImageViewBase::ImageViewBase(const NullImageViewParams&) : image_id{NULL_IMAGE_ID} {}
bool ImageViewBase::SupportsAnisotropy() const noexcept {
using namespace VideoCommon;
switch (format) {
case PixelFormat::R8_UNORM:
case PixelFormat::R8_SNORM:
case PixelFormat::R8_SINT:
case PixelFormat::R8_UINT:
case PixelFormat::BC4_UNORM:
case PixelFormat::BC4_SNORM:
case PixelFormat::BC5_UNORM:
case PixelFormat::BC5_SNORM:
case PixelFormat::R32G32_FLOAT:
case PixelFormat::R32G32_SINT:
case PixelFormat::R32_FLOAT:
case PixelFormat::R16_FLOAT:
case PixelFormat::R16_UNORM:
case PixelFormat::R16_SNORM:
case PixelFormat::R16_UINT:
case PixelFormat::R16_SINT:
case PixelFormat::R16G16_UNORM:
case PixelFormat::R16G16_FLOAT:
case PixelFormat::R16G16_UINT:
case PixelFormat::R16G16_SINT:
case PixelFormat::R16G16_SNORM:
case PixelFormat::R8G8_UNORM:
case PixelFormat::R8G8_SNORM:
case PixelFormat::R8G8_SINT:
case PixelFormat::R8G8_UINT:
case PixelFormat::R32G32_UINT:
case PixelFormat::R32_UINT:
case PixelFormat::R32_SINT:
case PixelFormat::G4R4_UNORM:
// Depth formats
case PixelFormat::D32_FLOAT:
case PixelFormat::D16_UNORM:
// Stencil formats
case PixelFormat::S8_UINT:
// DepthStencil formats
case PixelFormat::D24_UNORM_S8_UINT:
case PixelFormat::S8_UINT_D24_UNORM:
case PixelFormat::D32_FLOAT_S8_UINT:
return false;
default:
break;
}
return range.extent.levels > 1;
}
} // namespace VideoCommon } // namespace VideoCommon

View File

@ -33,6 +33,8 @@ struct ImageViewBase {
return type == ImageViewType::Buffer; return type == ImageViewType::Buffer;
} }
[[nodiscard]] bool SupportsAnisotropy() const noexcept;
ImageId image_id{}; ImageId image_id{};
GPUVAddr gpu_addr = 0; GPUVAddr gpu_addr = 0;
PixelFormat format{}; PixelFormat format{};

View File

@ -62,12 +62,14 @@ std::array<float, 4> TSCEntry::BorderColor() const noexcept {
} }
float TSCEntry::MaxAnisotropy() const noexcept { float TSCEntry::MaxAnisotropy() const noexcept {
const bool is_unsupported_mipmap_filter = Settings::values.use_aggressive_anisotropic_filtering const bool is_suitable_mipmap_filter = Settings::values.use_aggressive_anisotropic_filtering
? mipmap_filter == TextureMipmapFilter::None ? mipmap_filter != TextureMipmapFilter::None
: mipmap_filter != TextureMipmapFilter::Linear; : mipmap_filter == TextureMipmapFilter::Linear;
const bool has_regular_lods = min_lod_clamp == 0 && max_lod_clamp >= 256; const bool has_regular_lods = min_lod_clamp == 0 && max_lod_clamp >= 256;
if (max_anisotropy == 0 && const bool is_bilinear_filter = min_filter == TextureFilter::Linear &&
(depth_compare_enabled.Value() || !has_regular_lods || is_unsupported_mipmap_filter)) { reduction_filter == SamplerReduction::WeightedAverage;
if (max_anisotropy == 0 && (depth_compare_enabled || !has_regular_lods || !is_bilinear_filter ||
!is_suitable_mipmap_filter)) {
return 1.0f; return 1.0f;
} }
const auto anisotropic_settings = Settings::values.max_anisotropy.GetValue(); const auto anisotropic_settings = Settings::values.max_anisotropy.GetValue();