aboutsummaryrefslogtreecommitdiff
path: root/src/android/app
diff options
context:
space:
mode:
authorCamilleLaVey <camillelavey99@gmail.com>2026-08-17 00:30:19 +0200
committercrueter <crueter@eden-emu.dev>2026-08-17 00:30:19 +0200
commit2000fdfb7b27e5e2546098b44268fc74887b22a5 (patch)
tree924d533de5e75f9f08e02f67bddc946690ea5665 /src/android/app
parentdc95cd09eea9749250fe31a3072684d341d19417 (diff)
[vulkan, android] Initial implementation of LSFG-VK (#4263)
Pretty much self explanatory, own implementation based on LSFG-VK available with some additional reverse engineer work for the actual use of shaders inside Lossless.dll, we don't store/ bundle any tools that bypass security and self ownership: to use this feature is required to own your own copy of Lossless Scaling and I encourage to support developers behind this work. Special thanks: 1.- THS - Original developer behind Lossless Scaling 2.- PancakeTAS - Developer behind LSFG-VK 3.- The Big Smolio The Sleeper (@gidoly) Co-authored-by: lizzie <lizzie@eden-emu.dev> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4263
Diffstat (limited to 'src/android/app')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt34
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt1
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt103
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt39
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSubscreenActivity.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt15
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt20
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LosslessManagerFragment.kt174
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt47
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SystemInfoDialogFragment.kt14
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/LosslessScalingHelper.kt77
-rw-r--r--src/android/app/src/main/jni/android_config.cpp4
-rw-r--r--src/android/app/src/main/jni/android_settings.h2
-rw-r--r--src/android/app/src/main/jni/emu_window/emu_window.cpp19
-rw-r--r--src/android/app/src/main/jni/emu_window/emu_window.h1
-rw-r--r--src/android/app/src/main/jni/native.cpp93
-rw-r--r--src/android/app/src/main/res/drawable/ic_duck.xml19
-rw-r--r--src/android/app/src/main/res/layout/fragment_lossless_manager.xml63
-rw-r--r--src/android/app/src/main/res/navigation/settings_subscreen_navigation.xml6
-rw-r--r--src/android/app/src/main/res/values/arrays.xml52
-rw-r--r--src/android/app/src/main/res/values/strings.xml61
24 files changed, 845 insertions, 13 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 797ac803eb..4610574f21 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
@@ -540,6 +540,40 @@ object NativeLibrary {
external fun installKeys(path: String, ext: String): Int
/**
+ * @return Whether this GPU can run the Lossless Scaling frame generation shaders,
+ * which are built against the Vulkan memory model.
+ */
+ external fun supportsFrameGeneration(): Boolean
+
+ /**
+ * @return Path the user-supplied Lossless Scaling library is expected at.
+ */
+ external fun getLosslessDllPath(): String
+
+ /**
+ * Parses the installed Lossless Scaling library and checks that every shader the
+ * frame generation chain needs is present.
+ *
+ * @return The result code, matching the losslessDllResults array.
+ */
+ external fun validateLosslessDll(): Int
+
+ /**
+ * Translates the frame generation shaders out of the installed Lossless Scaling library
+ * and writes them to the SPIR-V cache. Slow, so call it off the main thread.
+ *
+ * @return The result code, matching the losslessDllResults array.
+ */
+ external fun prepareLosslessDll(): Int
+
+ /**
+ * Deletes the installed Lossless Scaling library.
+ *
+ * @return Whether the library is gone after the call.
+ */
+ external fun removeLosslessDll(): Boolean
+
+ /**
* Checks the PatchManager for any addons that are available
*
* @param path Path to game file. Can be a [Uri].
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
index 7f6e793199..39a76a8608 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
@@ -37,6 +37,10 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
RENDERER_PATCH_OLD_QCOM_DRIVERS("patch_old_qcom_drivers"),
RENDERER_VERTEX_INPUT_DYNAMIC_STATE("vertex_input_dynamic_state"),
RENDERER_SAMPLE_SHADING("sample_shading"),
+ RENDERER_FRAME_GEN("frame_gen"),
+ RENDERER_FRAME_GEN_FP16("frame_gen_fp16"),
+ RENDERER_FRAME_GEN_FLOW_SCALE_AUTO("frame_gen_flow_scale_auto"),
+ RENDERER_FRAME_GEN_DUMP_FLOW("frame_gen_dump_flow"),
GPU_UNSWIZZLE_ENABLED("gpu_unswizzle_enabled"),
PICTURE_IN_PICTURE("picture_in_picture"),
USE_CUSTOM_RTC("custom_rtc_enabled"),
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
index 117336ab64..ea31692fbf 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
@@ -19,6 +19,10 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
RENDERER_ASTC_DECODE_METHOD("accelerate_astc"),
RENDERER_ACCURACY("gpu_accuracy"),
RENDERER_RESOLUTION("resolution_setup"),
+ RENDERER_FRAME_GEN_MULTIPLIER("frame_gen_multiplier"),
+ RENDERER_FRAME_GEN_TARGET_RATE("frame_gen_target_rate"),
+ RENDERER_FRAME_GEN_QUEUE_TARGET("frame_gen_queue_target"),
+ RENDERER_FRAME_GEN_FLOW_SCALE("frame_gen_flow_scale"),
RENDERER_VSYNC("use_vsync"),
RENDERER_SCALING_FILTER("scaling_filter"),
RENDERER_ANTI_ALIASING("anti_aliasing"),
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
index b438812d58..2b449beaff 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
@@ -11,6 +11,7 @@ object Settings {
SECTION_ROOT(R.string.advanced_settings),
SECTION_SYSTEM(R.string.preferences_system),
SECTION_RENDERER(R.string.preferences_graphics),
+ SECTION_FRAME_GEN(R.string.frame_gen),
SECTION_PERFORMANCE_STATS(R.string.stats_overlay_options),
SECTION_INPUT_OVERLAY(R.string.input_overlay_options),
SECTION_SOC_OVERLAY(R.string.soc_overlay_options),
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
index 63ac7b0630..0ca31062b3 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
@@ -21,6 +21,7 @@ import org.yuzu.yuzu_emu.features.settings.model.LongSetting
import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.network.NetDataValidators
+import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
import org.yuzu.yuzu_emu.utils.NativeConfig
/**
@@ -65,6 +66,19 @@ abstract class SettingsItem(
return NativeLibrary.isFirmwareAvailable()
}
+ if (setting.key in frameGenKeys &&
+ !(LosslessScalingHelper.isInstalled() && LosslessScalingHelper.isSupportedByGpu())
+ ) {
+ return false
+ }
+
+ // A frame rate target moves the multiplier on its own
+ if (setting.key == IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.key &&
+ frameGenTargetRate != 0
+ ) {
+ return false
+ }
+
// Can't edit settings that aren't saveable in per-game config even if they are switchable
if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) {
return false
@@ -88,7 +102,31 @@ abstract class SettingsItem(
val clearable: Boolean
get() = !setting.global && NativeConfig.isPerGameConfigLoaded()
+ private val frameGenTargetRate: Int
+ get() {
+ val key = IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key
+ val needsGlobal = if (NativeLibrary.isRunning() &&
+ !NativeConfig.isPerGameConfigLoaded()
+ ) {
+ !NativeConfig.usingGlobal(key)
+ } else {
+ NativeConfig.usingGlobal(key)
+ }
+ return IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.getInt(needsGlobal)
+ }
+
companion object {
+ private val frameGenKeys = setOf(
+ BooleanSetting.RENDERER_FRAME_GEN.key,
+ IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.key,
+ IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key,
+ IntSetting.RENDERER_FRAME_GEN_QUEUE_TARGET.key,
+ BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.key,
+ IntSetting.RENDERER_FRAME_GEN_FLOW_SCALE.key,
+ BooleanSetting.RENDERER_FRAME_GEN_FP16.key,
+ BooleanSetting.RENDERER_FRAME_GEN_DUMP_FLOW.key
+ )
+
const val TYPE_HEADER = 0
const val TYPE_SWITCH = 1
const val TYPE_SINGLE_CHOICE = 2
@@ -608,6 +646,71 @@ abstract class SettingsItem(
)
)
put(
+ SwitchSetting(
+ BooleanSetting.RENDERER_FRAME_GEN,
+ titleId = R.string.frame_gen,
+ descriptionId = R.string.frame_gen_description
+ )
+ )
+ put(
+ SingleChoiceSetting(
+ IntSetting.RENDERER_FRAME_GEN_MULTIPLIER,
+ titleId = R.string.frame_gen_multiplier,
+ descriptionId = R.string.frame_gen_multiplier_description,
+ choicesId = R.array.frameGenMultiplierNames,
+ valuesId = R.array.frameGenMultiplierValues
+ )
+ )
+ put(
+ SingleChoiceSetting(
+ IntSetting.RENDERER_FRAME_GEN_TARGET_RATE,
+ titleId = R.string.frame_gen_target_rate,
+ descriptionId = R.string.frame_gen_target_rate_description,
+ choicesId = R.array.frameGenTargetRateNames,
+ valuesId = R.array.frameGenTargetRateValues
+ )
+ )
+ put(
+ SingleChoiceSetting(
+ IntSetting.RENDERER_FRAME_GEN_QUEUE_TARGET,
+ titleId = R.string.frame_gen_queue_target,
+ descriptionId = R.string.frame_gen_queue_target_description,
+ choicesId = R.array.frameGenQueueTargetNames,
+ valuesId = R.array.frameGenQueueTargetValues
+ )
+ )
+ put(
+ SwitchSetting(
+ BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO,
+ titleId = R.string.frame_gen_flow_scale_auto,
+ descriptionId = R.string.frame_gen_flow_scale_auto_description
+ )
+ )
+ put(
+ SliderSetting(
+ IntSetting.RENDERER_FRAME_GEN_FLOW_SCALE,
+ titleId = R.string.frame_gen_flow_scale,
+ descriptionId = R.string.frame_gen_flow_scale_description,
+ min = 25,
+ max = 100,
+ units = "%"
+ )
+ )
+ put(
+ SwitchSetting(
+ BooleanSetting.RENDERER_FRAME_GEN_FP16,
+ titleId = R.string.frame_gen_fp16,
+ descriptionId = R.string.frame_gen_fp16_description
+ )
+ )
+ put(
+ SwitchSetting(
+ BooleanSetting.RENDERER_FRAME_GEN_DUMP_FLOW,
+ titleId = R.string.frame_gen_dump_flow,
+ descriptionId = R.string.frame_gen_dump_flow_description
+ )
+ )
+ put(
SingleChoiceSetting(
IntSetting.RENDERER_SCREEN_LAYOUT,
titleId = R.string.renderer_screen_layout,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt
index 4e4ac47ea2..3ab864f906 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt
@@ -382,7 +382,9 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
}
scSetting.setSelectedValue(value)
- if (scSetting.setting.key == IntSetting.RENDERER_SCALING_FILTER.key) {
+ if (scSetting.setting.key == IntSetting.RENDERER_SCALING_FILTER.key ||
+ scSetting.setting.key == IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key
+ ) {
settingsViewModel.setShouldReloadSettingsList(true)
}
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 594099f159..1a3fa2fd17 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
@@ -27,6 +27,7 @@ import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.features.settings.model.view.*
import org.yuzu.yuzu_emu.utils.InputHandler
+import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.FullscreenHelper
@@ -76,6 +77,41 @@ class SettingsFragmentPresenter(
}
}
+ private fun addFrameGenSettings(sl: ArrayList<SettingsItem>) {
+ sl.apply {
+ if (!LosslessScalingHelper.isSupportedByGpu()) {
+ add(
+ RunnableSetting(
+ titleId = R.string.frame_gen_unsupported,
+ descriptionId = R.string.frame_gen_unsupported_description,
+ isRunnable = false
+ ) {}
+ )
+ } else if (!LosslessScalingHelper.isInstalled()) {
+ add(
+ RunnableSetting(
+ titleId = R.string.lossless_scaling_missing,
+ descriptionId = R.string.lossless_scaling_missing_description,
+ isRunnable = false
+ ) {}
+ )
+ }
+
+ add(BooleanSetting.RENDERER_FRAME_GEN.key)
+ add(IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key)
+ add(IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.key)
+ add(IntSetting.RENDERER_FRAME_GEN_QUEUE_TARGET.key)
+ add(BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.key)
+ if (!BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.getBoolean(
+ getNeedsGlobalForKey(BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.key)
+ )
+ ) {
+ add(IntSetting.RENDERER_FRAME_GEN_FLOW_SCALE.key)
+ }
+ add(BooleanSetting.RENDERER_FRAME_GEN_FP16.key)
+ }
+ }
+
private fun isSharpnessScalingFilterSelected(): Boolean {
val needsGlobal = getNeedsGlobalForKey(IntSetting.RENDERER_SCALING_FILTER.key)
val selectedFilter = IntSetting.RENDERER_SCALING_FILTER.getInt(needsGlobal)
@@ -120,6 +156,7 @@ class SettingsFragmentPresenter(
MenuTag.SECTION_ROOT -> addConfigSettings(sl)
MenuTag.SECTION_SYSTEM -> addSystemSettings(sl)
MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl)
+ MenuTag.SECTION_FRAME_GEN -> addFrameGenSettings(sl)
MenuTag.SECTION_PERFORMANCE_STATS -> addPerformanceOverlaySettings(sl)
MenuTag.SECTION_SOC_OVERLAY -> addSocOverlaySettings(sl)
MenuTag.SECTION_INPUT_OVERLAY -> addInputOverlaySettings(sl)
@@ -302,7 +339,6 @@ class SettingsFragmentPresenter(
add(BooleanSetting.SKIP_CPU_INNER_INVALIDATION.key)
add(BooleanSetting.FIX_BLOOM_EFFECTS.key)
add(BooleanSetting.EMULATE_BGR565.key)
- add(BooleanSetting.RESCALE_HACK.key)
add(BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS.key)
add(IntSetting.ANDROID_PIPELINE_WORKERS.key)
add(BooleanSetting.RENDERER_ASYNCHRONOUS_GPU_EMULATION.key)
@@ -1296,6 +1332,7 @@ class SettingsFragmentPresenter(
add(BooleanSetting.DUMP_GUEST_SHADERS.key)
add(BooleanSetting.GPU_LOG_SHADER_DUMPS.key)
add(BooleanSetting.DUMP_MACROS.key)
+ add(BooleanSetting.RENDERER_FRAME_GEN_DUMP_FLOW.key)
add(BooleanSetting.GPU_LOG_MEMORY_TRACKING.key)
add(BooleanSetting.GPU_LOG_DRIVER_DEBUG.key)
add(IntSetting.GPU_LOG_RING_BUFFER_SIZE.key)
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/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 37eda22c69..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
@@ -44,6 +44,7 @@ import org.yuzu.yuzu_emu.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.FileUtil
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import org.yuzu.yuzu_emu.utils.Log
+import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
class HomeSettingsFragment : Fragment() {
@@ -172,6 +173,24 @@ class HomeSettingsFragment : Fragment() {
}
add(
HomeSetting(
+ R.string.lossless_scaling,
+ R.string.lossless_scaling_description,
+ R.drawable.ic_duck,
+ {
+ val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
+ SettingsSubscreen.LOSSLESS_MANAGER,
+ null
+ )
+ binding.root.findNavController().navigate(action)
+ },
+ { true },
+ 0,
+ 0,
+ LosslessScalingHelper.statusText
+ )
+ )
+ add(
+ HomeSetting(
R.string.multiplayer,
R.string.multiplayer_description,
R.drawable.ic_two_users,
@@ -337,6 +356,7 @@ class HomeSettingsFragment : Fragment() {
override fun onResume() {
super.onResume()
driverViewModel.updateDriverNameForGame(null)
+ LosslessScalingHelper.refreshStatus()
}
override fun onDestroyView() {
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<HomeSetting> {
+ 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 41f2da08a7..fd59304ebb 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
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@@ -42,6 +42,7 @@ import org.yuzu.yuzu_emu.model.SetupPage
import org.yuzu.yuzu_emu.model.PageState
import org.yuzu.yuzu_emu.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
+import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.ViewUtils
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
@@ -204,6 +205,24 @@ class SetupFragment : Fragment() {
)
add(
PageButton(
+ R.drawable.ic_duck,
+ R.string.lossless_scaling,
+ R.string.lossless_scaling_setup_description,
+ {
+ pageButtonCallback = it
+ getLosslessDll.launch(arrayOf("*/*"))
+ },
+ {
+ if (LosslessScalingHelper.isInstalled()) {
+ ButtonState.BUTTON_ACTION_COMPLETE
+ } else {
+ ButtonState.BUTTON_ACTION_INCOMPLETE
+ }
+ }
+ )
+ )
+ add(
+ PageButton(
R.drawable.ic_controller,
R.string.games,
R.string.games_description,
@@ -446,6 +465,32 @@ class SetupFragment : Fragment() {
}
}
+ val getLosslessDll =
+ 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]
+ )
+ }
+ }.apply {
+ onDialogComplete = { checkForButtonState.invoke() }
+ }.show(parentFragmentManager, ProgressDialogFragment.TAG)
+ }
+
val getGamesDirectory =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
if (result != null) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SystemInfoDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SystemInfoDialogFragment.kt
index 5c914eeb16..2a691e7bf8 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SystemInfoDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SystemInfoDialogFragment.kt
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@@ -72,6 +72,18 @@ class SystemInfoDialogFragment : DialogFragment() {
val vulkanDriver = NativeLibrary.getVulkanDriverVersion()
appendLine("${getString(R.string.vulkan_driver_version)}: $vulkanDriver")
+
+ val frameGen = NativeLibrary.supportsFrameGeneration()
+ appendLine(
+ "${getString(R.string.frame_generation_support)}: " +
+ getString(
+ if (frameGen) {
+ R.string.frame_generation_supported
+ } else {
+ R.string.frame_generation_unsupported
+ }
+ )
+ )
} catch (e: Exception) {
appendLine("${getString(R.string.error_getting_emulator_info)}: ${e.message}")
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/LosslessScalingHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/LosslessScalingHelper.kt
new file mode 100644
index 0000000000..61f594a124
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/LosslessScalingHelper.kt
@@ -0,0 +1,77 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import android.net.Uri
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import java.io.File
+
+object LosslessScalingHelper {
+ const val RESULT_OK = 0
+ const val RESULT_NOT_INSTALLED = 1
+
+ private val _statusText = MutableStateFlow("")
+ val statusText: StateFlow<String> = _statusText.asStateFlow()
+
+ private var installed: Boolean? = null
+ private var gpuSupported: Boolean? = null
+
+ fun isInstalled(): Boolean = installed ?: refreshStatus()
+
+ fun isSupportedByGpu(): Boolean {
+ val cached = gpuSupported
+ if (cached != null) {
+ return cached
+ }
+ val result = NativeLibrary.supportsFrameGeneration()
+ gpuSupported = result
+ return result
+ }
+
+ fun refreshStatus(): Boolean {
+ val result = NativeLibrary.validateLosslessDll() == RESULT_OK
+ installed = result
+
+ val context = YuzuApplication.appContext
+ _statusText.value = if (result) {
+ context.getString(R.string.lossless_scaling_installed)
+ } else {
+ context.getString(R.string.lossless_scaling_not_installed)
+ }
+ return result
+ }
+
+ fun install(source: Uri): Int {
+ val destination = File(NativeLibrary.getLosslessDllPath())
+ destination.parentFile?.mkdirs()
+
+ val copied = FileUtil.copyUriToInternalStorage(
+ source,
+ destination.parent!!,
+ destination.name
+ )
+ if (copied == null) {
+ refreshStatus()
+ return RESULT_NOT_INSTALLED
+ }
+
+ val result = NativeLibrary.prepareLosslessDll()
+ if (result != RESULT_OK) {
+ NativeLibrary.removeLosslessDll()
+ }
+ refreshStatus()
+ return result
+ }
+
+ fun remove(): Boolean {
+ val removed = NativeLibrary.removeLosslessDll()
+ refreshStatus()
+ return removed
+ }
+}
diff --git a/src/android/app/src/main/jni/android_config.cpp b/src/android/app/src/main/jni/android_config.cpp
index e016322c23..6548667de4 100644
--- a/src/android/app/src/main/jni/android_config.cpp
+++ b/src/android/app/src/main/jni/android_config.cpp
@@ -33,8 +33,8 @@ void AndroidConfig::SaveAllValues() {
}
void AndroidConfig::ReadAndroidValues() {
+ ReadAndroidUIValues();
if (global) {
- ReadAndroidUIValues();
ReadUIValues();
BeginGroup(Settings::TranslateCategory(Settings::Category::DataStorage));
Settings::values.ext_content_from_game_dirs = ReadBooleanSetting(
@@ -223,8 +223,8 @@ void AndroidConfig::ReadAndroidControlValues() {
}
void AndroidConfig::SaveAndroidValues() {
+ SaveAndroidUIValues();
if (global) {
- SaveAndroidUIValues();
SaveUIValues();
SaveOverlayValues();
}
diff --git a/src/android/app/src/main/jni/android_settings.h b/src/android/app/src/main/jni/android_settings.h
index 304ff62f07..7cc3ad22d5 100644
--- a/src/android/app/src/main/jni/android_settings.h
+++ b/src/android/app/src/main/jni/android_settings.h
@@ -147,7 +147,7 @@ namespace AndroidSettings {
&show_performance_overlay};
- Settings::Setting<s32> pipeline_worker_count{linkage, 4, "pipeline_worker_count",
+ Settings::SwitchableSetting<s32> pipeline_worker_count{linkage, 2, "pipeline_worker_count",
Settings::Category::Android,
Settings::Specialization::Default,
true,
diff --git a/src/android/app/src/main/jni/emu_window/emu_window.cpp b/src/android/app/src/main/jni/emu_window/emu_window.cpp
index 6886dac172..8dfea40693 100644
--- a/src/android/app/src/main/jni/emu_window/emu_window.cpp
+++ b/src/android/app/src/main/jni/emu_window/emu_window.cpp
@@ -124,9 +124,18 @@ float EmuWindow_Android::GetFrameTimeVerifiedHint() const {
return QuantizeFrameRateHint(verified_rate);
}
+float EmuWindow_Android::GetPresentedFrameMultiplier() {
+ if (!Settings::values.frame_gen.GetValue()) {
+ return 1.0f;
+ }
+ return static_cast<float>(std::clamp<u32>(Settings::values.frame_gen_multiplier.GetValue(), 2, 4));
+}
+
float EmuWindow_Android::GetFrameRateHint() const {
- const float observed_rate = std::clamp(m_smoothed_present_rate, 0.0f, 240.0f);
- const float frame_time_verified_hint = GetFrameTimeVerifiedHint();
+ const float presented_multiplier = GetPresentedFrameMultiplier();
+ const float observed_rate =
+ std::clamp(m_smoothed_present_rate * presented_multiplier, 0.0f, 240.0f);
+ const float frame_time_verified_hint = GetFrameTimeVerifiedHint() * presented_multiplier;
if (m_last_frame_rate_hint > 0.0f && observed_rate > 0.0f) {
const float tolerance = std::max(m_last_frame_rate_hint * 0.12f, 4.0f);
@@ -150,9 +159,9 @@ float EmuWindow_Android::GetFrameRateHint() const {
return frame_time_verified_hint;
}
- constexpr float NominalFrameRate = 60.0f;
+ const float nominal_rate = 60.0f * presented_multiplier;
if (!Settings::values.use_speed_limit.GetValue()) {
- return NominalFrameRate;
+ return QuantizeFrameRateHint(nominal_rate);
}
const u16 speed_limit = Settings::SpeedLimit();
@@ -161,7 +170,7 @@ float EmuWindow_Android::GetFrameRateHint() const {
}
const float speed_limited_rate =
- NominalFrameRate * (static_cast<float>(std::min<u16>(speed_limit, 100)) / 100.0f);
+ nominal_rate * (static_cast<float>(std::min<u16>(speed_limit, 100)) / 100.0f);
return QuantizeFrameRateHint(speed_limited_rate);
}
diff --git a/src/android/app/src/main/jni/emu_window/emu_window.h b/src/android/app/src/main/jni/emu_window/emu_window.h
index b73e8b9b4d..fca83b91fa 100644
--- a/src/android/app/src/main/jni/emu_window/emu_window.h
+++ b/src/android/app/src/main/jni/emu_window/emu_window.h
@@ -61,6 +61,7 @@ private:
void UpdateObservedFrameRate();
[[nodiscard]] float GetFrameRateHint() const;
[[nodiscard]] float GetFrameTimeVerifiedHint() const;
+ [[nodiscard]] static float GetPresentedFrameMultiplier();
[[nodiscard]] static float QuantizeFrameRateHint(float frame_rate);
float m_window_width{};
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index c367a06dfd..4d0c39f5c7 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -44,6 +44,7 @@ extern "C" {
#include "common/android/android_common.h"
#include "common/android/id_cache.h"
#include "common/dynamic_library.h"
+#include "common/fs/fs_util.h"
#include "common/fs/path_util.h"
#include "common/logging.h"
#include "common/scm_rev.h"
@@ -90,6 +91,7 @@ extern "C" {
#include "hid_core/hid_types.h"
#include "input_common/drivers/virtual_amiibo.h"
#include "jni/native.h"
+#include "video_core/frame_gen/lossless_dll.h"
#include "video_core/renderer_base.h"
#include "video_core/renderer_vulkan/renderer_vulkan.h"
#include "video_core/capture.h"
@@ -998,7 +1000,7 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuSummary(JNIEnv* env, jobject
FILE* f = std::fopen(CPUINFO_PATH, "r");
if (!f) return Common::Android::ToJString(env, result);
- char buf[512];
+ char buf[4096];
if (f) {
std::set<std::string> feature_set;
@@ -1029,7 +1031,13 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuSummary(JNIEnv* env, jobject
bool has_dotprod = feature_set.count("asimddp") || feature_set.count("dotprod");
bool has_i8mm = feature_set.count("i8mm");
bool has_bf16 = feature_set.count("bf16");
+ bool has_fp16 = feature_set.count("fphp") || feature_set.count("asimdhp");
bool has_atomics = feature_set.count("atomics") || feature_set.count("lse");
+ bool has_lse2 = feature_set.count("uscat");
+ bool has_rcpc = feature_set.count("lrcpc");
+ bool has_rcpc2 = feature_set.count("ilrcpc");
+ bool has_flagm = feature_set.count("flagm");
+ bool has_flagm2 = feature_set.count("flagm2");
std::string features;
if (has_neon || has_fp) {
@@ -1037,6 +1045,7 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuSummary(JNIEnv* env, jobject
if (has_dotprod) features += "+DP";
if (has_i8mm) features += "+I8MM";
if (has_bf16) features += "+BF16";
+ if (has_fp16) features += "+FP16";
}
if (has_sve) {
@@ -1053,6 +1062,19 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuSummary(JNIEnv* env, jobject
if (has_atomics) {
if (!features.empty()) features += " | ";
features += "LSE";
+ if (has_lse2) features += "2";
+ }
+
+ if (has_rcpc) {
+ if (!features.empty()) features += " | ";
+ features += "RCpc";
+ if (has_rcpc2) features += "2";
+ }
+
+ if (has_flagm) {
+ if (!features.empty()) features += " | ";
+ features += "FlagM";
+ if (has_flagm2) features += "2";
}
if (!features.empty()) {
@@ -1090,6 +1112,34 @@ VkPhysicalDeviceProperties GetVulkanDeviceProperties() {
const Vulkan::vk::PhysicalDevice physical_device(physical_devices[0], dld);
return physical_device.GetProperties();
}
+
+bool GetVulkanMemoryModelSupport() {
+ Common::DynamicLibrary library;
+ if (!library.Open("libvulkan.so")) {
+ return false;
+ }
+
+ Vulkan::vk::InstanceDispatch dld;
+ const auto instance = Vulkan::CreateInstance(library, dld, VK_API_VERSION_1_1);
+ const auto physical_devices = instance.EnumeratePhysicalDevices();
+ if (physical_devices.empty()) {
+ return false;
+ }
+
+ const Vulkan::vk::PhysicalDevice physical_device(physical_devices[0], dld);
+
+ VkPhysicalDeviceVulkanMemoryModelFeatures memory_model{
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_MEMORY_MODEL_FEATURES,
+ .pNext = nullptr,
+ };
+ VkPhysicalDeviceFeatures2 features{
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
+ .pNext = &memory_model,
+ };
+ physical_device.GetFeatures2(features);
+
+ return memory_model.vulkanMemoryModel == VK_TRUE;
+}
} // namespace
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getVulkanDriverVersion(JNIEnv* env, jobject jobj) {
@@ -1165,6 +1215,14 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getVulkanApiVersion(JNIEnv* env, j
}
}
+jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_supportsFrameGeneration(JNIEnv* env, jobject jobj) {
+ try {
+ return static_cast<jboolean>(GetVulkanMemoryModelSupport());
+ } catch (...) {
+ return static_cast<jboolean>(false);
+ }
+}
+
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGpuModel(JNIEnv* env, jobject jobj) {
const auto props = GetVulkanDeviceProperties();
if (props.deviceID == 0) {
@@ -1390,6 +1448,39 @@ jint Java_org_yuzu_yuzu_1emu_NativeLibrary_installKeys(JNIEnv* env, jclass clazz
return static_cast<int>(FirmwareManager::InstallKeys(path, ext));
}
+jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getLosslessDllPath(JNIEnv* env, jclass clazz) {
+#ifdef HAS_LSFG
+ const auto path = VideoCore::FrameGen::GetLosslessDllPath();
+ return Common::Android::ToJString(env, Common::FS::PathToUTF8String(path));
+#else
+ return Common::Android::ToJString(env, "");
+#endif
+}
+
+jint Java_org_yuzu_yuzu_1emu_NativeLibrary_validateLosslessDll(JNIEnv* env, jclass clazz) {
+#ifdef HAS_LSFG
+ return static_cast<jint>(VideoCore::FrameGen::GetInstalledLosslessStatus());
+#else
+ return static_cast<jint>(VideoCore::FrameGen::LosslessStatus::NotInstalled);
+#endif
+}
+
+jint Java_org_yuzu_yuzu_1emu_NativeLibrary_prepareLosslessDll(JNIEnv* env, jclass clazz) {
+#ifdef HAS_LSFG
+ return static_cast<jint>(VideoCore::FrameGen::BuildShaderCache());
+#else
+ return static_cast<jint>(VideoCore::FrameGen::LosslessStatus::NotInstalled);
+#endif
+}
+
+jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_removeLosslessDll(JNIEnv* env, jclass clazz) {
+#ifdef HAS_LSFG
+ return static_cast<jboolean>(VideoCore::FrameGen::RemoveInstalledLosslessDll());
+#else
+ return static_cast<jboolean>(false);
+#endif
+}
+
jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env, jobject jobj,
jstring jpath,
jstring jprogramId) {
diff --git a/src/android/app/src/main/res/drawable/ic_duck.xml b/src/android/app/src/main/res/drawable/ic_duck.xml
new file mode 100644
index 0000000000..601e1e6c0f
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_duck.xml
@@ -0,0 +1,19 @@
+<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="M11,10.6a8,5.2 0 1,0 0,10.4a8,5.2 0 1,0 0,-10.4z" />
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M4.6,12.6L0.8,10.2L3.2,15.5z" />
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:fillType="evenOdd"
+ android:pathData="M15.3,3.5a4,4 0 1,0 0,8a4,4 0 1,0 0,-8zM16.6,4.9a1,1 0 1,0 0,2a1,1 0 1,0 0,-2z" />
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M18.6,6.5L23.2,7.8L18.6,9.3z" />
+</vector>
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 @@
+<?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:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/appbar_lossless"
+ style="@style/Widget.Eden.TransparentTopAppBarLayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fitsSystemWindows="true"
+ android:touchscreenBlocksFocus="false"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <com.google.android.material.appbar.MaterialToolbar
+ android:id="@+id/toolbar_lossless"
+ style="@style/Widget.Eden.TransparentTopToolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:touchscreenBlocksFocus="false"
+ app:navigationIcon="@drawable/ic_back"
+ app:title="@string/lossless_scaling" />
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <androidx.core.widget.NestedScrollView
+ android:id="@+id/scroll_view_lossless"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:background="@android:color/transparent"
+ android:clipToPadding="false"
+ android:defaultFocusHighlightEnabled="false"
+ android:fadeScrollbars="false"
+ android:scrollbars="vertical"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/appbar_lossless">
+
+ <androidx.appcompat.widget.LinearLayoutCompat
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingHorizontal="16dp"
+ android:paddingTop="16dp">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/lossless_options_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipToPadding="false"
+ android:nestedScrollingEnabled="false" />
+
+ </androidx.appcompat.widget.LinearLayoutCompat>
+
+ </androidx.core.widget.NestedScrollView>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
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
@@ -48,6 +48,12 @@
</fragment>
<fragment
+ android:id="@+id/losslessManagerFragment"
+ android:name="org.yuzu.yuzu_emu.fragments.LosslessManagerFragment"
+ android:label="@string/lossless_scaling"
+ tools:layout="@layout/fragment_lossless_manager" />
+
+ <fragment
android:id="@+id/appletLauncherFragment"
android:name="org.yuzu.yuzu_emu.fragments.AppletLauncherFragment"
android:label="AppletLauncherFragment">
diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml
index 1bad110d14..53dd0436dc 100644
--- a/src/android/app/src/main/res/values/arrays.xml
+++ b/src/android/app/src/main/res/values/arrays.xml
@@ -162,6 +162,48 @@
<item>@string/resolution_four</item>
</string-array>
+ <string-array name="frameGenMultiplierNames">
+ <item>@string/frame_gen_multiplier_2x</item>
+ <item>@string/frame_gen_multiplier_3x</item>
+ <item>@string/frame_gen_multiplier_4x</item>
+ </string-array>
+
+ <integer-array name="frameGenMultiplierValues">
+ <item>2</item>
+ <item>3</item>
+ <item>4</item>
+ </integer-array>
+
+ <string-array name="frameGenTargetRateNames">
+ <item>@string/frame_gen_target_rate_off</item>
+ <item>@string/frame_gen_target_rate_60</item>
+ <item>@string/frame_gen_target_rate_90</item>
+ <item>@string/frame_gen_target_rate_120</item>
+ <item>@string/frame_gen_target_rate_144</item>
+ <item>@string/frame_gen_target_rate_165</item>
+ </string-array>
+
+ <integer-array name="frameGenTargetRateValues">
+ <item>0</item>
+ <item>60</item>
+ <item>90</item>
+ <item>120</item>
+ <item>144</item>
+ <item>165</item>
+ </integer-array>
+
+ <string-array name="frameGenQueueTargetNames">
+ <item>@string/frame_gen_queue_target_0</item>
+ <item>@string/frame_gen_queue_target_1</item>
+ <item>@string/frame_gen_queue_target_2</item>
+ </string-array>
+
+ <integer-array name="frameGenQueueTargetValues">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ </integer-array>
+
<string-array name="rendererVSyncNames">
<item>@string/renderer_vsync_immediate</item>
<item>@string/renderer_vsync_mailbox</item>
@@ -638,6 +680,16 @@
<item>@string/error_keys_failed_init</item>
</string-array>
+ <string-array name="losslessDllResults">
+ <item>""</item>
+ <item>@string/error_lossless_copy_failed</item>
+ <item>@string/error_lossless_unreadable</item>
+ <item>@string/error_lossless_not_pe</item>
+ <item>@string/error_lossless_missing_shaders</item>
+ <item>@string/error_lossless_translation_failed</item>
+ <item>@string/error_lossless_cache_failed</item>
+ </string-array>
+
<!-- GPU Logging Arrays -->
<string-array name="gpuLogLevelEntries">
<item>Off</item>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index bb1d85db82..213bd1bee6 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -298,6 +298,67 @@
<string name="gpu_driver_fetcher">GPU driver fetcher</string>
<string name="gpu_driver_manager">GPU driver manager</string>
<string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
+ <string name="frame_gen">Frame generation</string>
+ <string name="frame_gen_per_game_description">Configure frame generation for this game</string>
+ <string name="frame_gen_description">Insert interpolated frames between rendered ones using Lossless Scaling. Forces FIFO presentation while enabled.</string>
+ <string name="frame_gen_multiplier">Frame multiplier</string>
+ <string name="frame_gen_multiplier_description">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.</string>
+ <string name="frame_gen_multiplier_2x">2x</string>
+ <string name="frame_gen_multiplier_3x">3x</string>
+ <string name="frame_gen_multiplier_4x">4x</string>
+ <string name="frame_gen_target_rate">Target frame rate</string>
+ <string name="frame_gen_target_rate_description">Pick the rate your display can actually show. The multiplier then rises or falls on its own to hold it, and rolls back any step that makes the game itself run slower.</string>
+ <string name="frame_gen_target_rate_off">Use a fixed multiplier</string>
+ <string name="frame_gen_target_rate_60">60 FPS</string>
+ <string name="frame_gen_target_rate_90">90 FPS</string>
+ <string name="frame_gen_target_rate_120">120 FPS</string>
+ <string name="frame_gen_target_rate_144">144 FPS</string>
+ <string name="frame_gen_target_rate_165">165 FPS</string>
+ <string name="frame_gen_queue_target">Frame queue target</string>
+ <string name="frame_gen_queue_target_description">How many finished frames may wait ahead of the display. Larger queues absorb GPU spikes at the cost of input latency.</string>
+ <string name="frame_gen_queue_target_0">Lowest latency (Unbuffered)</string>
+ <string name="frame_gen_queue_target_1">Balanced (1 frame)</string>
+ <string name="frame_gen_queue_target_2">Smoothest (2 frames)</string>
+ <string name="frame_gen_flow_scale_auto">Match motion estimation to the game</string>
+ <string name="frame_gen_flow_scale_auto_description">Estimate motion at the resolution the game actually renders instead of the upscaled output. Costs nothing in accuracy, since upscaling adds no motion detail.</string>
+ <string name="frame_gen_flow_scale">Motion estimation resolution</string>
+ <string name="frame_gen_flow_scale_description">Resolution of the optical flow pass, as a fraction of the output. Lowering it is the cheapest way to reclaim performance.</string>
+ <string name="frame_gen_fp16">Half precision shaders</string>
+ <string name="frame_gen_fp16_description">Use the 16-bit shader variant. Falls back automatically if the driver or the file lacks it.</string>
+ <string name="frame_gen_dump_flow">Dump generated frame</string>
+ <string name="frame_gen_dump_flow_description">Write the optical flow mip levels and the interpolated frame to the lossless/debug folder once, for troubleshooting</string>
+ <string name="frame_gen_unsupported">Frame generation unavailable</string>
+ <string name="frame_gen_unsupported_description">This GPU driver does not support the Vulkan memory model, which the Lossless Scaling shaders require.</string>
+ <string name="lossless_scaling_setup_description">Optional. Provide your own Lossless.dll to enable frame generation later</string>
+ <string name="lossless_scaling_install">Install Lossless.dll</string>
+ <string name="lossless_scaling_install_description">Frame generation needs your own legal copy of Lossless.dll from Lossless Scaling</string>
+ <string name="lossless_scaling_replace_description">Select a different copy of Lossless.dll</string>
+ <string name="frame_generation_support">Frame generation</string>
+ <string name="frame_generation_supported">Supported</string>
+ <string name="frame_generation_unsupported">Unsupported (no Vulkan memory model)</string>
+ <string name="lossless_scaling">Lossless Scaling</string>
+ <string name="lossless_scaling_description">Provide your own copy of Lossless.dll to enable frame generation</string>
+ <string name="lossless_scaling_installed">Installed</string>
+ <string name="lossless_scaling_not_installed">Not installed</string>
+ <string name="lossless_scaling_replace">Replace</string>
+ <string name="lossless_scaling_remove">Remove</string>
+ <string name="lossless_scaling_remove_description">Delete the installed Lossless.dll and its prepared shaders</string>
+ <string name="lossless_scaling_remove_confirmation">Frame generation will stop working until you install Lossless.dll again. Your original file is not affected.</string>
+ <string name="lossless_scaling_missing">Lossless.dll not installed</string>
+ <string name="lossless_scaling_missing_description">Install it from Settings › Lossless Scaling to use frame generation.</string>
+ <string name="lossless_scaling_locked">Close the game first</string>
+ <string name="lossless_scaling_locked_description">Lossless.dll cannot be changed while a game is running.</string>
+ <string name="lossless_scaling_remove_unavailable">Nothing to remove</string>
+ <string name="lossless_scaling_remove_unavailable_description">Lossless.dll is not installed yet.</string>
+ <string name="lossless_scaling_installing">Preparing frame generation shaders…</string>
+ <string name="lossless_scaling_install_success">Lossless.dll installed successfully</string>
+ <string name="lossless_scaling_install_failed">Could not install Lossless.dll</string>
+ <string name="error_lossless_copy_failed">The selected file could not be copied.</string>
+ <string name="error_lossless_unreadable">The selected file could not be read.</string>
+ <string name="error_lossless_not_pe">The selected file is not a Windows library. Select Lossless.dll from your Lossless Scaling installation.</string>
+ <string name="error_lossless_missing_shaders">This copy of Lossless.dll does not contain the frame generation shaders. Update Lossless Scaling and try again.</string>
+ <string name="error_lossless_translation_failed">The frame generation shaders could not be translated. This version of Lossless Scaling is not supported yet.</string>
+ <string name="error_lossless_cache_failed">The translated shaders could not be written to storage. Check that there is free space available.</string>
<string name="advanced_settings">Advanced settings</string>
<string name="settings_description">Configure emulator settings</string>
<string name="search_recently_played">Recently played</string>