diff options
41 files changed, 1095 insertions, 311 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index 6e39e542b..115f72710 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt @@ -15,13 +15,9 @@ import androidx.annotation.Keep  import androidx.fragment.app.DialogFragment  import com.google.android.material.dialog.MaterialAlertDialogBuilder  import java.lang.ref.WeakReference -import org.yuzu.yuzu_emu.YuzuApplication.Companion.appContext  import org.yuzu.yuzu_emu.activities.EmulationActivity  import org.yuzu.yuzu_emu.utils.DocumentsTree.Companion.isNativePath -import org.yuzu.yuzu_emu.utils.FileUtil.exists -import org.yuzu.yuzu_emu.utils.FileUtil.getFileSize -import org.yuzu.yuzu_emu.utils.FileUtil.isDirectory -import org.yuzu.yuzu_emu.utils.FileUtil.openContentUri +import org.yuzu.yuzu_emu.utils.FileUtil  import org.yuzu.yuzu_emu.utils.Log  import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable @@ -75,7 +71,7 @@ object NativeLibrary {          return if (isNativePath(path!!)) {              YuzuApplication.documentsTree!!.openContentUri(path, openmode)          } else { -            openContentUri(appContext, path, openmode) +            FileUtil.openContentUri(path, openmode)          }      } @@ -85,7 +81,7 @@ object NativeLibrary {          return if (isNativePath(path!!)) {              YuzuApplication.documentsTree!!.getFileSize(path)          } else { -            getFileSize(appContext, path) +            FileUtil.getFileSize(path)          }      } @@ -95,7 +91,7 @@ object NativeLibrary {          return if (isNativePath(path!!)) {              YuzuApplication.documentsTree!!.exists(path)          } else { -            exists(appContext, path) +            FileUtil.exists(path)          }      } @@ -105,7 +101,7 @@ object NativeLibrary {          return if (isNativePath(path!!)) {              YuzuApplication.documentsTree!!.isDirectory(path)          } else { -            isDirectory(appContext, path) +            FileUtil.isDirectory(path)          }      } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt index 9561748cb..8c053670c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt @@ -47,7 +47,7 @@ class YuzuApplication : Application() {          application = this          documentsTree = DocumentsTree()          DirectoryInitialization.start() -        GpuDriverHelper.initializeDriverParameters(applicationContext) +        GpuDriverHelper.initializeDriverParameters()          NativeLibrary.logDeviceInfo()          createNotificationChannels() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt new file mode 100644 index 000000000..0e818cab9 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.adapters + +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding +import org.yuzu.yuzu_emu.model.DriverViewModel +import org.yuzu.yuzu_emu.utils.GpuDriverHelper +import org.yuzu.yuzu_emu.utils.GpuDriverMetadata + +class DriverAdapter(private val driverViewModel: DriverViewModel) : +    ListAdapter<Pair<String, GpuDriverMetadata>, DriverAdapter.DriverViewHolder>( +        AsyncDifferConfig.Builder(DiffCallback()).build() +    ) { +    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverViewHolder { +        val binding = +            CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) +        return DriverViewHolder(binding) +    } + +    override fun getItemCount(): Int = currentList.size + +    override fun onBindViewHolder(holder: DriverViewHolder, position: Int) = +        holder.bind(currentList[position]) + +    private fun onSelectDriver(position: Int) { +        driverViewModel.setSelectedDriverIndex(position) +        notifyItemChanged(driverViewModel.previouslySelectedDriver) +        notifyItemChanged(driverViewModel.selectedDriver) +    } + +    private fun onDeleteDriver(driverData: Pair<String, GpuDriverMetadata>, position: Int) { +        if (driverViewModel.selectedDriver > position) { +            driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1) +        } +        if (GpuDriverHelper.customDriverData == driverData.second) { +            driverViewModel.setSelectedDriverIndex(0) +        } +        driverViewModel.driversToDelete.add(driverData.first) +        driverViewModel.removeDriver(driverData) +        notifyItemRemoved(position) +        notifyItemChanged(driverViewModel.selectedDriver) +    } + +    inner class DriverViewHolder(val binding: CardDriverOptionBinding) : +        RecyclerView.ViewHolder(binding.root) { +        private lateinit var driverData: Pair<String, GpuDriverMetadata> + +        fun bind(driverData: Pair<String, GpuDriverMetadata>) { +            this.driverData = driverData +            val driver = driverData.second + +            binding.apply { +                radioButton.isChecked = driverViewModel.selectedDriver == bindingAdapterPosition +                root.setOnClickListener { +                    onSelectDriver(bindingAdapterPosition) +                } +                buttonDelete.setOnClickListener { +                    onDeleteDriver(driverData, bindingAdapterPosition) +                } + +                // Delay marquee by 3s +                title.postDelayed( +                    { +                        title.isSelected = true +                        title.ellipsize = TextUtils.TruncateAt.MARQUEE +                        version.isSelected = true +                        version.ellipsize = TextUtils.TruncateAt.MARQUEE +                        description.isSelected = true +                        description.ellipsize = TextUtils.TruncateAt.MARQUEE +                    }, +                    3000 +                ) +                if (driver.name == null) { +                    title.setText(R.string.system_gpu_driver) +                    description.text = "" +                    version.text = "" +                    version.visibility = View.GONE +                    description.visibility = View.GONE +                    buttonDelete.visibility = View.GONE +                } else { +                    title.text = driver.name +                    version.text = driver.version +                    description.text = driver.description +                    version.visibility = View.VISIBLE +                    description.visibility = View.VISIBLE +                    buttonDelete.visibility = View.VISIBLE +                } +            } +        } +    } + +    private class DiffCallback : DiffUtil.ItemCallback<Pair<String, GpuDriverMetadata>>() { +        override fun areItemsTheSame( +            oldItem: Pair<String, GpuDriverMetadata>, +            newItem: Pair<String, GpuDriverMetadata> +        ): Boolean { +            return oldItem.first == newItem.first +        } + +        override fun areContentsTheSame( +            oldItem: Pair<String, GpuDriverMetadata>, +            newItem: Pair<String, GpuDriverMetadata> +        ): Boolean { +            return oldItem.second == newItem.second +        } +    } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt new file mode 100644 index 000000000..10b1d3547 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt @@ -0,0 +1,185 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.findNavController +import androidx.recyclerview.widget.GridLayoutManager +import com.google.android.material.transition.MaterialSharedAxis +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.adapters.DriverAdapter +import org.yuzu.yuzu_emu.databinding.FragmentDriverManagerBinding +import org.yuzu.yuzu_emu.model.DriverViewModel +import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.utils.FileUtil +import org.yuzu.yuzu_emu.utils.GpuDriverHelper +import java.io.IOException + +class DriverManagerFragment : Fragment() { +    private var _binding: FragmentDriverManagerBinding? = null +    private val binding get() = _binding!! + +    private val homeViewModel: HomeViewModel by activityViewModels() +    private val driverViewModel: DriverViewModel by activityViewModels() + +    override fun onCreate(savedInstanceState: Bundle?) { +        super.onCreate(savedInstanceState) +        enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) +        returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) +        reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) +    } + +    override fun onCreateView( +        inflater: LayoutInflater, +        container: ViewGroup?, +        savedInstanceState: Bundle? +    ): View { +        _binding = FragmentDriverManagerBinding.inflate(inflater) +        return binding.root +    } + +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) { +        super.onViewCreated(view, savedInstanceState) +        homeViewModel.setNavigationVisibility(visible = false, animated = true) +        homeViewModel.setStatusBarShadeVisibility(visible = false) + +        if (!driverViewModel.isInteractionAllowed) { +            DriversLoadingDialogFragment().show( +                childFragmentManager, +                DriversLoadingDialogFragment.TAG +            ) +        } + +        binding.toolbarDrivers.setNavigationOnClickListener { +            binding.root.findNavController().popBackStack() +        } + +        binding.buttonInstall.setOnClickListener { +            getDriver.launch(arrayOf("application/zip")) +        } + +        binding.listDrivers.apply { +            layoutManager = GridLayoutManager( +                requireContext(), +                resources.getInteger(R.integer.grid_columns) +            ) +            adapter = DriverAdapter(driverViewModel) +        } + +        viewLifecycleOwner.lifecycleScope.apply { +            launch { +                driverViewModel.driverList.collectLatest { +                    (binding.listDrivers.adapter as DriverAdapter).submitList(it) +                } +            } +            launch { +                driverViewModel.newDriverInstalled.collect { +                    if (_binding != null && it) { +                        (binding.listDrivers.adapter as DriverAdapter).apply { +                            notifyItemChanged(driverViewModel.previouslySelectedDriver) +                            notifyItemChanged(driverViewModel.selectedDriver) +                            driverViewModel.setNewDriverInstalled(false) +                        } +                    } +                } +            } +        } + +        setInsets() +    } + +    // Start installing requested driver +    override fun onStop() { +        super.onStop() +        driverViewModel.onCloseDriverManager() +    } + +    private fun setInsets() = +        ViewCompat.setOnApplyWindowInsetsListener( +            binding.root +        ) { _: View, windowInsets: WindowInsetsCompat -> +            val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) +            val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) + +            val leftInsets = barInsets.left + cutoutInsets.left +            val rightInsets = barInsets.right + cutoutInsets.right + +            val mlpAppBar = binding.toolbarDrivers.layoutParams as ViewGroup.MarginLayoutParams +            mlpAppBar.leftMargin = leftInsets +            mlpAppBar.rightMargin = rightInsets +            binding.toolbarDrivers.layoutParams = mlpAppBar + +            val mlplistDrivers = binding.listDrivers.layoutParams as ViewGroup.MarginLayoutParams +            mlplistDrivers.leftMargin = leftInsets +            mlplistDrivers.rightMargin = rightInsets +            binding.listDrivers.layoutParams = mlplistDrivers + +            val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab) +            val mlpFab = +                binding.buttonInstall.layoutParams as ViewGroup.MarginLayoutParams +            mlpFab.leftMargin = leftInsets + fabSpacing +            mlpFab.rightMargin = rightInsets + fabSpacing +            mlpFab.bottomMargin = barInsets.bottom + fabSpacing +            binding.buttonInstall.layoutParams = mlpFab + +            binding.listDrivers.updatePadding( +                bottom = barInsets.bottom + +                    resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab) +            ) + +            windowInsets +        } + +    private val getDriver = +        registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> +            if (result == null) { +                return@registerForActivityResult +            } + +            IndeterminateProgressDialogFragment.newInstance( +                requireActivity(), +                R.string.installing_driver, +                false +            ) { +                // Ignore file exceptions when a user selects an invalid zip +                try { +                    GpuDriverHelper.copyDriverToInternalStorage(result) +                } catch (_: IOException) { +                    return@newInstance getString(R.string.select_gpu_driver_error) +                } + +                val driverData = GpuDriverHelper.customDriverData +                if (driverData.name == null) { +                    return@newInstance getString(R.string.select_gpu_driver_error) +                } + +                val driverInList = +                    driverViewModel.driverList.value.firstOrNull { it.second == driverData } +                if (driverInList != null) { +                    return@newInstance getString(R.string.driver_already_installed) +                } else { +                    driverViewModel.addDriver( +                        Pair( +                            "${GpuDriverHelper.driverStoragePath}/${FileUtil.getFilename(result)}", +                            driverData +                        ) +                    ) +                    driverViewModel.setNewDriverInstalled(true) +                } +                return@newInstance Any() +            }.show(childFragmentManager, IndeterminateProgressDialogFragment.TAG) +        } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt new file mode 100644 index 000000000..f8c34346a --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt @@ -0,0 +1,75 @@ +// 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.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.launch +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding +import org.yuzu.yuzu_emu.model.DriverViewModel + +class DriversLoadingDialogFragment : DialogFragment() { +    private val driverViewModel: DriverViewModel by activityViewModels() + +    private lateinit var binding: DialogProgressBarBinding + +    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { +        binding = DialogProgressBarBinding.inflate(layoutInflater) +        binding.progressBar.isIndeterminate = true + +        isCancelable = false + +        return MaterialAlertDialogBuilder(requireContext()) +            .setTitle(R.string.loading) +            .setView(binding.root) +            .create() +    } + +    override fun onCreateView( +        inflater: LayoutInflater, +        container: ViewGroup?, +        savedInstanceState: Bundle? +    ): View = binding.root + +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) { +        super.onViewCreated(view, savedInstanceState) +        viewLifecycleOwner.lifecycleScope.apply { +            launch { +                repeatOnLifecycle(Lifecycle.State.RESUMED) { +                    driverViewModel.areDriversLoading.collect { checkForDismiss() } +                } +            } +            launch { +                repeatOnLifecycle(Lifecycle.State.RESUMED) { +                    driverViewModel.isDriverReady.collect { checkForDismiss() } +                } +            } +            launch { +                repeatOnLifecycle(Lifecycle.State.RESUMED) { +                    driverViewModel.isDeletingDrivers.collect { checkForDismiss() } +                } +            } +        } +    } + +    private fun checkForDismiss() { +        if (driverViewModel.isInteractionAllowed) { +            dismiss() +        } +    } + +    companion object { +        const val TAG = "DriversLoadingDialogFragment" +    } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index e6ad2aa77..598a9d42b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -39,6 +39,7 @@ import androidx.window.layout.WindowLayoutInfo  import com.google.android.material.dialog.MaterialAlertDialogBuilder  import com.google.android.material.slider.Slider  import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect  import kotlinx.coroutines.flow.collectLatest  import kotlinx.coroutines.launch  import org.yuzu.yuzu_emu.HomeNavigationDirections @@ -50,6 +51,7 @@ import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding  import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding  import org.yuzu.yuzu_emu.features.settings.model.IntSetting  import org.yuzu.yuzu_emu.features.settings.model.Settings +import org.yuzu.yuzu_emu.model.DriverViewModel  import org.yuzu.yuzu_emu.model.Game  import org.yuzu.yuzu_emu.model.EmulationViewModel  import org.yuzu.yuzu_emu.overlay.InputOverlay @@ -70,6 +72,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {      private lateinit var game: Game      private val emulationViewModel: EmulationViewModel by activityViewModels() +    private val driverViewModel: DriverViewModel by activityViewModels()      private var isInFoldableLayout = false @@ -299,6 +302,21 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {                      }                  }              } +            launch { +                repeatOnLifecycle(Lifecycle.State.RESUMED) { +                    driverViewModel.isDriverReady.collect { +                        if (it && !emulationState.isRunning) { +                            if (!DirectoryInitialization.areDirectoriesReady) { +                                DirectoryInitialization.start() +                            } + +                            updateScreenLayout() + +                            emulationState.run(emulationActivity!!.isActivityRecreated) +                        } +                    } +                } +            }          }      } @@ -332,17 +350,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {          }      } -    override fun onResume() { -        super.onResume() -        if (!DirectoryInitialization.areDirectoriesReady) { -            DirectoryInitialization.start() -        } - -        updateScreenLayout() - -        emulationState.run(emulationActivity!!.isActivityRecreated) -    } -      override fun onPause() {          if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) {              emulationState.pause() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index 8923c0ea2..fd9785075 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt @@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.fragments  import android.Manifest  import android.content.ActivityNotFoundException -import android.content.DialogInterface  import android.content.Intent  import android.content.pm.PackageManager  import android.os.Bundle @@ -28,7 +27,6 @@ import androidx.fragment.app.activityViewModels  import androidx.navigation.findNavController  import androidx.navigation.fragment.findNavController  import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.dialog.MaterialAlertDialogBuilder  import com.google.android.material.transition.MaterialSharedAxis  import org.yuzu.yuzu_emu.BuildConfig  import org.yuzu.yuzu_emu.HomeNavigationDirections @@ -37,6 +35,7 @@ import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter  import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding  import org.yuzu.yuzu_emu.features.DocumentProvider  import org.yuzu.yuzu_emu.features.settings.model.Settings +import org.yuzu.yuzu_emu.model.DriverViewModel  import org.yuzu.yuzu_emu.model.HomeSetting  import org.yuzu.yuzu_emu.model.HomeViewModel  import org.yuzu.yuzu_emu.ui.main.MainActivity @@ -50,6 +49,7 @@ class HomeSettingsFragment : Fragment() {      private lateinit var mainActivity: MainActivity      private val homeViewModel: HomeViewModel by activityViewModels() +    private val driverViewModel: DriverViewModel by activityViewModels()      override fun onCreate(savedInstanceState: Bundle?) {          super.onCreate(savedInstanceState) @@ -107,13 +107,17 @@ class HomeSettingsFragment : Fragment() {              )              add(                  HomeSetting( -                    R.string.install_gpu_driver, +                    R.string.gpu_driver_manager,                      R.string.install_gpu_driver_description, -                    R.drawable.ic_exit, -                    { driverInstaller() }, +                    R.drawable.ic_build, +                    { +                        binding.root.findNavController() +                            .navigate(R.id.action_homeSettingsFragment_to_driverManagerFragment) +                    },                      { GpuDriverHelper.supportsCustomDriverLoading() },                      R.string.custom_driver_not_supported, -                    R.string.custom_driver_not_supported_description +                    R.string.custom_driver_not_supported_description, +                    driverViewModel.selectedDriverMetadata                  )              )              add( @@ -292,31 +296,6 @@ class HomeSettingsFragment : Fragment() {          }      } -    private fun driverInstaller() { -        // Get the driver name for the dialog message. -        var driverName = GpuDriverHelper.customDriverName -        if (driverName == null) { -            driverName = getString(R.string.system_gpu_driver) -        } - -        MaterialAlertDialogBuilder(requireContext()) -            .setTitle(getString(R.string.select_gpu_driver_title)) -            .setMessage(driverName) -            .setNegativeButton(android.R.string.cancel, null) -            .setNeutralButton(R.string.select_gpu_driver_default) { _: DialogInterface?, _: Int -> -                GpuDriverHelper.installDefaultDriver(requireContext()) -                Toast.makeText( -                    requireContext(), -                    R.string.select_gpu_driver_use_default, -                    Toast.LENGTH_SHORT -                ).show() -            } -            .setPositiveButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int -> -                mainActivity.getDriver.launch(arrayOf("application/zip")) -            } -            .show() -    } -      private fun shareLog() {          val file = DocumentFile.fromSingleUri(              mainActivity, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt index f128deda8..7e467814d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt @@ -10,8 +10,8 @@ import android.view.View  import android.view.ViewGroup  import android.widget.Toast  import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity  import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentActivity  import androidx.fragment.app.activityViewModels  import androidx.lifecycle.Lifecycle  import androidx.lifecycle.ViewModelProvider @@ -78,6 +78,10 @@ class IndeterminateProgressDialogFragment : DialogFragment() {                                      requireActivity().supportFragmentManager,                                      MessageDialogFragment.TAG                                  ) + +                                else -> { +                                    // Do nothing +                                }                              }                              taskViewModel.clear()                          } @@ -115,7 +119,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {          private const val CANCELLABLE = "Cancellable"          fun newInstance( -            activity: AppCompatActivity, +            activity: FragmentActivity,              titleId: Int,              cancellable: Boolean = false,              task: () -> Any diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt new file mode 100644 index 000000000..62945ad65 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.model + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.utils.FileUtil +import org.yuzu.yuzu_emu.utils.GpuDriverHelper +import org.yuzu.yuzu_emu.utils.GpuDriverMetadata +import java.io.BufferedOutputStream +import java.io.File + +class DriverViewModel : ViewModel() { +    private val _areDriversLoading = MutableStateFlow(false) +    val areDriversLoading: StateFlow<Boolean> get() = _areDriversLoading + +    private val _isDriverReady = MutableStateFlow(true) +    val isDriverReady: StateFlow<Boolean> get() = _isDriverReady + +    private val _isDeletingDrivers = MutableStateFlow(false) +    val isDeletingDrivers: StateFlow<Boolean> get() = _isDeletingDrivers + +    private val _driverList = MutableStateFlow(mutableListOf<Pair<String, GpuDriverMetadata>>()) +    val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList + +    var previouslySelectedDriver = 0 +    var selectedDriver = -1 + +    private val _selectedDriverMetadata = +        MutableStateFlow( +            GpuDriverHelper.customDriverData.name +                ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver) +        ) +    val selectedDriverMetadata: StateFlow<String> get() = _selectedDriverMetadata + +    private val _newDriverInstalled = MutableStateFlow(false) +    val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled + +    val driversToDelete = mutableListOf<String>() + +    val isInteractionAllowed +        get() = !areDriversLoading.value && isDriverReady.value && !isDeletingDrivers.value + +    init { +        _areDriversLoading.value = true +        viewModelScope.launch { +            withContext(Dispatchers.IO) { +                val drivers = GpuDriverHelper.getDrivers() +                val currentDriverMetadata = GpuDriverHelper.customDriverData +                for (i in drivers.indices) { +                    if (drivers[i].second == currentDriverMetadata) { +                        setSelectedDriverIndex(i) +                        break +                    } +                } + +                // If a user had installed a driver before the manager was implemented, this zips +                // the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can +                // be indexed and exported as expected. +                if (selectedDriver == -1) { +                    val driverToSave = +                        File(GpuDriverHelper.driverStoragePath, "CustomDriver.zip") +                    driverToSave.createNewFile() +                    FileUtil.zipFromInternalStorage( +                        File(GpuDriverHelper.driverInstallationPath!!), +                        GpuDriverHelper.driverInstallationPath!!, +                        BufferedOutputStream(driverToSave.outputStream()) +                    ) +                    drivers.add(Pair(driverToSave.path, currentDriverMetadata)) +                    setSelectedDriverIndex(drivers.size - 1) +                } + +                _driverList.value = drivers +                _areDriversLoading.value = false +            } +        } +    } + +    fun setSelectedDriverIndex(value: Int) { +        if (selectedDriver != -1) { +            previouslySelectedDriver = selectedDriver +        } +        selectedDriver = value +    } + +    fun setNewDriverInstalled(value: Boolean) { +        _newDriverInstalled.value = value +    } + +    fun addDriver(driverData: Pair<String, GpuDriverMetadata>) { +        val driverIndex = _driverList.value.indexOfFirst { it == driverData } +        if (driverIndex == -1) { +            setSelectedDriverIndex(_driverList.value.size) +            _driverList.value.add(driverData) +            _selectedDriverMetadata.value = driverData.second.name +                ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver) +        } else { +            setSelectedDriverIndex(driverIndex) +        } +    } + +    fun removeDriver(driverData: Pair<String, GpuDriverMetadata>) { +        _driverList.value.remove(driverData) +    } + +    fun onCloseDriverManager() { +        _isDeletingDrivers.value = true +        viewModelScope.launch { +            withContext(Dispatchers.IO) { +                driversToDelete.forEach { +                    val driver = File(it) +                    if (driver.exists()) { +                        driver.delete() +                    } +                } +                driversToDelete.clear() +                _isDeletingDrivers.value = false +            } +        } + +        if (GpuDriverHelper.customDriverData == driverList.value[selectedDriver].second) { +            return +        } + +        _isDriverReady.value = false +        viewModelScope.launch { +            withContext(Dispatchers.IO) { +                if (selectedDriver == 0) { +                    GpuDriverHelper.installDefaultDriver() +                    setDriverReady() +                    return@withContext +                } + +                val driverToInstall = File(driverList.value[selectedDriver].first) +                if (driverToInstall.exists()) { +                    GpuDriverHelper.installCustomDriver(driverToInstall) +                } else { +                    GpuDriverHelper.installDefaultDriver() +                } +                setDriverReady() +            } +        } +    } + +    private fun setDriverReady() { +        _isDriverReady.value = true +        _selectedDriverMetadata.value = GpuDriverHelper.customDriverData.name +            ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver) +    } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 0fa5df5e5..233aa4101 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -29,12 +29,10 @@ import androidx.navigation.fragment.NavHostFragment  import androidx.navigation.ui.setupWithNavController  import androidx.preference.PreferenceManager  import com.google.android.material.color.MaterialColors -import com.google.android.material.dialog.MaterialAlertDialogBuilder  import com.google.android.material.navigation.NavigationBarView  import kotlinx.coroutines.CoroutineScope  import java.io.File  import java.io.FilenameFilter -import java.io.IOException  import kotlinx.coroutines.Dispatchers  import kotlinx.coroutines.launch  import kotlinx.coroutines.withContext @@ -43,7 +41,6 @@ import org.yuzu.yuzu_emu.NativeLibrary  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.activities.EmulationActivity  import org.yuzu.yuzu_emu.databinding.ActivityMainBinding -import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding  import org.yuzu.yuzu_emu.features.DocumentProvider  import org.yuzu.yuzu_emu.features.settings.model.Settings  import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment @@ -343,11 +340,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {          val dstPath = DirectoryInitialization.userDirectory + "/keys/"          if (FileUtil.copyUriToInternalStorage( -                applicationContext,                  result,                  dstPath,                  "prod.keys" -            ) +            ) != null          ) {              if (NativeLibrary.reloadKeys()) {                  Toast.makeText( @@ -446,11 +442,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {              val dstPath = DirectoryInitialization.userDirectory + "/keys/"              if (FileUtil.copyUriToInternalStorage( -                    applicationContext,                      result,                      dstPath,                      "key_retail.bin" -                ) +                ) != null              ) {                  if (NativeLibrary.reloadKeys()) {                      Toast.makeText( @@ -469,59 +464,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {              }          } -    val getDriver = -        registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> -            if (result == null) { -                return@registerForActivityResult -            } - -            val takeFlags = -                Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION -            contentResolver.takePersistableUriPermission( -                result, -                takeFlags -            ) - -            val progressBinding = DialogProgressBarBinding.inflate(layoutInflater) -            progressBinding.progressBar.isIndeterminate = true -            val installationDialog = MaterialAlertDialogBuilder(this) -                .setTitle(R.string.installing_driver) -                .setView(progressBinding.root) -                .show() - -            lifecycleScope.launch { -                withContext(Dispatchers.IO) { -                    // Ignore file exceptions when a user selects an invalid zip -                    try { -                        GpuDriverHelper.installCustomDriver(applicationContext, result) -                    } catch (_: IOException) { -                    } - -                    withContext(Dispatchers.Main) { -                        installationDialog.dismiss() - -                        val driverName = GpuDriverHelper.customDriverName -                        if (driverName != null) { -                            Toast.makeText( -                                applicationContext, -                                getString( -                                    R.string.select_gpu_driver_install_success, -                                    driverName -                                ), -                                Toast.LENGTH_SHORT -                            ).show() -                        } else { -                            Toast.makeText( -                                applicationContext, -                                R.string.select_gpu_driver_error, -                                Toast.LENGTH_LONG -                            ).show() -                        } -                    } -                } -            } -        } -      val installGameUpdate = registerForActivityResult(          ActivityResultContracts.OpenMultipleDocuments()      ) { documents: List<Uri> -> diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt index cf226ad94..eafcf9e42 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt @@ -7,7 +7,6 @@ import android.net.Uri  import androidx.documentfile.provider.DocumentFile  import java.io.File  import java.util.* -import org.yuzu.yuzu_emu.YuzuApplication  import org.yuzu.yuzu_emu.model.MinimalDocumentFile  class DocumentsTree { @@ -22,7 +21,7 @@ class DocumentsTree {      fun openContentUri(filepath: String, openMode: String?): Int {          val node = resolvePath(filepath) ?: return -1 -        return FileUtil.openContentUri(YuzuApplication.appContext, node.uri.toString(), openMode) +        return FileUtil.openContentUri(node.uri.toString(), openMode)      }      fun getFileSize(filepath: String): Long { @@ -30,7 +29,7 @@ class DocumentsTree {          return if (node == null || node.isDirectory) {              0          } else { -            FileUtil.getFileSize(YuzuApplication.appContext, node.uri.toString()) +            FileUtil.getFileSize(node.uri.toString())          }      } @@ -67,7 +66,7 @@ class DocumentsTree {       * @param parent parent node of this level       */      private fun structTree(parent: DocumentsNode) { -        val documents = FileUtil.listFiles(YuzuApplication.appContext, parent.uri!!) +        val documents = FileUtil.listFiles(parent.uri!!)          for (document in documents) {              val node = DocumentsNode(document)              node.parent = parent diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt index c3f53f1c5..5ee74a52c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt @@ -3,7 +3,6 @@  package org.yuzu.yuzu_emu.utils -import android.content.Context  import android.database.Cursor  import android.net.Uri  import android.provider.DocumentsContract @@ -11,7 +10,6 @@ import androidx.documentfile.provider.DocumentFile  import kotlinx.coroutines.flow.StateFlow  import java.io.BufferedInputStream  import java.io.File -import java.io.FileOutputStream  import java.io.IOException  import java.io.InputStream  import java.net.URLDecoder @@ -21,6 +19,8 @@ import org.yuzu.yuzu_emu.YuzuApplication  import org.yuzu.yuzu_emu.model.MinimalDocumentFile  import org.yuzu.yuzu_emu.model.TaskState  import java.io.BufferedOutputStream +import java.lang.NullPointerException +import java.nio.charset.StandardCharsets  import java.util.zip.ZipOutputStream  object FileUtil { @@ -29,6 +29,8 @@ object FileUtil {      const val APPLICATION_OCTET_STREAM = "application/octet-stream"      const val TEXT_PLAIN = "text/plain" +    private val context get() = YuzuApplication.appContext +      /**       * Create a file from directory with filename.       * @param context Application context @@ -36,11 +38,11 @@ object FileUtil {       * @param filename file display name.       * @return boolean       */ -    fun createFile(context: Context?, directory: String?, filename: String): DocumentFile? { +    fun createFile(directory: String?, filename: String): DocumentFile? {          var decodedFilename = filename          try {              val directoryUri = Uri.parse(directory) -            val parent = DocumentFile.fromTreeUri(context!!, directoryUri) ?: return null +            val parent = DocumentFile.fromTreeUri(context, directoryUri) ?: return null              decodedFilename = URLDecoder.decode(decodedFilename, DECODE_METHOD)              var mimeType = APPLICATION_OCTET_STREAM              if (decodedFilename.endsWith(".txt")) { @@ -56,16 +58,15 @@ object FileUtil {      /**       * Create a directory from directory with filename. -     * @param context Application context       * @param directory parent path for directory.       * @param directoryName directory display name.       * @return boolean       */ -    fun createDir(context: Context?, directory: String?, directoryName: String?): DocumentFile? { +    fun createDir(directory: String?, directoryName: String?): DocumentFile? {          var decodedDirectoryName = directoryName          try {              val directoryUri = Uri.parse(directory) -            val parent = DocumentFile.fromTreeUri(context!!, directoryUri) ?: return null +            val parent = DocumentFile.fromTreeUri(context, directoryUri) ?: return null              decodedDirectoryName = URLDecoder.decode(decodedDirectoryName, DECODE_METHOD)              val isExist = parent.findFile(decodedDirectoryName)              return isExist ?: parent.createDirectory(decodedDirectoryName) @@ -77,13 +78,12 @@ object FileUtil {      /**       * Open content uri and return file descriptor to JNI. -     * @param context Application context       * @param path Native content uri path       * @param openMode will be one of "r", "r", "rw", "wa", "rwa"       * @return file descriptor       */      @JvmStatic -    fun openContentUri(context: Context, path: String, openMode: String?): Int { +    fun openContentUri(path: String, openMode: String?): Int {          try {              val uri = Uri.parse(path)              val parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, openMode!!) @@ -103,11 +103,10 @@ object FileUtil {      /**       * Reference:  https://stackoverflow.com/questions/42186820/documentfile-is-very-slow       * This function will be faster than DoucmentFile.listFiles -     * @param context Application context       * @param uri Directory uri.       * @return CheapDocument lists.       */ -    fun listFiles(context: Context, uri: Uri): Array<MinimalDocumentFile> { +    fun listFiles(uri: Uri): Array<MinimalDocumentFile> {          val resolver = context.contentResolver          val columns = arrayOf(              DocumentsContract.Document.COLUMN_DOCUMENT_ID, @@ -145,7 +144,7 @@ object FileUtil {       * @param path Native content uri path       * @return bool       */ -    fun exists(context: Context, path: String?): Boolean { +    fun exists(path: String?): Boolean {          var c: Cursor? = null          try {              val mUri = Uri.parse(path) @@ -165,7 +164,7 @@ object FileUtil {       * @param path content uri path       * @return bool       */ -    fun isDirectory(context: Context, path: String): Boolean { +    fun isDirectory(path: String): Boolean {          val resolver = context.contentResolver          val columns = arrayOf(              DocumentsContract.Document.COLUMN_MIME_TYPE @@ -210,10 +209,10 @@ object FileUtil {          return filename      } -    fun getFilesName(context: Context, path: String): Array<String> { +    fun getFilesName(path: String): Array<String> {          val uri = Uri.parse(path)          val files: MutableList<String> = ArrayList() -        for (file in listFiles(context, uri)) { +        for (file in listFiles(uri)) {              files.add(file.filename)          }          return files.toTypedArray() @@ -225,7 +224,7 @@ object FileUtil {       * @return long file size       */      @JvmStatic -    fun getFileSize(context: Context, path: String): Long { +    fun getFileSize(path: String): Long {          val resolver = context.contentResolver          val columns = arrayOf(              DocumentsContract.Document.COLUMN_SIZE @@ -245,44 +244,38 @@ object FileUtil {          return size      } +    /** +     * Creates an input stream with a given [Uri] and copies its data to the given path. This will +     * overwrite any pre-existing files. +     * +     * @param sourceUri The [Uri] to copy data from +     * @param destinationParentPath Destination directory +     * @param destinationFilename Optionally renames the file once copied +     */      fun copyUriToInternalStorage( -        context: Context, -        sourceUri: Uri?, +        sourceUri: Uri,          destinationParentPath: String, -        destinationFilename: String -    ): Boolean { -        var input: InputStream? = null -        var output: FileOutputStream? = null +        destinationFilename: String = "" +    ): File? =          try { -            input = context.contentResolver.openInputStream(sourceUri!!) -            output = FileOutputStream("$destinationParentPath/$destinationFilename") -            val buffer = ByteArray(1024) -            var len: Int -            while (input!!.read(buffer).also { len = it } != -1) { -                output.write(buffer, 0, len) -            } -            output.flush() -            return true -        } catch (e: Exception) { -            Log.error("[FileUtil]: Cannot copy file, error: " + e.message) -        } finally { -            if (input != null) { -                try { -                    input.close() -                } catch (e: IOException) { -                    Log.error("[FileUtil]: Cannot close input file, error: " + e.message) -                } +            val fileName = +                if (destinationFilename == "") getFilename(sourceUri) else "/$destinationFilename" +            val inputStream = context.contentResolver.openInputStream(sourceUri)!! + +            val destinationFile = File("$destinationParentPath$fileName") +            if (destinationFile.exists()) { +                destinationFile.delete()              } -            if (output != null) { -                try { -                    output.close() -                } catch (e: IOException) { -                    Log.error("[FileUtil]: Cannot close output file, error: " + e.message) -                } + +            destinationFile.outputStream().use { fos -> +                inputStream.use { it.copyTo(fos) }              } +            destinationFile +        } catch (e: IOException) { +            null +        } catch (e: NullPointerException) { +            null          } -        return false -    }      /**       * Extracts the given zip file into the given directory. @@ -368,4 +361,12 @@ object FileUtil {          return fileName.substring(fileName.lastIndexOf(".") + 1)              .lowercase()      } + +    @Throws(IOException::class) +    fun getStringFromFile(file: File): String = +        String(file.readBytes(), StandardCharsets.UTF_8) + +    @Throws(IOException::class) +    fun getStringFromInputStream(stream: InputStream): String = +        String(stream.readBytes(), StandardCharsets.UTF_8)  } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt index e0ee29c9b..9001ca9ab 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt @@ -30,7 +30,7 @@ object GameHelper {          // Ensure keys are loaded so that ROM metadata can be decrypted.          NativeLibrary.reloadKeys() -        addGamesRecursive(games, FileUtil.listFiles(context, gamesUri), 3) +        addGamesRecursive(games, FileUtil.listFiles(gamesUri), 3)          // Cache list of games found on disk          val serializedGames = mutableSetOf<String>() @@ -58,7 +58,7 @@ object GameHelper {              if (it.isDirectory) {                  addGamesRecursive(                      games, -                    FileUtil.listFiles(YuzuApplication.appContext, it.uri), +                    FileUtil.listFiles(it.uri),                      depth - 1                  )              } else { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt index 1d4695a2a..f6882ce6c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt @@ -3,64 +3,33 @@  package org.yuzu.yuzu_emu.utils -import android.content.Context  import android.net.Uri +import android.os.Build  import java.io.BufferedInputStream  import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream  import java.io.IOException -import java.util.zip.ZipInputStream  import org.yuzu.yuzu_emu.NativeLibrary -import org.yuzu.yuzu_emu.utils.FileUtil.copyUriToInternalStorage +import org.yuzu.yuzu_emu.YuzuApplication +import java.util.zip.ZipException +import java.util.zip.ZipFile  object GpuDriverHelper {      private const val META_JSON_FILENAME = "meta.json" -    private const val DRIVER_INTERNAL_FILENAME = "gpu_driver.zip"      private var fileRedirectionPath: String? = null -    private var driverInstallationPath: String? = null +    var driverInstallationPath: String? = null      private var hookLibPath: String? = null -    @Throws(IOException::class) -    private fun unzip(zipFilePath: String, destDir: String) { -        val dir = File(destDir) - -        // Create output directory if it doesn't exist -        if (!dir.exists()) dir.mkdirs() - -        // Unpack the files. -        val inputStream = FileInputStream(zipFilePath) -        val zis = ZipInputStream(BufferedInputStream(inputStream)) -        val buffer = ByteArray(1024) -        var ze = zis.nextEntry -        while (ze != null) { -            val newFile = File(destDir, ze.name) -            val canonicalPath = newFile.canonicalPath -            if (!canonicalPath.startsWith(destDir + ze.name)) { -                throw SecurityException("Zip file attempted path traversal! " + ze.name) -            } - -            newFile.parentFile!!.mkdirs() -            val fos = FileOutputStream(newFile) -            var len: Int -            while (zis.read(buffer).also { len = it } > 0) { -                fos.write(buffer, 0, len) -            } -            fos.close() -            zis.closeEntry() -            ze = zis.nextEntry -        } -        zis.closeEntry() -    } +    val driverStoragePath get() = DirectoryInitialization.userDirectory!! + "/gpu_drivers/" -    fun initializeDriverParameters(context: Context) { +    fun initializeDriverParameters() {          try {              // Initialize the file redirection directory. -            fileRedirectionPath = -                context.getExternalFilesDir(null)!!.canonicalPath + "/gpu/vk_file_redirect/" +            fileRedirectionPath = YuzuApplication.appContext +                .getExternalFilesDir(null)!!.canonicalPath + "/gpu/vk_file_redirect/"              // Initialize the driver installation directory. -            driverInstallationPath = context.filesDir.canonicalPath + "/gpu_driver/" +            driverInstallationPath = YuzuApplication.appContext +                .filesDir.canonicalPath + "/gpu_driver/"          } catch (e: IOException) {              throw RuntimeException(e)          } @@ -69,68 +38,169 @@ object GpuDriverHelper {          initializeDirectories()          // Initialize hook libraries directory. -        hookLibPath = context.applicationInfo.nativeLibraryDir + "/" +        hookLibPath = YuzuApplication.appContext.applicationInfo.nativeLibraryDir + "/"          // Initialize GPU driver.          NativeLibrary.initializeGpuDriver(              hookLibPath,              driverInstallationPath, -            customDriverLibraryName, +            customDriverData.libraryName,              fileRedirectionPath          )      } -    fun installDefaultDriver(context: Context) { +    fun getDrivers(): MutableList<Pair<String, GpuDriverMetadata>> { +        val driverZips = File(driverStoragePath).listFiles() +        val drivers: MutableList<Pair<String, GpuDriverMetadata>> = +            driverZips +                ?.mapNotNull { +                    val metadata = getMetadataFromZip(it) +                    metadata.name?.let { _ -> Pair(it.path, metadata) } +                } +                ?.sortedByDescending { it: Pair<String, GpuDriverMetadata> -> it.second.name } +                ?.distinct() +                ?.toMutableList() ?: mutableListOf() + +        // TODO: Get system driver information +        drivers.add(0, Pair("", GpuDriverMetadata())) +        return drivers +    } + +    fun installDefaultDriver() {          // Removing the installed driver will result in the backend using the default system driver. -        val driverInstallationDir = File(driverInstallationPath!!) -        deleteRecursive(driverInstallationDir) -        initializeDriverParameters(context) +        File(driverInstallationPath!!).deleteRecursively() +        initializeDriverParameters() +    } + +    fun copyDriverToInternalStorage(driverUri: Uri): Boolean { +        // Ensure we have directories. +        initializeDirectories() + +        // Copy the zip file URI to user data +        val copiedFile = +            FileUtil.copyUriToInternalStorage(driverUri, driverStoragePath) ?: return false + +        // Validate driver +        val metadata = getMetadataFromZip(copiedFile) +        if (metadata.name == null) { +            copiedFile.delete() +            return false +        } + +        if (metadata.minApi > Build.VERSION.SDK_INT) { +            copiedFile.delete() +            return false +        } +        return true      } -    fun installCustomDriver(context: Context, driverPathUri: Uri?) { +    /** +     * Copies driver zip into user data directory so that it can be exported along with +     * other user data and also unzipped into the installation directory +     */ +    fun installCustomDriver(driverUri: Uri): Boolean {          // Revert to system default in the event the specified driver is bad. -        installDefaultDriver(context) +        installDefaultDriver()          // Ensure we have directories.          initializeDirectories() -        // Copy the zip file URI into our private storage. -        copyUriToInternalStorage( -            context, -            driverPathUri, -            driverInstallationPath!!, -            DRIVER_INTERNAL_FILENAME -        ) +        // Copy the zip file URI to user data +        val copiedFile = +            FileUtil.copyUriToInternalStorage(driverUri, driverStoragePath) ?: return false + +        // Validate driver +        val metadata = getMetadataFromZip(copiedFile) +        if (metadata.name == null) { +            copiedFile.delete() +            return false +        } + +        if (metadata.minApi > Build.VERSION.SDK_INT) { +            copiedFile.delete() +            return false +        }          // Unzip the driver.          try { -            unzip(driverInstallationPath + DRIVER_INTERNAL_FILENAME, driverInstallationPath!!) +            FileUtil.unzipToInternalStorage( +                BufferedInputStream(copiedFile.inputStream()), +                File(driverInstallationPath!!) +            )          } catch (e: SecurityException) { -            return +            return false          }          // Initialize the driver parameters. -        initializeDriverParameters(context) +        initializeDriverParameters() + +        return true      } -    external fun supportsCustomDriverLoading(): Boolean +    /** +     * Unzips driver into installation directory +     */ +    fun installCustomDriver(driver: File): Boolean { +        // Revert to system default in the event the specified driver is bad. +        installDefaultDriver() -    // Parse the custom driver metadata to retrieve the name. -    val customDriverName: String? -        get() { -            val metadata = GpuDriverMetadata(driverInstallationPath + META_JSON_FILENAME) -            return metadata.name +        // Ensure we have directories. +        initializeDirectories() + +        // Validate driver +        val metadata = getMetadataFromZip(driver) +        if (metadata.name == null) { +            driver.delete() +            return false          } -    // Parse the custom driver metadata to retrieve the library name. -    private val customDriverLibraryName: String? -        get() { -            // Parse the custom driver metadata to retrieve the library name. -            val metadata = GpuDriverMetadata(driverInstallationPath + META_JSON_FILENAME) -            return metadata.libraryName +        // Unzip the driver to the private installation directory +        try { +            FileUtil.unzipToInternalStorage( +                BufferedInputStream(driver.inputStream()), +                File(driverInstallationPath!!) +            ) +        } catch (e: SecurityException) { +            return false          } -    private fun initializeDirectories() { +        // Initialize the driver parameters. +        initializeDriverParameters() + +        return true +    } + +    /** +     * Takes in a zip file and reads the meta.json file for presentation to the UI +     * +     * @param driver Zip containing driver and meta.json file +     * @return A non-null [GpuDriverMetadata] instance that may have null members +     */ +    fun getMetadataFromZip(driver: File): GpuDriverMetadata { +        try { +            ZipFile(driver).use { zf -> +                val entries = zf.entries() +                while (entries.hasMoreElements()) { +                    val entry = entries.nextElement() +                    if (!entry.isDirectory && entry.name.lowercase().contains(".json")) { +                        zf.getInputStream(entry).use { +                            return GpuDriverMetadata(it, entry.size) +                        } +                    } +                } +            } +        } catch (_: ZipException) { +        } +        return GpuDriverMetadata() +    } + +    external fun supportsCustomDriverLoading(): Boolean + +    // Parse the custom driver metadata to retrieve the name. +    val customDriverData: GpuDriverMetadata +        get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME)) + +    fun initializeDirectories() {          // Ensure the file redirection directory exists.          val fileRedirectionDir = File(fileRedirectionPath!!)          if (!fileRedirectionDir.exists()) { @@ -141,14 +211,10 @@ object GpuDriverHelper {          if (!driverInstallationDir.exists()) {              driverInstallationDir.mkdirs()          } -    } - -    private fun deleteRecursive(fileOrDirectory: File) { -        if (fileOrDirectory.isDirectory) { -            for (child in fileOrDirectory.listFiles()!!) { -                deleteRecursive(child) -            } +        // Ensure the driver storage directory exists +        val driverStorageDirectory = File(driverStoragePath) +        if (!driverStorageDirectory.exists()) { +            driverStorageDirectory.mkdirs()          } -        fileOrDirectory.delete()      }  } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt index a4e64070a..511a4171a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt @@ -4,29 +4,29 @@  package org.yuzu.yuzu_emu.utils  import java.io.IOException -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import java.nio.file.Paths  import org.json.JSONException  import org.json.JSONObject +import java.io.File +import java.io.InputStream -class GpuDriverMetadata(metadataFilePath: String) { -    var name: String? = null -    var description: String? = null -    var author: String? = null -    var vendor: String? = null -    var driverVersion: String? = null -    var minApi = 0 -    var libraryName: String? = null +class GpuDriverMetadata { +    /** +     * Tries to get driver metadata information from a meta.json [File] +     * +     * @param metadataFile meta.json file provided with a GPU driver +     */ +    constructor(metadataFile: File) { +        if (metadataFile.length() > MAX_META_SIZE_BYTES) { +            return +        } -    init {          try { -            val json = JSONObject(getStringFromFile(metadataFilePath)) +            val json = JSONObject(FileUtil.getStringFromFile(metadataFile))              name = json.getString("name")              description = json.getString("description")              author = json.getString("author")              vendor = json.getString("vendor") -            driverVersion = json.getString("driverVersion") +            version = json.getString("driverVersion")              minApi = json.getInt("minApi")              libraryName = json.getString("libraryName")          } catch (e: JSONException) { @@ -36,12 +36,84 @@ class GpuDriverMetadata(metadataFilePath: String) {          }      } -    companion object { -        @Throws(IOException::class) -        private fun getStringFromFile(filePath: String): String { -            val path = Paths.get(filePath) -            val bytes = Files.readAllBytes(path) -            return String(bytes, StandardCharsets.UTF_8) +    /** +     * Tries to get driver metadata information from an input stream that's intended to be +     * from a zip file +     * +     * @param metadataStream ZipEntry input stream +     * @param size Size of the file in bytes +     */ +    constructor(metadataStream: InputStream, size: Long) { +        if (size > MAX_META_SIZE_BYTES) { +            return          } + +        try { +            val json = JSONObject(FileUtil.getStringFromInputStream(metadataStream)) +            name = json.getString("name") +            description = json.getString("description") +            author = json.getString("author") +            vendor = json.getString("vendor") +            version = json.getString("driverVersion") +            minApi = json.getInt("minApi") +            libraryName = json.getString("libraryName") +        } catch (e: JSONException) { +            // JSON is malformed, ignore and treat as unsupported metadata. +        } catch (e: IOException) { +            // File is inaccessible, ignore and treat as unsupported metadata. +        } +    } + +    /** +     * Creates an empty metadata instance +     */ +    constructor() + +    override fun equals(other: Any?): Boolean { +        if (other !is GpuDriverMetadata) { +            return false +        } + +        return other.name == name && +            other.description == description && +            other.author == author && +            other.vendor == vendor && +            other.version == version && +            other.minApi == minApi && +            other.libraryName == libraryName +    } + +    override fun hashCode(): Int { +        var result = name?.hashCode() ?: 0 +        result = 31 * result + (description?.hashCode() ?: 0) +        result = 31 * result + (author?.hashCode() ?: 0) +        result = 31 * result + (vendor?.hashCode() ?: 0) +        result = 31 * result + (version?.hashCode() ?: 0) +        result = 31 * result + minApi +        result = 31 * result + (libraryName?.hashCode() ?: 0) +        return result +    } + +    override fun toString(): String = +        """ +            Name - $name +            Description - $description +            Author - $author +            Vendor - $vendor +            Version - $version +            Min API - $minApi +            Library Name - $libraryName +        """.trimMargin().trimIndent() + +    var name: String? = null +    var description: String? = null +    var author: String? = null +    var vendor: String? = null +    var version: String? = null +    var minApi = 0 +    var libraryName: String? = null + +    companion object { +        private const val MAX_META_SIZE_BYTES = 500000      }  } diff --git a/src/android/app/src/main/res/drawable/ic_build.xml b/src/android/app/src/main/res/drawable/ic_build.xml new file mode 100644 index 000000000..91d52f1b8 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_build.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:width="24dp" +    android:height="24dp" +    android:viewportWidth="24" +    android:viewportHeight="24"> +    <path +        android:fillColor="?attr/colorControlNormal" +        android:pathData="M22.7,19l-9.1,-9.1c0.9,-2.3 0.4,-5 -1.5,-6.9 -2,-2 -5,-2.4 -7.4,-1.3L9,6 6,9 1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1c1.9,1.9 4.6,2.4 6.9,1.5l9.1,9.1c0.4,0.4 1,0.4 1.4,0l2.3,-2.3c0.5,-0.4 0.5,-1.1 0.1,-1.4z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_delete.xml b/src/android/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 000000000..d26a79711 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +    android:width="24dp" +    android:height="24dp" +    android:viewportWidth="24" +    android:viewportHeight="24"> +    <path +        android:fillColor="?attr/colorControlNormal" +        android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" /> +</vector> diff --git a/src/android/app/src/main/res/layout/card_driver_option.xml b/src/android/app/src/main/res/layout/card_driver_option.xml new file mode 100644 index 000000000..1dd9a6d7d --- /dev/null +++ b/src/android/app/src/main/res/layout/card_driver_option.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="utf-8"?> +<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" +    xmlns:app="http://schemas.android.com/apk/res-auto" +    xmlns:tools="http://schemas.android.com/tools" +    style="?attr/materialCardViewOutlinedStyle" +    android:layout_width="match_parent" +    android:layout_height="wrap_content" +    android:layout_marginHorizontal="16dp" +    android:layout_marginVertical="12dp" +    android:background="?attr/selectableItemBackground" +    android:clickable="true" +    android:focusable="true"> + +    <LinearLayout +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:orientation="horizontal" +        android:layout_gravity="center" +        android:padding="16dp"> + +        <RadioButton +            android:id="@+id/radio_button" +            android:layout_width="wrap_content" +            android:layout_height="wrap_content" +            android:layout_gravity="center_vertical" +            android:clickable="false" +            android:checked="false" /> + +        <LinearLayout +            android:layout_width="0dp" +            android:layout_height="wrap_content" +            android:layout_weight="1" +            android:orientation="vertical" +            android:layout_gravity="center_vertical"> + +            <com.google.android.material.textview.MaterialTextView +                android:id="@+id/title" +                style="@style/TextAppearance.Material3.TitleMedium" +                android:layout_width="match_parent" +                android:layout_height="wrap_content" +                android:ellipsize="none" +                android:marqueeRepeatLimit="marquee_forever" +                android:requiresFadingEdge="horizontal" +                android:singleLine="true" +                android:textAlignment="viewStart" +                tools:text="@string/select_gpu_driver_default" /> + +            <com.google.android.material.textview.MaterialTextView +                android:id="@+id/version" +                style="@style/TextAppearance.Material3.BodyMedium" +                android:layout_width="match_parent" +                android:layout_height="wrap_content" +                android:layout_marginTop="6dp" +                android:ellipsize="none" +                android:marqueeRepeatLimit="marquee_forever" +                android:requiresFadingEdge="horizontal" +                android:singleLine="true" +                android:textAlignment="viewStart" +                tools:text="@string/install_gpu_driver_description" /> + +            <com.google.android.material.textview.MaterialTextView +                android:id="@+id/description" +                style="@style/TextAppearance.Material3.BodyMedium" +                android:layout_width="match_parent" +                android:layout_height="wrap_content" +                android:layout_marginTop="6dp" +                android:ellipsize="none" +                android:marqueeRepeatLimit="marquee_forever" +                android:requiresFadingEdge="horizontal" +                android:singleLine="true" +                android:textAlignment="viewStart" +                tools:text="@string/install_gpu_driver_description" /> + +        </LinearLayout> + +        <Button +            android:id="@+id/button_delete" +            style="@style/Widget.Material3.Button.IconButton" +            android:layout_width="wrap_content" +            android:layout_height="wrap_content" +            android:layout_gravity="center_vertical" +            android:contentDescription="@string/delete" +            android:tooltipText="@string/delete" +            app:icon="@drawable/ic_delete" +            app:iconTint="?attr/colorControlNormal" /> + +    </LinearLayout> + +</com.google.android.material.card.MaterialCardView> diff --git a/src/android/app/src/main/res/layout/fragment_driver_manager.xml b/src/android/app/src/main/res/layout/fragment_driver_manager.xml new file mode 100644 index 000000000..6cea2d164 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_driver_manager.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" +    xmlns:app="http://schemas.android.com/apk/res-auto" +    android:id="@+id/coordinator_licenses" +    android:layout_width="match_parent" +    android:layout_height="match_parent" +    android:background="?attr/colorSurface"> + +    <androidx.coordinatorlayout.widget.CoordinatorLayout +        android:layout_width="match_parent" +        android:layout_height="match_parent"> + +        <com.google.android.material.appbar.AppBarLayout +            android:id="@+id/appbar_drivers" +            android:layout_width="match_parent" +            android:layout_height="wrap_content" +            android:fitsSystemWindows="true" +            app:liftOnScrollTargetViewId="@id/list_drivers"> + +            <com.google.android.material.appbar.MaterialToolbar +                android:id="@+id/toolbar_drivers" +                android:layout_width="match_parent" +                android:layout_height="?attr/actionBarSize" +                app:navigationIcon="@drawable/ic_back" +                app:title="@string/gpu_driver_manager" /> + +        </com.google.android.material.appbar.AppBarLayout> + +        <androidx.recyclerview.widget.RecyclerView +            android:id="@+id/list_drivers" +            android:layout_width="match_parent" +            android:layout_height="match_parent" +            android:clipToPadding="false" +            app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + +    </androidx.coordinatorlayout.widget.CoordinatorLayout> + +    <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton +        android:id="@+id/button_install" +        android:layout_width="wrap_content" +        android:layout_height="wrap_content" +        android:layout_gravity="bottom|end" +        android:text="@string/install" +        app:icon="@drawable/ic_add" +        app:layout_constraintBottom_toBottomOf="parent" +        app:layout_constraintEnd_toEndOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml index 2356b802b..82749359d 100644 --- a/src/android/app/src/main/res/navigation/home_navigation.xml +++ b/src/android/app/src/main/res/navigation/home_navigation.xml @@ -22,6 +22,9 @@          <action              android:id="@+id/action_homeSettingsFragment_to_installableFragment"              app:destination="@id/installableFragment" /> +        <action +            android:id="@+id/action_homeSettingsFragment_to_driverManagerFragment" +            app:destination="@id/driverManagerFragment" />      </fragment>      <fragment @@ -95,5 +98,9 @@          android:id="@+id/installableFragment"          android:name="org.yuzu.yuzu_emu.fragments.InstallableFragment"          android:label="InstallableFragment" /> +    <fragment +        android:id="@+id/driverManagerFragment" +        android:name="org.yuzu.yuzu_emu.fragments.DriverManagerFragment" +        android:label="DriverManagerFragment" />  </navigation> diff --git a/src/android/app/src/main/res/values-de/strings.xml b/src/android/app/src/main/res/values-de/strings.xml index dd0f36392..72a47fbdb 100644 --- a/src/android/app/src/main/res/values-de/strings.xml +++ b/src/android/app/src/main/res/values-de/strings.xml @@ -168,9 +168,7 @@      <string name="select_gpu_driver_title">Möchtest du deinen aktuellen GPU-Treiber ersetzen?</string>      <string name="select_gpu_driver_install">Installieren</string>      <string name="select_gpu_driver_default">Standard</string> -    <string name="select_gpu_driver_install_success">%s wurde installiert</string>      <string name="select_gpu_driver_use_default">Standard GPU-Treiber wird verwendet</string> -    <string name="select_gpu_driver_error">Ungültiger Treiber ausgewählt, Standard-Treiber wird verwendet!</string>      <string name="system_gpu_driver">System GPU-Treiber</string>      <string name="installing_driver">Treiber wird installiert...</string> diff --git a/src/android/app/src/main/res/values-es/strings.xml b/src/android/app/src/main/res/values-es/strings.xml index d398f862f..e5bdd5889 100644 --- a/src/android/app/src/main/res/values-es/strings.xml +++ b/src/android/app/src/main/res/values-es/strings.xml @@ -171,9 +171,7 @@      <string name="select_gpu_driver_title">¿Quiere reemplazar el driver de GPU actual?</string>      <string name="select_gpu_driver_install">Instalar</string>      <string name="select_gpu_driver_default">Predeterminado</string> -    <string name="select_gpu_driver_install_success">Instalado %s</string>      <string name="select_gpu_driver_use_default">Usando el driver de GPU por defecto </string> -    <string name="select_gpu_driver_error">¡Driver no válido, utilizando el predeterminado del sistema!</string>      <string name="system_gpu_driver">Driver GPU del sistema</string>      <string name="installing_driver">Instalando driver...</string> diff --git a/src/android/app/src/main/res/values-fr/strings.xml b/src/android/app/src/main/res/values-fr/strings.xml index a7abd9077..1e02828aa 100644 --- a/src/android/app/src/main/res/values-fr/strings.xml +++ b/src/android/app/src/main/res/values-fr/strings.xml @@ -171,9 +171,7 @@      <string name="select_gpu_driver_title">Souhaitez vous remplacer votre pilote actuel ?</string>      <string name="select_gpu_driver_install">Installer</string>      <string name="select_gpu_driver_default">Défaut</string> -    <string name="select_gpu_driver_install_success">%s Installé</string>      <string name="select_gpu_driver_use_default">Utilisation du pilote de GPU par défaut</string> -    <string name="select_gpu_driver_error">Pilote non valide sélectionné, utilisation du paramètre par défaut du système !</string>      <string name="system_gpu_driver">Pilote du GPU du système</string>      <string name="installing_driver">Installation du pilote...</string> diff --git a/src/android/app/src/main/res/values-it/strings.xml b/src/android/app/src/main/res/values-it/strings.xml index b18161801..09c9345b0 100644 --- a/src/android/app/src/main/res/values-it/strings.xml +++ b/src/android/app/src/main/res/values-it/strings.xml @@ -171,9 +171,7 @@      <string name="select_gpu_driver_title">Vuoi sostituire il driver della tua GPU attuale?</string>      <string name="select_gpu_driver_install">Installa</string>      <string name="select_gpu_driver_default">Predefinito</string> -    <string name="select_gpu_driver_install_success">Installato%s</string>      <string name="select_gpu_driver_use_default">Utilizza il driver predefinito della GPU.</string> -    <string name="select_gpu_driver_error">Il driver selezionato è invalido, è in utilizzo quello predefinito di sistema!</string>      <string name="system_gpu_driver">Driver GPU del sistema</string>      <string name="installing_driver">Installando i driver...</string> diff --git a/src/android/app/src/main/res/values-ja/strings.xml b/src/android/app/src/main/res/values-ja/strings.xml index 88fa5a0bb..a0ea78bef 100644 --- a/src/android/app/src/main/res/values-ja/strings.xml +++ b/src/android/app/src/main/res/values-ja/strings.xml @@ -170,9 +170,7 @@      <string name="select_gpu_driver_title">現在のGPUドライバーを置き換えますか?</string>      <string name="select_gpu_driver_install">インストール</string>      <string name="select_gpu_driver_default">デフォルト</string> -    <string name="select_gpu_driver_install_success">%s をインストールしました</string>      <string name="select_gpu_driver_use_default">デフォルトのGPUドライバーを使用します</string> -    <string name="select_gpu_driver_error">選択されたドライバが無効なため、システムのデフォルトを使用します!</string>      <string name="system_gpu_driver">システムのGPUドライバ</string>      <string name="installing_driver">インストール中…</string> diff --git a/src/android/app/src/main/res/values-ko/strings.xml b/src/android/app/src/main/res/values-ko/strings.xml index 4b658255c..214f95706 100644 --- a/src/android/app/src/main/res/values-ko/strings.xml +++ b/src/android/app/src/main/res/values-ko/strings.xml @@ -171,9 +171,7 @@      <string name="select_gpu_driver_title">현재 사용 중인 GPU 드라이버를 교체하겠습니까?</string>      <string name="select_gpu_driver_install">설치</string>      <string name="select_gpu_driver_default">기본값</string> -    <string name="select_gpu_driver_install_success">설치된 %s</string>      <string name="select_gpu_driver_use_default">기본 GPU 드라이버 사용</string> -    <string name="select_gpu_driver_error">시스템 기본값을 사용하여 잘못된 드라이버를 선택했습니다!</string>      <string name="system_gpu_driver">시스템 GPU 드라이버</string>      <string name="installing_driver">드라이버 설치 중...</string> diff --git a/src/android/app/src/main/res/values-nb/strings.xml b/src/android/app/src/main/res/values-nb/strings.xml index dd602a389..5443cef42 100644 --- a/src/android/app/src/main/res/values-nb/strings.xml +++ b/src/android/app/src/main/res/values-nb/strings.xml @@ -171,9 +171,7 @@      <string name="select_gpu_driver_title">Ønsker du å bytte ut din nåværende GPU-driver?</string>      <string name="select_gpu_driver_install">Installer</string>      <string name="select_gpu_driver_default">Standard</string> -    <string name="select_gpu_driver_install_success">Installert %s</string>      <string name="select_gpu_driver_use_default">Bruk av standard GPU-driver</string> -    <string name="select_gpu_driver_error">Ugyldig driver valgt, bruker systemstandard!</string>      <string name="system_gpu_driver">Systemets GPU-driver</string>      <string name="installing_driver">Installerer driver...</string> diff --git a/src/android/app/src/main/res/values-pl/strings.xml b/src/android/app/src/main/res/values-pl/strings.xml index 2fdd1f952..899e233d0 100644 --- a/src/android/app/src/main/res/values-pl/strings.xml +++ b/src/android/app/src/main/res/values-pl/strings.xml @@ -171,9 +171,7 @@      <string name="select_gpu_driver_title">Chcesz zastąpić obecny sterownik układu graficznego?</string>      <string name="select_gpu_driver_install">Zainstaluj</string>      <string name="select_gpu_driver_default">Domyślne</string> -    <string name="select_gpu_driver_install_success">Zainstalowano %s</string>      <string name="select_gpu_driver_use_default">Aktywny domyślny sterownik GPU</string> -    <string name="select_gpu_driver_error">Wybrano błędny sterownik, powrót do domyślnego. </string>      <string name="system_gpu_driver">Systemowy sterownik GPU</string>      <string name="installing_driver">Instalowanie sterownika...</string> diff --git a/src/android/app/src/main/res/values-pt-rBR/strings.xml b/src/android/app/src/main/res/values-pt-rBR/strings.xml index 2f26367fe..caa095364 100644 --- a/src/android/app/src/main/res/values-pt-rBR/strings.xml +++ b/src/android/app/src/main/res/values-pt-rBR/strings.xml @@ -171,9 +171,7 @@      <string name="select_gpu_driver_title">Queres substituir o driver do GPU atual? </string>      <string name="select_gpu_driver_install">Instalar</string>      <string name="select_gpu_driver_default">Padrão</string> -    <string name="select_gpu_driver_install_success">Instalado%s</string>      <string name="select_gpu_driver_use_default">Usar o driver padrão do GPU</string> -    <string name="select_gpu_driver_error">Driver selecionado inválido, a usar o padrão do sistema!</string>      <string name="system_gpu_driver">Driver do GPU padrão</string>      <string name="installing_driver">A instalar o Driver...</string> diff --git a/src/android/app/src/main/res/values-pt-rPT/strings.xml b/src/android/app/src/main/res/values-pt-rPT/strings.xml index 4e1eb4cd7..0a1a47fbb 100644 --- a/src/android/app/src/main/res/values-pt-rPT/strings.xml +++ b/src/android/app/src/main/res/values-pt-rPT/strings.xml @@ -171,9 +171,7 @@      <string name="select_gpu_driver_title">Queres substituir o driver do GPU atual? </string>      <string name="select_gpu_driver_install">Instalar</string>      <string name="select_gpu_driver_default">Padrão</string> -    <string name="select_gpu_driver_install_success">Instalado%s</string>      <string name="select_gpu_driver_use_default">Usar o driver padrão do GPU</string> -    <string name="select_gpu_driver_error">Driver selecionado inválido, a usar o padrão do sistema!</string>      <string name="system_gpu_driver">Driver do GPU padrão</string>      <string name="installing_driver">A instalar o Driver...</string> diff --git a/src/android/app/src/main/res/values-ru/strings.xml b/src/android/app/src/main/res/values-ru/strings.xml index f5695dc93..0bef035d6 100644 --- a/src/android/app/src/main/res/values-ru/strings.xml +++ b/src/android/app/src/main/res/values-ru/strings.xml @@ -171,9 +171,7 @@      <string name="select_gpu_driver_title">Хотите заменить текущий драйвер ГП?</string>      <string name="select_gpu_driver_install">Установить</string>      <string name="select_gpu_driver_default">По умолчанию</string> -    <string name="select_gpu_driver_install_success">Установлено %s</string>      <string name="select_gpu_driver_use_default">Используется стандартный драйвер ГП </string> -    <string name="select_gpu_driver_error">Выбран неверный драйвер, используется стандартный системный!</string>      <string name="system_gpu_driver">Системный драйвер ГП</string>      <string name="installing_driver">Установка драйвера...</string> diff --git a/src/android/app/src/main/res/values-uk/strings.xml b/src/android/app/src/main/res/values-uk/strings.xml index 061bc6f04..5b789ee98 100644 --- a/src/android/app/src/main/res/values-uk/strings.xml +++ b/src/android/app/src/main/res/values-uk/strings.xml @@ -171,9 +171,7 @@      <string name="select_gpu_driver_title">Хочете замінити поточний драйвер ГП?</string>      <string name="select_gpu_driver_install">Встановити</string>      <string name="select_gpu_driver_default">За замовчуванням</string> -    <string name="select_gpu_driver_install_success">Встановлено %s</string>      <string name="select_gpu_driver_use_default">Використовується стандартний драйвер ГП</string> -    <string name="select_gpu_driver_error">Обрано неправильний драйвер, використовується стандартний системний!</string>      <string name="system_gpu_driver">Системний драйвер ГП</string>      <string name="installing_driver">Встановлення драйвера...</string> diff --git a/src/android/app/src/main/res/values-zh-rCN/strings.xml b/src/android/app/src/main/res/values-zh-rCN/strings.xml index fe6dd5eaa..c0e885751 100644 --- a/src/android/app/src/main/res/values-zh-rCN/strings.xml +++ b/src/android/app/src/main/res/values-zh-rCN/strings.xml @@ -171,9 +171,7 @@      <string name="select_gpu_driver_title">要取代您当前的 GPU 驱动程序吗?</string>      <string name="select_gpu_driver_install">安装</string>      <string name="select_gpu_driver_default">系统默认</string> -    <string name="select_gpu_driver_install_success">已安装 %s</string>      <string name="select_gpu_driver_use_default">使用默认 GPU 驱动程序</string> -    <string name="select_gpu_driver_error">选择的驱动程序无效,将使用系统默认的驱动程序!</string>      <string name="system_gpu_driver">系统 GPU 驱动程序</string>      <string name="installing_driver">正在安装驱动程序…</string> diff --git a/src/android/app/src/main/res/values-zh-rTW/strings.xml b/src/android/app/src/main/res/values-zh-rTW/strings.xml index 9b3e54224..4a21bf893 100644 --- a/src/android/app/src/main/res/values-zh-rTW/strings.xml +++ b/src/android/app/src/main/res/values-zh-rTW/strings.xml @@ -171,9 +171,7 @@      <string name="select_gpu_driver_title">要取代您目前的 GPU 驅動程式嗎?</string>      <string name="select_gpu_driver_install">安裝</string>      <string name="select_gpu_driver_default">預設</string> -    <string name="select_gpu_driver_install_success">已安裝 %s</string>      <string name="select_gpu_driver_use_default">使用預設 GPU 驅動程式</string> -    <string name="select_gpu_driver_error">選取的驅動程式無效,將使用系統預設驅動程式!</string>      <string name="system_gpu_driver">系統 GPU 驅動程式</string>      <string name="installing_driver">正在安裝驅動程式…</string> diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml index 7b2296d95..ef855ea6f 100644 --- a/src/android/app/src/main/res/values/dimens.xml +++ b/src/android/app/src/main/res/values/dimens.xml @@ -13,6 +13,8 @@      <dimen name="menu_width">256dp</dimen>      <dimen name="card_width">165dp</dimen>      <dimen name="icon_inset">24dp</dimen> +    <dimen name="spacing_bottom_list_fab">72dp</dimen> +    <dimen name="spacing_fab">24dp</dimen>      <dimen name="dialog_margin">20dp</dimen>      <dimen name="elevated_app_bar">3dp</dimen> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index e51edf872..9e4854221 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -72,6 +72,7 @@      <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="gpu_driver_manager">GPU Driver Manager</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> @@ -234,15 +235,17 @@      <string name="export_failed">Export failed</string>      <string name="import_failed">Import failed</string>      <string name="cancelling">Cancelling</string> +    <string name="install">Install</string> +    <string name="delete">Delete</string>      <!-- GPU driver installation -->      <string name="select_gpu_driver">Select GPU driver</string>      <string name="select_gpu_driver_title">Would you like to replace your current GPU driver?</string>      <string name="select_gpu_driver_install">Install</string>      <string name="select_gpu_driver_default">Default</string> -    <string name="select_gpu_driver_install_success">Installed %s</string>      <string name="select_gpu_driver_use_default">Using default GPU driver</string> -    <string name="select_gpu_driver_error">Invalid driver selected, using system default!</string> +    <string name="select_gpu_driver_error">Invalid driver selected</string> +    <string name="driver_already_installed">Driver already installed</string>      <string name="system_gpu_driver">System GPU driver</string>      <string name="installing_driver">Installing driver…</string> diff --git a/src/common/common_funcs.h b/src/common/common_funcs.h index 0dad9338a..47d028d48 100644 --- a/src/common/common_funcs.h +++ b/src/common/common_funcs.h @@ -39,8 +39,12 @@  #define Crash() exit(1)  #endif +#define LTO_NOINLINE __attribute__((noinline)) +  #else // _MSC_VER +#define LTO_NOINLINE +  // Locale Cross-Compatibility  #define locale_t _locale_t diff --git a/src/common/elf.h b/src/common/elf.h index 14a5e9597..0b728dc54 100644 --- a/src/common/elf.h +++ b/src/common/elf.h @@ -211,6 +211,11 @@ struct Elf64_Rela {      Elf64_Sxword r_addend; /* Addend */  }; +/* RELR relocation table entry */ + +using Elf32_Relr = Elf32_Word; +using Elf64_Relr = Elf64_Xword; +  /* How to extract and insert information held in the r_info field.  */  static inline u32 Elf32RelSymIndex(Elf32_Word r_info) { @@ -328,6 +333,9 @@ constexpr u32 ElfDtFiniArray = 26;   /* Array with addresses of fini fct */  constexpr u32 ElfDtInitArraySz = 27; /* Size in bytes of DT_INIT_ARRAY */  constexpr u32 ElfDtFiniArraySz = 28; /* Size in bytes of DT_FINI_ARRAY */  constexpr u32 ElfDtSymtabShndx = 34; /* Address of SYMTAB_SHNDX section */ +constexpr u32 ElfDtRelrsz = 35;      /* Size of RELR relative relocations */ +constexpr u32 ElfDtRelr = 36;        /* Address of RELR relative relocations */ +constexpr u32 ElfDtRelrent = 37;     /* Size of one RELR relative relocation */  } // namespace ELF  } // namespace Common diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp index a1134b7e2..cb025c3d6 100644 --- a/src/core/hle/kernel/kernel.cpp +++ b/src/core/hle/kernel/kernel.cpp @@ -373,7 +373,7 @@ struct KernelCore::Impl {      static inline thread_local u8 host_thread_id = UINT8_MAX;      /// Sets the host thread ID for the caller. -    u32 SetHostThreadId(std::size_t core_id) { +    LTO_NOINLINE u32 SetHostThreadId(std::size_t core_id) {          // This should only be called during core init.          ASSERT(host_thread_id == UINT8_MAX); @@ -384,13 +384,13 @@ struct KernelCore::Impl {      }      /// Gets the host thread ID for the caller -    u32 GetHostThreadId() const { +    LTO_NOINLINE u32 GetHostThreadId() const {          return host_thread_id;      }      // Gets the dummy KThread for the caller, allocating a new one if this is the first time -    KThread* GetHostDummyThread(KThread* existing_thread) { -        const auto initialize{[](KThread* thread) { +    LTO_NOINLINE KThread* GetHostDummyThread(KThread* existing_thread) { +        const auto initialize{[](KThread* thread) LTO_NOINLINE {              ASSERT(KThread::InitializeDummyThread(thread, nullptr).IsSuccess());              return thread;          }}; @@ -424,11 +424,11 @@ struct KernelCore::Impl {      static inline thread_local bool is_phantom_mode_for_singlecore{false}; -    bool IsPhantomModeForSingleCore() const { +    LTO_NOINLINE bool IsPhantomModeForSingleCore() const {          return is_phantom_mode_for_singlecore;      } -    void SetIsPhantomModeForSingleCore(bool value) { +    LTO_NOINLINE void SetIsPhantomModeForSingleCore(bool value) {          ASSERT(!is_multicore);          is_phantom_mode_for_singlecore = value;      } @@ -439,14 +439,14 @@ struct KernelCore::Impl {      static inline thread_local KThread* current_thread{nullptr}; -    KThread* GetCurrentEmuThread() { +    LTO_NOINLINE KThread* GetCurrentEmuThread() {          if (!current_thread) {              current_thread = GetHostDummyThread(nullptr);          }          return current_thread;      } -    void SetCurrentEmuThread(KThread* thread) { +    LTO_NOINLINE void SetCurrentEmuThread(KThread* thread) {          current_thread = thread;      } diff --git a/src/core/hle/service/caps/caps_a.cpp b/src/core/hle/service/caps/caps_a.cpp index e22f72bf6..9925720a3 100644 --- a/src/core/hle/service/caps/caps_a.cpp +++ b/src/core/hle/service/caps/caps_a.cpp @@ -128,9 +128,9 @@ void IAlbumAccessorService::GetAlbumFileListEx0(HLERequestContext& ctx) {          ctx.WriteBuffer(entries);      } -    IPC::ResponseBuilder rb{ctx, 3}; +    IPC::ResponseBuilder rb{ctx, 4};      rb.Push(result); -    rb.Push(entries.size()); +    rb.Push<u64>(entries.size());  }  void IAlbumAccessorService::GetAutoSavingStorage(HLERequestContext& ctx) { diff --git a/src/core/hle/service/jit/jit_context.cpp b/src/core/hle/service/jit/jit_context.cpp index 4ed3f02e2..0090e8568 100644 --- a/src/core/hle/service/jit/jit_context.cpp +++ b/src/core/hle/service/jit/jit_context.cpp @@ -156,6 +156,8 @@ public:      bool LoadNRO(std::span<const u8> data) {          local_memory.clear(); + +        relocbase = local_memory.size();          local_memory.insert(local_memory.end(), data.begin(), data.end());          if (FixupRelocations()) { @@ -181,8 +183,8 @@ public:          // https://refspecs.linuxbase.org/elf/gabi4+/ch5.dynamic.html          // https://refspecs.linuxbase.org/elf/gabi4+/ch4.reloc.html          VAddr dynamic_offset{mod_offset + callbacks->MemoryRead32(mod_offset + 4)}; -        VAddr rela_dyn = 0; -        size_t num_rela = 0; +        VAddr rela_dyn = 0, relr_dyn = 0; +        size_t num_rela = 0, num_relr = 0;          while (true) {              const auto dyn{callbacks->ReadMemory<Elf64_Dyn>(dynamic_offset)};              dynamic_offset += sizeof(Elf64_Dyn); @@ -196,6 +198,12 @@ public:              if (dyn.d_tag == ElfDtRelasz) {                  num_rela = dyn.d_un.d_val / sizeof(Elf64_Rela);              } +            if (dyn.d_tag == ElfDtRelr) { +                relr_dyn = dyn.d_un.d_ptr; +            } +            if (dyn.d_tag == ElfDtRelrsz) { +                num_relr = dyn.d_un.d_val / sizeof(Elf64_Relr); +            }          }          for (size_t i = 0; i < num_rela; i++) { @@ -207,6 +215,29 @@ public:              callbacks->MemoryWrite64(rela.r_offset, contents + rela.r_addend);          } +        VAddr relr_where = 0; +        for (size_t i = 0; i < num_relr; i++) { +            const auto relr{callbacks->ReadMemory<Elf64_Relr>(relr_dyn + i * sizeof(Elf64_Relr))}; +            const auto incr{[&](VAddr where) { +                callbacks->MemoryWrite64(where, callbacks->MemoryRead64(where) + relocbase); +            }}; + +            if ((relr & 1) == 0) { +                // where pointer +                relr_where = relocbase + relr; +                incr(relr_where); +                relr_where += sizeof(Elf64_Addr); +            } else { +                // bitmap +                for (int bit = 1; bit < 64; bit++) { +                    if ((relr & (1ULL << bit)) != 0) { +                        incr(relr_where + i * sizeof(Elf64_Addr)); +                    } +                } +                relr_where += 63 * sizeof(Elf64_Addr); +            } +        } +          return true;      } @@ -313,6 +344,7 @@ public:      Core::Memory::Memory& memory;      VAddr top_of_stack;      VAddr heap_pointer; +    VAddr relocbase;  };  void DynarmicCallbacks64::CallSVC(u32 swi) {  | 
