From 0f763b6de7f32c79dd0f313c87c0f4cfdc4e05e6 Mon Sep 17 00:00:00 2001 From: CamilleLaVey Date: Fri, 14 Aug 2026 19:08:25 -0400 Subject: [PATCH] [vulkan, android] Initial implementation of LSFG-VK --- CMakeLists.txt | 23 +- cpmfile.json | 6 + externals/CMakeLists.txt | 4 + .../java/org/yuzu/yuzu_emu/NativeLibrary.kt | 34 ++ .../features/settings/model/BooleanSetting.kt | 4 + .../features/settings/model/IntSetting.kt | 4 + .../features/settings/model/Settings.kt | 1 + .../settings/model/view/SettingsItem.kt | 103 ++++ .../settings/ui/SettingsDialogFragment.kt | 4 +- .../settings/ui/SettingsFragmentPresenter.kt | 39 +- .../settings/ui/SettingsSubscreenActivity.kt | 2 + .../fragments/GamePropertiesFragment.kt | 15 + .../fragments/HomeSettingsFragment.kt | 20 + .../fragments/LosslessManagerFragment.kt | 174 ++++++ .../yuzu/yuzu_emu/fragments/SetupFragment.kt | 45 ++ .../fragments/SystemInfoDialogFragment.kt | 14 +- .../yuzu_emu/utils/LosslessScalingHelper.kt | 77 +++ .../app/src/main/jni/android_config.cpp | 4 +- .../app/src/main/jni/android_settings.h | 2 +- .../src/main/jni/emu_window/emu_window.cpp | 19 +- .../app/src/main/jni/emu_window/emu_window.h | 1 + src/android/app/src/main/jni/native.cpp | 55 ++ .../app/src/main/res/drawable/ic_duck.xml | 19 + .../res/layout/fragment_lossless_manager.xml | 63 ++ .../settings_subscreen_navigation.xml | 6 + .../app/src/main/res/values/arrays.xml | 52 ++ .../app/src/main/res/values/strings.xml | 61 ++ src/common/fs/fs_paths.h | 5 +- src/common/fs/path_util.cpp | 1 + src/common/fs/path_util.h | 3 +- src/common/settings.cpp | 22 + src/common/settings.h | 84 ++- src/video_core/CMakeLists.txt | 27 + src/video_core/frame_gen/lossless_dll.cpp | 575 ++++++++++++++++++ src/video_core/frame_gen/lossless_dll.h | 69 +++ src/video_core/frame_gen/lsfg_translate.cpp | 129 ++++ src/video_core/frame_gen/lsfg_translate.h | 19 + .../renderer_vulkan/present/frame_gen.cpp | 361 +++++++++++ .../renderer_vulkan/present/frame_gen.h | 57 ++ .../present/frame_gen_pacer.cpp | 240 ++++++++ .../renderer_vulkan/present/frame_gen_pacer.h | 46 ++ .../renderer_vulkan/present/lsfg_alpha.cpp | 140 +++++ .../renderer_vulkan/present/lsfg_alpha.h | 62 ++ .../renderer_vulkan/present/lsfg_beta.cpp | 147 +++++ .../renderer_vulkan/present/lsfg_beta.h | 48 ++ .../renderer_vulkan/present/lsfg_chain.cpp | 103 ++++ .../renderer_vulkan/present/lsfg_chain.h | 87 +++ .../renderer_vulkan/present/lsfg_common.cpp | 339 +++++++++++ .../renderer_vulkan/present/lsfg_common.h | 227 +++++++ .../renderer_vulkan/present/lsfg_delta.cpp | 283 +++++++++ .../renderer_vulkan/present/lsfg_delta.h | 63 ++ .../renderer_vulkan/present/lsfg_gamma.cpp | 185 ++++++ .../renderer_vulkan/present/lsfg_gamma.h | 54 ++ .../renderer_vulkan/present/lsfg_generate.cpp | 130 ++++ .../renderer_vulkan/present/lsfg_generate.h | 54 ++ .../renderer_vulkan/present/lsfg_mipmaps.cpp | 81 +++ .../renderer_vulkan/present/lsfg_mipmaps.h | 45 ++ .../renderer_vulkan/present/lsfg_shaders.cpp | 37 ++ .../renderer_vulkan/present/lsfg_shaders.h | 30 + .../renderer_vulkan/present/util.cpp | 38 +- src/video_core/renderer_vulkan/present/util.h | 10 +- .../renderer_vulkan/renderer_vulkan.cpp | 33 +- .../renderer_vulkan/renderer_vulkan.h | 4 +- .../renderer_vulkan/vk_blit_screen.cpp | 11 + .../renderer_vulkan/vk_blit_screen.h | 2 + .../renderer_vulkan/vk_present_manager.cpp | 57 +- .../renderer_vulkan/vk_present_manager.h | 6 + src/video_core/renderer_vulkan/vk_scheduler.h | 5 +- .../renderer_vulkan/vk_swapchain.cpp | 3 + src/video_core/vulkan_common/vulkan_device.h | 8 +- src/yuzu/configuration/shared_widget.cpp | 14 + 71 files changed, 4756 insertions(+), 39 deletions(-) create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LosslessManagerFragment.kt create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/LosslessScalingHelper.kt create mode 100644 src/android/app/src/main/res/drawable/ic_duck.xml create mode 100644 src/android/app/src/main/res/layout/fragment_lossless_manager.xml create mode 100644 src/video_core/frame_gen/lossless_dll.cpp create mode 100644 src/video_core/frame_gen/lossless_dll.h create mode 100644 src/video_core/frame_gen/lsfg_translate.cpp create mode 100644 src/video_core/frame_gen/lsfg_translate.h create mode 100644 src/video_core/renderer_vulkan/present/frame_gen.cpp create mode 100644 src/video_core/renderer_vulkan/present/frame_gen.h create mode 100644 src/video_core/renderer_vulkan/present/frame_gen_pacer.cpp create mode 100644 src/video_core/renderer_vulkan/present/frame_gen_pacer.h create mode 100644 src/video_core/renderer_vulkan/present/lsfg_alpha.cpp create mode 100644 src/video_core/renderer_vulkan/present/lsfg_alpha.h create mode 100644 src/video_core/renderer_vulkan/present/lsfg_beta.cpp create mode 100644 src/video_core/renderer_vulkan/present/lsfg_beta.h create mode 100644 src/video_core/renderer_vulkan/present/lsfg_chain.cpp create mode 100644 src/video_core/renderer_vulkan/present/lsfg_chain.h create mode 100644 src/video_core/renderer_vulkan/present/lsfg_common.cpp create mode 100644 src/video_core/renderer_vulkan/present/lsfg_common.h create mode 100644 src/video_core/renderer_vulkan/present/lsfg_delta.cpp create mode 100644 src/video_core/renderer_vulkan/present/lsfg_delta.h create mode 100644 src/video_core/renderer_vulkan/present/lsfg_gamma.cpp create mode 100644 src/video_core/renderer_vulkan/present/lsfg_gamma.h create mode 100644 src/video_core/renderer_vulkan/present/lsfg_generate.cpp create mode 100644 src/video_core/renderer_vulkan/present/lsfg_generate.h create mode 100644 src/video_core/renderer_vulkan/present/lsfg_mipmaps.cpp create mode 100644 src/video_core/renderer_vulkan/present/lsfg_mipmaps.h create mode 100644 src/video_core/renderer_vulkan/present/lsfg_shaders.cpp create mode 100644 src/video_core/renderer_vulkan/present/lsfg_shaders.h diff --git a/CMakeLists.txt b/CMakeLists.txt index fa1fa37d2e..46f3d2dad4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -339,13 +339,26 @@ if (CXX_GCC OR CXX_CLANG) endif() elseif(ARCHITECTURE_arm64) # See https://gcc.gnu.org/onlinedocs/gcc/AArch64-Options.html - set(YUZU_BUILD_PRESET "custom" CACHE STRING "Build preset to use. One of: custom, generic, armv9, native") set(mtune generic) + set(armv8_2_target armv8.2-a+fp16+dotprod) - if (${YUZU_BUILD_PRESET} STREQUAL "generic") - set(march armv8-a) - elseif (${YUZU_BUILD_PRESET} STREQUAL "armv9") - set(march armv9-a) + if (ANDROID) + set(YUZU_BUILD_PRESET "custom" CACHE STRING "Build preset to use. One of: custom, armv8.2, armv9, native") + set(march ${armv8_2_target}) + + if (${YUZU_BUILD_PRESET} STREQUAL "armv9") + set(march armv9-a) + endif() + else() + set(YUZU_BUILD_PRESET "custom" CACHE STRING "Build preset to use. One of: custom, generic, armv8.2, armv9, native") + + if (${YUZU_BUILD_PRESET} STREQUAL "generic") + set(march armv8-a) + elseif (${YUZU_BUILD_PRESET} STREQUAL "armv8.2") + set(march ${armv8_2_target}) + elseif (${YUZU_BUILD_PRESET} STREQUAL "armv9") + set(march armv9-a) + endif() endif() endif() diff --git a/cpmfile.json b/cpmfile.json index cac6c357b0..7035cfc040 100644 --- a/cpmfile.json +++ b/cpmfile.json @@ -65,6 +65,12 @@ "repo": "eden-emulator/discord-rpc", "version": "0d8b2d6a37" }, + "dxbc": { + "bundled": true, + "hash": "196d26c07747d7aa2ced6fb1a5ae6f665e9b917de223a540038690b037c70a1eeca51c21d42aa5bec9fff7df0a6656a75ee8b4e2cb4008469f18a6812280f831", + "repo": "PancakeTAS/dxbc", + "version": "78ab59a8aaeb43cd1b0a5e91ba86722433a10b78" + }, "enet": { "find_args": "MODULE", "hash": "a0d2fa8c957704dd49e00a726284ac5ca034b50b00d2b20a94fa1bbfbb80841467834bfdc84aa0ed0d6aab894608fd6c86c3b94eee46343f0e6d9c22e391dbf9", diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index f190f68b9d..4b0763140e 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -92,6 +92,10 @@ AddDependentPackages(vulkan-headers vulkan-utility-libraries) # frozen AddJsonPackage(frozen) +# DXVK's DXBC compiler, used to translate the frame generation shaders +# out of a user-supplied Lossless.dll into SPIR-V +AddJsonPackage(dxbc) + # DiscordRPC if (USE_DISCORD_PRESENCE) if (ARCHITECTURE_arm64) 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 @@ -539,6 +539,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 * 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 @@ -607,6 +645,71 @@ abstract class SettingsItem( valuesId = R.array.rendererAntiAliasingValues ) ) + 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, 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) { + 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() { @@ -170,6 +171,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, @@ -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 { + 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..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 @@ -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 @@ -202,6 +203,24 @@ class SetupFragment : Fragment() { R.string.install_firmware_warning_help, ) ) + 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, @@ -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 = _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 pipeline_worker_count{linkage, 4, "pipeline_worker_count", + Settings::SwitchableSetting 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(std::clamp(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(std::min(speed_limit, 100)) / 100.0f); + nominal_rate * (static_cast(std::min(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..7173ccddd0 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" @@ -1090,6 +1092,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 +1195,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(GetVulkanMemoryModelSupport()); + } catch (...) { + return static_cast(false); + } +} + jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGpuModel(JNIEnv* env, jobject jobj) { const auto props = GetVulkanDeviceProperties(); if (props.deviceID == 0) { @@ -1390,6 +1428,23 @@ jint Java_org_yuzu_yuzu_1emu_NativeLibrary_installKeys(JNIEnv* env, jclass clazz return static_cast(FirmwareManager::InstallKeys(path, ext)); } +jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getLosslessDllPath(JNIEnv* env, jclass clazz) { + const auto path = VideoCore::FrameGen::GetLosslessDllPath(); + return Common::Android::ToJString(env, Common::FS::PathToUTF8String(path)); +} + +jint Java_org_yuzu_yuzu_1emu_NativeLibrary_validateLosslessDll(JNIEnv* env, jclass clazz) { + return static_cast(VideoCore::FrameGen::GetInstalledLosslessStatus()); +} + +jint Java_org_yuzu_yuzu_1emu_NativeLibrary_prepareLosslessDll(JNIEnv* env, jclass clazz) { + return static_cast(VideoCore::FrameGen::BuildShaderCache()); +} + +jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_removeLosslessDll(JNIEnv* env, jclass clazz) { + return static_cast(VideoCore::FrameGen::RemoveInstalledLosslessDll()); +} + 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 @@ + + + + + + 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" /> + + @string/resolution_four + + @string/frame_gen_multiplier_2x + @string/frame_gen_multiplier_3x + @string/frame_gen_multiplier_4x + + + + 2 + 3 + 4 + + + + @string/frame_gen_target_rate_off + @string/frame_gen_target_rate_60 + @string/frame_gen_target_rate_90 + @string/frame_gen_target_rate_120 + @string/frame_gen_target_rate_144 + @string/frame_gen_target_rate_165 + + + + 0 + 60 + 90 + 120 + 144 + 165 + + + + @string/frame_gen_queue_target_0 + @string/frame_gen_queue_target_1 + @string/frame_gen_queue_target_2 + + + + 0 + 1 + 2 + + @string/renderer_vsync_immediate @string/renderer_vsync_mailbox @@ -638,6 +680,16 @@ @string/error_keys_failed_init + + "" + @string/error_lossless_copy_failed + @string/error_lossless_unreadable + @string/error_lossless_not_pe + @string/error_lossless_missing_shaders + @string/error_lossless_translation_failed + @string/error_lossless_cache_failed + + Off 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 @@ GPU driver fetcher GPU driver manager Install alternative drivers for potentially better performance or accuracy + 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. + 2x + 3x + 4x + Target frame rate + 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. + Use a fixed multiplier + 60 FPS + 90 FPS + 120 FPS + 144 FPS + 165 FPS + Frame queue target + How many finished frames may wait ahead of the display. Larger queues absorb GPU spikes at the cost of input latency. + Lowest latency (Unbuffered) + Balanced (1 frame) + Smoothest (2 frames) + Match motion estimation to the game + Estimate motion at the resolution the game actually renders instead of the upscaled output. Costs nothing in accuracy, since upscaling adds no motion detail. + Motion estimation resolution + Resolution of the optical flow pass, as a fraction of the output. Lowering it is the cheapest way to reclaim performance. + Half precision shaders + Use the 16-bit shader variant. Falls back automatically if the driver or the file lacks it. + Dump generated frame + Write the optical flow mip levels and the interpolated frame to the lossless/debug folder once, for troubleshooting + Frame generation unavailable + This GPU driver does not support the Vulkan memory model, which the Lossless Scaling shaders require. + Optional. Provide your own Lossless.dll to enable frame generation later + Install Lossless.dll + Frame generation needs your own legal copy of Lossless.dll from Lossless Scaling + Select a different copy of Lossless.dll + Frame generation + Supported + Unsupported (no Vulkan memory model) + Lossless Scaling + Provide your own copy of Lossless.dll to enable frame generation + Installed + Not installed + Replace + 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 + The selected file could not be copied. + The selected file could not be read. + The selected file is not a Windows library. Select Lossless.dll from your Lossless Scaling installation. + This copy of Lossless.dll does not contain the frame generation shaders. Update Lossless Scaling and try again. + The frame generation shaders could not be translated. This version of Lossless Scaling is not supported yet. + The translated shaders could not be written to storage. Check that there is free space available. Advanced settings Configure emulator settings Recently played diff --git a/src/common/fs/fs_paths.h b/src/common/fs/fs_paths.h index 640a83c44b..a31e609bda 100644 --- a/src/common/fs/fs_paths.h +++ b/src/common/fs/fs_paths.h @@ -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 // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project @@ -20,6 +20,7 @@ #define KEYS_DIR "keys" #define LOAD_DIR "load" #define LOG_DIR "log" +#define LOSSLESS_DIR "lossless" #define NAND_DIR "nand" #define PLAY_TIME_DIR "play_time" #define SCREENSHOTS_DIR "screenshots" @@ -37,3 +38,5 @@ // yuzu-specific files #define LOG_FILE "eden_log.txt" +#define LOSSLESS_DLL_FILE "Lossless.dll" +#define LOSSLESS_CACHE_FILE "lsfg_spirv.cache" diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp index 4105855866..9adf210ab7 100644 --- a/src/common/fs/path_util.cpp +++ b/src/common/fs/path_util.cpp @@ -157,6 +157,7 @@ public: GenerateEdenPath(EdenPath::KeysDir, eden_path / KEYS_DIR); GenerateEdenPath(EdenPath::LoadDir, eden_path / LOAD_DIR); GenerateEdenPath(EdenPath::LogDir, eden_path / LOG_DIR); + GenerateEdenPath(EdenPath::LosslessDir, eden_path / LOSSLESS_DIR); GenerateEdenPath(EdenPath::NANDDir, eden_path / NAND_DIR); GenerateEdenPath(EdenPath::PlayTimeDir, eden_path / PLAY_TIME_DIR); GenerateEdenPath(EdenPath::SaveDir, eden_path / NAND_DIR); diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h index 9f597232a5..febed9c388 100644 --- a/src/common/fs/path_util.h +++ b/src/common/fs/path_util.h @@ -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 // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project @@ -23,6 +23,7 @@ enum class EdenPath { KeysDir, // Where key files are stored. LoadDir, // Where cheat/mod files are stored. LogDir, // Where log files are stored. + LosslessDir, // Where the user-supplied Lossless Scaling library is stored. NANDDir, // Where the emulated NAND is stored. PlayTimeDir, // Where play time data is stored. SaveDir, // Where save data is stored. diff --git a/src/common/settings.cpp b/src/common/settings.cpp index db318613dd..6cc976fe51 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -380,6 +380,28 @@ void UpdateRescalingInfo() { TranslateResolutionInfo(setup, info); } +u32 FrameGenMultiplier() { + return std::clamp(values.frame_gen_multiplier.GetValue(), MIN_FRAME_GEN_MULTIPLIER, + MAX_FRAME_GEN_MULTIPLIER); +} + +size_t FrameGenGenerations() { + if (!values.frame_gen.GetValue()) { + return 0; + } + return FrameGenMultiplier() - 1; +} + +size_t FrameGenMaxGenerations() { + if (!values.frame_gen.GetValue()) { + return 0; + } + if (values.frame_gen_target_rate.GetValue() != 0) { + return MAX_FRAME_GEN_MULTIPLIER - 1; + } + return FrameGenMultiplier() - 1; +} + void RestoreGlobalState(bool is_powered_on) { // If a game is running, DO NOT restore the global settings state if (is_powered_on) { diff --git a/src/common/settings.h b/src/common/settings.h index 0f35045622..7ac4b11173 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -352,7 +352,7 @@ struct Values { true}; SwitchableSetting scaling_filter{linkage, - ScalingFilter::Bilinear, + ScalingFilter::NearestNeighbor, "scaling_filter", Category::Renderer, Specialization::Default, @@ -388,6 +388,69 @@ struct Values { true, true}; + SwitchableSetting frame_gen{linkage, false, "frame_gen", Category::Renderer, + Specialization::Default, true, false}; + + SwitchableSetting frame_gen_multiplier{linkage, + 2, + 2, + 4, + "frame_gen_multiplier", + Category::Renderer, + Specialization::Countable, + true, + false, + &frame_gen}; + + SwitchableSetting frame_gen_target_rate{linkage, + 0, + 0, + 240, + "frame_gen_target_rate", + Category::Renderer, + Specialization::Countable, + true, + true, + &frame_gen}; + + SwitchableSetting frame_gen_flow_scale_auto{linkage, + true, + "frame_gen_flow_scale_auto", + Category::Renderer, + Specialization::Default, + true, + false, + &frame_gen}; + + SwitchableSetting frame_gen_flow_scale{linkage, + 75, + 25, + 100, + "frame_gen_flow_scale", + Category::Renderer, + Specialization::Countable | + Specialization::Percentage, + true, + true, + &frame_gen}; + + SwitchableSetting frame_gen_queue_target{linkage, + 1, + 0, + 2, + "frame_gen_queue_target", + Category::Renderer, + Specialization::Countable, + true, + false, + &frame_gen}; + + SwitchableSetting frame_gen_fp16{linkage, true, "frame_gen_fp16", Category::Renderer, + Specialization::Default, true, false, &frame_gen}; + + SwitchableSetting frame_gen_dump_flow{linkage, false, "frame_gen_dump_flow", + Category::Renderer}; + SwitchableSetting use_asynchronous_gpu_emulation{linkage, #ifdef __ANDROID__ false, @@ -569,13 +632,8 @@ struct Values { SwitchableSetting emulate_bgr565{linkage, false, "emulate_bgr565", Category::RendererHacks}; - SwitchableSetting rescale_hack{linkage, -#ifdef __ANDROID__ - true, -#else - false, -#endif - "rescale_hack", Category::RendererHacks}; + SwitchableSetting rescale_hack{linkage, false, "rescale_hack", + Category::RendererHacks}; SwitchableSetting enable_gpu_buffer_readback{linkage, false, "enable_gpu_buffer_readback", @@ -875,10 +933,20 @@ struct Values { // Per-game overrides bool use_squashed_iterated_blend; + }; extern Values values; +constexpr u32 MIN_FRAME_GEN_MULTIPLIER = 2; +constexpr u32 MAX_FRAME_GEN_MULTIPLIER = 4; + +[[nodiscard]] u32 FrameGenMultiplier(); + +[[nodiscard]] size_t FrameGenGenerations(); + +[[nodiscard]] size_t FrameGenMaxGenerations(); + bool getDebugKnobAt(u8 i); void UpdateGPUAccuracy(); diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 6720195c0e..10afffb743 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -59,6 +59,10 @@ add_library(video_core STATIC engines/maxwell_dma.h engines/puller.cpp engines/puller.h + frame_gen/lossless_dll.cpp + frame_gen/lossless_dll.h + frame_gen/lsfg_translate.cpp + frame_gen/lsfg_translate.h framebuffer_config.cpp framebuffer_config.h fsr.cpp @@ -121,6 +125,28 @@ add_library(video_core STATIC renderer_vulkan/present/anti_alias_pass.h renderer_vulkan/present/filters.cpp renderer_vulkan/present/filters.h + renderer_vulkan/present/frame_gen.cpp + renderer_vulkan/present/frame_gen.h + renderer_vulkan/present/frame_gen_pacer.cpp + renderer_vulkan/present/frame_gen_pacer.h + renderer_vulkan/present/lsfg_alpha.cpp + renderer_vulkan/present/lsfg_alpha.h + renderer_vulkan/present/lsfg_beta.cpp + renderer_vulkan/present/lsfg_beta.h + renderer_vulkan/present/lsfg_chain.cpp + renderer_vulkan/present/lsfg_chain.h + renderer_vulkan/present/lsfg_common.cpp + renderer_vulkan/present/lsfg_common.h + renderer_vulkan/present/lsfg_delta.cpp + renderer_vulkan/present/lsfg_delta.h + renderer_vulkan/present/lsfg_gamma.cpp + renderer_vulkan/present/lsfg_gamma.h + renderer_vulkan/present/lsfg_generate.cpp + renderer_vulkan/present/lsfg_generate.h + renderer_vulkan/present/lsfg_mipmaps.cpp + renderer_vulkan/present/lsfg_mipmaps.h + renderer_vulkan/present/lsfg_shaders.cpp + renderer_vulkan/present/lsfg_shaders.h renderer_vulkan/present/fsr.cpp renderer_vulkan/present/fsr.h renderer_vulkan/present/fxaa.cpp @@ -345,6 +371,7 @@ add_dependencies(video_core host_shaders) target_include_directories(video_core PRIVATE ${HOST_SHADERS_INCLUDE}) target_link_libraries(video_core PRIVATE sirit::sirit) +target_link_libraries(video_core PRIVATE dxbc) # Header-only stuff needed by all dependent targets target_link_libraries(video_core PUBLIC Vulkan::Headers Vulkan::UtilityHeaders GPUOpen::VulkanMemoryAllocator) diff --git a/src/video_core/frame_gen/lossless_dll.cpp b/src/video_core/frame_gen/lossless_dll.cpp new file mode 100644 index 0000000000..03228b8669 --- /dev/null +++ b/src/video_core/frame_gen/lossless_dll.cpp @@ -0,0 +1,575 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include + +#include "common/cityhash.h" +#include "common/fs/file.h" +#include "common/fs/fs.h" +#include "common/fs/fs_paths.h" +#include "common/fs/path_util.h" +#include "video_core/frame_gen/lossless_dll.h" +#include "video_core/frame_gen/lsfg_translate.h" + +namespace VideoCore::FrameGen { + +namespace { + +constexpr u16 DOS_MAGIC = 0x5A4D; +constexpr u32 PE_SIGNATURE = 0x00004550; +constexpr u16 PE32_MAGIC = 0x010B; +constexpr u16 PE32_PLUS_MAGIC = 0x020B; + +constexpr size_t DOS_LFANEW_OFFSET = 0x3C; +constexpr size_t COFF_HEADER_SIZE = 20; +constexpr size_t OPTIONAL_HEADER_SIZE_OFFSET = 16; +constexpr size_t SECTION_HEADER_SIZE = 40; +constexpr size_t DATA_DIRECTORY_ENTRY_SIZE = 8; +constexpr size_t DATA_DIRECTORY_OFFSET_PE32 = 96; +constexpr size_t DATA_DIRECTORY_OFFSET_PE32_PLUS = 112; +constexpr size_t RESOURCE_DATA_DIRECTORY_INDEX = 2; + +constexpr size_t RESOURCE_DIRECTORY_SIZE = 16; +constexpr size_t RESOURCE_NAMED_COUNT_OFFSET = 12; +constexpr size_t RESOURCE_ID_COUNT_OFFSET = 14; +constexpr size_t RESOURCE_ENTRY_SIZE = 8; +constexpr u32 RESOURCE_SUBDIRECTORY_FLAG = 0x80000000; +constexpr u32 RESOURCE_TYPE_RCDATA = 10; + +constexpr u32 MIPMAPS_SHADER_ID = 255; +constexpr u32 GENERATE_SHADER_ID = 256; +constexpr u32 PERFORMANCE_SHADER_ID_FIRST = 280; +constexpr u32 PERFORMANCE_SHADER_ID_LAST = 302; + +constexpr u32 CACHE_MAGIC = 0x4746534C; +constexpr u32 CACHE_VERSION = 2; + +struct CacheHeader { + u32 magic; + u32 version; + u64 source_size; + u64 source_hash; + u32 module_count; + u32 variant; +}; + +struct Section { + u32 virtual_address; + u32 virtual_size; + u32 raw_address; + u32 raw_size; +}; + +struct ResourceEntry { + u32 id; + u32 offset; + bool is_directory; + bool is_named; +}; + +class ImageReader { +public: + explicit ImageReader(std::span image_) : image{image_} {} + + template + [[nodiscard]] bool Read(size_t offset, T& out_value) const { + if (offset > image.size() || image.size() - offset < sizeof(T)) { + return false; + } + std::memcpy(&out_value, image.data() + offset, sizeof(T)); + return true; + } + + [[nodiscard]] bool Slice(size_t offset, size_t size, std::span& out_slice) const { + if (offset > image.size() || image.size() - offset < size) { + return false; + } + out_slice = image.subspan(offset, size); + return true; + } + +private: + std::span image; +}; + +[[nodiscard]] std::optional FindPeHeader(const ImageReader& reader) { + u16 dos_magic{}; + if (!reader.Read(0, dos_magic) || dos_magic != DOS_MAGIC) { + return std::nullopt; + } + + u32 pe_offset{}; + if (!reader.Read(DOS_LFANEW_OFFSET, pe_offset)) { + return std::nullopt; + } + + u32 pe_signature{}; + if (!reader.Read(pe_offset, pe_signature) || pe_signature != PE_SIGNATURE) { + return std::nullopt; + } + + return static_cast(pe_offset); +} + +[[nodiscard]] std::optional FindDataDirectory(const ImageReader& reader, + size_t optional_header_offset) { + u16 optional_magic{}; + if (!reader.Read(optional_header_offset, optional_magic)) { + return std::nullopt; + } + + switch (optional_magic) { + case PE32_MAGIC: + return optional_header_offset + DATA_DIRECTORY_OFFSET_PE32; + case PE32_PLUS_MAGIC: + return optional_header_offset + DATA_DIRECTORY_OFFSET_PE32_PLUS; + default: + return std::nullopt; + } +} + +[[nodiscard]] bool ReadSections(const ImageReader& reader, size_t pe_offset, + std::vector
& out_sections) { + u16 section_count{}; + u16 optional_header_size{}; + if (!reader.Read(pe_offset + 4 + 2, section_count) || + !reader.Read(pe_offset + 4 + OPTIONAL_HEADER_SIZE_OFFSET, optional_header_size)) { + return false; + } + + const size_t table_offset = pe_offset + 4 + COFF_HEADER_SIZE + optional_header_size; + out_sections.reserve(section_count); + for (size_t i = 0; i < section_count; ++i) { + const size_t offset = table_offset + i * SECTION_HEADER_SIZE; + Section section{}; + if (!reader.Read(offset + 8, section.virtual_size) || + !reader.Read(offset + 12, section.virtual_address) || + !reader.Read(offset + 16, section.raw_size) || + !reader.Read(offset + 20, section.raw_address)) { + return false; + } + out_sections.push_back(section); + } + return true; +} + +[[nodiscard]] std::optional RvaToFileOffset(std::span sections, u32 rva) { + for (const Section& section : sections) { + const u32 span = std::max(section.virtual_size, section.raw_size); + if (span == 0 || rva < section.virtual_address) { + continue; + } + const u32 relative = rva - section.virtual_address; + if (relative < span) { + return static_cast(section.raw_address) + relative; + } + } + return std::nullopt; +} + +[[nodiscard]] bool ReadResourceEntries(const ImageReader& reader, size_t directory_offset, + std::vector& out_entries) { + u16 named_count{}; + u16 id_count{}; + if (!reader.Read(directory_offset + RESOURCE_NAMED_COUNT_OFFSET, named_count) || + !reader.Read(directory_offset + RESOURCE_ID_COUNT_OFFSET, id_count)) { + return false; + } + + const size_t total = size_t{named_count} + size_t{id_count}; + out_entries.clear(); + out_entries.reserve(total); + for (size_t i = 0; i < total; ++i) { + const size_t offset = directory_offset + RESOURCE_DIRECTORY_SIZE + i * RESOURCE_ENTRY_SIZE; + u32 name{}; + u32 data{}; + if (!reader.Read(offset, name) || !reader.Read(offset + 4, data)) { + return false; + } + out_entries.push_back(ResourceEntry{ + .id = name & ~RESOURCE_SUBDIRECTORY_FLAG, + .offset = data & ~RESOURCE_SUBDIRECTORY_FLAG, + .is_directory = (data & RESOURCE_SUBDIRECTORY_FLAG) != 0, + .is_named = (name & RESOURCE_SUBDIRECTORY_FLAG) != 0, + }); + } + return true; +} + +[[nodiscard]] bool ReadResourceLeaf(const ImageReader& reader, std::span sections, + size_t leaf_offset, std::span& out_data) { + u32 data_rva{}; + u32 data_size{}; + if (!reader.Read(leaf_offset, data_rva) || !reader.Read(leaf_offset + 4, data_size) || + data_size == 0) { + return false; + } + + const std::optional data_offset = RvaToFileOffset(sections, data_rva); + if (!data_offset) { + return false; + } + return reader.Slice(*data_offset, data_size, out_data); +} + +using ResourceSpans = std::map>; + +[[nodiscard]] bool CollectRcData(const ImageReader& reader, std::span sections, + size_t resource_base, ResourceSpans& out_resources) { + std::vector type_entries; + if (!ReadResourceEntries(reader, resource_base, type_entries)) { + return false; + } + + for (const ResourceEntry& type_entry : type_entries) { + if (type_entry.is_named || type_entry.id != RESOURCE_TYPE_RCDATA || + !type_entry.is_directory) { + continue; + } + + std::vector name_entries; + if (!ReadResourceEntries(reader, resource_base + type_entry.offset, name_entries)) { + return false; + } + + for (const ResourceEntry& name_entry : name_entries) { + if (name_entry.is_named || !name_entry.is_directory) { + continue; + } + + std::vector language_entries; + if (!ReadResourceEntries(reader, resource_base + name_entry.offset, language_entries)) { + return false; + } + + for (const ResourceEntry& language_entry : language_entries) { + if (language_entry.is_directory) { + continue; + } + std::span data; + if (!ReadResourceLeaf(reader, sections, resource_base + language_entry.offset, + data)) { + continue; + } + out_resources.insert_or_assign(name_entry.id, data); + break; + } + } + } + + return true; +} + +[[nodiscard]] std::vector PerformanceShaderIds() { + std::vector ids{MIPMAPS_SHADER_ID, GENERATE_SHADER_ID}; + for (u32 id = PERFORMANCE_SHADER_ID_FIRST; id <= PERFORMANCE_SHADER_ID_LAST; ++id) { + ids.push_back(id); + } + return ids; +} + +template +[[nodiscard]] bool HasPerformanceShaders(const Map& resources) { + const std::vector ids = PerformanceShaderIds(); + return std::ranges::all_of(ids, [&](u32 id) { return resources.contains(id); }); +} + +[[nodiscard]] u32 VariantOffset(ShaderVariant variant) { + switch (variant) { + case ShaderVariant::NativeFp16: + return PerformanceShader::NATIVE_FP16_OFFSET; + case ShaderVariant::NativeFp32: + return PerformanceShader::NATIVE_FP32_OFFSET; + default: + return 0; + } +} + +template +[[nodiscard]] bool HasNativeVariant(const Map& resources, ShaderVariant variant) { + const u32 offset = VariantOffset(variant); + return std::ranges::all_of(PerformanceShaderIds(), [&](u32 id) { + const auto hit = resources.find(id + offset); + return hit != resources.end() && IsSpirvModule(hit->second); + }); +} + +[[nodiscard]] ShaderVariant SelectVariant(const ResourceSpans& resources, bool prefer_fp16) { + if (prefer_fp16 && HasNativeVariant(resources, ShaderVariant::NativeFp16)) { + return ShaderVariant::NativeFp16; + } + if (HasNativeVariant(resources, ShaderVariant::NativeFp32)) { + return ShaderVariant::NativeFp32; + } + return ShaderVariant::TranslatedDxbc; +} + +[[nodiscard]] LosslessStatus TranslateAll(const ResourceSpans& resources, + ShaderModules& out_modules, + ShaderVariant variant) { + const u32 offset = VariantOffset(variant); + out_modules.clear(); + for (const u32 id : PerformanceShaderIds()) { + const auto hit = resources.find(id + offset); + if (hit == resources.end()) { + return LosslessStatus::MissingShaders; + } + if (variant != ShaderVariant::TranslatedDxbc) { + std::vector adopted = AdoptSpirvModule(hit->second); + if (adopted.empty()) { + return LosslessStatus::TranslationFailed; + } + out_modules.emplace(id, std::move(adopted)); + continue; + } + + std::vector words = TranslateComputeShader(hit->second); + if (words.empty()) { + return LosslessStatus::TranslationFailed; + } + out_modules.emplace(id, std::move(words)); + } + return LosslessStatus::Ok; +} + +[[nodiscard]] bool WriteShaderCache(const std::filesystem::path& path, const CacheHeader& header, + const ShaderModules& modules) { + Common::FS::IOFile file{path, Common::FS::FileAccessMode::Write, + Common::FS::FileType::BinaryFile}; + if (!file.IsOpen() || file.Write(header) != 1) { + return false; + } + + for (const auto& [id, words] : modules) { + const u32 word_count = static_cast(words.size()); + if (file.Write(id) != 1 || file.Write(word_count) != 1 || + file.Write(words) != words.size()) { + return false; + } + } + return file.Flush(); +} + +[[nodiscard]] bool ReadShaderCache(const std::filesystem::path& path, u64 source_size, + u64 source_hash, u32 variant, ShaderModules& out_modules) { + if (!Common::FS::Exists(path)) { + return false; + } + + Common::FS::IOFile file{path, Common::FS::FileAccessMode::Read, + Common::FS::FileType::BinaryFile}; + CacheHeader header{}; + if (!file.IsOpen() || file.Read(header) != 1) { + return false; + } + if (header.magic != CACHE_MAGIC || header.version != CACHE_VERSION || + header.source_size != source_size || header.source_hash != source_hash || + header.variant != variant) { + return false; + } + + out_modules.clear(); + for (u32 i = 0; i < header.module_count; ++i) { + u32 id{}; + u32 word_count{}; + if (file.Read(id) != 1 || file.Read(word_count) != 1 || word_count == 0) { + return false; + } + + std::vector words(word_count); + if (file.Read(words) != words.size()) { + return false; + } + out_modules.emplace(id, std::move(words)); + } + + return HasPerformanceShaders(out_modules); +} + +[[nodiscard]] LosslessStatus ReadImageFile(const std::filesystem::path& path, + std::vector& out_image) { + if (!Common::FS::Exists(path)) { + return LosslessStatus::NotInstalled; + } + + Common::FS::IOFile file{path, Common::FS::FileAccessMode::Read, + Common::FS::FileType::BinaryFile}; + if (!file.IsOpen()) { + return LosslessStatus::UnreadableFile; + } + + out_image.resize(static_cast(file.GetSize())); + if (out_image.empty() || file.Read(out_image) != out_image.size()) { + return LosslessStatus::UnreadableFile; + } + return LosslessStatus::Ok; +} + +[[nodiscard]] LosslessStatus ParseShaderSpans(std::span image, + ResourceSpans& out_resources) { + const ImageReader reader{image}; + const std::optional pe_offset = FindPeHeader(reader); + if (!pe_offset) { + return LosslessStatus::NotPortableExecutable; + } + + const std::optional data_directory = + FindDataDirectory(reader, *pe_offset + 4 + COFF_HEADER_SIZE); + if (!data_directory) { + return LosslessStatus::NotPortableExecutable; + } + + std::vector
sections; + if (!ReadSections(reader, *pe_offset, sections)) { + return LosslessStatus::NotPortableExecutable; + } + + u32 resource_rva{}; + if (!reader.Read(*data_directory + RESOURCE_DATA_DIRECTORY_INDEX * DATA_DIRECTORY_ENTRY_SIZE, + resource_rva) || + resource_rva == 0) { + return LosslessStatus::MissingShaders; + } + + const std::optional resource_base = RvaToFileOffset(sections, resource_rva); + if (!resource_base) { + return LosslessStatus::NotPortableExecutable; + } + + out_resources.clear(); + if (!CollectRcData(reader, sections, *resource_base, out_resources)) { + return LosslessStatus::MissingShaders; + } + + return HasPerformanceShaders(out_resources) ? LosslessStatus::Ok + : LosslessStatus::MissingShaders; +} + +} // Anonymous namespace + +std::filesystem::path GetLosslessDllPath() { + return Common::FS::GetEdenPath(Common::FS::EdenPath::LosslessDir) / LOSSLESS_DLL_FILE; +} + +std::filesystem::path GetShaderCachePath() { + return Common::FS::GetEdenPath(Common::FS::EdenPath::LosslessDir) / LOSSLESS_CACHE_FILE; +} + +LosslessStatus ReadShaderResources(const std::filesystem::path& path, + ShaderResources& out_resources) { + std::vector image; + const LosslessStatus read_status = ReadImageFile(path, image); + if (read_status != LosslessStatus::Ok) { + return read_status; + } + + ResourceSpans spans; + const LosslessStatus parse_status = ParseShaderSpans(image, spans); + if (parse_status != LosslessStatus::Ok) { + return parse_status; + } + + out_resources.clear(); + for (const auto& [id, data] : spans) { + out_resources.emplace(id, std::vector{data.begin(), data.end()}); + } + return LosslessStatus::Ok; +} + +LosslessStatus ValidateLosslessDll(const std::filesystem::path& path) { + std::vector image; + const LosslessStatus read_status = ReadImageFile(path, image); + if (read_status != LosslessStatus::Ok) { + return read_status; + } + + ResourceSpans spans; + return ParseShaderSpans(image, spans); +} + +LosslessStatus GetInstalledLosslessStatus() { + return ValidateLosslessDll(GetLosslessDllPath()); +} + +ShaderVariant GetAvailableVariant(bool prefer_fp16) { + std::vector image; + if (ReadImageFile(GetLosslessDllPath(), image) != LosslessStatus::Ok) { + return ShaderVariant::TranslatedDxbc; + } + + ResourceSpans spans; + if (ParseShaderSpans(image, spans) != LosslessStatus::Ok) { + return ShaderVariant::TranslatedDxbc; + } + + return SelectVariant(spans, prefer_fp16); +} + +LosslessStatus LoadShaderModules(ShaderModules& out_modules, bool prefer_fp16) { + std::vector image; + const LosslessStatus read_status = ReadImageFile(GetLosslessDllPath(), image); + if (read_status != LosslessStatus::Ok) { + return read_status; + } + + const u64 source_size = image.size(); + const u64 source_hash = + Common::CityHash64(reinterpret_cast(image.data()), image.size()); + const std::filesystem::path cache_path = GetShaderCachePath(); + + ResourceSpans spans; + const LosslessStatus parse_status = ParseShaderSpans(image, spans); + if (parse_status != LosslessStatus::Ok) { + return parse_status; + } + + const ShaderVariant variant = SelectVariant(spans, prefer_fp16); + + if (ReadShaderCache(cache_path, source_size, source_hash, static_cast(variant), + out_modules)) { + return LosslessStatus::Ok; + } + + const LosslessStatus translate_status = TranslateAll(spans, out_modules, variant); + if (translate_status != LosslessStatus::Ok) { + return translate_status; + } + + const CacheHeader header{ + .magic = CACHE_MAGIC, + .version = CACHE_VERSION, + .source_size = source_size, + .source_hash = source_hash, + .module_count = static_cast(out_modules.size()), + .variant = static_cast(variant), + }; + if (!WriteShaderCache(cache_path, header, out_modules)) { + void(Common::FS::RemoveFile(cache_path)); + return LosslessStatus::CacheUnusable; + } + + return LosslessStatus::Ok; +} + +LosslessStatus BuildShaderCache() { + ShaderModules modules; + return LoadShaderModules(modules); +} + +bool RemoveInstalledLosslessDll() { + const std::filesystem::path cache_path = GetShaderCachePath(); + if (Common::FS::Exists(cache_path)) { + void(Common::FS::RemoveFile(cache_path)); + } + + const std::filesystem::path path = GetLosslessDllPath(); + if (!Common::FS::Exists(path)) { + return true; + } + return Common::FS::RemoveFile(path); +} + +} // namespace VideoCore::FrameGen diff --git a/src/video_core/frame_gen/lossless_dll.h b/src/video_core/frame_gen/lossless_dll.h new file mode 100644 index 0000000000..0246935f4d --- /dev/null +++ b/src/video_core/frame_gen/lossless_dll.h @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "common/common_types.h" + +namespace VideoCore::FrameGen { + +enum class LosslessStatus : u32 { + Ok, + NotInstalled, + UnreadableFile, + NotPortableExecutable, + MissingShaders, + TranslationFailed, + CacheUnusable, +}; + +using ShaderResources = std::map>; +using ShaderModules = std::map>; + +enum class ShaderVariant : u32 { + TranslatedDxbc, + NativeFp32, + NativeFp16, +}; + +namespace PerformanceShader { +constexpr u32 MIPMAPS = 255; +constexpr u32 GENERATE = 256; +constexpr std::array ALPHA{290, 291, 292, 293}; +constexpr std::array BETA{298, 299, 300, 301, 302}; +constexpr std::array GAMMA{280, 282, 283, 284, 285}; +constexpr std::array DELTA{280, 286, 287, 288, 289, 281, 294, 295, 296, 297}; + +constexpr u32 NATIVE_FP16_OFFSET = 49; +constexpr u32 NATIVE_FP32_OFFSET = 98; +} // namespace PerformanceShader + +[[nodiscard]] std::filesystem::path GetLosslessDllPath(); + +[[nodiscard]] std::filesystem::path GetShaderCachePath(); + +[[nodiscard]] LosslessStatus ReadShaderResources(const std::filesystem::path& path, + ShaderResources& out_resources); + +[[nodiscard]] LosslessStatus ValidateLosslessDll(const std::filesystem::path& path); + +[[nodiscard]] LosslessStatus GetInstalledLosslessStatus(); + +[[nodiscard]] LosslessStatus BuildShaderCache(); + +[[nodiscard]] ShaderVariant GetAvailableVariant(bool prefer_fp16); + +[[nodiscard]] LosslessStatus LoadShaderModules(ShaderModules& out_modules, + bool prefer_fp16 = false); + +bool RemoveInstalledLosslessDll(); + +} // namespace VideoCore::FrameGen diff --git a/src/video_core/frame_gen/lsfg_translate.cpp b/src/video_core/frame_gen/lsfg_translate.cpp new file mode 100644 index 0000000000..758711ed5d --- /dev/null +++ b/src/video_core/frame_gen/lsfg_translate.cpp @@ -0,0 +1,129 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "video_core/frame_gen/lsfg_translate.h" + +namespace VideoCore::FrameGen { + +namespace { + +constexpr u32 DECORATION_LITERAL_WORD = 3; +constexpr size_t SPIRV_HEADER_WORDS = 5; + +void RenumberBindings(dxvk::SpirvCodeBuffer& code) { + std::vector literal_offsets; + for (const auto instruction : code) { + if (instruction.opCode() == spv::OpFunction) { + break; + } + if (instruction.opCode() == spv::OpDecorate && + instruction.arg(2) == spv::DecorationBinding) { + literal_offsets.push_back(instruction.offset() + DECORATION_LITERAL_WORD); + } + } + + for (size_t i = 0; i < literal_offsets.size(); ++i) { + code.data()[literal_offsets[i]] = static_cast(i); + } +} + +void RenumberBindingsInOrder(std::vector& words) { + struct Slot { + u32 set; + u32 binding; + size_t literal_offset; + }; + + std::map sets; + std::vector slots; + + size_t offset = SPIRV_HEADER_WORDS; + while (offset + 1 <= words.size()) { + const u32 length = words[offset] >> spv::WordCountShift; + const u32 opcode = words[offset] & spv::OpCodeMask; + if (length == 0 || offset + length > words.size()) { + return; + } + if (opcode == spv::OpFunction) { + break; + } + if (opcode == spv::OpDecorate && length >= 4) { + if (words[offset + 2] == spv::DecorationDescriptorSet) { + sets[words[offset + 1]] = words[offset + 3]; + } else if (words[offset + 2] == spv::DecorationBinding) { + slots.push_back(Slot{0, words[offset + 3], offset + DECORATION_LITERAL_WORD}); + } + } + offset += length; + } + + for (Slot& slot : slots) { + const auto hit = sets.find(words[slot.literal_offset - 2]); + slot.set = hit == sets.end() ? 0 : hit->second; + } + + std::ranges::stable_sort(slots, [](const Slot& lhs, const Slot& rhs) { + return std::tie(lhs.set, lhs.binding) < std::tie(rhs.set, rhs.binding); + }); + + for (size_t i = 0; i < slots.size(); ++i) { + words[slots[i].literal_offset] = static_cast(i); + } +} + +} // Anonymous namespace + +bool IsSpirvModule(std::span blob) { + if (blob.size() < SPIRV_HEADER_WORDS * sizeof(u32) || blob.size() % sizeof(u32) != 0) { + return false; + } + u32 magic{}; + std::memcpy(&magic, blob.data(), sizeof(magic)); + return magic == spv::MagicNumber; +} + +std::vector AdoptSpirvModule(std::span blob) { + if (!IsSpirvModule(blob)) { + return {}; + } + + std::vector words(blob.size() / sizeof(u32)); + std::memcpy(words.data(), blob.data(), blob.size()); + + RenumberBindingsInOrder(words); + return words; +} + +std::vector TranslateComputeShader(std::span dxbc) { + if (dxbc.empty()) { + return {}; + } + + try { + dxvk::DxbcReader reader{reinterpret_cast(dxbc.data()), dxbc.size()}; + dxvk::DxbcModule module{reader}; + + const dxvk::DxbcModuleInfo module_info{}; + dxvk::SpirvCodeBuffer code = module.compile(module_info, "CS"); + if (code.dwords() == 0) { + return {}; + } + + RenumberBindings(code); + return std::vector{code.data(), code.data() + code.dwords()}; + } catch (...) { + return {}; + } +} + +} // namespace VideoCore::FrameGen diff --git a/src/video_core/frame_gen/lsfg_translate.h b/src/video_core/frame_gen/lsfg_translate.h new file mode 100644 index 0000000000..291db1c4f5 --- /dev/null +++ b/src/video_core/frame_gen/lsfg_translate.h @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#include "common/common_types.h" + +namespace VideoCore::FrameGen { + +[[nodiscard]] bool IsSpirvModule(std::span blob); + +[[nodiscard]] std::vector AdoptSpirvModule(std::span blob); + +[[nodiscard]] std::vector TranslateComputeShader(std::span dxbc); + +} // namespace VideoCore::FrameGen diff --git a/src/video_core/renderer_vulkan/present/frame_gen.cpp b/src/video_core/renderer_vulkan/present/frame_gen.cpp new file mode 100644 index 0000000000..b0d16819e5 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/frame_gen.cpp @@ -0,0 +1,361 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include + +#include "common/fs/file.h" +#include "common/fs/fs.h" +#include "common/fs/path_util.h" +#include "common/settings.h" +#include "video_core/renderer_vulkan/present/frame_gen.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/renderer_vulkan/vk_present_manager.h" +#include "video_core/renderer_vulkan/vk_scheduler.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr size_t COLOR_CHANNELS = 4; +constexpr u64 LSFG_REQUIRED_FRAMES = 2; +constexpr u32 LSFG_RECURRENCE_FRAMES = 2; + +[[nodiscard]] f32 ManualFlowScale() { + return static_cast(Settings::values.frame_gen_flow_scale.GetValue()) / 100.0f; +} + +[[nodiscard]] f32 ConfiguredFlowScale(VkExtent2D guest_extent, VkExtent2D presented_extent) { + if (!Settings::values.frame_gen_flow_scale_auto.GetValue()) { + return ManualFlowScale(); + } + if (guest_extent.width == 0 || presented_extent.width == 0) { + return 1.0f; + } + + const f32 rendered_width = static_cast(guest_extent.width) * + Settings::values.resolution_info.up_factor; + const f32 ratio = rendered_width / static_cast(presented_extent.width); + + constexpr f32 FLOW_SCALE_STEPS = 20.0f; + const f32 stepped = std::ceil(ratio * FLOW_SCALE_STEPS) / FLOW_SCALE_STEPS; + return std::clamp(stepped, 0.25f, 1.0f); +} + +bool IsBlueFirst(VkFormat format) { + return format == VK_FORMAT_B8G8R8A8_UNORM || format == VK_FORMAT_B8G8R8A8_SRGB; +} + +VkDeviceSize BytesPerTexel(VkFormat format) { + switch (format) { + case VK_FORMAT_R8_UNORM: + return 1; + case VK_FORMAT_R16G16B16A16_SFLOAT: + return 8; + default: + return COLOR_CHANNELS; + } +} + +void WritePortablePixmap(const std::filesystem::path& path, const std::string& magic, + VkExtent2D extent, std::span pixels) { + Common::FS::IOFile file{path, Common::FS::FileAccessMode::Write, + Common::FS::FileType::BinaryFile}; + if (!file.IsOpen()) { + return; + } + + const std::string header = magic + "\n" + std::to_string(extent.width) + " " + + std::to_string(extent.height) + "\n255\n"; + if (file.Write(header) != header.size()) { + return; + } + + void(file.Write(pixels)); + void(file.Flush()); +} + +void WriteGrayscalePgm(const std::filesystem::path& path, VkExtent2D extent, + std::span pixels) { + const size_t expected = static_cast(extent.width) * extent.height; + WritePortablePixmap(path, "P5", extent, pixels.subspan(0, std::min(expected, pixels.size()))); +} + +void WriteRaw(const std::filesystem::path& path, std::span pixels) { + Common::FS::IOFile file{path, Common::FS::FileAccessMode::Write, + Common::FS::FileType::BinaryFile}; + if (!file.IsOpen()) { + return; + } + void(file.Write(pixels)); + void(file.Flush()); +} + +void WriteColorPpm(const std::filesystem::path& path, VkExtent2D extent, + std::span pixels, bool blue_first) { + const size_t pixel_count = static_cast(extent.width) * extent.height; + if (pixels.size() < pixel_count * COLOR_CHANNELS) { + return; + } + + std::vector rgb(pixel_count * 3); + for (size_t i = 0; i < pixel_count; ++i) { + const u8 first = pixels[i * COLOR_CHANNELS]; + const u8 green = pixels[i * COLOR_CHANNELS + 1]; + const u8 third = pixels[i * COLOR_CHANNELS + 2]; + rgb[i * 3] = blue_first ? third : first; + rgb[i * 3 + 1] = green; + rgb[i * 3 + 2] = blue_first ? first : third; + } + + WritePortablePixmap(path, "P6", extent, rgb); +} + +VkImageMemoryBarrier MakeTransitionBarrier(VkImage image, VkAccessFlags src_access, + VkAccessFlags dst_access, VkImageLayout old_layout, + VkImageLayout new_layout) { + return VkImageMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = src_access, + .dstAccessMask = dst_access, + .oldLayout = old_layout, + .newLayout = new_layout, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; +} + +VkImageCopy MakeCopyRegion(VkExtent2D extent) { + return VkImageCopy{ + .srcSubresource{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1, + }, + .srcOffset = {}, + .dstSubresource{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1, + }, + .dstOffset = {}, + .extent = {.width = extent.width, .height = extent.height, .depth = 1}, + }; +} + +void CopyPresentedFrame(vk::CommandBuffer cmdbuf, VkImage source, LsfgImage& destination, + VkExtent2D extent) { + const auto make_barrier = MakeTransitionBarrier; + + const std::array before{ + make_barrier(source, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, + VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL), + make_barrier(destination.Handle(), VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + destination.Layout(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL), + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, {}, {}, before); + + cmdbuf.CopyImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.Handle(), + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, MakeCopyRegion(extent)); + + const std::array after{ + make_barrier(source, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + make_barrier(destination.Handle(), VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + 0, {}, {}, after); + + destination.SetLayout(VK_IMAGE_LAYOUT_GENERAL); +} + +} // Anonymous namespace + +FrameGen::FrameGen(MemoryAllocator& memory_allocator_, Scheduler& scheduler_) + : memory_allocator{memory_allocator_}, scheduler{scheduler_} {} + +FrameGen::~FrameGen() = default; + +void FrameGen::Process(const Device& device, Frame* frame, VkFormat format, + VkExtent2D guest_extent) { + generated = false; + + if (unavailable || !Settings::values.frame_gen.GetValue()) { + if (chain) { + scheduler.Finish(); + chain.reset(); + } + warm_streak = 0; + return; + } + + if (!frame->storage_view) { + unavailable = true; + return; + } + + if (!shaders) { + shaders.emplace(device); + if (!shaders->IsValid()) { + unavailable = true; + return; + } + } + + peak_guest_extent.width = std::max(peak_guest_extent.width, guest_extent.width); + peak_guest_extent.height = std::max(peak_guest_extent.height, guest_extent.height); + + const VkExtent2D extent{.width = frame->width, .height = frame->height}; + const f32 flow_scale = ConfiguredFlowScale(peak_guest_extent, extent); + if (!chain || built_extent.width != extent.width || built_extent.height != extent.height || + built_format != format || built_flow_scale != flow_scale) { + Rebuild(device, extent, format, flow_scale); + } + + const u64 count = frame_count++; + last_count = count; + last_generations = plan.generations; + + const bool warm = plan.warm && count + 1 >= LSFG_REQUIRED_FRAMES; + warm_streak = warm ? warm_streak + 1 : 0; + generated = warm && warm_streak >= LSFG_RECURRENCE_FRAMES && plan.generations > 0; + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([this, source = *frame->image, extent, count, + dispatch = warm](vk::CommandBuffer cmdbuf) { + CopyPresentedFrame(cmdbuf, source, chain->Input(count), extent); + if (dispatch) { + chain->DispatchShared(cmdbuf, count); + } + }); + + const bool dump_requested = generated && Settings::values.frame_gen_dump_flow.GetValue(); + if (!dump_requested) { + dumped = false; + } else if (!dumped) { + DumpDebugImages(count); + dumped = true; + } +} + +size_t FrameGen::WantedGenerations(size_t capacity) { + if (unavailable) { + plan = {}; + return 0; + } + plan = pacer.Plan(capacity); + return plan.generations; +} + +size_t FrameGen::GeneratedFrameCount() const { + return generated ? last_generations : 0; +} + +void FrameGen::GenerateInto(const Device& device, Frame* destination, size_t generation) { + chain->SetTarget(device, last_generations, generation, destination->index, + *destination->storage_view); + + const VkExtent2D extent{.width = destination->width, .height = destination->height}; + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([this, count = last_count, generation_count = last_generations, generation, + target = destination->index, image = *destination->image, + extent](vk::CommandBuffer cmdbuf) { + chain->DispatchGeneration(cmdbuf, count, generation_count, generation, target, image, + extent); + }); +} + +void FrameGen::Rebuild(const Device& device, VkExtent2D extent, VkFormat format, f32 flow_scale) { + scheduler.Finish(); + chain.reset(); + + built_flow_scale = flow_scale; + + chain.emplace(device, memory_allocator, *shaders, extent, format, built_flow_scale); + built_extent = extent; + built_format = format; + frame_count = 0; + warm_streak = 0; + generated = false; +} + +void FrameGen::DumpDebugImages(u64 count) { + const std::filesystem::path directory = + Common::FS::GetEdenPath(Common::FS::EdenPath::LosslessDir) / "debug"; + if (!Common::FS::CreateDirs(directory)) { + return; + } + + const auto dump = [&](const std::string& name, LsfgImage& image) { + const VkExtent2D extent = image.Extent(); + const VkFormat format = image.Format(); + const VkDeviceSize texel_size = BytesPerTexel(format); + const VkDeviceSize size = + static_cast(extent.width) * extent.height * texel_size; + + vk::Buffer buffer = CreateWrappedBuffer(memory_allocator, size, MemoryUsage::Download); + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record( + [handle = image.Handle(), dst = *buffer, extent](vk::CommandBuffer cmdbuf) { + DownloadColorImage( + cmdbuf, handle, dst, + VkExtent3D{.width = extent.width, .height = extent.height, .depth = 1}); + }); + scheduler.Finish(); + + buffer.Invalidate(); + const std::span mapped = buffer.Mapped(); + + if (format == LSFG_FLOW_FORMAT) { + WriteGrayscalePgm(directory / (name + ".pgm"), extent, mapped); + } else if (texel_size == COLOR_CHANNELS) { + WriteColorPpm(directory / (name + ".ppm"), extent, mapped, IsBlueFirst(format)); + } else { + WriteRaw(directory / (name + "_" + std::to_string(extent.width) + "x" + + std::to_string(extent.height) + ".f16"), + mapped.subspan(0, std::min(size, mapped.size()))); + } + }; + + dump("in0", chain->Input(0)); + dump("in1", chain->Input(1)); + + for (size_t level = 0; level < LSFG_MIP_LEVELS; ++level) { + dump("flow_mip" + std::to_string(level), chain->FlowLevel(level)); + } + for (size_t index = 0; index < 2; ++index) { + dump("alpha0_" + std::to_string(index), chain->AlphaOutput(0, count, index)); + dump("alpha6_" + std::to_string(index), chain->AlphaOutput(LSFG_MIP_LEVELS - 1, count, + index)); + } + for (size_t level = 0; level < LSFG_BETA_OUTPUTS; ++level) { + dump("beta_" + std::to_string(level), chain->BetaOutput(level)); + } + + dump("gamma0", chain->GammaOutput(0)); + dump("gamma6", chain->GammaOutput(LSFG_MIP_LEVELS - 1)); + dump("delta2_out1", chain->DeltaOutput1(LSFG_DELTA_INSTANCES - 1)); + dump("delta2_out2", chain->DeltaOutput2(LSFG_DELTA_INSTANCES - 1)); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/frame_gen.h b/src/video_core/renderer_vulkan/present/frame_gen.h new file mode 100644 index 0000000000..19c6d04289 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/frame_gen.h @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" +#include "video_core/renderer_vulkan/present/frame_gen_pacer.h" +#include "video_core/renderer_vulkan/present/lsfg_chain.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/vulkan_common/vulkan_memory_allocator.h" + +namespace Vulkan { + +class Device; +class Scheduler; +struct Frame; + +class FrameGen { +public: + explicit FrameGen(MemoryAllocator& memory_allocator, Scheduler& scheduler); + ~FrameGen(); + + void Process(const Device& device, Frame* frame, VkFormat format, VkExtent2D guest_extent); + + [[nodiscard]] size_t WantedGenerations(size_t capacity); + + [[nodiscard]] size_t GeneratedFrameCount() const; + + void GenerateInto(const Device& device, Frame* destination, size_t generation); + +private: + void Rebuild(const Device& device, VkExtent2D extent, VkFormat format, f32 flow_scale); + void DumpDebugImages(u64 count); + + MemoryAllocator& memory_allocator; + Scheduler& scheduler; + + std::optional shaders; + std::optional chain; + FrameGenPacer pacer; + FrameGenPlan plan{}; + VkExtent2D peak_guest_extent{}; + VkExtent2D built_extent{}; + VkFormat built_format{VK_FORMAT_UNDEFINED}; + f32 built_flow_scale{}; + u64 frame_count{}; + u64 last_count{}; + size_t last_generations{}; + u32 warm_streak{}; + bool generated{}; + bool unavailable{}; + bool dumped{}; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/frame_gen_pacer.cpp b/src/video_core/renderer_vulkan/present/frame_gen_pacer.cpp new file mode 100644 index 0000000000..78a8c83b5a --- /dev/null +++ b/src/video_core/renderer_vulkan/present/frame_gen_pacer.cpp @@ -0,0 +1,240 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include + +#include "common/settings.h" +#include "video_core/renderer_vulkan/present/frame_gen_pacer.h" + +namespace Vulkan { + +namespace { + +using Clock = std::chrono::steady_clock; + +constexpr f32 INTERVAL_SMOOTHING = 0.25f; +constexpr f32 MINIMUM_BASE_RATE = 10.0f; +constexpr f32 BURST_CADENCE_RATIO = 3.0f; +constexpr f32 BURST_TARGET_RATIO = 2.0f; +constexpr f32 PROBE_THROUGHPUT_TOLERANCE = 0.95f; +constexpr f32 PROBE_BASE_COLLAPSE_RATIO = 0.70f; +constexpr f32 PROBE_MARGINAL_GAIN = 1.15f; +constexpr f32 TARGET_SATISFIED_RATIO = 0.95f; +constexpr f32 UNLOADED_BASE_RETENTION = 0.75f; +constexpr f32 CREDIT_EPSILON = 1.0e-4f; +constexpr u32 MAX_PROBE_FAILURES = 4; + +constexpr auto STABILIZATION_DURATION = std::chrono::seconds(1); +constexpr auto PROBE_DURATION = std::chrono::seconds(1); +constexpr auto DEFICIT_DURATION = std::chrono::seconds(1); +constexpr auto PROBE_STEP_DELAY = std::chrono::milliseconds(250); + +[[nodiscard]] Clock::duration ProbeBackoff(u32 failures) { + switch (failures) { + case 1: + return std::chrono::seconds(5); + case 2: + return std::chrono::seconds(15); + case 3: + return std::chrono::seconds(30); + default: + return std::chrono::seconds(60); + } +} + +} // Anonymous namespace + +FrameGenPlan FrameGenPacer::Plan(size_t capacity) { + const size_t ceiling = std::min(capacity, Settings::FrameGenMaxGenerations()); + if (ceiling == 0) { + Reset(); + return {}; + } + + const Clock::time_point now = Clock::now(); + const size_t previous_generations = std::exchange(issued_generations, 0); + if (!last_frame) { + last_frame = now; + return {}; + } + + const Clock::duration interval = now - *last_frame; + const f32 interval_seconds = std::chrono::duration(interval).count(); + last_frame = now; + + if (interval_seconds <= 0.0f) { + Stabilize(now); + return {}; + } + + const f32 target_rate = static_cast(Settings::values.frame_gen_target_rate.GetValue()); + + if (smoothed_interval > 0.0f) { + f32 burst_threshold = BURST_CADENCE_RATIO / smoothed_interval; + if (target_rate > 0.0f) { + burst_threshold = std::max(burst_threshold, target_rate * BURST_TARGET_RATIO); + } + if (1.0f / interval_seconds > burst_threshold) { + DeferEvaluations(interval); + output_credit = 0.0f; + return {}; + } + } + + if (interval_seconds > 1.0f / MINIMUM_BASE_RATE) { + Stabilize(now); + return {}; + } + + smoothed_interval = smoothed_interval > 0.0f + ? smoothed_interval + + (interval_seconds - smoothed_interval) * INTERVAL_SMOOTHING + : interval_seconds; + + if (previous_generations == 0) { + const f32 measured = 1.0f / smoothed_interval; + unloaded_base_rate = + unloaded_base_rate > 0.0f + ? unloaded_base_rate + (measured - unloaded_base_rate) * INTERVAL_SMOOTHING + : measured; + } + + if (stable_until) { + if (now < *stable_until) { + return {}; + } + stable_until.reset(); + } + + if (target_rate == 0.0f) { + limit = std::min(Settings::FrameGenGenerations(), ceiling); + output_credit = 0.0f; + issued_generations = limit; + return {.generations = limit, .warm = limit > 0}; + } + + UpdateLimit(now, 1.0f / smoothed_interval, target_rate, ceiling); + + const size_t allowed = std::min(limit, ceiling); + const f32 desired_outputs = smoothed_interval * target_rate; + if (allowed == 0 || desired_outputs <= 1.0f) { + output_credit = 0.0f; + return {}; + } + + output_credit += desired_outputs; + const size_t outputs = + std::max(1, static_cast(std::floor(output_credit + CREDIT_EPSILON))); + const size_t generations = std::min(outputs - 1, allowed); + + output_credit -= static_cast(generations + 1); + if (output_credit < 0.0f) { + output_credit = 0.0f; + } else if (generations == allowed && output_credit >= 1.0f) { + output_credit = std::fmod(output_credit, 1.0f); + } + + issued_generations = generations; + return {.generations = generations, .warm = true}; +} + +void FrameGenPacer::UpdateLimit(Clock::time_point now, f32 base_rate, f32 target_rate, + size_t ceiling) { + limit = std::min(limit, ceiling); + + if (probe_until) { + if (now < *probe_until) { + return; + } + probe_until.reset(); + output_credit = 0.0f; + + const f32 previous_output = + std::min(target_rate, probe_base_rate * static_cast(probe_previous_limit + 1)); + const f32 current_output = + std::min(target_rate, base_rate * static_cast(limit + 1)); + + const bool throughput_regressed = + current_output < previous_output * PROBE_THROUGHPUT_TOLERANCE; + const bool collapsed_for_marginal_gain = + base_rate < probe_base_rate * PROBE_BASE_COLLAPSE_RATIO && + current_output < previous_output * PROBE_MARGINAL_GAIN; + const bool emulation_slowed = unloaded_base_rate > 0.0f && + base_rate < unloaded_base_rate * UNLOADED_BASE_RETENTION; + + if (throughput_regressed || collapsed_for_marginal_gain || emulation_slowed) { + limit = probe_previous_limit; + probe_failures = std::min(probe_failures + 1, MAX_PROBE_FAILURES); + next_probe = now + ProbeBackoff(probe_failures); + deficit_since.reset(); + return; + } + + probe_failures = 0; + next_probe = now + PROBE_STEP_DELAY; + } + + if (base_rate * static_cast(limit + 1) >= target_rate * TARGET_SATISFIED_RATIO || + limit >= ceiling) { + deficit_since.reset(); + return; + } + + if (!deficit_since) { + deficit_since = now; + return; + } + if (now - *deficit_since < DEFICIT_DURATION) { + return; + } + if (next_probe && now < *next_probe) { + return; + } + + probe_previous_limit = limit; + probe_base_rate = base_rate; + ++limit; + probe_until = now + PROBE_DURATION; + deficit_since.reset(); + output_credit = 0.0f; +} + +void FrameGenPacer::DeferEvaluations(Clock::duration amount) { + const auto defer = [amount](std::optional& deadline) { + if (deadline) { + *deadline += amount; + } + }; + defer(stable_until); + defer(probe_until); + defer(next_probe); + deficit_since.reset(); +} + +void FrameGenPacer::Stabilize(Clock::time_point now) { + stable_until = now + STABILIZATION_DURATION; + probe_until.reset(); + deficit_since.reset(); + smoothed_interval = 0.0f; + output_credit = 0.0f; +} + +void FrameGenPacer::Reset() { + last_frame.reset(); + stable_until.reset(); + probe_until.reset(); + next_probe.reset(); + deficit_since.reset(); + smoothed_interval = 0.0f; + output_credit = 0.0f; + probe_base_rate = 0.0f; + unloaded_base_rate = 0.0f; + issued_generations = 0; + probe_previous_limit = 0; + limit = 0; + probe_failures = 0; +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/frame_gen_pacer.h b/src/video_core/renderer_vulkan/present/frame_gen_pacer.h new file mode 100644 index 0000000000..7f4b23920c --- /dev/null +++ b/src/video_core/renderer_vulkan/present/frame_gen_pacer.h @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#include "common/common_types.h" + +namespace Vulkan { + +struct FrameGenPlan { + size_t generations{}; + bool warm{}; +}; + +class FrameGenPacer { +public: + [[nodiscard]] FrameGenPlan Plan(size_t capacity); + + void Reset(); + +private: + using Clock = std::chrono::steady_clock; + + void Stabilize(Clock::time_point now); + void DeferEvaluations(Clock::duration amount); + void UpdateLimit(Clock::time_point now, f32 base_rate, f32 target_rate, size_t ceiling); + + std::optional last_frame; + std::optional stable_until; + std::optional probe_until; + std::optional next_probe; + std::optional deficit_since; + f32 smoothed_interval{}; + f32 output_credit{}; + f32 probe_base_rate{}; + f32 unloaded_base_rate{}; + size_t issued_generations{}; + size_t probe_previous_limit{}; + size_t limit{}; + u32 probe_failures{}; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_alpha.cpp b/src/video_core/renderer_vulkan/present/lsfg_alpha.cpp new file mode 100644 index 0000000000..a735dd11f4 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_alpha.cpp @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "video_core/frame_gen/lossless_dll.h" +#include "video_core/renderer_vulkan/present/lsfg_alpha.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr u32 DISPATCH_TILE_SHIFT = 3; + +[[nodiscard]] u32 GroupCount(u32 size) { + return (size + (1u << DISPATCH_TILE_SHIFT) - 1) >> DISPATCH_TILE_SHIFT; +} + +[[nodiscard]] VkExtent2D HalveExtent(VkExtent2D extent) { + return VkExtent2D{ + .width = (extent.width + 1) >> 1, + .height = (extent.height + 1) >> 1, + }; +} + +} // Anonymous namespace + +LsfgAlphaPasses::LsfgAlphaPasses(const Device& device, const LsfgShaders& shaders) { + using namespace VideoCore::FrameGen::PerformanceShader; + + passes[0] = LsfgPass(device, shaders, ALPHA[0], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[1] = LsfgPass(device, shaders, ALPHA[1], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[2] = LsfgPass(device, shaders, ALPHA[2], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[3] = LsfgPass(device, shaders, ALPHA[3], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); +} + +LsfgAlpha::LsfgAlpha(const Device& device, MemoryAllocator& memory_allocator, + const LsfgAlphaPasses& passes_, LsfgResources& resources, + vk::DescriptorPool& descriptor_pool, LsfgImage& input_) + : passes{&passes_}, input{&input_} { + const VkExtent2D half_extent = HalveExtent(input->Extent()); + const VkExtent2D quarter_extent = HalveExtent(half_extent); + + temp1 = LsfgImage(device, memory_allocator, half_extent); + temp2 = LsfgImage(device, memory_allocator, half_extent); + for (size_t i = 0; i < temp3.size(); ++i) { + temp3[i] = LsfgImage(device, memory_allocator, quarter_extent); + for (size_t j = 0; j < LSFG_HISTORY_SLOTS; ++j) { + out_images[j][i] = LsfgImage(device, memory_allocator, quarter_extent); + } + } + + std::vector layouts; + for (size_t i = 0; i < LSFG_ALPHA_STAGES - 1; ++i) { + layouts.push_back(passes->Get(i).SetLayout()); + } + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + layouts.push_back(passes->Get(3).SetLayout()); + } + owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts); + + for (size_t i = 0; i < LSFG_ALPHA_STAGES - 1; ++i) { + descriptor_sets[i] = owned_sets[i]; + } + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + last_descriptor_sets[i] = owned_sets[LSFG_ALPHA_STAGES - 1 + i]; + } + + const VkSampler sampler = resources.GetSampler(); + + LsfgDescriptorWriter(descriptor_sets[0]) + .AddSampler(sampler) + .AddSampledImage(*input) + .AddStorageImage(temp1) + .Build(device); + LsfgDescriptorWriter(descriptor_sets[1]) + .AddSampler(sampler) + .AddSampledImage(temp1) + .AddStorageImage(temp2) + .Build(device); + LsfgDescriptorWriter(descriptor_sets[2]) + .AddSampler(sampler) + .AddSampledImage(temp2) + .AddStorageImages(temp3) + .Build(device); + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + LsfgDescriptorWriter(last_descriptor_sets[i]) + .AddSampler(sampler) + .AddSampledImages(temp3) + .AddStorageImages(out_images[i]) + .Build(device); + } +} + +void LsfgAlpha::PushBarriers(LsfgBarriers& barriers, u64 frame_count, size_t stage) { + switch (stage) { + case 0: + barriers.WriteToRead(*input).ReadToWrite(temp1); + break; + case 1: + barriers.WriteToRead(temp1).ReadToWrite(temp2); + break; + case 2: + barriers.WriteToRead(temp2).ReadToWriteAll(temp3); + break; + default: + barriers.WriteToReadAll(temp3).ReadToWriteAll(out_images[frame_count % LSFG_HISTORY_SLOTS]); + break; + } +} + +void LsfgAlpha::DispatchStage(vk::CommandBuffer cmdbuf, u64 frame_count, size_t stage) { + const VkExtent2D extent = stage < 2 ? temp1.Extent() : temp3[0].Extent(); + const VkDescriptorSet set = stage < LSFG_ALPHA_STAGES - 1 + ? descriptor_sets[stage] + : last_descriptor_sets[frame_count % LSFG_HISTORY_SLOTS]; + + passes->Get(stage).BindSet(cmdbuf, set); + cmdbuf.Dispatch(GroupCount(extent.width), GroupCount(extent.height), 1); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_alpha.h b/src/video_core/renderer_vulkan/present/lsfg_alpha.h new file mode 100644 index 0000000000..1dc8864431 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_alpha.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" +#include "video_core/renderer_vulkan/present/lsfg_common.h" + +namespace Vulkan { + +class Device; +class LsfgShaders; + +constexpr size_t LSFG_ALPHA_STAGES = 4; + +class LsfgAlphaPasses { +public: + LsfgAlphaPasses() = default; + LsfgAlphaPasses(const Device& device, const LsfgShaders& shaders); + + [[nodiscard]] const LsfgPass& Get(size_t stage) const { + return passes[stage]; + } + +private: + std::array passes; +}; + +class LsfgAlpha { +public: + LsfgAlpha() = default; + LsfgAlpha(const Device& device, MemoryAllocator& memory_allocator, + const LsfgAlphaPasses& passes_, LsfgResources& resources, + vk::DescriptorPool& descriptor_pool, LsfgImage& input); + + void PushBarriers(LsfgBarriers& barriers, u64 frame_count, size_t stage); + void DispatchStage(vk::CommandBuffer cmdbuf, u64 frame_count, size_t stage); + + [[nodiscard]] LsfgImageHistory& Outputs() { + return out_images; + } + +private: + const LsfgAlphaPasses* passes{}; + LsfgImage* input{}; + + std::array descriptor_sets{}; + std::array last_descriptor_sets{}; + vk::DescriptorSets owned_sets; + + LsfgImage temp1; + LsfgImage temp2; + LsfgImagePair temp3; + LsfgImageHistory out_images; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_beta.cpp b/src/video_core/renderer_vulkan/present/lsfg_beta.cpp new file mode 100644 index 0000000000..6dddc7c21d --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_beta.cpp @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "video_core/frame_gen/lossless_dll.h" +#include "video_core/renderer_vulkan/present/lsfg_beta.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr u32 DISPATCH_TILE_SHIFT = 3; +constexpr u32 OUTPUT_TILE_SHIFT = 5; + +[[nodiscard]] u32 GroupCount(u32 size, u32 shift) { + return (size + (1u << shift) - 1) >> shift; +} + +} // Anonymous namespace + +LsfgBeta::LsfgBeta(const Device& device, MemoryAllocator& memory_allocator, + const LsfgShaders& shaders, LsfgResources& resources, + vk::DescriptorPool& descriptor_pool, LsfgImageHistory& inputs_) + : inputs{&inputs_} { + using namespace VideoCore::FrameGen::PerformanceShader; + + passes[0] = LsfgPass(device, shaders, BETA[0], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {6, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + for (size_t i = 1; i < LSFG_BETA_STAGES - 1; ++i) { + passes[i] = LsfgPass(device, shaders, BETA[i], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + } + passes[4] = LsfgPass(device, shaders, BETA[4], + {{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + {1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {6, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + + const VkExtent2D extent = (*inputs)[0][0].Extent(); + for (size_t i = 0; i < temp1.size(); ++i) { + temp1[i] = LsfgImage(device, memory_allocator, extent); + temp2[i] = LsfgImage(device, memory_allocator, extent); + } + for (size_t i = 0; i < LSFG_BETA_OUTPUTS; ++i) { + const VkExtent2D level_extent{ + .width = extent.width >> i, + .height = extent.height >> i, + }; + out_images[i] = LsfgImage(device, memory_allocator, level_extent, LSFG_FLOW_FORMAT); + } + + std::vector layouts; + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + layouts.push_back(passes[0].SetLayout()); + } + for (size_t i = 1; i < LSFG_BETA_STAGES; ++i) { + layouts.push_back(passes[i].SetLayout()); + } + owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts); + + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + first_descriptor_sets[i] = owned_sets[i]; + } + for (size_t i = 0; i < LSFG_BETA_STAGES - 1; ++i) { + descriptor_sets[i] = owned_sets[LSFG_HISTORY_SLOTS + i]; + } + + const VkSampler sampler = resources.GetSampler(); + const VkSampler border_sampler = resources.GetSampler( + VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, VK_COMPARE_OP_NEVER, true); + + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + LsfgDescriptorWriter(first_descriptor_sets[i]) + .AddSampler(border_sampler) + .AddSampledImages((*inputs)[(i + 1) % LSFG_HISTORY_SLOTS]) + .AddSampledImages((*inputs)[(i + 2) % LSFG_HISTORY_SLOTS]) + .AddSampledImages((*inputs)[i % LSFG_HISTORY_SLOTS]) + .AddStorageImages(temp1) + .Build(device); + } + LsfgDescriptorWriter(descriptor_sets[0]) + .AddSampler(sampler) + .AddSampledImages(temp1) + .AddStorageImages(temp2) + .Build(device); + LsfgDescriptorWriter(descriptor_sets[1]) + .AddSampler(sampler) + .AddSampledImages(temp2) + .AddStorageImages(temp1) + .Build(device); + LsfgDescriptorWriter(descriptor_sets[2]) + .AddSampler(sampler) + .AddSampledImages(temp1) + .AddStorageImages(temp2) + .Build(device); + LsfgDescriptorWriter(descriptor_sets[3]) + .AddUniformBuffer(resources.GetBuffer(0.5f), LsfgResources::BufferSize()) + .AddSampler(sampler) + .AddSampledImages(temp2) + .AddStorageImages(out_images) + .Build(device); +} + +void LsfgBeta::Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count) { + const VkExtent2D extent = temp1[0].Extent(); + const u32 groups_x = GroupCount(extent.width, DISPATCH_TILE_SHIFT); + const u32 groups_y = GroupCount(extent.height, DISPATCH_TILE_SHIFT); + + LsfgBarriers barriers(cmdbuf); + for (auto& slot : *inputs) { + barriers.WriteToReadAll(slot); + } + barriers.ReadToWriteAll(temp1).Build(); + + passes[0].Bind(cmdbuf, first_descriptor_sets[frame_count % LSFG_HISTORY_SLOTS]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp1).ReadToWriteAll(temp2).Build(); + passes[1].Bind(cmdbuf, descriptor_sets[0]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp2).ReadToWriteAll(temp1).Build(); + passes[2].Bind(cmdbuf, descriptor_sets[1]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp1).ReadToWriteAll(temp2).Build(); + passes[3].Bind(cmdbuf, descriptor_sets[2]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp2).ReadToWriteAll(out_images).Build(); + passes[4].Bind(cmdbuf, descriptor_sets[3]); + cmdbuf.Dispatch(GroupCount(extent.width, OUTPUT_TILE_SHIFT), + GroupCount(extent.height, OUTPUT_TILE_SHIFT), 1); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_beta.h b/src/video_core/renderer_vulkan/present/lsfg_beta.h new file mode 100644 index 0000000000..1e24b235cf --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_beta.h @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" +#include "video_core/renderer_vulkan/present/lsfg_common.h" + +namespace Vulkan { + +class Device; +class LsfgShaders; + +constexpr size_t LSFG_BETA_STAGES = 5; +constexpr size_t LSFG_BETA_OUTPUTS = 6; + +class LsfgBeta { +public: + LsfgBeta() = default; + LsfgBeta(const Device& device, MemoryAllocator& memory_allocator, const LsfgShaders& shaders, + LsfgResources& resources, vk::DescriptorPool& descriptor_pool, + LsfgImageHistory& inputs); + + void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count); + + [[nodiscard]] LsfgImage& Output(size_t level) { + return out_images[level]; + } + +private: + LsfgImageHistory* inputs{}; + + std::array passes; + std::array first_descriptor_sets{}; + std::array descriptor_sets{}; + vk::DescriptorSets owned_sets; + + LsfgImagePair temp1; + LsfgImagePair temp2; + std::array out_images; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_chain.cpp b/src/video_core/renderer_vulkan/present/lsfg_chain.cpp new file mode 100644 index 0000000000..972b13a1c4 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_chain.cpp @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "video_core/renderer_vulkan/present/lsfg_chain.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr u32 FIXED_DESCRIPTOR_SETS = 64; +constexpr u32 DESCRIPTOR_SETS_PER_SLOT = 112; +constexpr size_t FIRST_DELTA_LEVEL = 4; + +} // Anonymous namespace + +LsfgChain::LsfgChain(const Device& device, MemoryAllocator& memory_allocator, + const LsfgShaders& shaders, VkExtent2D extent, VkFormat format, + f32 flow_scale) + : resources{device, memory_allocator, flow_scale}, + descriptor_pool{CreateLsfgDescriptorPool( + device, FIXED_DESCRIPTOR_SETS + + DESCRIPTOR_SETS_PER_SLOT * static_cast(LSFG_GENERATION_SLOTS))} { + for (auto& image : frames) { + image = LsfgImage(device, memory_allocator, extent, format); + } + + mipmaps = LsfgMipmaps(device, memory_allocator, shaders, resources, descriptor_pool, frames, + flow_scale); + + alpha_passes = LsfgAlphaPasses(device, shaders); + for (size_t i = 0; i < LSFG_MIP_LEVELS; ++i) { + alpha[i] = LsfgAlpha(device, memory_allocator, alpha_passes, resources, descriptor_pool, + mipmaps.Output(i)); + } + + beta = LsfgBeta(device, memory_allocator, shaders, resources, descriptor_pool, + alpha[0].Outputs()); + + for (size_t i = 0; i < LSFG_MIP_LEVELS; ++i) { + const size_t level = LSFG_MIP_LEVELS - 1 - i; + gamma[i] = LsfgGamma(device, memory_allocator, shaders, resources, descriptor_pool, + alpha[level].Outputs(), + beta.Output(std::min(level, LSFG_BETA_OUTPUTS - 1)), + i == 0 ? nullptr : &gamma[i - 1].Output()); + + if (i < FIRST_DELTA_LEVEL) { + continue; + } + + const size_t index = i - FIRST_DELTA_LEVEL; + delta[index] = LsfgDelta( + device, memory_allocator, shaders, resources, descriptor_pool, alpha[level].Outputs(), + beta.Output(level), i == FIRST_DELTA_LEVEL ? nullptr : &gamma[i - 1].Output(), + i == FIRST_DELTA_LEVEL ? nullptr : &delta[index - 1].Output1(), + i == FIRST_DELTA_LEVEL ? nullptr : &delta[index - 1].Output2()); + } + + generate = LsfgGenerate(device, shaders, resources, descriptor_pool, frames, + gamma[LSFG_MIP_LEVELS - 1].Output(), + delta[LSFG_DELTA_INSTANCES - 1].Output1(), + delta[LSFG_DELTA_INSTANCES - 1].Output2()); +} + +void LsfgChain::DispatchShared(vk::CommandBuffer cmdbuf, u64 frame_count) { + mipmaps.Dispatch(cmdbuf, frame_count); + + for (size_t stage = 0; stage < LSFG_ALPHA_STAGES; ++stage) { + LsfgBarriers barriers(cmdbuf); + for (auto& level : alpha) { + level.PushBarriers(barriers, frame_count, stage); + } + barriers.Build(); + + alpha_passes.Get(stage).BindPipeline(cmdbuf); + for (auto& level : alpha) { + level.DispatchStage(cmdbuf, frame_count, stage); + } + } + + beta.Dispatch(cmdbuf, frame_count); +} + +void LsfgChain::DispatchGeneration(vk::CommandBuffer cmdbuf, u64 frame_count, + size_t generation_count, size_t generation, u32 target, + VkImage image, VkExtent2D extent) { + const size_t slot = LsfgGenerationSlot(generation_count, generation); + for (size_t i = 0; i < LSFG_MIP_LEVELS; ++i) { + gamma[i].Dispatch(cmdbuf, frame_count, slot); + if (i >= FIRST_DELTA_LEVEL) { + delta[i - FIRST_DELTA_LEVEL].Dispatch(cmdbuf, frame_count, slot); + } + } + generate.Dispatch(cmdbuf, frame_count, slot, target, image, extent); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_chain.h b/src/video_core/renderer_vulkan/present/lsfg_chain.h new file mode 100644 index 0000000000..c8879f74a9 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_chain.h @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" +#include "video_core/renderer_vulkan/present/lsfg_alpha.h" +#include "video_core/renderer_vulkan/present/lsfg_beta.h" +#include "video_core/renderer_vulkan/present/lsfg_common.h" +#include "video_core/renderer_vulkan/present/lsfg_delta.h" +#include "video_core/renderer_vulkan/present/lsfg_gamma.h" +#include "video_core/renderer_vulkan/present/lsfg_generate.h" +#include "video_core/renderer_vulkan/present/lsfg_mipmaps.h" + +namespace Vulkan { + +class Device; +class LsfgShaders; + +constexpr size_t LSFG_DELTA_INSTANCES = 3; + +class LsfgChain { +public: + LsfgChain(const Device& device, MemoryAllocator& memory_allocator, const LsfgShaders& shaders, + VkExtent2D extent, VkFormat format, f32 flow_scale); + + LsfgChain(const LsfgChain&) = delete; + LsfgChain& operator=(const LsfgChain&) = delete; + + void DispatchShared(vk::CommandBuffer cmdbuf, u64 frame_count); + + void DispatchGeneration(vk::CommandBuffer cmdbuf, u64 frame_count, size_t generation_count, + size_t generation, u32 target, VkImage image, VkExtent2D extent); + + void SetTarget(const Device& device, size_t generation_count, size_t generation, u32 target, + VkImageView view) { + generate.SetTarget(device, LsfgGenerationSlot(generation_count, generation), target, view); + } + + [[nodiscard]] LsfgImage& Input(u64 frame_count) { + return frames[frame_count % frames.size()]; + } + + [[nodiscard]] LsfgImage& FlowLevel(size_t level) { + return mipmaps.Output(level); + } + + [[nodiscard]] LsfgImage& AlphaOutput(size_t level, u64 frame_count, size_t index) { + return alpha[level].Outputs()[frame_count % LSFG_HISTORY_SLOTS][index]; + } + + [[nodiscard]] LsfgImage& BetaOutput(size_t level) { + return beta.Output(level); + } + + [[nodiscard]] LsfgImage& GammaOutput(size_t index) { + return gamma[index].Output(); + } + + [[nodiscard]] LsfgImage& DeltaOutput1(size_t index) { + return delta[index].Output1(); + } + + [[nodiscard]] LsfgImage& DeltaOutput2(size_t index) { + return delta[index].Output2(); + } + +private: + LsfgResources resources; + vk::DescriptorPool descriptor_pool; + + LsfgImagePair frames; + LsfgMipmaps mipmaps; + LsfgAlphaPasses alpha_passes; + std::array alpha; + LsfgBeta beta; + std::array gamma; + std::array delta; + LsfgGenerate generate; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_common.cpp b/src/video_core/renderer_vulkan/present/lsfg_common.cpp new file mode 100644 index 0000000000..d2df2755e5 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_common.cpp @@ -0,0 +1,339 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +#include "video_core/renderer_vulkan/present/lsfg_common.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr u32 DESCRIPTORS_PER_TYPE = 4096; + +struct LsfgConstants { + std::array input_offset; + u32 first_iter; + u32 first_iter_s; + u32 advanced_color_kind; + u32 hdr_support; + f32 resolution_inv_scale; + f32 timestamp; + f32 ui_threshold; + std::array padding; +}; +static_assert(sizeof(LsfgConstants) == 48); + +vk::Image CreateChainImage(MemoryAllocator& memory_allocator, VkExtent2D extent, VkFormat format) { + const VkImageCreateInfo image_ci{ + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .imageType = VK_IMAGE_TYPE_2D, + .format = format, + .extent = {.width = extent.width, .height = extent.height, .depth = 1}, + .mipLevels = 1, + .arrayLayers = 1, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_OPTIMAL, + .usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | + VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + }; + return memory_allocator.CreateImage(image_ci); +} + +vk::Buffer CreateUniformBuffer(MemoryAllocator& memory_allocator, VkDeviceSize size) { + const VkBufferCreateInfo buffer_ci{ + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .size = size, + .usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + }; + return memory_allocator.CreateBuffer(buffer_ci, MemoryUsage::Upload); +} + +VkImageMemoryBarrier MakeBarrier(const LsfgImage& image, VkAccessFlags src_access, + VkAccessFlags dst_access) { + return VkImageMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = src_access, + .dstAccessMask = dst_access, + .oldLayout = image.Layout(), + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image.Handle(), + .subresourceRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; +} + +} // Anonymous namespace + +LsfgImage::LsfgImage(const Device& device, MemoryAllocator& memory_allocator, VkExtent2D extent_, + VkFormat format_) + : extent{std::max(1u, extent_.width), std::max(1u, extent_.height)}, format{format_} { + image = CreateChainImage(memory_allocator, extent, format); + view = CreateWrappedImageView(device, image, format); +} + +LsfgBarriers& LsfgBarriers::Push(LsfgImage& image, VkAccessFlags src_access, + VkAccessFlags dst_access) { + barriers.push_back(MakeBarrier(image, src_access, dst_access)); + image.SetLayout(VK_IMAGE_LAYOUT_GENERAL); + return *this; +} + +LsfgBarriers& LsfgBarriers::WriteToRead(LsfgImage& image) { + return Push(image, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); +} + +LsfgBarriers& LsfgBarriers::ReadToWrite(LsfgImage& image) { + return Push(image, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_SHADER_WRITE_BIT); +} + +LsfgBarriers& LsfgBarriers::WriteToRead(LsfgImage* image) { + return image == nullptr ? *this : WriteToRead(*image); +} + +LsfgBarriers& LsfgBarriers::ReadToWrite(LsfgImage* image) { + return image == nullptr ? *this : ReadToWrite(*image); +} + +LsfgBarriers& LsfgBarriers::DiscardToWrite(VkImage image) { + barriers.push_back(VkImageMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = 0, + .dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT, + .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }); + return *this; +} + +VkDeviceSize LsfgResources::BufferSize() { + return sizeof(LsfgConstants); +} + +VkSampler LsfgResources::GetSampler(VkSamplerAddressMode address_mode, VkCompareOp compare_op, + bool white_border) { + const u64 key = static_cast(address_mode) | (static_cast(compare_op) << 8) | + (static_cast(white_border) << 16); + + const auto it = samplers.find(key); + if (it != samplers.end()) { + return *it->second; + } + + const auto [entry, inserted] = + samplers.emplace(key, CreateLsfgSampler(*device, address_mode, compare_op, white_border)); + return *entry->second; +} + +VkBuffer LsfgResources::GetBuffer(f32 timestamp, bool first_iter, bool first_iter_s) { + u32 timestamp_bits{}; + std::memcpy(×tamp_bits, ×tamp, sizeof(timestamp_bits)); + const u64 key = static_cast(timestamp_bits) | (static_cast(first_iter) << 32) | + (static_cast(first_iter_s) << 33); + + const auto it = buffers.find(key); + if (it != buffers.end()) { + return *it->second; + } + + vk::Buffer buffer = CreateUniformBuffer(*memory_allocator, sizeof(LsfgConstants)); + + const LsfgConstants constants{ + .input_offset = {0, 0}, + .first_iter = first_iter ? 1u : 0u, + .first_iter_s = first_iter_s ? 1u : 0u, + .advanced_color_kind = 0, + .hdr_support = 0, + .resolution_inv_scale = 1.0f / flow_scale, + .timestamp = timestamp, + .ui_threshold = 0.5f, + .padding = {0, 0, 0}, + }; + + const std::span mapped = buffer.Mapped(); + std::memcpy(mapped.data(), &constants, sizeof(constants)); + buffer.Flush(); + + const auto [entry, inserted] = buffers.emplace(key, std::move(buffer)); + return *entry->second; +} + +void LsfgBarriers::Build() { + if (barriers.empty()) { + return; + } + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, {}, {}, barriers); + barriers.clear(); +} + +LsfgDescriptorWriter& LsfgDescriptorWriter::PushImage(VkDescriptorType type, VkSampler sampler, + VkImageView view) { + image_infos.push_back(VkDescriptorImageInfo{ + .sampler = sampler, + .imageView = view, + .imageLayout = view == VK_NULL_HANDLE ? VK_IMAGE_LAYOUT_UNDEFINED + : VK_IMAGE_LAYOUT_GENERAL, + }); + writes.push_back(VkWriteDescriptorSet{ + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .pNext = nullptr, + .dstSet = set, + .dstBinding = binding++, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = type, + .pImageInfo = &image_infos.back(), + .pBufferInfo = nullptr, + .pTexelBufferView = nullptr, + }); + return *this; +} + +LsfgDescriptorWriter& LsfgDescriptorWriter::AddSampler(VkSampler sampler) { + return PushImage(VK_DESCRIPTOR_TYPE_SAMPLER, sampler, VK_NULL_HANDLE); +} + +LsfgDescriptorWriter& LsfgDescriptorWriter::AddSampledImage(const LsfgImage& image) { + return PushImage(VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_NULL_HANDLE, image.View()); +} + +LsfgDescriptorWriter& LsfgDescriptorWriter::AddSampledImage(const LsfgImage* image) { + return PushImage(VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_NULL_HANDLE, + image == nullptr ? VK_NULL_HANDLE : image->View()); +} + +LsfgDescriptorWriter& LsfgDescriptorWriter::AddStorageImage(const LsfgImage& image) { + return PushImage(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_NULL_HANDLE, image.View()); +} + +LsfgDescriptorWriter& LsfgDescriptorWriter::AddStorageView(VkImageView view) { + return PushImage(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_NULL_HANDLE, view); +} + +LsfgDescriptorWriter& LsfgDescriptorWriter::AddUniformBuffer(VkBuffer buffer, VkDeviceSize size) { + buffer_infos.push_back(VkDescriptorBufferInfo{ + .buffer = buffer, + .offset = 0, + .range = size, + }); + writes.push_back(VkWriteDescriptorSet{ + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .pNext = nullptr, + .dstSet = set, + .dstBinding = binding++, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + .pImageInfo = nullptr, + .pBufferInfo = &buffer_infos.back(), + .pTexelBufferView = nullptr, + }); + return *this; +} + +void LsfgDescriptorWriter::Build(const Device& device) { + if (writes.empty()) { + return; + } + device.GetLogical().UpdateDescriptorSets(writes, {}); + writes.clear(); +} + +LsfgPass::LsfgPass(const Device& device, const LsfgShaders& shaders, u32 shader_id, + LsfgBindings bindings) { + std::vector types; + for (const auto& [count, type] : bindings) { + types.insert(types.end(), count, type); + } + descriptor_count = static_cast(types.size()); + + descriptor_set_layout = CreateWrappedDescriptorSetLayout( + device, std::span{types}, VK_SHADER_STAGE_COMPUTE_BIT); + pipeline_layout = CreateWrappedPipelineLayout(device, descriptor_set_layout); + pipeline = CreateWrappedComputePipeline(device, pipeline_layout, shaders.Get(shader_id)); +} + +void LsfgPass::Bind(vk::CommandBuffer cmdbuf, VkDescriptorSet set) const { + BindPipeline(cmdbuf); + BindSet(cmdbuf, set); +} + +void LsfgPass::BindPipeline(vk::CommandBuffer cmdbuf) const { + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *pipeline); +} + +void LsfgPass::BindSet(vk::CommandBuffer cmdbuf, VkDescriptorSet set) const { + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *pipeline_layout, 0, set, {}); +} + +vk::DescriptorPool CreateLsfgDescriptorPool(const Device& device, u32 max_sets) { + return CreateWrappedDescriptorPool( + device, DESCRIPTORS_PER_TYPE, max_sets, + {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_DESCRIPTOR_TYPE_SAMPLER, + VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}); +} + +vk::Sampler CreateLsfgSampler(const Device& device, VkSamplerAddressMode address_mode, + VkCompareOp compare_op, bool white_border) { + return device.GetLogical().CreateSampler(VkSamplerCreateInfo{ + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .magFilter = VK_FILTER_LINEAR, + .minFilter = VK_FILTER_LINEAR, + .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR, + .addressModeU = address_mode, + .addressModeV = address_mode, + .addressModeW = address_mode, + .mipLodBias = 0.0f, + .anisotropyEnable = VK_FALSE, + .maxAnisotropy = 0.0f, + .compareEnable = VK_FALSE, + .compareOp = compare_op, + .minLod = 0.0f, + .maxLod = VK_LOD_CLAMP_NONE, + .borderColor = white_border ? VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE + : VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK, + .unnormalizedCoordinates = VK_FALSE, + }); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_common.h b/src/video_core/renderer_vulkan/present/lsfg_common.h new file mode 100644 index 0000000000..d1e3271a6d --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_common.h @@ -0,0 +1,227 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "common/common_types.h" +#include "video_core/vulkan_common/vulkan_memory_allocator.h" +#include "video_core/vulkan_common/vulkan_wrapper.h" + +namespace Vulkan { + +class Device; +class LsfgShaders; + +constexpr VkFormat LSFG_DEFAULT_FORMAT = VK_FORMAT_R8G8B8A8_UNORM; +constexpr VkFormat LSFG_FLOW_FORMAT = VK_FORMAT_R8_UNORM; +constexpr VkFormat LSFG_MOTION_FORMAT = VK_FORMAT_R16G16B16A16_SFLOAT; + +constexpr size_t LSFG_HISTORY_SLOTS = 3; +constexpr size_t LSFG_MAX_TARGETS = 7; +constexpr size_t LSFG_MAX_GENERATIONS = 3; + +constexpr size_t LSFG_GENERATION_SLOTS = LSFG_MAX_GENERATIONS * (LSFG_MAX_GENERATIONS + 1) / 2; + +[[nodiscard]] constexpr size_t LsfgGenerationSlot(size_t generation_count, size_t generation) { + return (generation_count - 1) * generation_count / 2 + generation; +} + +[[nodiscard]] constexpr f32 LsfgTimestamp(size_t generation, size_t generation_count) { + return static_cast(generation + 1) / static_cast(generation_count + 1); +} + +[[nodiscard]] constexpr size_t LsfgSlotCount(size_t slot) { + size_t count = 1; + while (LsfgGenerationSlot(count + 1, 0) <= slot) { + ++count; + } + return count; +} + +[[nodiscard]] constexpr f32 LsfgSlotTimestamp(size_t slot) { + const size_t count = LsfgSlotCount(slot); + return LsfgTimestamp(slot - LsfgGenerationSlot(count, 0), count); +} + +class LsfgImage { +public: + LsfgImage() = default; + LsfgImage(const Device& device, MemoryAllocator& memory_allocator, VkExtent2D extent_, + VkFormat format = LSFG_DEFAULT_FORMAT); + + [[nodiscard]] VkImage Handle() const { + return *image; + } + + [[nodiscard]] VkImageView View() const { + return *view; + } + + [[nodiscard]] VkExtent2D Extent() const { + return extent; + } + + [[nodiscard]] VkFormat Format() const { + return format; + } + + [[nodiscard]] VkImageLayout Layout() const { + return layout; + } + + void SetLayout(VkImageLayout new_layout) { + layout = new_layout; + } + +private: + vk::Image image; + vk::ImageView view; + VkExtent2D extent{}; + VkFormat format{VK_FORMAT_UNDEFINED}; + VkImageLayout layout{VK_IMAGE_LAYOUT_UNDEFINED}; +}; + +using LsfgImagePair = std::array; +using LsfgImageHistory = std::array; + +class LsfgResources { +public: + LsfgResources() = default; + LsfgResources(const Device& device_, MemoryAllocator& memory_allocator_, f32 flow_scale_) + : device{&device_}, memory_allocator{&memory_allocator_}, flow_scale{flow_scale_} {} + + [[nodiscard]] VkSampler GetSampler( + VkSamplerAddressMode address_mode = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, + VkCompareOp compare_op = VK_COMPARE_OP_NEVER, bool white_border = false); + + [[nodiscard]] VkBuffer GetBuffer(f32 timestamp = 0.0f, bool first_iter = false, + bool first_iter_s = false); + + [[nodiscard]] static VkDeviceSize BufferSize(); + +private: + const Device* device{}; + MemoryAllocator* memory_allocator{}; + f32 flow_scale{1.0f}; + + std::map samplers; + std::map buffers; +}; + +class LsfgBarriers { +public: + explicit LsfgBarriers(vk::CommandBuffer cmdbuf_) : cmdbuf{cmdbuf_} {} + + LsfgBarriers& WriteToRead(LsfgImage& image); + LsfgBarriers& ReadToWrite(LsfgImage& image); + LsfgBarriers& WriteToRead(LsfgImage* image); + LsfgBarriers& ReadToWrite(LsfgImage* image); + LsfgBarriers& DiscardToWrite(VkImage image); + + template + LsfgBarriers& WriteToReadAll(Range& images) { + for (auto& image : images) { + WriteToRead(image); + } + return *this; + } + + template + LsfgBarriers& ReadToWriteAll(Range& images) { + for (auto& image : images) { + ReadToWrite(image); + } + return *this; + } + + void Build(); + +private: + LsfgBarriers& Push(LsfgImage& image, VkAccessFlags src_access, VkAccessFlags dst_access); + + vk::CommandBuffer cmdbuf; + std::vector barriers; +}; + +class LsfgDescriptorWriter { +public: + explicit LsfgDescriptorWriter(VkDescriptorSet set_) : set{set_} {} + + LsfgDescriptorWriter& AddSampler(VkSampler sampler); + LsfgDescriptorWriter& AddSampledImage(const LsfgImage& image); + LsfgDescriptorWriter& AddSampledImage(const LsfgImage* image); + LsfgDescriptorWriter& AddStorageImage(const LsfgImage& image); + LsfgDescriptorWriter& AddStorageView(VkImageView view); + LsfgDescriptorWriter& AddUniformBuffer(VkBuffer buffer, VkDeviceSize size); + + template + LsfgDescriptorWriter& AddSampledImages(const Range& images) { + for (const auto& image : images) { + AddSampledImage(image); + } + return *this; + } + + template + LsfgDescriptorWriter& AddStorageImages(const Range& images) { + for (const auto& image : images) { + AddStorageImage(image); + } + return *this; + } + + void Build(const Device& device); + +private: + LsfgDescriptorWriter& PushImage(VkDescriptorType type, VkSampler sampler, VkImageView view); + + VkDescriptorSet set; + u32 binding{}; + std::deque image_infos; + std::deque buffer_infos; + std::vector writes; +}; + +using LsfgBindings = std::initializer_list>; + +class LsfgPass { +public: + LsfgPass() = default; + LsfgPass(const Device& device, const LsfgShaders& shaders, u32 shader_id, + LsfgBindings bindings); + + [[nodiscard]] VkDescriptorSetLayout SetLayout() const { + return *descriptor_set_layout; + } + + [[nodiscard]] u32 DescriptorCount() const { + return descriptor_count; + } + + void Bind(vk::CommandBuffer cmdbuf, VkDescriptorSet set) const; + void BindPipeline(vk::CommandBuffer cmdbuf) const; + void BindSet(vk::CommandBuffer cmdbuf, VkDescriptorSet set) const; + +private: + vk::DescriptorSetLayout descriptor_set_layout; + vk::PipelineLayout pipeline_layout; + vk::Pipeline pipeline; + u32 descriptor_count{}; +}; + +[[nodiscard]] vk::DescriptorPool CreateLsfgDescriptorPool(const Device& device, u32 max_sets); + +[[nodiscard]] vk::Sampler CreateLsfgSampler(const Device& device, VkSamplerAddressMode address_mode, + VkCompareOp compare_op, bool white_border); + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_delta.cpp b/src/video_core/renderer_vulkan/present/lsfg_delta.cpp new file mode 100644 index 0000000000..dc6eec383f --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_delta.cpp @@ -0,0 +1,283 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "video_core/frame_gen/lossless_dll.h" +#include "video_core/renderer_vulkan/present/lsfg_delta.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr u32 DISPATCH_TILE_SHIFT = 3; + +[[nodiscard]] u32 GroupCount(u32 size) { + return (size + (1u << DISPATCH_TILE_SHIFT) - 1) >> DISPATCH_TILE_SHIFT; +} + +} // Anonymous namespace + +LsfgDelta::LsfgDelta(const Device& device, MemoryAllocator& memory_allocator, + const LsfgShaders& shaders, LsfgResources& resources, + vk::DescriptorPool& descriptor_pool, LsfgImageHistory& inputs_, + LsfgImage& flow_input_, LsfgImage* previous_gamma_, LsfgImage* previous1_, + LsfgImage* previous2_) + : inputs{&inputs_}, flow_input{&flow_input_}, previous_gamma{previous_gamma_}, + previous1{previous1_}, previous2{previous2_} { + using namespace VideoCore::FrameGen::PerformanceShader; + + passes[0] = LsfgPass(device, shaders, DELTA[0], + {{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLER}, + {5, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {3, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[1] = LsfgPass(device, shaders, DELTA[1], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {3, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[2] = LsfgPass(device, shaders, DELTA[2], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[3] = LsfgPass(device, shaders, DELTA[3], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[4] = LsfgPass(device, shaders, DELTA[4], + {{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLER}, + {4, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[5] = LsfgPass(device, shaders, DELTA[5], + {{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLER}, + {6, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + for (size_t i = 6; i < LSFG_DELTA_STAGES - 1; ++i) { + passes[i] = LsfgPass(device, shaders, DELTA[i], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + } + passes[9] = LsfgPass(device, shaders, DELTA[9], + {{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + + const VkExtent2D extent = (*inputs)[0][0].Extent(); + for (auto& image : temp1) { + image = LsfgImage(device, memory_allocator, extent); + } + for (auto& image : temp2) { + image = LsfgImage(device, memory_allocator, extent); + } + out_image1 = LsfgImage(device, memory_allocator, extent, LSFG_MOTION_FORMAT); + out_image2 = LsfgImage(device, memory_allocator, extent, LSFG_MOTION_FORMAT); + + std::vector layouts; + for (size_t slot = 0; slot < LSFG_GENERATION_SLOTS; ++slot) { + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + layouts.push_back(passes[0].SetLayout()); + } + for (size_t i = 1; i <= 4; ++i) { + layouts.push_back(passes[i].SetLayout()); + } + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + layouts.push_back(passes[5].SetLayout()); + } + for (size_t i = 6; i < LSFG_DELTA_STAGES; ++i) { + layouts.push_back(passes[i].SetLayout()); + } + } + owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts); + + const VkSampler sampler = resources.GetSampler(); + const VkSampler border_sampler = resources.GetSampler( + VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, VK_COMPARE_OP_NEVER, true); + const VkSampler edge_sampler = + resources.GetSampler(VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, VK_COMPARE_OP_ALWAYS, false); + size_t next = 0; + for (size_t slot = 0; slot < LSFG_GENERATION_SLOTS; ++slot) { + Generation& pass = generations[slot]; + const VkBuffer buffer = + resources.GetBuffer(LsfgSlotTimestamp(slot), false, previous_gamma == nullptr); + + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + pass.first_descriptor_sets[i] = owned_sets[next++]; + } + for (size_t i = 0; i < 4; ++i) { + pass.descriptor_sets[i] = owned_sets[next++]; + } + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + pass.sixth_descriptor_sets[i] = owned_sets[next++]; + } + for (size_t i = 4; i < LSFG_DELTA_STAGES - 2; ++i) { + pass.descriptor_sets[i] = owned_sets[next++]; + } + + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + LsfgDescriptorWriter(pass.first_descriptor_sets[i]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(border_sampler) + .AddSampler(edge_sampler) + .AddSampledImages((*inputs)[(i + 2) % LSFG_HISTORY_SLOTS]) + .AddSampledImages((*inputs)[i % LSFG_HISTORY_SLOTS]) + .AddSampledImage(previous_gamma) + .AddStorageImages(temp1) + .Build(device); + LsfgDescriptorWriter(pass.sixth_descriptor_sets[i]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(border_sampler) + .AddSampler(edge_sampler) + .AddSampledImages((*inputs)[(i + 2) % LSFG_HISTORY_SLOTS]) + .AddSampledImages((*inputs)[i % LSFG_HISTORY_SLOTS]) + .AddSampledImage(previous_gamma) + .AddSampledImage(previous1) + .AddStorageImage(temp2[0]) + .Build(device); + } + LsfgDescriptorWriter(pass.descriptor_sets[0]) + .AddSampler(sampler) + .AddSampledImages(temp1) + .AddStorageImages(temp2) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[1]) + .AddSampler(sampler) + .AddSampledImages(temp2) + .AddStorageImage(temp1[0]) + .AddStorageImage(temp1[1]) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[2]) + .AddSampler(sampler) + .AddSampledImage(temp1[0]) + .AddSampledImage(temp1[1]) + .AddStorageImages(temp2) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[3]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(sampler) + .AddSampler(edge_sampler) + .AddSampledImages(temp2) + .AddSampledImage(previous_gamma) + .AddSampledImage(*flow_input) + .AddStorageImage(out_image1) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[4]) + .AddSampler(sampler) + .AddSampledImage(temp2[0]) + .AddStorageImage(temp1[0]) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[5]) + .AddSampler(sampler) + .AddSampledImage(temp1[0]) + .AddStorageImage(temp2[0]) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[6]) + .AddSampler(sampler) + .AddSampledImage(temp2[0]) + .AddStorageImage(temp1[0]) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[7]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(sampler) + .AddSampler(edge_sampler) + .AddSampledImage(temp1[0]) + .AddSampledImage(previous2) + .AddStorageImage(out_image2) + .Build(device); + } +} + +void LsfgDelta::Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t slot) { + const Generation& pass = generations[slot]; + + const VkExtent2D extent = temp1[0].Extent(); + const u32 groups_x = GroupCount(extent.width); + const u32 groups_y = GroupCount(extent.height); + + const size_t history = frame_count % LSFG_HISTORY_SLOTS; + const size_t previous_history = (frame_count + 2) % LSFG_HISTORY_SLOTS; + + LsfgBarriers(cmdbuf) + .WriteToReadAll((*inputs)[previous_history]) + .WriteToReadAll((*inputs)[history]) + .WriteToRead(previous_gamma) + .ReadToWriteAll(temp1) + .Build(); + passes[0].Bind(cmdbuf, pass.first_descriptor_sets[history]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp1).ReadToWriteAll(temp2).Build(); + passes[1].Bind(cmdbuf, pass.descriptor_sets[0]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp2).ReadToWriteAll(temp1).Build(); + passes[2].Bind(cmdbuf, pass.descriptor_sets[1]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp1).ReadToWriteAll(temp2).Build(); + passes[3].Bind(cmdbuf, pass.descriptor_sets[2]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToReadAll(temp2) + .WriteToRead(previous_gamma) + .WriteToRead(*flow_input) + .ReadToWrite(out_image1) + .Build(); + passes[4].Bind(cmdbuf, pass.descriptor_sets[3]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToReadAll((*inputs)[previous_history]) + .WriteToReadAll((*inputs)[history]) + .WriteToRead(previous_gamma) + .WriteToRead(previous1) + .ReadToWriteAll(temp2) + .Build(); + passes[5].Bind(cmdbuf, pass.sixth_descriptor_sets[history]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToReadAll(temp2) + .ReadToWrite(temp1[0]) + .ReadToWrite(temp1[1]) + .Build(); + passes[6].Bind(cmdbuf, pass.descriptor_sets[4]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToRead(temp1[0]) + .WriteToRead(temp1[1]) + .ReadToWriteAll(temp2) + .Build(); + passes[7].Bind(cmdbuf, pass.descriptor_sets[5]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToReadAll(temp2) + .ReadToWrite(temp1[0]) + .ReadToWrite(temp1[1]) + .Build(); + passes[8].Bind(cmdbuf, pass.descriptor_sets[6]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToRead(temp1[0]) + .WriteToRead(temp1[1]) + .WriteToRead(previous2) + .ReadToWrite(out_image2) + .Build(); + passes[9].Bind(cmdbuf, pass.descriptor_sets[7]); + cmdbuf.Dispatch(groups_x, groups_y, 1); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_delta.h b/src/video_core/renderer_vulkan/present/lsfg_delta.h new file mode 100644 index 0000000000..1bbcfe6745 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_delta.h @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" +#include "video_core/renderer_vulkan/present/lsfg_common.h" + +namespace Vulkan { + +class Device; +class LsfgShaders; + +constexpr size_t LSFG_DELTA_STAGES = 10; +constexpr size_t LSFG_DELTA_TEMPS = 3; + +class LsfgDelta { +public: + LsfgDelta() = default; + LsfgDelta(const Device& device, MemoryAllocator& memory_allocator, const LsfgShaders& shaders, + LsfgResources& resources, vk::DescriptorPool& descriptor_pool, + LsfgImageHistory& inputs, LsfgImage& flow_input, LsfgImage* previous_gamma, + LsfgImage* previous1, LsfgImage* previous2); + + void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t slot); + + [[nodiscard]] LsfgImage& Output1() { + return out_image1; + } + + [[nodiscard]] LsfgImage& Output2() { + return out_image2; + } + +private: + struct Generation { + std::array first_descriptor_sets{}; + std::array sixth_descriptor_sets{}; + std::array descriptor_sets{}; + }; + + LsfgImageHistory* inputs{}; + LsfgImage* flow_input{}; + LsfgImage* previous_gamma{}; + LsfgImage* previous1{}; + LsfgImage* previous2{}; + + std::array passes; + std::array generations{}; + vk::DescriptorSets owned_sets; + + std::array temp1; + LsfgImagePair temp2; + LsfgImage out_image1; + LsfgImage out_image2; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_gamma.cpp b/src/video_core/renderer_vulkan/present/lsfg_gamma.cpp new file mode 100644 index 0000000000..067d124b6f --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_gamma.cpp @@ -0,0 +1,185 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "video_core/frame_gen/lossless_dll.h" +#include "video_core/renderer_vulkan/present/lsfg_gamma.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr u32 DISPATCH_TILE_SHIFT = 3; + +[[nodiscard]] u32 GroupCount(u32 size) { + return (size + (1u << DISPATCH_TILE_SHIFT) - 1) >> DISPATCH_TILE_SHIFT; +} + +} // Anonymous namespace + +LsfgGamma::LsfgGamma(const Device& device, MemoryAllocator& memory_allocator, + const LsfgShaders& shaders, LsfgResources& resources, + vk::DescriptorPool& descriptor_pool, LsfgImageHistory& inputs_, + LsfgImage& flow_input_, LsfgImage* previous_) + : inputs{&inputs_}, flow_input{&flow_input_}, previous{previous_} { + using namespace VideoCore::FrameGen::PerformanceShader; + + passes[0] = LsfgPass(device, shaders, GAMMA[0], + {{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLER}, + {5, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {3, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[1] = LsfgPass(device, shaders, GAMMA[1], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {3, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[2] = LsfgPass(device, shaders, GAMMA[2], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[3] = LsfgPass(device, shaders, GAMMA[3], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[4] = LsfgPass(device, shaders, GAMMA[4], + {{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLER}, + {4, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + + const VkExtent2D extent = (*inputs)[0][0].Extent(); + for (auto& image : temp1) { + image = LsfgImage(device, memory_allocator, extent); + } + for (auto& image : temp2) { + image = LsfgImage(device, memory_allocator, extent); + } + out_image = LsfgImage(device, memory_allocator, extent, LSFG_MOTION_FORMAT); + + std::vector layouts; + for (size_t slot = 0; slot < LSFG_GENERATION_SLOTS; ++slot) { + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + layouts.push_back(passes[0].SetLayout()); + } + for (size_t i = 1; i < LSFG_GAMMA_STAGES; ++i) { + layouts.push_back(passes[i].SetLayout()); + } + } + owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts); + + const VkSampler sampler = resources.GetSampler(); + const VkSampler border_sampler = resources.GetSampler( + VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, VK_COMPARE_OP_NEVER, true); + const VkSampler edge_sampler = + resources.GetSampler(VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, VK_COMPARE_OP_ALWAYS, false); + + size_t next = 0; + for (size_t slot = 0; slot < LSFG_GENERATION_SLOTS; ++slot) { + Generation& pass = generations[slot]; + const VkBuffer buffer = + resources.GetBuffer(LsfgSlotTimestamp(slot), previous == nullptr); + + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + pass.first_descriptor_sets[i] = owned_sets[next++]; + } + for (size_t i = 0; i < LSFG_GAMMA_STAGES - 1; ++i) { + pass.descriptor_sets[i] = owned_sets[next++]; + } + + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + LsfgDescriptorWriter(pass.first_descriptor_sets[i]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(border_sampler) + .AddSampler(edge_sampler) + .AddSampledImages((*inputs)[(i + 2) % LSFG_HISTORY_SLOTS]) + .AddSampledImages((*inputs)[i % LSFG_HISTORY_SLOTS]) + .AddSampledImage(previous) + .AddStorageImages(temp1) + .Build(device); + } + LsfgDescriptorWriter(pass.descriptor_sets[0]) + .AddSampler(sampler) + .AddSampledImages(temp1) + .AddStorageImages(temp2) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[1]) + .AddSampler(sampler) + .AddSampledImages(temp2) + .AddStorageImage(temp1[0]) + .AddStorageImage(temp1[1]) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[2]) + .AddSampler(sampler) + .AddSampledImage(temp1[0]) + .AddSampledImage(temp1[1]) + .AddStorageImages(temp2) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[3]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(sampler) + .AddSampler(edge_sampler) + .AddSampledImages(temp2) + .AddSampledImage(previous) + .AddSampledImage(*flow_input) + .AddStorageImage(out_image) + .Build(device); + } +} + +void LsfgGamma::Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t slot) { + const Generation& pass = generations[slot]; + + const VkExtent2D extent = temp1[0].Extent(); + const u32 groups_x = GroupCount(extent.width); + const u32 groups_y = GroupCount(extent.height); + + const size_t history = frame_count % LSFG_HISTORY_SLOTS; + const size_t previous_history = (frame_count + 2) % LSFG_HISTORY_SLOTS; + + LsfgBarriers(cmdbuf) + .WriteToReadAll((*inputs)[previous_history]) + .WriteToReadAll((*inputs)[history]) + .WriteToRead(previous) + .ReadToWriteAll(temp1) + .Build(); + passes[0].Bind(cmdbuf, pass.first_descriptor_sets[history]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp1).ReadToWriteAll(temp2).Build(); + passes[1].Bind(cmdbuf, pass.descriptor_sets[0]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToReadAll(temp2) + .ReadToWrite(temp1[0]) + .ReadToWrite(temp1[1]) + .Build(); + passes[2].Bind(cmdbuf, pass.descriptor_sets[1]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToRead(temp1[0]) + .WriteToRead(temp1[1]) + .ReadToWriteAll(temp2) + .Build(); + passes[3].Bind(cmdbuf, pass.descriptor_sets[2]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToReadAll(temp2) + .WriteToRead(previous) + .WriteToRead(*flow_input) + .ReadToWrite(out_image) + .Build(); + passes[4].Bind(cmdbuf, pass.descriptor_sets[3]); + cmdbuf.Dispatch(groups_x, groups_y, 1); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_gamma.h b/src/video_core/renderer_vulkan/present/lsfg_gamma.h new file mode 100644 index 0000000000..848c380bfe --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_gamma.h @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" +#include "video_core/renderer_vulkan/present/lsfg_common.h" + +namespace Vulkan { + +class Device; +class LsfgShaders; + +constexpr size_t LSFG_GAMMA_STAGES = 5; +constexpr size_t LSFG_GAMMA_TEMPS = 3; + +class LsfgGamma { +public: + LsfgGamma() = default; + LsfgGamma(const Device& device, MemoryAllocator& memory_allocator, const LsfgShaders& shaders, + LsfgResources& resources, vk::DescriptorPool& descriptor_pool, + LsfgImageHistory& inputs, LsfgImage& flow_input, LsfgImage* previous); + + void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t slot); + + [[nodiscard]] LsfgImage& Output() { + return out_image; + } + +private: + struct Generation { + std::array first_descriptor_sets{}; + std::array descriptor_sets{}; + }; + + LsfgImageHistory* inputs{}; + LsfgImage* flow_input{}; + LsfgImage* previous{}; + + std::array passes; + std::array generations{}; + vk::DescriptorSets owned_sets; + + std::array temp1; + LsfgImagePair temp2; + LsfgImage out_image; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_generate.cpp b/src/video_core/renderer_vulkan/present/lsfg_generate.cpp new file mode 100644 index 0000000000..4473b59a2f --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_generate.cpp @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "video_core/frame_gen/lossless_dll.h" +#include "video_core/renderer_vulkan/present/lsfg_generate.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr u32 DISPATCH_TILE_SHIFT = 4; + +[[nodiscard]] u32 GroupCount(u32 size) { + return (size + (1u << DISPATCH_TILE_SHIFT) - 1) >> DISPATCH_TILE_SHIFT; +} + +VkImageMemoryBarrier MakeTargetBarrier(VkImage image, VkAccessFlags src_access, + VkAccessFlags dst_access, VkImageLayout old_layout) { + return VkImageMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = src_access, + .dstAccessMask = dst_access, + .oldLayout = old_layout, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; +} + +} // Anonymous namespace + +LsfgGenerate::LsfgGenerate(const Device& device, const LsfgShaders& shaders, + LsfgResources& resources, vk::DescriptorPool& descriptor_pool, + LsfgImagePair& frames_, LsfgImage& motion_, LsfgImage& detail1_, + LsfgImage& detail2_) + : frames{&frames_}, motion{&motion_}, detail1{&detail1_}, detail2{&detail2_} { + using namespace VideoCore::FrameGen::PerformanceShader; + + pass = LsfgPass(device, shaders, GENERATE, + {{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLER}, + {5, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + + sampler = resources.GetSampler(); + edge_sampler = + resources.GetSampler(VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, VK_COMPARE_OP_ALWAYS, false); + + const std::vector layouts( + LSFG_GENERATION_SLOTS * LSFG_MAX_TARGETS * 2, pass.SetLayout()); + owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts); + + size_t next = 0; + for (size_t slot = 0; slot < LSFG_GENERATION_SLOTS; ++slot) { + Generation& target = generations[slot]; + target.buffer = resources.GetBuffer(LsfgSlotTimestamp(slot)); + + for (auto& entry : target.targets) { + for (auto& set : entry.descriptor_sets) { + set = owned_sets[next++]; + } + } + } +} + +void LsfgGenerate::SetTarget(const Device& device, size_t slot, u32 target, VkImageView view) { + Target& entry = generations[slot].targets[target]; + if (entry.view == view) { + return; + } + entry.view = view; + + for (size_t i = 0; i < entry.descriptor_sets.size(); ++i) { + LsfgDescriptorWriter(entry.descriptor_sets[i]) + .AddUniformBuffer(generations[slot].buffer, LsfgResources::BufferSize()) + .AddSampler(sampler) + .AddSampler(edge_sampler) + .AddSampledImage((*frames)[1 - i]) + .AddSampledImage((*frames)[i]) + .AddSampledImage(*motion) + .AddSampledImage(*detail1) + .AddSampledImage(*detail2) + .AddStorageView(view) + .Build(device); + } +} + +void LsfgGenerate::Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t slot, u32 target, + VkImage image, VkExtent2D extent) { + const Target& entry = generations[slot].targets[target]; + + LsfgBarriers(cmdbuf) + .WriteToReadAll(*frames) + .WriteToRead(*motion) + .WriteToRead(*detail1) + .WriteToRead(*detail2) + .DiscardToWrite(image) + .Build(); + + pass.Bind(cmdbuf, entry.descriptor_sets[frame_count % entry.descriptor_sets.size()]); + cmdbuf.Dispatch(GroupCount(extent.width), GroupCount(extent.height), 1); + + const std::array after{MakeTargetBarrier( + image, VK_ACCESS_SHADER_WRITE_BIT, + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT, + VK_IMAGE_LAYOUT_GENERAL)}; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, {}, {}, after); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_generate.h b/src/video_core/renderer_vulkan/present/lsfg_generate.h new file mode 100644 index 0000000000..75cbd8d2e4 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_generate.h @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" +#include "video_core/renderer_vulkan/present/lsfg_common.h" + +namespace Vulkan { + +class Device; +class LsfgShaders; + +class LsfgGenerate { +public: + LsfgGenerate() = default; + LsfgGenerate(const Device& device, const LsfgShaders& shaders, LsfgResources& resources, + vk::DescriptorPool& descriptor_pool, LsfgImagePair& frames, LsfgImage& motion, + LsfgImage& detail1, LsfgImage& detail2); + + void SetTarget(const Device& device, size_t slot, u32 target, VkImageView view); + + void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t slot, u32 target, + VkImage image, VkExtent2D extent); + +private: + struct Target { + std::array descriptor_sets{}; + VkImageView view{}; + }; + + struct Generation { + std::array targets{}; + VkBuffer buffer{}; + }; + + LsfgImagePair* frames{}; + LsfgImage* motion{}; + LsfgImage* detail1{}; + LsfgImage* detail2{}; + VkSampler sampler{}; + VkSampler edge_sampler{}; + + LsfgPass pass; + std::array generations{}; + vk::DescriptorSets owned_sets; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_mipmaps.cpp b/src/video_core/renderer_vulkan/present/lsfg_mipmaps.cpp new file mode 100644 index 0000000000..3f31f58d68 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_mipmaps.cpp @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +#include "video_core/frame_gen/lossless_dll.h" +#include "video_core/renderer_vulkan/present/lsfg_mipmaps.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr u32 DISPATCH_TILE_SHIFT = 6; + +[[nodiscard]] u32 GroupCount(u32 size) { + return (size + (1u << DISPATCH_TILE_SHIFT) - 1) >> DISPATCH_TILE_SHIFT; +} + +} // Anonymous namespace + +LsfgMipmaps::LsfgMipmaps(const Device& device, MemoryAllocator& memory_allocator, + const LsfgShaders& shaders, LsfgResources& resources, + vk::DescriptorPool& descriptor_pool, LsfgImagePair& frames_, + f32 flow_scale) + : frames{&frames_} { + using namespace VideoCore::FrameGen::PerformanceShader; + + pass = LsfgPass(device, shaders, MIPMAPS, + {{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + {1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {LSFG_MIP_LEVELS, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + + const VkExtent2D input_extent = (*frames)[0].Extent(); + flow_extent = VkExtent2D{ + .width = std::max(1u, static_cast(static_cast(input_extent.width) * flow_scale)), + .height = std::max(1u, static_cast(static_cast(input_extent.height) * flow_scale)), + }; + + for (size_t i = 0; i < LSFG_MIP_LEVELS; ++i) { + const VkExtent2D level_extent{ + .width = flow_extent.width >> i, + .height = flow_extent.height >> i, + }; + out_images[i] = LsfgImage(device, memory_allocator, level_extent, LSFG_FLOW_FORMAT); + } + + const std::vector layouts(descriptor_sets.size(), pass.SetLayout()); + owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts); + + const VkSampler sampler = resources.GetSampler(); + const VkBuffer buffer = resources.GetBuffer(); + + for (size_t i = 0; i < descriptor_sets.size(); ++i) { + descriptor_sets[i] = owned_sets[i]; + LsfgDescriptorWriter(descriptor_sets[i]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(sampler) + .AddSampledImage((*frames)[i]) + .AddStorageImages(out_images) + .Build(device); + } +} + +void LsfgMipmaps::Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count) { + const size_t slot = frame_count % descriptor_sets.size(); + + LsfgBarriers(cmdbuf).WriteToRead((*frames)[slot]).ReadToWriteAll(out_images).Build(); + + pass.Bind(cmdbuf, descriptor_sets[slot]); + cmdbuf.Dispatch(GroupCount(flow_extent.width), GroupCount(flow_extent.height), 1); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_mipmaps.h b/src/video_core/renderer_vulkan/present/lsfg_mipmaps.h new file mode 100644 index 0000000000..7fa5b7b224 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_mipmaps.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" +#include "video_core/renderer_vulkan/present/lsfg_common.h" + +namespace Vulkan { + +class Device; +class LsfgShaders; + +constexpr size_t LSFG_MIP_LEVELS = 7; + +class LsfgMipmaps { +public: + LsfgMipmaps() = default; + LsfgMipmaps(const Device& device, MemoryAllocator& memory_allocator, const LsfgShaders& shaders, + LsfgResources& resources, vk::DescriptorPool& descriptor_pool, + LsfgImagePair& frames, f32 flow_scale); + + void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count); + + [[nodiscard]] LsfgImage& Output(size_t level) { + return out_images[level]; + } + +private: + LsfgImagePair* frames{}; + + LsfgPass pass; + std::array descriptor_sets{}; + vk::DescriptorSets owned_sets; + + VkExtent2D flow_extent{}; + std::array out_images; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_shaders.cpp b/src/video_core/renderer_vulkan/present/lsfg_shaders.cpp new file mode 100644 index 0000000000..0955a9aa54 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_shaders.cpp @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "common/settings.h" +#include "video_core/frame_gen/lossless_dll.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +LsfgShaders::LsfgShaders(const Device& device) { + if (!device.IsVulkanMemoryModelSupported() || !device.HasNullDescriptor()) { + return; + } + + const bool prefer_fp16 = + Settings::values.frame_gen_fp16.GetValue() && device.IsFloat16Supported(); + + VideoCore::FrameGen::ShaderModules code; + if (VideoCore::FrameGen::LoadShaderModules(code, prefer_fp16) != + VideoCore::FrameGen::LosslessStatus::Ok) { + return; + } + + for (const auto& [id, words] : code) { + modules.emplace(id, CreateWrappedShaderModule(device, words)); + } + valid = true; +} + +VkShaderModule LsfgShaders::Get(u32 shader_id) const { + const auto hit = modules.find(shader_id); + return hit == modules.end() ? VK_NULL_HANDLE : *hit->second; +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_shaders.h b/src/video_core/renderer_vulkan/present/lsfg_shaders.h new file mode 100644 index 0000000000..cab37110d6 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_shaders.h @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "common/common_types.h" +#include "video_core/vulkan_common/vulkan_wrapper.h" + +namespace Vulkan { + +class Device; + +class LsfgShaders { +public: + explicit LsfgShaders(const Device& device); + + [[nodiscard]] bool IsValid() const { + return valid; + } + + [[nodiscard]] VkShaderModule Get(u32 shader_id) const; + +private: + std::map modules; + bool valid{}; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/util.cpp b/src/video_core/renderer_vulkan/present/util.cpp index 2b83902bc0..11250687d6 100644 --- a/src/video_core/renderer_vulkan/present/util.cpp +++ b/src/video_core/renderer_vulkan/present/util.cpp @@ -320,15 +320,16 @@ vk::DescriptorPool CreateWrappedDescriptorPool(const Device& device, size_t max_ }); } -vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout( - const Device& device, std::initializer_list types) { +vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout(const Device& device, + std::span types, + VkShaderStageFlags stages) { std::vector bindings(types.size()); for (size_t i = 0; i < types.size(); i++) { bindings[i] = { .binding = static_cast(i), - .descriptorType = std::data(types)[i], + .descriptorType = types[i], .descriptorCount = 1, - .stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + .stageFlags = stages, .pImmutableSamplers = nullptr, }; } @@ -342,6 +343,13 @@ vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout( }); } +vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout( + const Device& device, std::initializer_list types, + VkShaderStageFlags stages) { + return CreateWrappedDescriptorSetLayout( + device, std::span{std::data(types), types.size()}, stages); +} + vk::DescriptorSets CreateWrappedDescriptorSets(vk::DescriptorPool& pool, vk::Span layouts) { return pool.Allocate(VkDescriptorSetAllocateInfo{ @@ -353,6 +361,28 @@ vk::DescriptorSets CreateWrappedDescriptorSets(vk::DescriptorPool& pool, }); } +vk::Pipeline CreateWrappedComputePipeline(const Device& device, vk::PipelineLayout& layout, + VkShaderModule shader) { + return device.GetLogical().CreateComputePipeline(VkComputePipelineCreateInfo{ + .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .stage = + { + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .stage = VK_SHADER_STAGE_COMPUTE_BIT, + .module = shader, + .pName = "main", + .pSpecializationInfo = nullptr, + }, + .layout = *layout, + .basePipelineHandle = VK_NULL_HANDLE, + .basePipelineIndex = 0, + }); +} + vk::PipelineLayout CreateWrappedPipelineLayout(const Device& device, vk::DescriptorSetLayout& layout) { return device.GetLogical().CreatePipelineLayout(VkPipelineLayoutCreateInfo{ diff --git a/src/video_core/renderer_vulkan/present/util.h b/src/video_core/renderer_vulkan/present/util.h index 38cc6203c5..ecc263286d 100644 --- a/src/video_core/renderer_vulkan/present/util.h +++ b/src/video_core/renderer_vulkan/present/util.h @@ -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 // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project @@ -38,7 +38,11 @@ vk::DescriptorPool CreateWrappedDescriptorPool(const Device& device, size_t max_ std::initializer_list types = { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER}); vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout( - const Device& device, std::initializer_list types); + const Device& device, std::initializer_list types, + VkShaderStageFlags stages = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT); +vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout(const Device& device, + std::span types, + VkShaderStageFlags stages); vk::DescriptorSets CreateWrappedDescriptorSets(vk::DescriptorPool& pool, vk::Span layouts); vk::PipelineLayout CreateWrappedPipelineLayout(const Device& device, @@ -46,6 +50,8 @@ vk::PipelineLayout CreateWrappedPipelineLayout(const Device& device, vk::Pipeline CreateWrappedPipeline(const Device& device, vk::RenderPass& renderpass, vk::PipelineLayout& layout, std::tuple shaders); +vk::Pipeline CreateWrappedComputePipeline(const Device& device, vk::PipelineLayout& layout, + VkShaderModule shader); vk::Pipeline CreateWrappedPremultipliedBlendingPipeline( const Device& device, vk::RenderPass& renderpass, vk::PipelineLayout& layout, std::tuple shaders); diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp index b0a7d0c579..411a9c5fe2 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp +++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp @@ -49,6 +49,21 @@ constexpr VkExtent2D CaptureImageSize{ .height = VideoCore::Capture::LinearHeight, }; +[[nodiscard]] VkExtent2D GuestExtent(std::span framebuffers) { + if (framebuffers.empty()) { + return VkExtent2D{}; + } + + const auto& framebuffer = framebuffers.front(); + if (framebuffer.crop_rect.IsEmpty()) { + return VkExtent2D{.width = framebuffer.width, .height = framebuffer.height}; + } + return VkExtent2D{ + .width = static_cast(framebuffer.crop_rect.GetWidth()), + .height = static_cast(framebuffer.crop_rect.GetHeight()), + }; +} + constexpr VkExtent3D CaptureImageExtent{ .width = VideoCore::Capture::LinearWidth, .height = VideoCore::Capture::LinearHeight, @@ -155,7 +170,8 @@ try present_manager, scheduler, PresentFiltersForAppletCapture) - , rasterizer(render_window, gpu, device_memory, device, memory_allocator, state_tracker, scheduler) { + , rasterizer(render_window, gpu, device_memory, device, memory_allocator, state_tracker, scheduler) + , frame_gen(memory_allocator, scheduler) { if (Settings::values.renderer_force_max_clock.GetValue() && device.ShouldBoostClocks()) { turbo_mode.emplace(instance, dld); @@ -191,9 +207,24 @@ void RendererVulkan::Composite(std::span framebu blit_swapchain.DrawToFrame(device, rasterizer, frame, framebuffers, render_window.GetFramebufferLayout(), swapchain.GetImageCount(), swapchain.GetImageViewFormat()); + + void(frame_gen.WantedGenerations(present_manager.MaxExtraFrames())); + + frame_gen.Process(device, frame, swapchain.GetImageFormat(), GuestExtent(framebuffers)); + + const size_t generated_frames = frame_gen.GeneratedFrameCount(); + for (size_t generation = 0; generation < generated_frames; ++generation) { + Frame* generated = present_manager.GetRenderFrame(); + blit_swapchain.PrepareFrame(device, generated, render_window.GetFramebufferLayout()); + frame_gen.GenerateInto(device, generated, generation); + scheduler.Flush(*generated->render_ready); + present_manager.Present(generated); + } + scheduler.Flush(*frame->render_ready); present_manager.Present(frame); + scheduler.DispatchWork(); gpu.RendererFrameEndNotify(); rasterizer.TickFrame(); diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.h b/src/video_core/renderer_vulkan/renderer_vulkan.h index 4fb88b29de..151d829aed 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.h +++ b/src/video_core/renderer_vulkan/renderer_vulkan.h @@ -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 // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project @@ -13,6 +13,7 @@ #include "common/dynamic_library.h" #include "video_core/host1x/gpu_device_memory_manager.h" #include "video_core/renderer_base.h" +#include "video_core/renderer_vulkan/present/frame_gen.h" #include "video_core/renderer_vulkan/vk_blit_screen.h" #include "video_core/renderer_vulkan/vk_present_manager.h" #include "video_core/renderer_vulkan/vk_rasterizer.h" @@ -95,6 +96,7 @@ private: BlitScreen blit_capture; BlitScreen blit_applet; RasterizerVulkan rasterizer; + FrameGen frame_gen; std::optional turbo_mode; Frame applet_frame; diff --git a/src/video_core/renderer_vulkan/vk_blit_screen.cpp b/src/video_core/renderer_vulkan/vk_blit_screen.cpp index 05314fea27..96eeeb8045 100644 --- a/src/video_core/renderer_vulkan/vk_blit_screen.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_screen.cpp @@ -85,6 +85,17 @@ void BlitScreen::SetWindowAdaptPass(const Device& device) { } } +void BlitScreen::PrepareFrame(const Device& device, Frame* frame, + const Layout::FramebufferLayout& layout) { + if (!window_adapt || (frame->width == layout.width && frame->height == layout.height)) { + return; + } + + WaitIdle(device); + present_manager.RecreateFrame(frame, layout.width, layout.height, swapchain_view_format, + window_adapt->GetRenderPass()); +} + void BlitScreen::DrawToFrame(const Device& device, RasterizerVulkan& rasterizer, Frame* frame, std::span framebuffers, const Layout::FramebufferLayout& layout, diff --git a/src/video_core/renderer_vulkan/vk_blit_screen.h b/src/video_core/renderer_vulkan/vk_blit_screen.h index e50f0bcf33..56b429ae53 100644 --- a/src/video_core/renderer_vulkan/vk_blit_screen.h +++ b/src/video_core/renderer_vulkan/vk_blit_screen.h @@ -60,6 +60,8 @@ public: const Layout::FramebufferLayout& layout, size_t current_swapchain_image_count, VkFormat current_swapchain_view_format); + void PrepareFrame(const Device& device, Frame* frame, const Layout::FramebufferLayout& layout); + [[nodiscard]] vk::Framebuffer CreateFramebuffer(const Device& device, const Layout::FramebufferLayout& layout, VkImageView image_view, VkFormat current_view_format); diff --git a/src/video_core/renderer_vulkan/vk_present_manager.cpp b/src/video_core/renderer_vulkan/vk_present_manager.cpp index 90ebb55d38..580d35e6d4 100644 --- a/src/video_core/renderer_vulkan/vk_present_manager.cpp +++ b/src/video_core/renderer_vulkan/vk_present_manager.cpp @@ -7,6 +7,7 @@ #include "common/settings.h" #include "common/thread.h" #include "core/frontend/emu_window.h" +#include "video_core/renderer_vulkan/present/lsfg_common.h" #include "video_core/renderer_vulkan/vk_present_manager.h" #include "video_core/renderer_vulkan/vk_scheduler.h" #include "video_core/renderer_vulkan/vk_swapchain.h" @@ -19,6 +20,17 @@ namespace Vulkan { namespace { +constexpr size_t MAX_FRAMES_IN_FLIGHT = 7; +static_assert(MAX_FRAMES_IN_FLIGHT <= LSFG_MAX_TARGETS); + +bool CanStoreToFrame(const vk::PhysicalDevice& physical_device, VkFormat format) { + if (!Settings::values.frame_gen.GetValue()) { + return false; + } + const VkFormatProperties props{physical_device.GetFormatProperties(format)}; + return (props.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0; +} + bool CanBlitToSwapchain(const vk::PhysicalDevice& physical_device, VkFormat format) { const VkFormatProperties props{physical_device.GetFormatProperties(format)}; return (props.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT); @@ -110,6 +122,7 @@ PresentManager::PresentManager(const vk::Instance& instance_, , swapchain{swapchain_} , surface{surface_} , blit_supported{CanBlitToSwapchain(device.GetPhysical(), swapchain.GetImageViewFormat())} + , storage_supported{CanStoreToFrame(device.GetPhysical(), swapchain.GetImageFormat())} , use_present_thread{Settings::values.async_presentation.GetValue()} { SetImageCount(); @@ -127,6 +140,7 @@ PresentManager::PresentManager(const vk::Instance& instance_, frames.resize(image_count); for (u32 i = 0; i < frames.size(); i++) { Frame& frame = frames[i]; + frame.index = i; frame.cmdbuf = vk::CommandBuffer{cmdbuffers[i], device.GetDispatchLoader()}; frame.render_ready = dld.CreateSemaphore({ .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, @@ -179,6 +193,10 @@ void PresentManager::Present(Frame* frame) { } } +size_t PresentManager::MaxExtraFrames() const { + return image_count - 1; +} + void PresentManager::RecreateFrame(Frame* frame, u32 width, u32 height, VkFormat image_view_format, VkRenderPass rd) { auto& dld = device.GetLogical(); @@ -186,6 +204,9 @@ void PresentManager::RecreateFrame(Frame* frame, u32 width, u32 height, VkFormat frame->width = width; frame->height = height; + const VkImageUsageFlags storage_usage = + storage_supported ? static_cast(VK_IMAGE_USAGE_STORAGE_BIT) : 0; + frame->image = memory_allocator.CreateImage({ .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .pNext = nullptr, @@ -202,7 +223,8 @@ void PresentManager::RecreateFrame(Frame* frame, u32 width, u32 height, VkFormat .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_OPTIMAL, - .usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, + .usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | storage_usage, .sharingMode = VK_SHARING_MODE_EXCLUSIVE, .queueFamilyIndexCount = 0, .pQueueFamilyIndices = nullptr, @@ -233,6 +255,33 @@ void PresentManager::RecreateFrame(Frame* frame, u32 width, u32 height, VkFormat }, }); + frame->storage_view = vk::ImageView{}; + if (storage_supported) { + frame->storage_view = dld.CreateImageView({ + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .image = *frame->image, + .viewType = VK_IMAGE_VIEW_TYPE_2D, + .format = swapchain.GetImageFormat(), + .components = + { + .r = VK_COMPONENT_SWIZZLE_IDENTITY, + .g = VK_COMPONENT_SWIZZLE_IDENTITY, + .b = VK_COMPONENT_SWIZZLE_IDENTITY, + .a = VK_COMPONENT_SWIZZLE_IDENTITY, + }, + .subresourceRange = + { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }); + } + const VkImageView image_view{*frame->image_view}; frame->framebuffer = dld.CreateFramebuffer({ .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, @@ -301,7 +350,11 @@ void PresentManager::SetImageCount() { // We cannot have more than 7 images in flight at any given time. // FRAMES_IN_FLIGHT is 8, and the cache TICKS_TO_DESTROY is 8. // Mali drivers will give us 6. - image_count = std::min(swapchain.GetImageCount(), 7); + const size_t generations = Settings::FrameGenMaxGenerations(); + const size_t queued_composites = Settings::values.frame_gen_queue_target.GetValue() + 1; + image_count = + std::clamp((generations + 1) * queued_composites, swapchain.GetImageCount(), + MAX_FRAMES_IN_FLIGHT); } void PresentManager::CopyToSwapchain(Frame* frame) { diff --git a/src/video_core/renderer_vulkan/vk_present_manager.h b/src/video_core/renderer_vulkan/vk_present_manager.h index c51f8ed77f..577a82098a 100644 --- a/src/video_core/renderer_vulkan/vk_present_manager.h +++ b/src/video_core/renderer_vulkan/vk_present_manager.h @@ -28,8 +28,10 @@ class Swapchain; struct Frame { u32 width; u32 height; + u32 index; vk::Image image; vk::ImageView image_view; + vk::ImageView storage_view; vk::Framebuffer framebuffer; vk::CommandBuffer cmdbuf; vk::Semaphore render_ready; @@ -60,6 +62,9 @@ public: /// Waits for the present thread to finish presenting all queued frames. void WaitPresent(); + /// How many additional frames can be queued without stalling the render thread + [[nodiscard]] size_t MaxExtraFrames() const; + private: void PresentThread(std::stop_token token); @@ -90,6 +95,7 @@ private: std::mutex free_mutex; std::jthread present_thread; bool blit_supported; + bool storage_supported; bool use_present_thread; std::size_t image_count{}; }; diff --git a/src/video_core/renderer_vulkan/vk_scheduler.h b/src/video_core/renderer_vulkan/vk_scheduler.h index b810862f73..c2aa3fb996 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.h +++ b/src/video_core/renderer_vulkan/vk_scheduler.h @@ -146,9 +146,10 @@ public: frame_counter++; auto target_time = start_time + frame_interval * frame_counter; if (target_time >= now) { + constexpr auto spin_tail = std::chrono::milliseconds(1); auto sleep_time = target_time - now; - if (sleep_time > std::chrono::milliseconds(15)) { - std::this_thread::sleep_for(sleep_time - std::chrono::milliseconds(1)); + if (sleep_time > spin_tail * 2) { + std::this_thread::sleep_for(sleep_time - spin_tail); } while (std::chrono::steady_clock::now() < target_time) { std::this_thread::yield(); diff --git a/src/video_core/renderer_vulkan/vk_swapchain.cpp b/src/video_core/renderer_vulkan/vk_swapchain.cpp index fdbad16721..fea5398557 100644 --- a/src/video_core/renderer_vulkan/vk_swapchain.cpp +++ b/src/video_core/renderer_vulkan/vk_swapchain.cpp @@ -48,6 +48,9 @@ static VkPresentModeKHR ChooseSwapPresentMode(bool has_imm, bool has_mailbox, Settings::VSyncMode setting = [has_imm, has_mailbox]() { // Choose Mailbox or Immediate if unlocked and those modes are supported const auto mode = Settings::values.vsync_mode.GetValue(); + if (Settings::values.frame_gen.GetValue()) { + return mode == Settings::VSyncMode::FifoRelaxed ? mode : Settings::VSyncMode::Fifo; + } if (Settings::values.use_speed_limit.GetValue() && Settings::values.current_speed_mode.GetValue() != Settings::SpeedMode::Turbo) { return mode; diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index 2708281297..1ae073c279 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -37,7 +37,8 @@ VK_DEFINE_HANDLE(VmaAllocator) FEATURE(EXT, HostQueryReset, HOST_QUERY_RESET, host_query_reset) \ FEATURE(KHR, 8BitStorage, 8BIT_STORAGE, bit8_storage) \ FEATURE(KHR, BufferDeviceAddress, BUFFER_DEVICE_ADDRESS, buffer_device_address) \ - FEATURE(KHR, TimelineSemaphore, TIMELINE_SEMAPHORE, timeline_semaphore) + FEATURE(KHR, TimelineSemaphore, TIMELINE_SEMAPHORE, timeline_semaphore) \ + FEATURE(KHR, VulkanMemoryModel, VULKAN_MEMORY_MODEL, vulkan_memory_model) #define FOR_EACH_VK_FEATURE_1_3(FEATURE) \ FEATURE(EXT, ImageRobustness, IMAGE_ROBUSTNESS, robust_image_access) \ @@ -416,6 +417,11 @@ FN_MAX_LIMIT_LIST return features.shader_float16_int8.shaderFloat16; } + /// Returns true if the device can run shaders built against the Vulkan memory model. + bool IsVulkanMemoryModelSupported() const { + return features.vulkan_memory_model.vulkanMemoryModel; + } + /// Returns true if the device supports int8 natively. bool IsInt8Supported() const { return features.shader_float16_int8.shaderInt8; diff --git a/src/yuzu/configuration/shared_widget.cpp b/src/yuzu/configuration/shared_widget.cpp index ce11763f91..5f28299065 100644 --- a/src/yuzu/configuration/shared_widget.cpp +++ b/src/yuzu/configuration/shared_widget.cpp @@ -772,10 +772,24 @@ Builder::Builder(QWidget* parent_, bool runtime_lock_) Builder::~Builder() = default; +static bool IsAndroidOnly(const Settings::BasicSetting& setting) { + const auto id = setting.Id(); + return id == Settings::values.frame_gen.Id() || + id == Settings::values.frame_gen_multiplier.Id() || + id == Settings::values.frame_gen_flow_scale.Id() || + id == Settings::values.frame_gen_fp16.Id() || + id == Settings::values.frame_gen_dump_flow.Id() || + id == Settings::values.emulate_bgr565.Id(); +} + Widget* Builder::BuildWidget(Settings::BasicSetting* setting, std::vector>& apply_funcs, RequestType request, bool managed, float multiplier, Settings::BasicSetting* other_setting, const QString& suffix) const { + if (IsAndroidOnly(*setting)) { + return nullptr; + } + if (!Settings::IsConfiguringGlobal() && !setting->Switchable()) { return nullptr; }