early-access version 3629

This commit is contained in:
pineappleEA 2023-06-02 10:28:14 +02:00
parent ab55aa0871
commit 5d6ba5745f
50 changed files with 19144 additions and 15830 deletions

View file

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

1283
dist/languages/ca.ts vendored

File diff suppressed because it is too large Load diff

1283
dist/languages/cs.ts vendored

File diff suppressed because it is too large Load diff

1283
dist/languages/da.ts vendored

File diff suppressed because it is too large Load diff

1396
dist/languages/de.ts vendored

File diff suppressed because it is too large Load diff

1349
dist/languages/el.ts vendored

File diff suppressed because it is too large Load diff

1293
dist/languages/es.ts vendored

File diff suppressed because it is too large Load diff

1271
dist/languages/fr.ts vendored

File diff suppressed because it is too large Load diff

1285
dist/languages/id.ts vendored

File diff suppressed because it is too large Load diff

1267
dist/languages/it.ts vendored

File diff suppressed because it is too large Load diff

1331
dist/languages/ja_JP.ts vendored

File diff suppressed because it is too large Load diff

1332
dist/languages/ko_KR.ts vendored

File diff suppressed because it is too large Load diff

2286
dist/languages/nb.ts vendored

File diff suppressed because it is too large Load diff

1275
dist/languages/nl.ts vendored

File diff suppressed because it is too large Load diff

1284
dist/languages/pl.ts vendored

File diff suppressed because it is too large Load diff

1267
dist/languages/pt_BR.ts vendored

File diff suppressed because it is too large Load diff

1267
dist/languages/pt_PT.ts vendored

File diff suppressed because it is too large Load diff

1273
dist/languages/ru_RU.ts vendored

File diff suppressed because it is too large Load diff

1283
dist/languages/sv.ts vendored

File diff suppressed because it is too large Load diff

1679
dist/languages/tr_TR.ts vendored

File diff suppressed because it is too large Load diff

1486
dist/languages/uk.ts vendored

File diff suppressed because it is too large Load diff

2069
dist/languages/vi.ts vendored

File diff suppressed because it is too large Load diff

2069
dist/languages/vi_VN.ts vendored

File diff suppressed because it is too large Load diff

1273
dist/languages/zh_CN.ts vendored

File diff suppressed because it is too large Load diff

1293
dist/languages/zh_TW.ts vendored

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,7 @@ import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Rect
import android.hardware.Sensor
import android.hardware.SensorEvent
@ -31,6 +32,7 @@ import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.fragments.EmulationFragment
import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.utils.ControllerMappingHelper
import org.yuzu.yuzu_emu.utils.EmulationMenuSettings
import org.yuzu.yuzu_emu.utils.ForegroundService
import org.yuzu.yuzu_emu.utils.InputHandler
import org.yuzu.yuzu_emu.utils.NfcReader
@ -128,6 +130,11 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
super.onResume()
nfcReader.startScanning()
startMotionSensorListener()
NativeLibrary.notifyOrientationChange(
EmulationMenuSettings.landscapeScreenLayout,
getAdjustedRotation()
)
}
override fun onPause() {
@ -233,6 +240,23 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
override fun onAccuracyChanged(sensor: Sensor, i: Int) {}
private fun getAdjustedRotation():Int {
val rotation = windowManager.defaultDisplay.rotation;
val config: Configuration = resources.configuration
if ((config.screenLayout and Configuration.SCREENLAYOUT_LONG_YES) != 0 ||
(config.screenLayout and Configuration.SCREENLAYOUT_LONG_NO) == 0) {
return rotation;
}
when (rotation) {
Surface.ROTATION_0 -> return Surface.ROTATION_90;
Surface.ROTATION_90 -> return Surface.ROTATION_0;
Surface.ROTATION_180 -> return Surface.ROTATION_270;
Surface.ROTATION_270 -> return Surface.ROTATION_180;
}
return rotation;
}
private fun restoreState(savedInstanceState: Bundle) {
game = savedInstanceState.parcelable(EXTRA_SELECTED_GAME)!!
}
@ -252,39 +276,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
View.SYSTEM_UI_FLAG_IMMERSIVE
}
private fun editControlsPlacement() {
if (emulationFragment!!.isConfiguringControls) {
emulationFragment!!.stopConfiguringControls()
} else {
emulationFragment!!.startConfiguringControls()
}
}
private fun adjustScale() {
val sliderBinding = DialogSliderBinding.inflate(layoutInflater)
sliderBinding.slider.valueTo = 150F
sliderBinding.slider.value =
PreferenceManager.getDefaultSharedPreferences(applicationContext)
.getInt(Settings.PREF_CONTROL_SCALE, 50).toFloat()
sliderBinding.slider.addOnChangeListener(OnChangeListener { _, value, _ ->
sliderBinding.textValue.text = value.toString()
setControlScale(value.toInt())
})
sliderBinding.textValue.text = sliderBinding.slider.value.toString()
sliderBinding.textUnits.text = "%"
MaterialAlertDialogBuilder(this)
.setTitle(R.string.emulation_control_scale)
.setView(sliderBinding.root)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
setControlScale(sliderBinding.slider.value.toInt())
}
.setNeutralButton(R.string.slider_default) { _: DialogInterface?, _: Int ->
setControlScale(50)
}
.show()
}
private fun startMotionSensorListener() {
val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
@ -302,22 +293,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
sensorManager.unregisterListener(this, accelSensor)
}
private fun setControlScale(scale: Int) {
PreferenceManager.getDefaultSharedPreferences(applicationContext).edit()
.putInt(Settings.PREF_CONTROL_SCALE, scale)
.apply()
emulationFragment!!.refreshInputOverlay()
}
private fun resetOverlay() {
MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.emulation_touch_overlay_reset))
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> emulationFragment!!.resetInputOverlay() }
.setNegativeButton(android.R.string.cancel, null)
.create()
.show()
}
companion object {
const val EXTRA_SELECTED_GAME = "SelectedGame"

View file

@ -111,6 +111,7 @@ class Settings {
const val PREF_OVERLAY_INIT = "OverlayInit"
const val PREF_CONTROL_SCALE = "controlScale"
const val PREF_CONTROL_OPACITY = "controlOpacity"
const val PREF_TOUCH_ENABLED = "isTouchEnabled"
const val PREF_BUTTON_TOGGLE_0 = "buttonToggle0"
const val PREF_BUTTON_TOGGLE_1 = "buttonToggle1"

View file

@ -118,12 +118,6 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
override fun onStop() {
super.onStop()
presenter.onStop(isFinishing)
// Update framebuffer layout when closing the settings
NativeLibrary.notifyOrientationChange(
EmulationMenuSettings.landscapeScreenLayout,
windowManager.defaultDisplay.rotation
)
}
override fun showSettingsFragment(menuTag: String, addToStack: Boolean, gameId: String) {

View file

@ -3,8 +3,10 @@
package org.yuzu.yuzu_emu.fragments
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.content.DialogInterface
import android.content.SharedPreferences
import android.graphics.Color
import android.os.Bundle
@ -21,10 +23,12 @@ import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.slider.Slider
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
@ -168,14 +172,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
super.onDetach()
}
fun refreshInputOverlay() {
private fun refreshInputOverlay() {
binding.surfaceInputOverlay.refreshControls()
}
fun resetInputOverlay() {
// Reset button scale
private fun resetInputOverlay() {
preferences.edit()
.putInt(Settings.PREF_CONTROL_SCALE, 50)
.remove(Settings.PREF_CONTROL_SCALE)
.remove(Settings.PREF_CONTROL_OPACITY)
.apply()
binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.resetButtonPlacement() }
}
@ -251,6 +255,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
true
}
R.id.menu_adjust_overlay -> {
adjustOverlay()
true
}
R.id.menu_toggle_controls -> {
val preferences =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
@ -278,9 +287,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
// Override normal behaviour so the dialog doesn't close
dialog.getButton(AlertDialog.BUTTON_NEUTRAL)
.setOnClickListener {
val isChecked = !optionsArray[0];
val isChecked = !optionsArray[0]
for (i in 0..14) {
optionsArray[i] = isChecked;
optionsArray[i] = isChecked
dialog.listView.setItemChecked(i, isChecked)
preferences.edit()
.putBoolean("buttonToggle$i", isChecked)
@ -328,18 +337,64 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
popup.show()
}
fun startConfiguringControls() {
private fun startConfiguringControls() {
binding.doneControlConfig.visibility = View.VISIBLE
binding.surfaceInputOverlay.setIsInEditMode(true)
}
fun stopConfiguringControls() {
private fun stopConfiguringControls() {
binding.doneControlConfig.visibility = View.GONE
binding.surfaceInputOverlay.setIsInEditMode(false)
}
val isConfiguringControls: Boolean
get() = binding.surfaceInputOverlay.isInEditMode
@SuppressLint("SetTextI18n")
private fun adjustOverlay() {
val adjustBinding = DialogOverlayAdjustBinding.inflate(layoutInflater)
adjustBinding.apply {
inputScaleSlider.apply {
valueTo = 150F
value = preferences.getInt(Settings.PREF_CONTROL_SCALE, 50).toFloat()
addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
inputScaleValue.text = "${value.toInt()}%"
setControlScale(value.toInt())
})
}
inputOpacitySlider.apply {
valueTo = 100F
value = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100).toFloat()
addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
inputOpacityValue.text = "${value.toInt()}%"
setControlOpacity(value.toInt())
})
}
inputScaleValue.text = "${inputScaleSlider.value.toInt()}%"
inputOpacityValue.text = "${inputOpacitySlider.value.toInt()}%"
}
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.emulation_control_adjust)
.setView(adjustBinding.root)
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(R.string.slider_default) { _: DialogInterface?, _: Int ->
setControlScale(50)
setControlOpacity(100)
}
.show()
}
private fun setControlScale(scale: Int) {
preferences.edit()
.putInt(Settings.PREF_CONTROL_SCALE, scale)
.apply()
refreshInputOverlay()
}
private fun setControlOpacity(opacity: Int) {
preferences.edit()
.putInt(Settings.PREF_CONTROL_OPACITY, opacity)
.apply()
refreshInputOverlay()
}
private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener(binding.inGameMenu) { v: View, windowInsets: WindowInsetsCompat ->
@ -415,8 +470,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
if (state != State.PAUSED) {
Log.debug("[EmulationFragment] Pausing emulation.")
// Release the surface before pausing, since emulation has to be running for that.
NativeLibrary.surfaceDestroyed()
NativeLibrary.pauseEmulation()
state = State.PAUSED
@ -461,7 +514,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
Log.debug("[EmulationFragment] Surface destroyed.")
when (state) {
State.RUNNING -> {
NativeLibrary.surfaceDestroyed()
state = State.PAUSED
}

View file

@ -99,7 +99,7 @@ class HomeSettingsFragment : Fragment() {
R.drawable.ic_add
) { mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) },
HomeSetting(
R.string.import_export_saves,
R.string.manage_save_data,
R.string.import_export_saves_description,
R.drawable.ic_save
) { ImportExportSavesFragment().show(parentFragmentManager, ImportExportSavesFragment.TAG) },

View file

@ -68,19 +68,21 @@ class ImportExportSavesFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return if (savesFolderRoot == "") {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.import_export_saves)
.setTitle(R.string.manage_save_data)
.setMessage(R.string.import_export_saves_no_profile)
.setPositiveButton(android.R.string.ok, null)
.show()
} else {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.import_export_saves)
.setPositiveButton(R.string.export_saves) { _, _ ->
.setTitle(R.string.manage_save_data)
.setMessage(R.string.manage_save_data_description)
.setNegativeButton(R.string.export_saves) { _, _ ->
exportSave()
}
.setNeutralButton(R.string.import_saves) { _, _ ->
.setPositiveButton(R.string.import_saves) { _, _ ->
documentPicker.launch(arrayOf("application/zip"))
}
.setNeutralButton(android.R.string.cancel, null)
.show()
}
}
@ -95,7 +97,10 @@ class ImportExportSavesFragment : DialogFragment() {
tempFolder.mkdirs()
val saveFolder = File(savesFolderRoot)
val outputZipFile = File(
tempFolder, "yuzu saves - ${LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))}.zip"
tempFolder,
"yuzu saves - ${
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
}.zip"
)
outputZipFile.createNewFile()
ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos ->
@ -206,11 +211,10 @@ class ImportExportSavesFragment : DialogFragment() {
withContext(Dispatchers.Main) {
if (!validZip) {
Toast.makeText(
context,
context.getString(R.string.save_file_invalid_zip_structure),
Toast.LENGTH_LONG
).show()
MessageDialogFragment.newInstance(
R.string.save_file_invalid_zip_structure,
R.string.save_file_invalid_zip_structure_description
).show(childFragmentManager, MessageDialogFragment.TAG)
return@withContext
}
Toast.makeText(

View file

@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.app.Dialog
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.R
class MessageDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val titleId = requireArguments().getInt(TITLE)
val descriptionId = requireArguments().getInt(DESCRIPTION)
val helpLinkId = requireArguments().getInt(HELP_LINK)
val dialog = MaterialAlertDialogBuilder(requireContext())
.setPositiveButton(R.string.close, null)
.setTitle(titleId)
.setMessage(descriptionId)
if (helpLinkId != 0) {
dialog.setNeutralButton(R.string.learn_more) { _, _ ->
openLink(getString(helpLinkId))
}
}
return dialog.show()
}
private fun openLink(link: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
startActivity(intent)
}
companion object {
const val TAG = "MessageDialogFragment"
private const val TITLE = "Title"
private const val DESCRIPTION = "Description"
private const val HELP_LINK = "Link"
fun newInstance(
titleId: Int,
descriptionId: Int,
helpLinkId: Int = 0
): MessageDialogFragment {
val dialog = MessageDialogFragment()
val bundle = Bundle()
bundle.apply {
putInt(TITLE, titleId)
putInt(DESCRIPTION, descriptionId)
putInt(HELP_LINK, helpLinkId)
}
dialog.arguments = bundle
return dialog
}
}
}

View file

@ -9,6 +9,7 @@ import android.content.SharedPreferences
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Point
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.graphics.drawable.VectorDrawable
@ -48,9 +49,6 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
private var dpadBeingConfigured: InputOverlayDrawableDpad? = null
private var joystickBeingConfigured: InputOverlayDrawableJoystick? = null
private val preferences: SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
private lateinit var windowInsets: WindowInsets
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
@ -343,10 +341,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
}
private fun addOverlayControls(orientation: String) {
val windowSize = getSafeScreenSize(context)
if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_0, true)) {
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.facebutton_a,
R.drawable.facebutton_a_depressed,
ButtonType.BUTTON_A,
@ -358,6 +358,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.facebutton_b,
R.drawable.facebutton_b_depressed,
ButtonType.BUTTON_B,
@ -369,6 +370,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.facebutton_x,
R.drawable.facebutton_x_depressed,
ButtonType.BUTTON_X,
@ -380,6 +382,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.facebutton_y,
R.drawable.facebutton_y_depressed,
ButtonType.BUTTON_Y,
@ -391,6 +394,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.l_shoulder,
R.drawable.l_shoulder_depressed,
ButtonType.TRIGGER_L,
@ -402,6 +406,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.r_shoulder,
R.drawable.r_shoulder_depressed,
ButtonType.TRIGGER_R,
@ -413,6 +418,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.zl_trigger,
R.drawable.zl_trigger_depressed,
ButtonType.TRIGGER_ZL,
@ -424,6 +430,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.zr_trigger,
R.drawable.zr_trigger_depressed,
ButtonType.TRIGGER_ZR,
@ -435,6 +442,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.facebutton_plus,
R.drawable.facebutton_plus_depressed,
ButtonType.BUTTON_PLUS,
@ -446,6 +454,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.facebutton_minus,
R.drawable.facebutton_minus_depressed,
ButtonType.BUTTON_MINUS,
@ -457,6 +466,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
overlayDpads.add(
initializeOverlayDpad(
context,
windowSize,
R.drawable.dpad_standard,
R.drawable.dpad_standard_cardinal_depressed,
R.drawable.dpad_standard_diagonal_depressed,
@ -468,6 +478,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
overlayJoysticks.add(
initializeOverlayJoystick(
context,
windowSize,
R.drawable.joystick_range,
R.drawable.joystick,
R.drawable.joystick_depressed,
@ -481,6 +492,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
overlayJoysticks.add(
initializeOverlayJoystick(
context,
windowSize,
R.drawable.joystick_range,
R.drawable.joystick,
R.drawable.joystick_depressed,
@ -494,6 +506,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.facebutton_home,
R.drawable.facebutton_home_depressed,
ButtonType.BUTTON_HOME,
@ -505,6 +518,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.facebutton_screenshot,
R.drawable.facebutton_screenshot_depressed,
ButtonType.BUTTON_CAPTURE,
@ -530,9 +544,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
}
private fun saveControlPosition(sharedPrefsId: Int, x: Int, y: Int, orientation: String) {
val windowSize = getSafeScreenSize(context)
val min = windowSize.first
val max = windowSize.second
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
.putFloat("$sharedPrefsId$orientation-X", x.toFloat())
.putFloat("$sharedPrefsId$orientation-Y", y.toFloat())
.putFloat("$sharedPrefsId$orientation-X", (x - min.x).toFloat() / max.x)
.putFloat("$sharedPrefsId$orientation-Y", (y - min.y).toFloat() / max.y)
.apply()
}
@ -557,170 +574,129 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
}
private fun defaultOverlayLandscape() {
// Get screen size
val windowMetrics =
WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(context as Activity)
var maxY = windowMetrics.bounds.height().toFloat()
var maxX = windowMetrics.bounds.width().toFloat()
var minY = 0
var minX = 0
// If we have API access, calculate the safe area to draw the overlay
var cutoutLeft = 0
var cutoutBottom = 0
val insets = windowInsets.displayCutout
if (insets != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
maxY =
if (insets.boundingRectTop.bottom != 0) insets.boundingRectTop.bottom.toFloat() else maxY
maxX =
if (insets.boundingRectRight.left != 0) insets.boundingRectRight.left.toFloat() else maxX
minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left
minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom
cutoutLeft = insets.boundingRectRight.right - insets.boundingRectRight.left
cutoutBottom = insets.boundingRectTop.top - insets.boundingRectTop.bottom
}
// This makes sure that if we have an inset on one side of the screen, we mirror it on
// the other side. Since removing space from one of the max values messes with the scale,
// we also have to account for it using our min values.
if (maxX.toInt() != windowMetrics.bounds.width()) minX += cutoutLeft
if (maxY.toInt() != windowMetrics.bounds.height()) minY += cutoutBottom
if (minX > 0 && maxX.toInt() == windowMetrics.bounds.width()) {
maxX -= (minX * 2)
} else if (minX > 0) {
maxX -= minX
}
if (minY > 0 && maxY.toInt() == windowMetrics.bounds.height()) {
maxY -= (minY * 2)
} else if (minY > 0) {
maxY -= minY
}
// Each value is a percent from max X/Y stored as an int. Have to bring that value down
// to a decimal before multiplying by MAX X/Y.
// Each value represents the position of the button in relation to the screen size without insets.
preferences.edit()
.putFloat(
ButtonType.BUTTON_A.toString() + "-X",
resources.getInteger(R.integer.SWITCH_BUTTON_A_X).toFloat() / 1000 * maxX + minX
resources.getInteger(R.integer.SWITCH_BUTTON_A_X).toFloat() / 1000
)
.putFloat(
ButtonType.BUTTON_A.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_BUTTON_A_Y).toFloat() / 1000 * maxY + minY
resources.getInteger(R.integer.SWITCH_BUTTON_A_Y).toFloat() / 1000
)
.putFloat(
ButtonType.BUTTON_B.toString() + "-X",
resources.getInteger(R.integer.SWITCH_BUTTON_B_X).toFloat() / 1000 * maxX + minX
resources.getInteger(R.integer.SWITCH_BUTTON_B_X).toFloat() / 1000
)
.putFloat(
ButtonType.BUTTON_B.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_BUTTON_B_Y).toFloat() / 1000 * maxY + minY
resources.getInteger(R.integer.SWITCH_BUTTON_B_Y).toFloat() / 1000
)
.putFloat(
ButtonType.BUTTON_X.toString() + "-X",
resources.getInteger(R.integer.SWITCH_BUTTON_X_X).toFloat() / 1000 * maxX + minX
resources.getInteger(R.integer.SWITCH_BUTTON_X_X).toFloat() / 1000
)
.putFloat(
ButtonType.BUTTON_X.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_BUTTON_X_Y).toFloat() / 1000 * maxY + minY
resources.getInteger(R.integer.SWITCH_BUTTON_X_Y).toFloat() / 1000
)
.putFloat(
ButtonType.BUTTON_Y.toString() + "-X",
resources.getInteger(R.integer.SWITCH_BUTTON_Y_X).toFloat() / 1000 * maxX + minX
resources.getInteger(R.integer.SWITCH_BUTTON_Y_X).toFloat() / 1000
)
.putFloat(
ButtonType.BUTTON_Y.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_BUTTON_Y_Y).toFloat() / 1000 * maxY + minY
resources.getInteger(R.integer.SWITCH_BUTTON_Y_Y).toFloat() / 1000
)
.putFloat(
ButtonType.TRIGGER_ZL.toString() + "-X",
resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_X).toFloat() / 1000 * maxX + minX
resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_X).toFloat() / 1000
)
.putFloat(
ButtonType.TRIGGER_ZL.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_Y).toFloat() / 1000 * maxY + minY
resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_Y).toFloat() / 1000
)
.putFloat(
ButtonType.TRIGGER_ZR.toString() + "-X",
resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_X).toFloat() / 1000 * maxX + minX
resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_X).toFloat() / 1000
)
.putFloat(
ButtonType.TRIGGER_ZR.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_Y).toFloat() / 1000 * maxY + minY
resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_Y).toFloat() / 1000
)
.putFloat(
ButtonType.DPAD_UP.toString() + "-X",
resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_X).toFloat() / 1000 * maxX + minX
resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_X).toFloat() / 1000
)
.putFloat(
ButtonType.DPAD_UP.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_Y).toFloat() / 1000 * maxY + minY
resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_Y).toFloat() / 1000
)
.putFloat(
ButtonType.TRIGGER_L.toString() + "-X",
resources.getInteger(R.integer.SWITCH_TRIGGER_L_X).toFloat() / 1000 * maxX + minX
resources.getInteger(R.integer.SWITCH_TRIGGER_L_X).toFloat() / 1000
)
.putFloat(
ButtonType.TRIGGER_L.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_TRIGGER_L_Y).toFloat() / 1000 * maxY + minY
resources.getInteger(R.integer.SWITCH_TRIGGER_L_Y).toFloat() / 1000
)
.putFloat(
ButtonType.TRIGGER_R.toString() + "-X",
resources.getInteger(R.integer.SWITCH_TRIGGER_R_X).toFloat() / 1000 * maxX + minX
resources.getInteger(R.integer.SWITCH_TRIGGER_R_X).toFloat() / 1000
)
.putFloat(
ButtonType.TRIGGER_R.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_TRIGGER_R_Y).toFloat() / 1000 * maxY + minY
resources.getInteger(R.integer.SWITCH_TRIGGER_R_Y).toFloat() / 1000
)
.putFloat(
ButtonType.BUTTON_PLUS.toString() + "-X",
resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_X).toFloat() / 1000 * maxX + minX
resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_X).toFloat() / 1000
)
.putFloat(
ButtonType.BUTTON_PLUS.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_Y).toFloat() / 1000 * maxY + minY
resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_Y).toFloat() / 1000
)
.putFloat(
ButtonType.BUTTON_MINUS.toString() + "-X",
resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_X).toFloat() / 1000 * maxX + minX
resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_X).toFloat() / 1000
)
.putFloat(
ButtonType.BUTTON_MINUS.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_Y).toFloat() / 1000 * maxY + minY
resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_Y).toFloat() / 1000
)
.putFloat(
ButtonType.BUTTON_HOME.toString() + "-X",
resources.getInteger(R.integer.SWITCH_BUTTON_HOME_X).toFloat() / 1000 * maxX + minX
resources.getInteger(R.integer.SWITCH_BUTTON_HOME_X).toFloat() / 1000
)
.putFloat(
ButtonType.BUTTON_HOME.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_BUTTON_HOME_Y).toFloat() / 1000 * maxY + minY
resources.getInteger(R.integer.SWITCH_BUTTON_HOME_Y).toFloat() / 1000
)
.putFloat(
ButtonType.BUTTON_CAPTURE.toString() + "-X",
resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_X)
.toFloat() / 1000 * maxX + minX
.toFloat() / 1000
)
.putFloat(
ButtonType.BUTTON_CAPTURE.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_Y)
.toFloat() / 1000 * maxY + minY
.toFloat() / 1000
)
.putFloat(
ButtonType.STICK_R.toString() + "-X",
resources.getInteger(R.integer.SWITCH_STICK_R_X).toFloat() / 1000 * maxX + minX
resources.getInteger(R.integer.SWITCH_STICK_R_X).toFloat() / 1000
)
.putFloat(
ButtonType.STICK_R.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_STICK_R_Y).toFloat() / 1000 * maxY + minY
resources.getInteger(R.integer.SWITCH_STICK_R_Y).toFloat() / 1000
)
.putFloat(
ButtonType.STICK_L.toString() + "-X",
resources.getInteger(R.integer.SWITCH_STICK_L_X).toFloat() / 1000 * maxX + minX
resources.getInteger(R.integer.SWITCH_STICK_L_X).toFloat() / 1000
)
.putFloat(
ButtonType.STICK_L.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_STICK_L_Y).toFloat() / 1000 * maxY + minY
resources.getInteger(R.integer.SWITCH_STICK_L_Y).toFloat() / 1000
)
.apply()
}
@ -730,6 +706,9 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
}
companion object {
private val preferences: SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
/**
* Resizes a [Bitmap] by a given scale factor
*
@ -766,6 +745,59 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
return scaledBitmap
}
/**
* Gets the safe screen size for drawing the overlay
*
* @param context Context for getting the window metrics
* @return A pair of points, the first being the top left corner of the safe area,
* the second being the bottom right corner of the safe area
*/
private fun getSafeScreenSize(context: Context): Pair<Point, Point> {
// Get screen size
val windowMetrics =
WindowMetricsCalculator.getOrCreate()
.computeCurrentWindowMetrics(context as Activity)
var maxY = windowMetrics.bounds.height().toFloat()
var maxX = windowMetrics.bounds.width().toFloat()
var minY = 0
var minX = 0
// If we have API access, calculate the safe area to draw the overlay
var cutoutLeft = 0
var cutoutBottom = 0
val insets = context.windowManager.currentWindowMetrics.windowInsets.displayCutout
if (insets != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (insets.boundingRectTop.bottom != 0 && insets.boundingRectTop.bottom > maxY / 2)
insets.boundingRectTop.bottom.toFloat() else maxY
if (insets.boundingRectRight.left != 0 && insets.boundingRectRight.left > maxX / 2)
insets.boundingRectRight.left.toFloat() else maxX
minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left
minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom
cutoutLeft = insets.boundingRectRight.right - insets.boundingRectRight.left
cutoutBottom = insets.boundingRectTop.top - insets.boundingRectTop.bottom
}
// This makes sure that if we have an inset on one side of the screen, we mirror it on
// the other side. Since removing space from one of the max values messes with the scale,
// we also have to account for it using our min values.
if (maxX.toInt() != windowMetrics.bounds.width()) minX += cutoutLeft
if (maxY.toInt() != windowMetrics.bounds.height()) minY += cutoutBottom
if (minX > 0 && maxX.toInt() == windowMetrics.bounds.width()) {
maxX -= (minX * 2)
} else if (minX > 0) {
maxX -= minX
}
if (minY > 0 && maxY.toInt() == windowMetrics.bounds.height()) {
maxY -= (minY * 2)
} else if (minY > 0) {
maxY -= minY
}
return Pair(Point(minX, minY), Point(maxX.toInt(), maxY.toInt()))
}
/**
* Initializes an InputOverlayDrawableButton, given by resId, with all of the
* parameters set for it to be properly shown on the InputOverlay.
@ -795,6 +827,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
* for Android to call the onDraw method.
*
* @param context The current [Context].
* @param windowSize The size of the window to draw the overlay on.
* @param defaultResId The resource ID of the [Drawable] to get the [Bitmap] of (Default State).
* @param pressedResId The resource ID of the [Drawable] to get the [Bitmap] of (Pressed State).
* @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawableButton represents.
@ -802,6 +835,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
*/
private fun initializeOverlayButton(
context: Context,
windowSize: Pair<Point, Point>,
defaultResId: Int,
pressedResId: Int,
buttonId: Int,
@ -836,12 +870,18 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
val overlayDrawable =
InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId)
// Get the minimum and maximum coordinates of the screen where the button can be placed.
val min = windowSize.first
val max = windowSize.second
// The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
// These were set in the input overlay configuration menu.
val xKey = "$buttonId$orientation-X"
val yKey = "$buttonId$orientation-Y"
val drawableX = sPrefs.getFloat(xKey, 0f).toInt()
val drawableY = sPrefs.getFloat(yKey, 0f).toInt()
val drawableXPercent = sPrefs.getFloat(xKey, 0f)
val drawableYPercent = sPrefs.getFloat(yKey, 0f)
val drawableX = (drawableXPercent * max.x + min.x).toInt()
val drawableY = (drawableYPercent * max.y + min.y).toInt()
val width = overlayDrawable.width
val height = overlayDrawable.height
@ -859,6 +899,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
drawableX - (width / 2),
drawableY - (height / 2)
)
val savedOpacity = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100)
overlayDrawable.setOpacity(savedOpacity * 255 / 100)
return overlayDrawable
}
@ -866,6 +908,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
* Initializes an [InputOverlayDrawableDpad]
*
* @param context The current [Context].
* @param windowSize The size of the window to draw the overlay on.
* @param defaultResId The [Bitmap] resource ID of the default state.
* @param pressedOneDirectionResId The [Bitmap] resource ID of the pressed state in one direction.
* @param pressedTwoDirectionsResId The [Bitmap] resource ID of the pressed state in two directions.
@ -873,6 +916,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
*/
private fun initializeOverlayDpad(
context: Context,
windowSize: Pair<Point, Point>,
defaultResId: Int,
pressedOneDirectionResId: Int,
pressedTwoDirectionsResId: Int,
@ -907,10 +951,16 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
ButtonType.DPAD_RIGHT
)
// Get the minimum and maximum coordinates of the screen where the button can be placed.
val min = windowSize.first
val max = windowSize.second
// The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay.
// These were set in the input overlay configuration menu.
val drawableX = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-X", 0f).toInt()
val drawableY = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-Y", 0f).toInt()
val drawableXPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-X", 0f)
val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-Y", 0f)
val drawableX = (drawableXPercent * max.x + min.x).toInt()
val drawableY = (drawableYPercent * max.y + min.y).toInt()
val width = overlayDrawable.width
val height = overlayDrawable.height
@ -925,6 +975,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
// Need to set the image's position
overlayDrawable.setPosition(drawableX - (width / 2), drawableY - (height / 2))
val savedOpacity = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100)
overlayDrawable.setOpacity(savedOpacity * 255 / 100)
return overlayDrawable
}
@ -932,6 +984,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
* Initializes an [InputOverlayDrawableJoystick]
*
* @param context The current [Context]
* @param windowSize The size of the window to draw the overlay on.
* @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds).
* @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around).
* @param pressedResInner Resource ID for the pressed inner image of the joystick.
@ -941,6 +994,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
*/
private fun initializeOverlayJoystick(
context: Context,
windowSize: Pair<Point, Point>,
resOuter: Int,
defaultResInner: Int,
pressedResInner: Int,
@ -964,10 +1018,16 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
val bitmapInnerDefault = getBitmap(context, defaultResInner, 1.0f)
val bitmapInnerPressed = getBitmap(context, pressedResInner, 1.0f)
// Get the minimum and maximum coordinates of the screen where the button can be placed.
val min = windowSize.first
val max = windowSize.second
// The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
// These were set in the input overlay configuration menu.
val drawableX = sPrefs.getFloat("$button$orientation-X", 0f).toInt()
val drawableY = sPrefs.getFloat("$button$orientation-Y", 0f).toInt()
val drawableXPercent = sPrefs.getFloat("$button$orientation-X", 0f)
val drawableYPercent = sPrefs.getFloat("$button$orientation-Y", 0f)
val drawableX = (drawableXPercent * max.x + min.x).toInt()
val drawableY = (drawableYPercent * max.y + min.y).toInt()
val outerScale = 1.66f
// Now set the bounds for the InputOverlayDrawableJoystick.
@ -996,6 +1056,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
// Need to set the image's position
overlayDrawable.setPosition(drawableX, drawableY)
val savedOpacity = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100)
overlayDrawable.setOpacity(savedOpacity * 255 / 100)
return overlayDrawable
}
}

View file

@ -114,6 +114,7 @@ class InputOverlayDrawableButton(
controlPositionX = fingerPositionX - (width / 2)
controlPositionY = fingerPositionY - (height / 2)
}
MotionEvent.ACTION_MOVE -> {
controlPositionX += fingerPositionX - previousTouchX
controlPositionY += fingerPositionY - previousTouchY
@ -135,6 +136,11 @@ class InputOverlayDrawableButton(
pressedStateBitmap.setBounds(left, top, right, bottom)
}
fun setOpacity(value: Int) {
defaultStateBitmap.alpha = value
pressedStateBitmap.alpha = value
}
val status: Int
get() = if (pressedState) ButtonState.PRESSED else ButtonState.RELEASED
val bounds: Rect

View file

@ -231,6 +231,7 @@ class InputOverlayDrawableDpad(
previousTouchX = fingerPositionX
previousTouchY = fingerPositionY
}
MotionEvent.ACTION_MOVE -> {
controlPositionX += fingerPositionX - previousTouchX
controlPositionY += fingerPositionY - previousTouchY
@ -258,6 +259,12 @@ class InputOverlayDrawableDpad(
pressedTwoDirectionsStateBitmap.setBounds(left, top, right, bottom)
}
fun setOpacity(value: Int) {
defaultStateBitmap.alpha = value
pressedOneDirectionStateBitmap.alpha = value
pressedTwoDirectionsStateBitmap.alpha = value
}
val bounds: Rect
get() = defaultStateBitmap.bounds

View file

@ -48,6 +48,8 @@ class InputOverlayDrawableJoystick(
val width: Int
val height: Int
private var opacity: Int = 0
private var virtBounds: Rect
private var origBounds: Rect
@ -121,7 +123,7 @@ class InputOverlayDrawableJoystick(
}
pressedState = true
outerBitmap.alpha = 0
boundsBoxBitmap.alpha = 255
boundsBoxBitmap.alpha = opacity
if (EmulationMenuSettings.joystickRelCenter) {
virtBounds.offset(
xPosition - virtBounds.centerX(),
@ -139,7 +141,7 @@ class InputOverlayDrawableJoystick(
pressedState = false
xAxis = 0.0f
yAxis = 0.0f
outerBitmap.alpha = 255
outerBitmap.alpha = opacity
boundsBoxBitmap.alpha = 0
virtBounds = Rect(
origBounds.left,
@ -203,6 +205,7 @@ class InputOverlayDrawableJoystick(
controlPositionX = fingerPositionX - (width / 2)
controlPositionY = fingerPositionY - (height / 2)
}
MotionEvent.ACTION_MOVE -> {
controlPositionX += fingerPositionX - previousTouchX
controlPositionY += fingerPositionY - previousTouchY
@ -261,4 +264,19 @@ class InputOverlayDrawableJoystick(
controlPositionX = x
controlPositionY = y
}
fun setOpacity(value: Int) {
opacity = value
defaultStateInnerBitmap.alpha = value
pressedStateInnerBitmap.alpha = value
if (trackId == -1) {
outerBitmap.alpha = value
boundsBoxBitmap.alpha = 0
} else {
outerBitmap.alpha = 0
boundsBoxBitmap.alpha = value
}
}
}

View file

@ -37,6 +37,7 @@ import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.utils.*
@ -251,11 +252,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
if (result == null)
return@registerForActivityResult
val takeFlags =
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
contentResolver.takePersistableUriPermission(
result,
takeFlags
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
// When a new directory is picked, we currently will reset the existing games
@ -279,19 +278,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
return@registerForActivityResult
if (!FileUtil.hasExtension(result.toString(), "keys")) {
Toast.makeText(
applicationContext,
R.string.invalid_keys_file,
Toast.LENGTH_SHORT
).show()
MessageDialogFragment.newInstance(
R.string.reading_keys_failure,
R.string.install_keys_failure_extension_description
).show(supportFragmentManager, MessageDialogFragment.TAG)
return@registerForActivityResult
}
val takeFlags =
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
contentResolver.takePersistableUriPermission(
result,
takeFlags
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
val dstPath = DirectoryInitialization.userDirectory + "/keys/"
@ -310,11 +306,11 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
).show()
gamesViewModel.reloadGames(true)
} else {
Toast.makeText(
applicationContext,
R.string.install_keys_failure,
Toast.LENGTH_LONG
).show()
MessageDialogFragment.newInstance(
R.string.invalid_keys_error,
R.string.install_keys_failure_description,
R.string.dumping_keys_quickstart_link
).show(supportFragmentManager, MessageDialogFragment.TAG)
}
}
}
@ -325,19 +321,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
return@registerForActivityResult
if (!FileUtil.hasExtension(result.toString(), "bin")) {
Toast.makeText(
applicationContext,
R.string.invalid_keys_file,
Toast.LENGTH_SHORT
).show()
MessageDialogFragment.newInstance(
R.string.reading_keys_failure,
R.string.install_keys_failure_extension_description
).show(supportFragmentManager, MessageDialogFragment.TAG)
return@registerForActivityResult
}
val takeFlags =
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
contentResolver.takePersistableUriPermission(
result,
takeFlags
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
val dstPath = DirectoryInitialization.userDirectory + "/keys/"
@ -355,11 +348,11 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
applicationContext,
R.string.install_amiibo_keys_failure,
Toast.LENGTH_LONG
).show()
MessageDialogFragment.newInstance(
R.string.invalid_keys_error,
R.string.install_keys_failure_description,
R.string.dumping_keys_quickstart_link
).show(supportFragmentManager, MessageDialogFragment.TAG)
}
}
}

View file

@ -103,7 +103,11 @@ object GpuDriverHelper {
)
// Unzip the driver.
unzip(driverInstallationPath + DRIVER_INTERNAL_FILENAME, driverInstallationPath!!)
try {
unzip(driverInstallationPath + DRIVER_INTERNAL_FILENAME, driverInstallationPath!!)
} catch (e: SecurityException) {
return
}
// Initialize the driver parameters.
initializeDriverParameters(context)

View file

@ -89,8 +89,16 @@ public:
return m_native_window;
}
void SetNativeWindow(ANativeWindow* m_native_window_) {
m_native_window = m_native_window_;
void SetNativeWindow(ANativeWindow* native_window) {
m_native_window = native_window;
}
u32 ScreenRotation() const {
return m_screen_rotation;
}
void SetScreenRotation(u32 screen_rotation) {
m_screen_rotation = screen_rotation;
}
void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir,
@ -140,6 +148,7 @@ public:
return;
}
m_window->OnSurfaceChanged(m_native_window);
m_system.Renderer().NotifySurfaceChanged();
}
Core::SystemResultStatus InitializeEmulation(const std::string& filepath) {
@ -379,6 +388,7 @@ private:
// Window management
std::unique_ptr<EmuWindow_Android> m_window;
ANativeWindow* m_native_window{};
u32 m_screen_rotation{};
// Core emulation
Core::System m_system;
@ -404,6 +414,10 @@ private:
} // Anonymous namespace
u32 GetAndroidScreenRotation() {
return EmulationSession::GetInstance().ScreenRotation();
}
static Core::SystemResultStatus RunEmulation(const std::string& filepath) {
Common::Log::Initialize();
Common::Log::SetColorConsoleBackendEnabled(true);
@ -450,7 +464,9 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceDestroyed(JNIEnv* env,
void Java_org_yuzu_yuzu_1emu_NativeLibrary_notifyOrientationChange(JNIEnv* env,
[[maybe_unused]] jclass clazz,
jint layout_option,
jint rotation) {}
jint rotation) {
return EmulationSession::GetInstance().SetScreenRotation(static_cast<u32>(rotation));
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env,
[[maybe_unused]] jclass clazz,

View file

@ -38,7 +38,8 @@
<ImageView
android:id="@+id/image_game_screen"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
tools:src="@drawable/default_icon" />
</com.google.android.material.card.MaterialCardView>

View file

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/input_scale_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/emulation_control_scale"
android:textAlignment="viewStart"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="@+id/input_scale_slider"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.slider.Slider
android:id="@+id/input_scale_slider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/input_scale_name" />
<TextView
android:id="@+id/input_scale_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
app:layout_constraintBottom_toTopOf="@+id/input_scale_slider"
app:layout_constraintEnd_toEndOf="@+id/input_scale_slider"
tools:text="100%" />
<TextView
android:id="@+id/input_opacity_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/emulation_control_opacity"
android:textAlignment="viewStart"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="@+id/input_opacity_slider"
app:layout_constraintTop_toBottomOf="@+id/input_scale_slider" />
<com.google.android.material.slider.Slider
android:id="@+id/input_opacity_slider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/input_opacity_name" />
<TextView
android:id="@+id/input_opacity_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
app:layout_constraintBottom_toTopOf="@+id/input_opacity_slider"
app:layout_constraintEnd_toEndOf="@+id/input_opacity_slider"
tools:text="100%" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -10,6 +10,10 @@
android:id="@+id/menu_edit_overlay"
android:title="@string/emulation_touch_overlay_edit" />
<item
android:id="@+id/menu_adjust_overlay"
android:title="@string/emulation_control_adjust" />
<item
android:id="@+id/menu_toggle_controls"
android:title="@string/emulation_toggle_controls" />

View file

@ -64,8 +64,15 @@
<string name="install_amiibo_keys_description">Required to use Amiibo in game</string>
<string name="invalid_keys_file">Invalid keys file selected</string>
<string name="install_keys_success">Keys successfully installed</string>
<string name="install_keys_failure">Keys file (prod.keys) is invalid</string>
<string name="install_amiibo_keys_failure">Keys file (key_retail.bin) is invalid</string>
<string name="reading_keys_failure">Error reading encryption keys</string>
<string name="install_keys_failure_extension_description">
1. Verify your keys have the .keys extension.\n\n
2. Keys must not be stored in the Downloads folder.\n\n
Resolve the issue(s) and try again.
</string>
<string name="invalid_keys_error">Invalid encryption keys</string>
<string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
<string name="install_keys_failure_description">The selected file is incorrect or corrupt. Please redump your keys.</string>
<string name="install_gpu_driver">Install GPU driver</string>
<string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
<string name="advanced_settings">Advanced settings</string>
@ -80,11 +87,13 @@
<string name="no_file_manager">No file manager found</string>
<string name="notification_no_directory_link">Could not open yuzu directory</string>
<string name="notification_no_directory_link_description">Please locate the user folder with the file manager\'s side panel manually.</string>
<string name="import_export_saves">Import/export saves</string>
<string name="manage_save_data">Manage save data</string>
<string name="manage_save_data_description">Save data found. Please select an option below.</string>
<string name="import_export_saves_description">Import or export save files</string>
<string name="import_export_saves_no_profile">No user profile found. Please launch a game first and retry.</string>
<string name="save_file_imported_success">Save files were imported successfully</string>
<string name="save_file_invalid_zip_structure">Invalid save directory structure: The first subfolder name must be the title ID of the game.</string>
<string name="import_export_saves_no_profile">No save data found. Please launch a game and retry.</string>
<string name="save_file_imported_success">Imported successfully</string>
<string name="save_file_invalid_zip_structure">Invalid save directory structure</string>
<string name="save_file_invalid_zip_structure_description">The first subfolder name must be the title ID of the game.</string>
<string name="import_saves">Import</string>
<string name="export_saves">Export</string>
@ -164,6 +173,8 @@
<string name="reset_all_settings">Reset all settings?</string>
<string name="reset_all_settings_description">All Advanced Settings will be reset to their default configuration. This can not be undone.</string>
<string name="settings_reset">Settings reset</string>
<string name="close">Close</string>
<string name="learn_more">Learn More</string>
<!-- GPU driver installation -->
<string name="select_gpu_driver">Select GPU driver</string>
@ -204,12 +215,14 @@
<string name="emulation_haptics">Haptics</string>
<string name="emulation_show_overlay">Show Overlay</string>
<string name="emulation_toggle_all">Toggle All</string>
<string name="emulation_control_scale">Adjust Scale</string>
<string name="emulation_control_adjust">Adjust Overlay</string>
<string name="emulation_control_scale">Scale</string>
<string name="emulation_control_opacity">Opacity</string>
<string name="emulation_touch_overlay_reset">Reset Overlay</string>
<string name="emulation_touch_overlay_edit">Edit Overlay</string>
<string name="emulation_pause">Pause Emulation</string>
<string name="emulation_unpause">Unpause Emulation</string>
<string name="emulation_input_overlay">Input Overlay</string>
<string name="emulation_input_overlay">Overlay Options</string>
<string name="emulation_game_loading">Game loading…</string>
<string name="load_settings">Loading Settings…</string>

View file

@ -89,6 +89,9 @@ public:
void RequestScreenshot(void* data, std::function<void(bool)> callback,
const Layout::FramebufferLayout& layout);
/// This is called to notify the rendering backend of a surface change
virtual void NotifySurfaceChanged() {}
protected:
Core::Frontend::EmuWindow& render_window; ///< Reference to the render window handle.
std::unique_ptr<Core::Frontend::GraphicsContext> context;

View file

@ -54,6 +54,10 @@ public:
return device.GetDriverName();
}
void NotifySurfaceChanged() override {
present_manager.NotifySurfaceChanged();
}
private:
void Report() const;

View file

@ -37,6 +37,10 @@
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
#ifdef ANDROID
extern u32 GetAndroidScreenRotation();
#endif
namespace Vulkan {
namespace {
@ -74,23 +78,58 @@ struct ScreenRectVertex {
}
};
constexpr std::array<f32, 4 * 4> MakeOrthographicMatrix(f32 width, f32 height) {
// clang-format off
#ifdef ANDROID
// Android renders in portrait, so rotate the matrix.
return { 0.f, 2.f / width, 0.f, 0.f,
-2.f / height, 0.f, 0.f, 0.f,
0.f, 0.f, 1.f, 0.f,
1.f, -1.f, 0.f, 1.f};
std::array<f32, 4 * 4> MakeOrthographicMatrix(f32 width, f32 height) {
constexpr u32 ROTATION_0 = 0;
constexpr u32 ROTATION_90 = 1;
constexpr u32 ROTATION_180 = 2;
constexpr u32 ROTATION_270 = 3;
// clang-format off
switch (GetAndroidScreenRotation()) {
case ROTATION_0:
// Desktop
return { 2.f / width, 0.f, 0.f, 0.f,
0.f, 2.f / height, 0.f, 0.f,
0.f, 0.f, 1.f, 0.f,
-1.f, -1.f, 0.f, 1.f};
case ROTATION_180:
// Reverse desktop
return {-2.f / width, 0.f, 0.f, 0.f,
0.f, -2.f / height, 0.f, 0.f,
0.f, 0.f, 1.f, 0.f,
1.f, 1.f, 0.f, 1.f};
case ROTATION_270:
// Reverse landscape
return { 0.f, -2.f / width, 0.f, 0.f,
2.f / height, 0.f, 0.f, 0.f,
0.f, 0.f, 1.f, 0.f,
-1.f, 1.f, 0.f, 1.f};
case ROTATION_90:
default:
// Landscape
return { 0.f, 2.f / width, 0.f, 0.f,
-2.f / height, 0.f, 0.f, 0.f,
0.f, 0.f, 1.f, 0.f,
1.f, -1.f, 0.f, 1.f};
}
// clang-format on
}
#else
std::array<f32, 4 * 4> MakeOrthographicMatrix(f32 width, f32 height) {
// clang-format off
return { 2.f / width, 0.f, 0.f, 0.f,
0.f, 2.f / height, 0.f, 0.f,
0.f, 0.f, 1.f, 0.f,
-1.f, -1.f, 0.f, 1.f};
#endif // ANDROID
// clang-format on
}
#endif
u32 GetBytesPerPixel(const Tegra::FramebufferConfig& framebuffer) {
using namespace VideoCore::Surface;
return BytesPerBlock(PixelFormatFromGPUPixelFormat(framebuffer.pixel_format));

View file

@ -291,6 +291,13 @@ void PresentManager::PresentThread(std::stop_token token) {
}
}
void PresentManager::NotifySurfaceChanged() {
#ifdef ANDROID
std::scoped_lock lock{recreate_surface_mutex};
recreate_surface_cv.notify_one();
#endif
}
void PresentManager::CopyToSwapchain(Frame* frame) {
MICROPROFILE_SCOPE(Vulkan_CopyToSwapchain);
@ -300,6 +307,21 @@ void PresentManager::CopyToSwapchain(Frame* frame) {
};
#ifdef ANDROID
std::unique_lock lock{recreate_surface_mutex};
const auto needs_recreation = [&] {
if (last_render_surface != render_window.GetWindowInfo().render_surface) {
return true;
}
if (swapchain.NeedsRecreation(frame->is_srgb)) {
return true;
}
return false;
};
recreate_surface_cv.wait_for(lock, std::chrono::milliseconds(400),
[&]() { return !needs_recreation(); });
// If the frontend recreated the surface, recreate the renderer surface and swapchain.
if (last_render_surface != render_window.GetWindowInfo().render_surface) {
last_render_surface = render_window.GetWindowInfo().render_surface;
@ -450,7 +472,7 @@ void PresentManager::CopyToSwapchain(Frame* frame) {
// Submit the image copy/blit to the swapchain
{
std::scoped_lock lock{scheduler.submit_mutex};
std::scoped_lock submit_lock{scheduler.submit_mutex};
switch (const VkResult result =
device.GetGraphicsQueue().Submit(submit_info, *frame->present_done)) {
case VK_SUCCESS:

View file

@ -55,6 +55,9 @@ public:
/// Waits for the present thread to finish presenting all queued frames.
void WaitPresent();
/// This is called to notify the rendering backend of a surface change
void NotifySurfaceChanged();
private:
void PresentThread(std::stop_token token);
@ -74,7 +77,9 @@ private:
std::queue<Frame*> free_queue;
std::condition_variable_any frame_cv;
std::condition_variable free_cv;
std::condition_variable recreate_surface_cv;
std::mutex swapchain_mutex;
std::mutex recreate_surface_mutex;
std::mutex queue_mutex;
std::mutex free_mutex;
std::jthread present_thread;

View file

@ -17,7 +17,10 @@ namespace Vulkan {
using namespace Common::Literals;
TurboMode::TurboMode(const vk::Instance& instance, const vk::InstanceDispatch& dld)
: m_device{CreateDevice(instance, dld, VK_NULL_HANDLE)}, m_allocator{m_device, false} {
#ifndef ANDROID
: m_device{CreateDevice(instance, dld, VK_NULL_HANDLE)}, m_allocator{m_device, false}
#endif
{
{
std::scoped_lock lk{m_submission_lock};
m_submission_time = std::chrono::steady_clock::now();
@ -34,6 +37,7 @@ void TurboMode::QueueSubmitted() {
}
void TurboMode::Run(std::stop_token stop_token) {
#ifndef ANDROID
auto& dld = m_device.GetLogical();
// Allocate buffer. 2MiB should be sufficient.
@ -146,10 +150,13 @@ void TurboMode::Run(std::stop_token stop_token) {
// Create a single command buffer.
auto cmdbufs = command_pool.Allocate(1, VK_COMMAND_BUFFER_LEVEL_PRIMARY);
auto cmdbuf = vk::CommandBuffer{cmdbufs[0], m_device.GetDispatchLoader()};
#endif
while (!stop_token.stop_requested()) {
#if defined(ANDROID) && defined(ARCHITECTURE_arm64)
#ifdef ANDROID
#ifdef ARCHITECTURE_arm64
adrenotools_set_turbo(true);
#endif
#else
// Reset the fence.
fence.Reset();

View file

@ -23,8 +23,10 @@ public:
private:
void Run(std::stop_token stop_token);
#ifndef ANDROID
Device m_device;
MemoryAllocator m_allocator;
#endif
std::mutex m_submission_lock;
std::condition_variable_any m_submission_cv;
std::chrono::time_point<std::chrono::steady_clock> m_submission_time{};