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..e29b7bc8c4 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,8 @@ 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_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/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt index 63ac7b0630..22d09769c1 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 @@ -607,6 +607,20 @@ 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( + 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/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt index 667141725d..0082e425ae 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt @@ -33,6 +33,7 @@ import org.yuzu.yuzu_emu.features.input.NativeInput import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.view.PathSetting import org.yuzu.yuzu_emu.fragments.MessageDialogFragment +import org.yuzu.yuzu_emu.fragments.ProgressDialogFragment import org.yuzu.yuzu_emu.utils.PathUtil import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins import org.yuzu.yuzu_emu.utils.* @@ -114,6 +115,10 @@ class SettingsFragment : Fragment() { viewLifecycleOwner, resetState = { settingsViewModel.setShouldReloadSettingsList(false) } ) { if (it) presenter.loadSettingsList() } + settingsViewModel.shouldShowLosslessInstaller.collect( + viewLifecycleOwner, + resetState = { settingsViewModel.setShouldShowLosslessInstaller(false) } + ) { if (it) losslessDllPickerLauncher.launch(arrayOf("*/*")) } settingsViewModel.adapterItemChanged.collect( viewLifecycleOwner, resetState = { settingsViewModel.setAdapterItemChanged(-1) } @@ -267,6 +272,33 @@ private fun getPlayerIndex(): Int = directoryPickerLauncher.launch(null) } + private val losslessDllPickerLauncher = registerForActivityResult( + ActivityResultContracts.OpenDocument() + ) { uri -> + if (uri == null) { + return@registerForActivityResult + } + + val resultStrings = resources.getStringArray(R.array.losslessDllResults) + ProgressDialogFragment.newInstance( + requireActivity(), + R.string.lossless_scaling_installing, + false + ) { _, _ -> + val result = LosslessScalingHelper.install(uri) + if (result == LosslessScalingHelper.RESULT_OK) { + getString(R.string.lossless_scaling_install_success) + } else { + MessageDialogFragment.newInstance( + titleId = R.string.lossless_scaling_install_failed, + descriptionString = resultStrings[result] + ) + } + }.apply { + onDialogComplete = { settingsViewModel.setShouldReloadSettingsList(true) } + }.show(parentFragmentManager, ProgressDialogFragment.TAG) + } + private val directoryPickerLauncher = registerForActivityResult( ActivityResultContracts.OpenDocumentTree() ) { uri -> diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 594099f159..a15c7fb73f 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,46 @@ class SettingsFragmentPresenter( } } + private fun addFrameGenSettings(sl: ArrayList) { + sl.apply { + add(HeaderSetting(R.string.frame_gen)) + + if (!LosslessScalingHelper.isSupportedByGpu()) { + add( + RunnableSetting( + titleId = R.string.frame_gen_unsupported, + descriptionId = R.string.frame_gen_unsupported_description, + isRunnable = false + ) {} + ) + return@apply + } + + if (!LosslessScalingHelper.isInstalled()) { + add( + RunnableSetting( + titleId = R.string.lossless_scaling_install, + descriptionId = R.string.lossless_scaling_install_description, + isRunnable = !NativeLibrary.isRunning(), + iconId = R.drawable.ic_install + ) { settingsViewModel.setShouldShowLosslessInstaller(true) } + ) + return@apply + } + + add(BooleanSetting.RENDERER_FRAME_GEN.key) + add(BooleanSetting.RENDERER_FRAME_GEN_DUMP_FLOW.key) + add( + RunnableSetting( + titleId = R.string.lossless_scaling_replace, + descriptionId = R.string.lossless_scaling_replace_description, + isRunnable = !NativeLibrary.isRunning(), + iconId = R.drawable.ic_install + ) { settingsViewModel.setShouldShowLosslessInstaller(true) } + ) + } + } + private fun isSharpnessScalingFilterSelected(): Boolean { val needsGlobal = getNeedsGlobalForKey(IntSetting.RENDERER_SCALING_FILTER.key) val selectedFilter = IntSetting.RENDERER_SCALING_FILTER.getInt(needsGlobal) @@ -280,6 +321,8 @@ class SettingsFragmentPresenter( } add(IntSetting.RENDERER_ANTI_ALIASING.key) + addFrameGenSettings(this) + add(HeaderSetting(R.string.advanced)) add(IntSetting.RENDERER_ACCURACY.key) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt index b1914c3169..d9c3a7d9d3 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt @@ -36,6 +36,9 @@ class SettingsViewModel : ViewModel() { val shouldReloadSettingsList: StateFlow get() = _shouldReloadSettingsList private val _shouldReloadSettingsList = MutableStateFlow(false) + val shouldShowLosslessInstaller: StateFlow get() = _shouldShowLosslessInstaller + private val _shouldShowLosslessInstaller = MutableStateFlow(false) + val sliderProgress: StateFlow get() = _sliderProgress private val _sliderProgress = MutableStateFlow(-1) @@ -85,6 +88,10 @@ class SettingsViewModel : ViewModel() { _shouldReloadSettingsList.value = value } + fun setShouldShowLosslessInstaller(value: Boolean) { + _shouldShowLosslessInstaller.value = value + } + fun setSliderTextValue(value: Float, units: String) { _sliderProgress.value = value.toInt() _sliderTextValue.value = String.format( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index 37eda22c69..5973c60868 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt @@ -13,6 +13,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat @@ -44,6 +45,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 +172,18 @@ class HomeSettingsFragment : Fragment() { ) ) } + add( + HomeSetting( + R.string.lossless_scaling, + R.string.lossless_scaling_description, + R.drawable.ic_frames, + { onLosslessScalingClicked() }, + { true }, + 0, + 0, + LosslessScalingHelper.statusText + ) + ) add( HomeSetting( R.string.multiplayer, @@ -337,8 +351,51 @@ class HomeSettingsFragment : Fragment() { override fun onResume() { super.onResume() driverViewModel.updateDriverNameForGame(null) + LosslessScalingHelper.refreshStatus() } + private fun onLosslessScalingClicked() { + if (!LosslessScalingHelper.isInstalled()) { + getLosslessDllLauncher.launch(arrayOf("*/*")) + return + } + + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.lossless_scaling, + descriptionId = R.string.lossless_scaling_installed_description, + positiveButtonTitleId = R.string.lossless_scaling_replace, + positiveAction = { getLosslessDllLauncher.launch(arrayOf("*/*")) }, + showNegativeButton = true, + negativeButtonTitleId = R.string.lossless_scaling_remove, + negativeAction = { LosslessScalingHelper.remove() } + ).show(parentFragmentManager, MessageDialogFragment.TAG) + } + + private val getLosslessDllLauncher = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result == null) { + return@registerForActivityResult + } + + val resultStrings = resources.getStringArray(R.array.losslessDllResults) + ProgressDialogFragment.newInstance( + requireActivity(), + R.string.lossless_scaling_installing, + false + ) { _, _ -> + val installResult = LosslessScalingHelper.install(result) + if (installResult == LosslessScalingHelper.RESULT_OK) { + getString(R.string.lossless_scaling_install_success) + } else { + MessageDialogFragment.newInstance( + titleId = R.string.lossless_scaling_install_failed, + descriptionString = resultStrings[installResult] + ) + } + }.show(parentFragmentManager, ProgressDialogFragment.TAG) + } + override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SystemInfoDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SystemInfoDialogFragment.kt index 5c914eeb16..098e46e027 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 @@ -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/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/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index 1bad110d14..341d89d6b4 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -638,6 +638,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..d7abc3b42a 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -298,6 +298,34 @@ GPU driver fetcher GPU driver manager Install alternative drivers for potentially better performance or accuracy + Frame generation + Insert an interpolated frame between every pair of real frames using Lossless Scaling. Adds about one frame of input latency. + Dump flow pyramid + Write the optical flow mip levels 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. + Install Lossless.dll + Frame generation needs your own 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 + Lossless.dll is installed and contains every shader frame generation needs. + Replace + Remove + 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..20e5c364ae 100644 --- a/src/common/fs/fs_paths.h +++ b/src/common/fs/fs_paths.h @@ -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..794259da42 100644 --- a/src/common/fs/path_util.h +++ b/src/common/fs/path_util.h @@ -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.h b/src/common/settings.h index 0f35045622..b1b6cec526 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -388,6 +388,11 @@ struct Values { true, true}; + SwitchableSetting frame_gen{linkage, false, "frame_gen", Category::Renderer, + Specialization::Default, true, true}; + SwitchableSetting frame_gen_dump_flow{linkage, false, "frame_gen_dump_flow", + Category::Renderer}; + SwitchableSetting use_asynchronous_gpu_emulation{linkage, #ifdef __ANDROID__ false, diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 6720195c0e..b687c95115 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,12 @@ 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/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 +355,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..84acbdd5d3 --- /dev/null +++ b/src/video_core/frame_gen/lossless_dll.cpp @@ -0,0 +1,515 @@ +// 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 = 1; + +struct CacheHeader { + u32 magic; + u32 version; + u64 source_size; + u64 source_hash; + u32 module_count; +}; + +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]] LosslessStatus TranslateAll(const ResourceSpans& resources, + ShaderModules& out_modules) { + out_modules.clear(); + for (const u32 id : PerformanceShaderIds()) { + const auto hit = resources.find(id); + if (hit == resources.end()) { + return LosslessStatus::MissingShaders; + } + + 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, 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) { + 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()); +} + +LosslessStatus LoadShaderModules(ShaderModules& out_modules) { + 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(); + + if (ReadShaderCache(cache_path, source_size, source_hash, out_modules)) { + return LosslessStatus::Ok; + } + + ResourceSpans spans; + const LosslessStatus parse_status = ParseShaderSpans(image, spans); + if (parse_status != LosslessStatus::Ok) { + return parse_status; + } + + const LosslessStatus translate_status = TranslateAll(spans, out_modules); + 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()), + }; + 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..5c87a1afb6 --- /dev/null +++ b/src/video_core/frame_gen/lossless_dll.h @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// 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>; + +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}; +} // 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]] LosslessStatus LoadShaderModules(ShaderModules& out_modules); + +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..0221553b7c --- /dev/null +++ b/src/video_core/frame_gen/lsfg_translate.cpp @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include + +#include "video_core/frame_gen/lsfg_translate.h" + +namespace VideoCore::FrameGen { + +namespace { + +constexpr u32 DECORATION_LITERAL_WORD = 3; + +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); + } +} + +} // Anonymous namespace + +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..60dd104439 --- /dev/null +++ b/src/video_core/frame_gen/lsfg_translate.h @@ -0,0 +1,15 @@ +// 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]] 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..6cf3ac61dd --- /dev/null +++ b/src/video_core/renderer_vulkan/present/frame_gen.cpp @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#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 f32 LSFG_FLOW_SCALE = 1.0f; + +void WriteGrayscalePgm(const std::filesystem::path& path, 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 = + "P5\n" + std::to_string(extent.width) + " " + std::to_string(extent.height) + "\n255\n"; + if (file.Write(header) != header.size()) { + return; + } + + const size_t expected = static_cast(extent.width) * extent.height; + void(file.Write(pixels.subspan(0, std::min(expected, pixels.size())))); + void(file.Flush()); +} + +} // 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) { + if (unavailable || !Settings::values.frame_gen.GetValue()) { + return; + } + + if (!shaders) { + shaders.emplace(device); + if (!shaders->IsValid()) { + unavailable = true; + return; + } + } + + const VkExtent2D extent{.width = frame->width, .height = frame->height}; + if (!mipmaps || built_extent.width != extent.width || built_extent.height != extent.height) { + Rebuild(device, extent); + } + + mipmaps->Dispatch(device, scheduler, *frame->image_view, frame_count); + ++frame_count; + + if (!dumped && Settings::values.frame_gen_dump_flow.GetValue()) { + DumpFlowPyramid(device); + dumped = true; + } +} + +void FrameGen::Rebuild(const Device& device, VkExtent2D extent) { + scheduler.Finish(); + mipmaps.emplace(device, memory_allocator, *shaders, extent, LSFG_FLOW_SCALE); + built_extent = extent; + frame_count = 0; +} + +void FrameGen::DumpFlowPyramid(const Device& device) { + const std::filesystem::path directory = + Common::FS::GetEdenPath(Common::FS::EdenPath::LosslessDir) / "debug"; + if (!Common::FS::CreateDirs(directory)) { + return; + } + + for (size_t level = 0; level < LSFG_MIP_LEVELS; ++level) { + const VkExtent2D extent = mipmaps->GetLevelExtent(level); + const VkDeviceSize size = static_cast(extent.width) * extent.height; + vk::Buffer readback = CreateWrappedBuffer(memory_allocator, size, MemoryUsage::Download); + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([image = mipmaps->GetLevelImage(level), dst = *readback, + extent](vk::CommandBuffer cmdbuf) { + DownloadColorImage(cmdbuf, image, dst, + VkExtent3D{.width = extent.width, .height = extent.height, + .depth = 1}); + }); + scheduler.Finish(); + + readback.Invalidate(); + WriteGrayscalePgm(directory / ("flow_mip" + std::to_string(level) + ".pgm"), extent, + readback.Mapped()); + } +} + +} // 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..e81a4fcba9 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/frame_gen.h @@ -0,0 +1,41 @@ +// 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/lsfg_mipmaps.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); + +private: + void Rebuild(const Device& device, VkExtent2D extent); + void DumpFlowPyramid(const Device& device); + + MemoryAllocator& memory_allocator; + Scheduler& scheduler; + + std::optional shaders; + std::optional mipmaps; + VkExtent2D built_extent{}; + u64 frame_count{}; + bool unavailable{}; + bool dumped{}; +}; + +} // 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..9388fa574c --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_mipmaps.cpp @@ -0,0 +1,212 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// 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/renderer_vulkan/vk_scheduler.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr VkFormat FLOW_FORMAT = VK_FORMAT_R8_UNORM; +constexpr u32 DISPATCH_TILE_SHIFT = 6; +constexpr size_t DESCRIPTOR_SET_COUNT = 2; + +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); + +constexpr std::array MIPMAPS_BINDINGS{ + VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_DESCRIPTOR_TYPE_SAMPLER, + VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, +}; + +vk::Sampler CreateFlowSampler(const Device& device) { + 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 = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, + .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, + .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, + .mipLodBias = 0.0f, + .anisotropyEnable = VK_FALSE, + .maxAnisotropy = 0.0f, + .compareEnable = VK_FALSE, + .compareOp = VK_COMPARE_OP_NEVER, + .minLod = 0.0f, + .maxLod = VK_LOD_CLAMP_NONE, + .borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK, + .unnormalizedCoordinates = VK_FALSE, + }); +} + +} // Anonymous namespace + +LsfgMipmaps::LsfgMipmaps(const Device& device, MemoryAllocator& memory_allocator_, + const LsfgShaders& shaders, VkExtent2D input_extent, f32 flow_scale) + : memory_allocator{memory_allocator_} { + 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)), + }; + + CreateImages(device); + CreateUniformBuffer(flow_scale); + + sampler = CreateFlowSampler(device); + descriptor_pool = CreateWrappedDescriptorPool( + device, DESCRIPTOR_SET_COUNT * MIPMAPS_BINDINGS.size(), DESCRIPTOR_SET_COUNT, + {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_DESCRIPTOR_TYPE_SAMPLER, + VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}); + descriptor_set_layout = CreateWrappedDescriptorSetLayout( + device, std::span{MIPMAPS_BINDINGS}, VK_SHADER_STAGE_COMPUTE_BIT); + + const std::vector layouts(DESCRIPTOR_SET_COUNT, + *descriptor_set_layout); + descriptor_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts); + + pipeline_layout = CreateWrappedPipelineLayout(device, descriptor_set_layout); + pipeline = CreateWrappedComputePipeline( + device, pipeline_layout, shaders.Get(VideoCore::FrameGen::PerformanceShader::MIPMAPS)); +} + +VkExtent2D LsfgMipmaps::GetLevelExtent(size_t level) const { + return VkExtent2D{ + .width = std::max(1u, flow_extent.width >> level), + .height = std::max(1u, flow_extent.height >> level), + }; +} + +void LsfgMipmaps::CreateImages(const Device& device) { + for (size_t i = 0; i < LSFG_MIP_LEVELS; ++i) { + images[i] = CreateWrappedImage(memory_allocator, GetLevelExtent(i), FLOW_FORMAT); + image_views[i] = CreateWrappedImageView(device, images[i], FLOW_FORMAT); + } +} + +void LsfgMipmaps::CreateUniformBuffer(f32 flow_scale) { + uniform_buffer = CreateWrappedBuffer(memory_allocator, sizeof(LsfgConstants), + MemoryUsage::Upload); + + const LsfgConstants constants{ + .input_offset = {0, 0}, + .first_iter = 0, + .first_iter_s = 0, + .advanced_color_kind = 0, + .hdr_support = 0, + .resolution_inv_scale = 1.0f / flow_scale, + .timestamp = 0.0f, + .ui_threshold = 0.5f, + .padding = {0, 0, 0}, + }; + + const std::span mapped = uniform_buffer.Mapped(); + std::memcpy(mapped.data(), &constants, sizeof(constants)); + uniform_buffer.Flush(); +} + +void LsfgMipmaps::Dispatch(const Device& device, Scheduler& scheduler, VkImageView current_view, + u64 frame_count) { + const size_t set_index = frame_count % DESCRIPTOR_SET_COUNT; + const VkDescriptorSet set = descriptor_sets[set_index]; + + const VkDescriptorBufferInfo buffer_info{ + .buffer = *uniform_buffer, + .offset = 0, + .range = sizeof(LsfgConstants), + }; + const VkDescriptorImageInfo sampler_info{ + .sampler = *sampler, + .imageView = VK_NULL_HANDLE, + .imageLayout = VK_IMAGE_LAYOUT_UNDEFINED, + }; + const VkDescriptorImageInfo sampled_info{ + .sampler = VK_NULL_HANDLE, + .imageView = current_view, + .imageLayout = VK_IMAGE_LAYOUT_GENERAL, + }; + std::array storage_infos{}; + for (size_t i = 0; i < LSFG_MIP_LEVELS; ++i) { + storage_infos[i] = VkDescriptorImageInfo{ + .sampler = VK_NULL_HANDLE, + .imageView = *image_views[i], + .imageLayout = VK_IMAGE_LAYOUT_GENERAL, + }; + } + + std::vector writes; + writes.reserve(MIPMAPS_BINDINGS.size()); + const auto push = [&](u32 binding, VkDescriptorType type, + const VkDescriptorImageInfo* image_info, + const VkDescriptorBufferInfo* buf_info) { + 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_info, + .pBufferInfo = buf_info, + .pTexelBufferView = nullptr, + }); + }; + + push(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, nullptr, &buffer_info); + push(1, VK_DESCRIPTOR_TYPE_SAMPLER, &sampler_info, nullptr); + push(2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, &sampled_info, nullptr); + for (u32 i = 0; i < LSFG_MIP_LEVELS; ++i) { + push(3 + i, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, &storage_infos[i], nullptr); + } + + device.GetLogical().UpdateDescriptorSets(writes, {}); + + const u32 groups_x = (flow_extent.width + (1u << DISPATCH_TILE_SHIFT) - 1) >> + DISPATCH_TILE_SHIFT; + const u32 groups_y = (flow_extent.height + (1u << DISPATCH_TILE_SHIFT) - 1) >> + DISPATCH_TILE_SHIFT; + + std::array raw_images{}; + for (size_t i = 0; i < LSFG_MIP_LEVELS; ++i) { + raw_images[i] = *images[i]; + } + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([raw_images, set, groups_x, groups_y, layout = *pipeline_layout, + compute_pipeline = *pipeline](vk::CommandBuffer cmdbuf) { + for (const VkImage image : raw_images) { + TransitionImageLayout(cmdbuf, image, VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_UNDEFINED); + } + + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, compute_pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, layout, 0, set, {}); + cmdbuf.Dispatch(groups_x, groups_y, 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..a43adff44e --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_mipmaps.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/vulkan_common/vulkan_memory_allocator.h" +#include "video_core/vulkan_common/vulkan_wrapper.h" + +namespace Vulkan { + +class Device; +class LsfgShaders; +class Scheduler; + +constexpr size_t LSFG_MIP_LEVELS = 7; + +class LsfgMipmaps { +public: + explicit LsfgMipmaps(const Device& device, MemoryAllocator& memory_allocator, + const LsfgShaders& shaders, VkExtent2D input_extent, f32 flow_scale); + + void Dispatch(const Device& device, Scheduler& scheduler, VkImageView current_view, + u64 frame_count); + + [[nodiscard]] VkImageView GetLevelView(size_t level) const { + return *image_views[level]; + } + + [[nodiscard]] VkImage GetLevelImage(size_t level) const { + return *images[level]; + } + + [[nodiscard]] VkExtent2D GetLevelExtent(size_t level) const; + +private: + void CreateImages(const Device& device); + void CreateUniformBuffer(f32 flow_scale); + + MemoryAllocator& memory_allocator; + VkExtent2D flow_extent{}; + + vk::Buffer uniform_buffer; + vk::Sampler sampler; + vk::DescriptorPool descriptor_pool; + vk::DescriptorSetLayout descriptor_set_layout; + vk::DescriptorSets descriptor_sets; + vk::PipelineLayout pipeline_layout; + vk::Pipeline pipeline; + + std::array images; + std::array image_views; +}; + +} // 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..73468bf874 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_shaders.cpp @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#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()) { + return; + } + + VideoCore::FrameGen::ShaderModules code; + if (VideoCore::FrameGen::LoadShaderModules(code) != 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..64dbd2126a 100644 --- a/src/video_core/renderer_vulkan/present/util.h +++ b/src/video_core/renderer_vulkan/present/util.h @@ -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..3924163c6d 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp +++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp @@ -155,7 +155,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,6 +192,9 @@ void RendererVulkan::Composite(std::span framebu blit_swapchain.DrawToFrame(device, rasterizer, frame, framebuffers, render_window.GetFramebufferLayout(), swapchain.GetImageCount(), swapchain.GetImageViewFormat()); + + frame_gen.Process(device, frame); + scheduler.Flush(*frame->render_ready); present_manager.Present(frame); diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.h b/src/video_core/renderer_vulkan/renderer_vulkan.h index 4fb88b29de..6fd1e2fa12 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.h +++ b/src/video_core/renderer_vulkan/renderer_vulkan.h @@ -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_present_manager.cpp b/src/video_core/renderer_vulkan/vk_present_manager.cpp index 90ebb55d38..1c803368d6 100644 --- a/src/video_core/renderer_vulkan/vk_present_manager.cpp +++ b/src/video_core/renderer_vulkan/vk_present_manager.cpp @@ -202,7 +202,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_COLOR_ATTACHMENT_BIT | + VK_IMAGE_USAGE_SAMPLED_BIT, .sharingMode = VK_SHARING_MODE_EXCLUSIVE, .queueFamilyIndexCount = 0, .pQueueFamilyIndices = nullptr, 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;