diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt index e1684e0115..667141725d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt @@ -33,7 +33,6 @@ import org.yuzu.yuzu_emu.features.input.NativeInput import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.view.PathSetting import org.yuzu.yuzu_emu.fragments.MessageDialogFragment -import org.yuzu.yuzu_emu.fragments.ProgressDialogFragment import org.yuzu.yuzu_emu.utils.PathUtil import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins import org.yuzu.yuzu_emu.utils.* @@ -115,29 +114,6 @@ class SettingsFragment : Fragment() { viewLifecycleOwner, resetState = { settingsViewModel.setShouldReloadSettingsList(false) } ) { if (it) presenter.loadSettingsList() } - settingsViewModel.shouldShowLosslessInstaller.collect( - viewLifecycleOwner, - resetState = { settingsViewModel.setShouldShowLosslessInstaller(false) } - ) { if (it) losslessDllPickerLauncher.launch(arrayOf("*/*")) } - settingsViewModel.shouldShowLosslessRemoveDialog.collect( - viewLifecycleOwner, - resetState = { settingsViewModel.setShouldShowLosslessRemoveDialog(false) } - ) { - if (it) { - MessageDialogFragment.newInstance( - activity = requireActivity(), - titleId = R.string.lossless_scaling_remove, - descriptionId = R.string.lossless_scaling_remove_confirmation, - positiveButtonTitleId = R.string.lossless_scaling_remove, - positiveAction = { - LosslessScalingHelper.remove() - settingsViewModel.setShouldReloadSettingsList(true) - }, - showNegativeButton = true, - negativeAction = {} - ).show(parentFragmentManager, MessageDialogFragment.TAG) - } - } settingsViewModel.adapterItemChanged.collect( viewLifecycleOwner, resetState = { settingsViewModel.setAdapterItemChanged(-1) } @@ -291,33 +267,6 @@ private fun getPlayerIndex(): Int = directoryPickerLauncher.launch(null) } - private val losslessDllPickerLauncher = registerForActivityResult( - ActivityResultContracts.OpenDocument() - ) { uri -> - if (uri == null) { - return@registerForActivityResult - } - - val resultStrings = resources.getStringArray(R.array.losslessDllResults) - ProgressDialogFragment.newInstance( - requireActivity(), - R.string.lossless_scaling_installing, - false - ) { _, _ -> - val result = LosslessScalingHelper.install(uri) - if (result == LosslessScalingHelper.RESULT_OK) { - getString(R.string.lossless_scaling_install_success) - } else { - MessageDialogFragment.newInstance( - titleId = R.string.lossless_scaling_install_failed, - descriptionString = resultStrings[result] - ) - } - }.apply { - onDialogComplete = { settingsViewModel.setShouldReloadSettingsList(true) } - }.show(parentFragmentManager, ProgressDialogFragment.TAG) - } - private val directoryPickerLauncher = registerForActivityResult( ActivityResultContracts.OpenDocumentTree() ) { uri -> diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 6efc3ca71e..fba97859ea 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -79,12 +79,7 @@ class SettingsFragmentPresenter( private fun addFrameGenSettings(sl: ArrayList) { sl.apply { - val installed = LosslessScalingHelper.isInstalled() - val supported = LosslessScalingHelper.isSupportedByGpu() - - add(HeaderSetting(R.string.lossless_scaling)) - - if (!supported) { + if (!LosslessScalingHelper.isSupportedByGpu()) { add( RunnableSetting( titleId = R.string.frame_gen_unsupported, @@ -92,38 +87,16 @@ class SettingsFragmentPresenter( isRunnable = false ) {} ) - } - - add( - RunnableSetting( - titleId = if (installed) { - R.string.lossless_scaling_replace - } else { - R.string.lossless_scaling_install - }, - descriptionId = if (installed) { - R.string.lossless_scaling_replace_description - } else { - R.string.lossless_scaling_install_description - }, - isRunnable = !NativeLibrary.isRunning(), - iconId = R.drawable.ic_install - ) { settingsViewModel.setShouldShowLosslessInstaller(true) } - ) - - if (installed) { + } else if (!LosslessScalingHelper.isInstalled()) { add( RunnableSetting( - titleId = R.string.lossless_scaling_remove, - descriptionId = R.string.lossless_scaling_remove_description, - isRunnable = !NativeLibrary.isRunning(), - iconId = R.drawable.ic_delete - ) { settingsViewModel.setShouldShowLosslessRemoveDialog(true) } + titleId = R.string.lossless_scaling_missing, + descriptionId = R.string.lossless_scaling_missing_description, + isRunnable = false + ) {} ) } - add(HeaderSetting(R.string.frame_gen)) - add(BooleanSetting.RENDERER_FRAME_GEN.key) add(IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key) if (IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.getInt( @@ -234,14 +207,6 @@ class SettingsFragmentPresenter( menuKey = MenuTag.SECTION_RENDERER ) ) - add( - SubmenuSetting( - titleId = R.string.frame_gen, - descriptionId = R.string.frame_gen_submenu_description, - iconId = R.drawable.ic_duck, - menuKey = MenuTag.SECTION_FRAME_GEN - ) - ) if (!NativeConfig.isPerGameConfigLoaded()) { add( SubmenuSetting( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSubscreenActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSubscreenActivity.kt index 91888dce12..8c0d6d055e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSubscreenActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSubscreenActivity.kt @@ -29,6 +29,7 @@ enum class SettingsSubscreen { DRIVER_MANAGER, DRIVER_FETCHER, FREEDRENO_SETTINGS, + LOSSLESS_MANAGER, APPLET_LAUNCHER, INSTALLABLE, GAME_FOLDERS, @@ -126,6 +127,7 @@ class SettingsSubscreenActivity : AppCompatActivity() { SettingsSubscreen.DRIVER_MANAGER -> R.id.driverManagerFragment SettingsSubscreen.DRIVER_FETCHER -> R.id.driverFetcherFragment SettingsSubscreen.FREEDRENO_SETTINGS -> R.id.freedrenoSettingsFragment + SettingsSubscreen.LOSSLESS_MANAGER -> R.id.losslessManagerFragment SettingsSubscreen.APPLET_LAUNCHER -> R.id.appletLauncherFragment SettingsSubscreen.INSTALLABLE -> R.id.installableFragment SettingsSubscreen.GAME_FOLDERS -> R.id.gameFoldersFragment diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt index 466780577b..b1914c3169 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt @@ -36,12 +36,6 @@ class SettingsViewModel : ViewModel() { val shouldReloadSettingsList: StateFlow get() = _shouldReloadSettingsList private val _shouldReloadSettingsList = MutableStateFlow(false) - val shouldShowLosslessInstaller: StateFlow get() = _shouldShowLosslessInstaller - private val _shouldShowLosslessInstaller = MutableStateFlow(false) - - val shouldShowLosslessRemoveDialog: StateFlow get() = _shouldShowLosslessRemoveDialog - private val _shouldShowLosslessRemoveDialog = MutableStateFlow(false) - val sliderProgress: StateFlow get() = _sliderProgress private val _sliderProgress = MutableStateFlow(-1) @@ -91,14 +85,6 @@ class SettingsViewModel : ViewModel() { _shouldReloadSettingsList.value = value } - fun setShouldShowLosslessInstaller(value: Boolean) { - _shouldShowLosslessInstaller.value = value - } - - fun setShouldShowLosslessRemoveDialog(value: Boolean) { - _shouldShowLosslessRemoveDialog.value = value - } - fun setSliderTextValue(value: Float, units: String) { _sliderProgress.value = value.toInt() _sliderTextValue.value = String.format( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt index aa7d8bb6be..a2d6563e50 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt @@ -369,6 +369,21 @@ class GamePropertiesFragment : Fragment() { ) ) } + add( + SubmenuProperty( + R.string.frame_gen, + R.string.frame_gen_per_game_description, + R.drawable.ic_duck, + action = { + val action = HomeNavigationDirections.actionGlobalSettingsActivity( + args.game, + Settings.MenuTag.SECTION_FRAME_GEN + ) + binding.root.findNavController().navigate(action) + } + ) + ) + if (GpuDriverHelper.isAdrenoGpu()) { add( SubmenuProperty( 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 5973c60868..bd4d8465f3 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 @@ -13,7 +13,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat @@ -176,8 +175,14 @@ class HomeSettingsFragment : Fragment() { HomeSetting( R.string.lossless_scaling, R.string.lossless_scaling_description, - R.drawable.ic_frames, - { onLosslessScalingClicked() }, + R.drawable.ic_duck, + { + val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity( + SettingsSubscreen.LOSSLESS_MANAGER, + null + ) + binding.root.findNavController().navigate(action) + }, { true }, 0, 0, @@ -354,48 +359,6 @@ class HomeSettingsFragment : Fragment() { LosslessScalingHelper.refreshStatus() } - private fun onLosslessScalingClicked() { - if (!LosslessScalingHelper.isInstalled()) { - getLosslessDllLauncher.launch(arrayOf("*/*")) - return - } - - MessageDialogFragment.newInstance( - requireActivity(), - titleId = R.string.lossless_scaling, - descriptionId = R.string.lossless_scaling_installed_description, - positiveButtonTitleId = R.string.lossless_scaling_replace, - positiveAction = { getLosslessDllLauncher.launch(arrayOf("*/*")) }, - showNegativeButton = true, - negativeButtonTitleId = R.string.lossless_scaling_remove, - negativeAction = { LosslessScalingHelper.remove() } - ).show(parentFragmentManager, MessageDialogFragment.TAG) - } - - private val getLosslessDllLauncher = - registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> - if (result == null) { - return@registerForActivityResult - } - - val resultStrings = resources.getStringArray(R.array.losslessDllResults) - ProgressDialogFragment.newInstance( - requireActivity(), - R.string.lossless_scaling_installing, - false - ) { _, _ -> - val installResult = LosslessScalingHelper.install(result) - if (installResult == LosslessScalingHelper.RESULT_OK) { - getString(R.string.lossless_scaling_install_success) - } else { - MessageDialogFragment.newInstance( - titleId = R.string.lossless_scaling_install_failed, - descriptionString = resultStrings[installResult] - ) - } - }.show(parentFragmentManager, ProgressDialogFragment.TAG) - } - override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LosslessManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LosslessManagerFragment.kt new file mode 100644 index 0000000000..52cb887fdb --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LosslessManagerFragment.kt @@ -0,0 +1,174 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.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.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.GridLayoutManager +import com.google.android.material.transition.MaterialSharedAxis +import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter +import org.yuzu.yuzu_emu.databinding.FragmentLosslessManagerBinding +import org.yuzu.yuzu_emu.features.fetcher.SpacingItemDecoration +import org.yuzu.yuzu_emu.model.HomeSetting +import org.yuzu.yuzu_emu.utils.LosslessScalingHelper +import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins +import org.yuzu.yuzu_emu.utils.collect + +class LosslessManagerFragment : Fragment() { + private var _binding: FragmentLosslessManagerBinding? = null + private val binding get() = _binding!! + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) + returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentLosslessManagerBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.toolbarLossless.setNavigationOnClickListener { + requireActivity().onBackPressedDispatcher.onBackPressed() + } + + binding.losslessOptionsList.apply { + layoutManager = + GridLayoutManager(requireContext(), resources.getInteger(R.integer.grid_columns)) + addItemDecoration( + SpacingItemDecoration(resources.getDimensionPixelSize(R.dimen.spacing_small)) + ) + } + + LosslessScalingHelper.statusText.collect(viewLifecycleOwner) { refreshOptions() } + + setInsets() + } + + private fun refreshOptions() { + binding.losslessOptionsList.adapter = HomeSettingAdapter( + requireActivity() as AppCompatActivity, + viewLifecycleOwner, + buildOptions() + ) + } + + private fun buildOptions(): List { + val installed = LosslessScalingHelper.isInstalled() + return listOf( + HomeSetting( + if (installed) R.string.lossless_scaling_replace else R.string.lossless_scaling_install, + if (installed) { + R.string.lossless_scaling_replace_description + } else { + R.string.lossless_scaling_install_description + }, + R.drawable.ic_install, + { dllPickerLauncher.launch(arrayOf("*/*")) }, + { !NativeLibrary.isRunning() }, + R.string.lossless_scaling_locked, + R.string.lossless_scaling_locked_description, + LosslessScalingHelper.statusText + ), + HomeSetting( + R.string.lossless_scaling_remove, + R.string.lossless_scaling_remove_description, + R.drawable.ic_delete, + { confirmRemoval() }, + { installed && !NativeLibrary.isRunning() }, + if (installed) { + R.string.lossless_scaling_locked + } else { + R.string.lossless_scaling_remove_unavailable + }, + if (installed) { + R.string.lossless_scaling_locked_description + } else { + R.string.lossless_scaling_remove_unavailable_description + } + ) + ) + } + + private fun confirmRemoval() { + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.lossless_scaling_remove, + descriptionId = R.string.lossless_scaling_remove_confirmation, + positiveButtonTitleId = R.string.lossless_scaling_remove, + positiveAction = { LosslessScalingHelper.remove() }, + showNegativeButton = true, + negativeAction = {} + ).show(parentFragmentManager, MessageDialogFragment.TAG) + } + + private val dllPickerLauncher = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result == null) { + return@registerForActivityResult + } + + val resultStrings = resources.getStringArray(R.array.losslessDllResults) + ProgressDialogFragment.newInstance( + requireActivity(), + R.string.lossless_scaling_installing, + false + ) { _, _ -> + val installResult = LosslessScalingHelper.install(result) + if (installResult == LosslessScalingHelper.RESULT_OK) { + getString(R.string.lossless_scaling_install_success) + } else { + MessageDialogFragment.newInstance( + titleId = R.string.lossless_scaling_install_failed, + descriptionString = resultStrings[installResult] + ) + } + }.show(parentFragmentManager, ProgressDialogFragment.TAG) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun setInsets() = + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets -> + val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) + + binding.appbarLossless.updateMargins( + left = barInsets.left + cutoutInsets.left, + right = barInsets.right + cutoutInsets.right + ) + + binding.scrollViewLossless.updatePadding(bottom = barInsets.bottom) + + binding.losslessOptionsList.updatePadding( + left = barInsets.left + cutoutInsets.left, + right = barInsets.right + cutoutInsets.right + ) + + windowInsets + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt index 1bae6e5269..cfbdc413be 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt @@ -205,7 +205,7 @@ class SetupFragment : Fragment() { ) add( PageButton( - R.drawable.ic_frames, + R.drawable.ic_duck, R.string.lossless_scaling, R.string.lossless_scaling_setup_description, { diff --git a/src/android/app/src/main/res/layout/fragment_lossless_manager.xml b/src/android/app/src/main/res/layout/fragment_lossless_manager.xml new file mode 100644 index 0000000000..841d109e29 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_lossless_manager.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/android/app/src/main/res/navigation/settings_subscreen_navigation.xml b/src/android/app/src/main/res/navigation/settings_subscreen_navigation.xml index f03024e7f6..fff7cdb2d4 100644 --- a/src/android/app/src/main/res/navigation/settings_subscreen_navigation.xml +++ b/src/android/app/src/main/res/navigation/settings_subscreen_navigation.xml @@ -47,6 +47,12 @@ android:defaultValue="@null" /> + + GPU driver manager Install alternative drivers for potentially better performance or accuracy Frame generation - Manage and configure frame generation + Configure frame generation for this game Insert interpolated frames between rendered ones using Lossless Scaling. Forces FIFO presentation while enabled. Frame multiplier How many frames to display for each rendered frame. Higher values cost proportionally more GPU time. Asking for more than your display can present will slow emulation down. @@ -345,6 +345,12 @@ Remove Delete the installed Lossless.dll and its prepared shaders Frame generation will stop working until you install Lossless.dll again. Your original file is not affected. + Lossless.dll not installed + Install it from Settings › Lossless Scaling to use frame generation. + Close the game first + Lossless.dll cannot be changed while a game is running. + Nothing to remove + Lossless.dll is not installed yet. Preparing frame generation shaders… Lossless.dll installed successfully Could not install Lossless.dll