Compare commits

...

21 Commits

Author SHA1 Message Date
CamilleLaVey f069ff50a6 Settings refinement 2026-08-13 15:52:37 -04:00
CamilleLaVey ae395352a7 Adjustments to the frames multiplier 2026-08-13 14:30:03 -04:00
CamilleLaVey 4ed084153f Licenses added 2026-08-13 12:47:05 -04:00
CamilleLaVey 81f396e70b Just an small experiment 2026-08-13 04:19:22 -04:00
CamilleLaVey 3725b4d46d Some sanitize changes on latency 2026-08-13 04:05:25 -04:00
CamilleLaVey 42b8ca5f9d Remove LSFG HDR + other optimizations on extra frame 2026-08-13 03:48:54 -04:00
CamilleLaVey 52ffc8a7ce Let's try FP16 2026-08-13 03:16:13 -04:00
CamilleLaVey e26bf22d36 Expose new settings 2026-08-13 01:41:35 -04:00
CamilleLaVey 42c5a12577 LETS OFFER THIS SACRIFICE TO LORD SMOLIO THE SMOLIARD 2026-08-13 00:36:33 -04:00
CamilleLaVey a18d2b5c05 Dump every lsfg stage and require null descriptors 2026-08-13 00:24:18 -04:00
CamilleLaVey 30be8a5e18 Correct the frame generation setting description 2026-08-12 23:57:36 -04:00
CamilleLaVey 44e3211d1c Wire the full lsfg chain into the present path 2026-08-12 23:56:52 -04:00
CamilleLaVey 52e0b2a005 Generate pass on lsfg 2026-08-12 23:43:34 -04:00
CamilleLaVey 9a1258c59d Delta pass on lsfg 2026-08-12 23:42:30 -04:00
CamilleLaVey 232656df4e Gamma pass on lsfg 2026-08-12 23:40:40 -04:00
CamilleLaVey 367e5084da Beta pass on lsfg 2026-08-12 23:38:38 -04:00
CamilleLaVey 7467506cfa Shared sampler and constant buffer pools on lsfg 2026-08-12 23:37:21 -04:00
CamilleLaVey 61f3a1d135 Alphas on lsfg 2026-08-12 23:24:57 -04:00
CamilleLaVey 4f4f4a3586 Add lsfg-vk to Initial setup 2026-08-12 22:50:21 -04:00
CamilleLaVey 8470ad4b2a Just a quick fix 2026-08-12 22:28:50 -04:00
CamilleLaVey 4b33a2746c First try lsfg-vk to Eden 2026-08-12 22:22:11 -04:00
63 changed files with 4096 additions and 33 deletions
+18 -5
View File
@@ -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()
+6
View File
@@ -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",
+4
View File
@@ -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)
@@ -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
*
@@ -37,6 +37,9 @@ 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_DUMP_FLOW("frame_gen_dump_flow"),
GPU_UNSWIZZLE_ENABLED("gpu_unswizzle_enabled"),
PICTURE_IN_PICTURE("picture_in_picture"),
USE_CUSTOM_RTC("custom_rtc_enabled"),
@@ -19,6 +19,8 @@ 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_FLOW_SCALE("frame_gen_flow_scale"),
RENDERER_VSYNC("use_vsync"),
RENDERER_SCALING_FILTER("scaling_filter"),
RENDERER_ANTI_ALIASING("anti_aliasing"),
@@ -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),
@@ -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,12 @@ abstract class SettingsItem(
return NativeLibrary.isFirmwareAvailable()
}
if (setting.key in frameGenKeys &&
!(LosslessScalingHelper.isInstalled() && LosslessScalingHelper.isSupportedByGpu())
) {
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
@@ -89,6 +96,14 @@ abstract class SettingsItem(
get() = !setting.global && NativeConfig.isPerGameConfigLoaded()
companion object {
private val frameGenKeys = setOf(
BooleanSetting.RENDERER_FRAME_GEN.key,
IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.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 +622,46 @@ 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(
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,
@@ -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,29 @@ class SettingsFragment : Fragment() {
viewLifecycleOwner,
resetState = { settingsViewModel.setShouldReloadSettingsList(false) }
) { if (it) presenter.loadSettingsList() }
settingsViewModel.shouldShowLosslessInstaller.collect(
viewLifecycleOwner,
resetState = { settingsViewModel.setShouldShowLosslessInstaller(false) }
) { if (it) losslessDllPickerLauncher.launch(arrayOf("*/*")) }
settingsViewModel.shouldShowLosslessRemoveDialog.collect(
viewLifecycleOwner,
resetState = { settingsViewModel.setShouldShowLosslessRemoveDialog(false) }
) {
if (it) {
MessageDialogFragment.newInstance(
activity = requireActivity(),
titleId = R.string.lossless_scaling_remove,
descriptionId = R.string.lossless_scaling_remove_confirmation,
positiveButtonTitleId = R.string.lossless_scaling_remove,
positiveAction = {
LosslessScalingHelper.remove()
settingsViewModel.setShouldReloadSettingsList(true)
},
showNegativeButton = true,
negativeAction = {}
).show(parentFragmentManager, MessageDialogFragment.TAG)
}
}
settingsViewModel.adapterItemChanged.collect(
viewLifecycleOwner,
resetState = { settingsViewModel.setAdapterItemChanged(-1) }
@@ -267,6 +291,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 ->
@@ -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,60 @@ class SettingsFragmentPresenter(
}
}
private fun addFrameGenSettings(sl: ArrayList<SettingsItem>) {
sl.apply {
val installed = LosslessScalingHelper.isInstalled()
val supported = LosslessScalingHelper.isSupportedByGpu()
add(HeaderSetting(R.string.lossless_scaling))
if (!supported) {
add(
RunnableSetting(
titleId = R.string.frame_gen_unsupported,
descriptionId = R.string.frame_gen_unsupported_description,
isRunnable = false
) {}
)
}
add(
RunnableSetting(
titleId = if (installed) {
R.string.lossless_scaling_replace
} else {
R.string.lossless_scaling_install
},
descriptionId = if (installed) {
R.string.lossless_scaling_replace_description
} else {
R.string.lossless_scaling_install_description
},
isRunnable = !NativeLibrary.isRunning(),
iconId = R.drawable.ic_install
) { settingsViewModel.setShouldShowLosslessInstaller(true) }
)
if (installed) {
add(
RunnableSetting(
titleId = R.string.lossless_scaling_remove,
descriptionId = R.string.lossless_scaling_remove_description,
isRunnable = !NativeLibrary.isRunning(),
iconId = R.drawable.ic_delete
) { settingsViewModel.setShouldShowLosslessRemoveDialog(true) }
)
}
add(HeaderSetting(R.string.frame_gen))
add(BooleanSetting.RENDERER_FRAME_GEN.key)
add(IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.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 +175,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)
@@ -280,6 +336,15 @@ class SettingsFragmentPresenter(
}
add(IntSetting.RENDERER_ANTI_ALIASING.key)
add(
SubmenuSetting(
titleId = R.string.frame_gen,
descriptionId = R.string.frame_gen_submenu_description,
iconId = R.drawable.ic_frames,
menuKey = MenuTag.SECTION_FRAME_GEN
)
)
add(HeaderSetting(R.string.advanced))
add(IntSetting.RENDERER_ACCURACY.key)
@@ -302,7 +367,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 +1360,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)
@@ -36,6 +36,12 @@ class SettingsViewModel : ViewModel() {
val shouldReloadSettingsList: StateFlow<Boolean> get() = _shouldReloadSettingsList
private val _shouldReloadSettingsList = MutableStateFlow(false)
val shouldShowLosslessInstaller: StateFlow<Boolean> get() = _shouldShowLosslessInstaller
private val _shouldShowLosslessInstaller = MutableStateFlow(false)
val shouldShowLosslessRemoveDialog: StateFlow<Boolean> get() = _shouldShowLosslessRemoveDialog
private val _shouldShowLosslessRemoveDialog = MutableStateFlow(false)
val sliderProgress: StateFlow<Int> get() = _sliderProgress
private val _sliderProgress = MutableStateFlow(-1)
@@ -85,6 +91,14 @@ class SettingsViewModel : ViewModel() {
_shouldReloadSettingsList.value = value
}
fun setShouldShowLosslessInstaller(value: Boolean) {
_shouldShowLosslessInstaller.value = value
}
fun setShouldShowLosslessRemoveDialog(value: Boolean) {
_shouldShowLosslessRemoveDialog.value = value
}
fun setSliderTextValue(value: Float, units: String) {
_sliderProgress.value = value.toInt()
_sliderTextValue.value = String.format(
@@ -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
@@ -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_frames,
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) {
@@ -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}")
}
@@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.utils
import android.net.Uri
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import java.io.File
object LosslessScalingHelper {
const val RESULT_OK = 0
const val RESULT_NOT_INSTALLED = 1
private val _statusText = MutableStateFlow("")
val statusText: StateFlow<String> = _statusText.asStateFlow()
private var installed: Boolean? = null
private var gpuSupported: Boolean? = null
fun isInstalled(): Boolean = installed ?: refreshStatus()
fun isSupportedByGpu(): Boolean {
val cached = gpuSupported
if (cached != null) {
return cached
}
val result = NativeLibrary.supportsFrameGeneration()
gpuSupported = result
return result
}
fun refreshStatus(): Boolean {
val result = NativeLibrary.validateLosslessDll() == RESULT_OK
installed = result
val context = YuzuApplication.appContext
_statusText.value = if (result) {
context.getString(R.string.lossless_scaling_installed)
} else {
context.getString(R.string.lossless_scaling_not_installed)
}
return result
}
fun install(source: Uri): Int {
val destination = File(NativeLibrary.getLosslessDllPath())
destination.parentFile?.mkdirs()
val copied = FileUtil.copyUriToInternalStorage(
source,
destination.parent!!,
destination.name
)
if (copied == null) {
refreshStatus()
return RESULT_NOT_INSTALLED
}
val result = NativeLibrary.prepareLosslessDll()
if (result != RESULT_OK) {
NativeLibrary.removeLosslessDll()
}
refreshStatus()
return result
}
fun remove(): Boolean {
val removed = NativeLibrary.removeLosslessDll()
refreshStatus()
return removed
}
}
@@ -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();
}
@@ -147,7 +147,7 @@ namespace AndroidSettings {
&show_performance_overlay};
Settings::Setting<s32> pipeline_worker_count{linkage, 4, "pipeline_worker_count",
Settings::SwitchableSetting<s32> pipeline_worker_count{linkage, 2, "pipeline_worker_count",
Settings::Category::Android,
Settings::Specialization::Default,
true,
@@ -124,9 +124,18 @@ float EmuWindow_Android::GetFrameTimeVerifiedHint() const {
return QuantizeFrameRateHint(verified_rate);
}
float EmuWindow_Android::GetPresentedFrameMultiplier() {
if (!Settings::values.frame_gen.GetValue()) {
return 1.0f;
}
return static_cast<float>(std::clamp<u32>(Settings::values.frame_gen_multiplier.GetValue(), 2, 4));
}
float EmuWindow_Android::GetFrameRateHint() const {
const float observed_rate = std::clamp(m_smoothed_present_rate, 0.0f, 240.0f);
const float frame_time_verified_hint = GetFrameTimeVerifiedHint();
const float presented_multiplier = GetPresentedFrameMultiplier();
const float observed_rate =
std::clamp(m_smoothed_present_rate * presented_multiplier, 0.0f, 240.0f);
const float frame_time_verified_hint = GetFrameTimeVerifiedHint() * presented_multiplier;
if (m_last_frame_rate_hint > 0.0f && observed_rate > 0.0f) {
const float tolerance = std::max(m_last_frame_rate_hint * 0.12f, 4.0f);
@@ -150,9 +159,9 @@ float EmuWindow_Android::GetFrameRateHint() const {
return frame_time_verified_hint;
}
constexpr float NominalFrameRate = 60.0f;
const float nominal_rate = 60.0f * presented_multiplier;
if (!Settings::values.use_speed_limit.GetValue()) {
return NominalFrameRate;
return QuantizeFrameRateHint(nominal_rate);
}
const u16 speed_limit = Settings::SpeedLimit();
@@ -161,7 +170,7 @@ float EmuWindow_Android::GetFrameRateHint() const {
}
const float speed_limited_rate =
NominalFrameRate * (static_cast<float>(std::min<u16>(speed_limit, 100)) / 100.0f);
nominal_rate * (static_cast<float>(std::min<u16>(speed_limit, 100)) / 100.0f);
return QuantizeFrameRateHint(speed_limited_rate);
}
@@ -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{};
+55
View File
@@ -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<jboolean>(GetVulkanMemoryModelSupport());
} catch (...) {
return static_cast<jboolean>(false);
}
}
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGpuModel(JNIEnv* env, jobject jobj) {
const auto props = GetVulkanDeviceProperties();
if (props.deviceID == 0) {
@@ -1390,6 +1428,23 @@ jint Java_org_yuzu_yuzu_1emu_NativeLibrary_installKeys(JNIEnv* env, jclass clazz
return static_cast<int>(FirmwareManager::InstallKeys(path, ext));
}
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getLosslessDllPath(JNIEnv* env, jclass clazz) {
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<jint>(VideoCore::FrameGen::GetInstalledLosslessStatus());
}
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_prepareLosslessDll(JNIEnv* env, jclass clazz) {
return static_cast<jint>(VideoCore::FrameGen::BuildShaderCache());
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_removeLosslessDll(JNIEnv* env, jclass clazz) {
return static_cast<jboolean>(VideoCore::FrameGen::RemoveInstalledLosslessDll());
}
jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env, jobject jobj,
jstring jpath,
jstring jprogramId) {
@@ -162,6 +162,18 @@
<item>@string/resolution_four</item>
</string-array>
<string-array name="frameGenMultiplierNames">
<item>@string/frame_gen_multiplier_2x</item>
<item>@string/frame_gen_multiplier_3x</item>
<item>@string/frame_gen_multiplier_4x</item>
</string-array>
<integer-array name="frameGenMultiplierValues">
<item>2</item>
<item>3</item>
<item>4</item>
</integer-array>
<string-array name="rendererVSyncNames">
<item>@string/renderer_vsync_immediate</item>
<item>@string/renderer_vsync_mailbox</item>
@@ -638,6 +650,16 @@
<item>@string/error_keys_failed_init</item>
</string-array>
<string-array name="losslessDllResults">
<item>""</item>
<item>@string/error_lossless_copy_failed</item>
<item>@string/error_lossless_unreadable</item>
<item>@string/error_lossless_not_pe</item>
<item>@string/error_lossless_missing_shaders</item>
<item>@string/error_lossless_translation_failed</item>
<item>@string/error_lossless_cache_failed</item>
</string-array>
<!-- GPU Logging Arrays -->
<string-array name="gpuLogLevelEntries">
<item>Off</item>
@@ -298,6 +298,47 @@
<string name="gpu_driver_fetcher">GPU driver fetcher</string>
<string name="gpu_driver_manager">GPU driver manager</string>
<string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
<string name="frame_gen">Frame generation</string>
<string name="frame_gen_submenu_description">Manage and configure frame generation</string>
<string name="frame_gen_description">Insert interpolated frames between rendered ones using Lossless Scaling. Forces FIFO presentation while enabled.</string>
<string name="frame_gen_multiplier">Frame multiplier</string>
<string name="frame_gen_multiplier_description">How many frames to display for each rendered frame. Higher values cost proportionally more GPU time.</string>
<string name="frame_gen_multiplier_2x">2x</string>
<string name="frame_gen_multiplier_3x">3x</string>
<string name="frame_gen_multiplier_4x">4x</string>
<string name="frame_gen_flow_scale">Motion estimation resolution</string>
<string name="frame_gen_flow_scale_description">Resolution of the optical flow pass, as a fraction of the output. Lowering it is the cheapest way to reclaim performance.</string>
<string name="frame_gen_fp16">Half precision shaders</string>
<string name="frame_gen_fp16_description">Use the 16-bit shader variant shipped in Lossless.dll. Falls back automatically if the driver or the file lacks it.</string>
<string name="frame_gen_dump_flow">Dump generated frame</string>
<string name="frame_gen_dump_flow_description">Write the optical flow mip levels and the interpolated frame to the lossless/debug folder once, for troubleshooting</string>
<string name="frame_gen_unsupported">Frame generation unavailable</string>
<string name="frame_gen_unsupported_description">This GPU driver does not support the Vulkan memory model, which the Lossless Scaling shaders require.</string>
<string name="lossless_scaling_setup_description">Optional. Provide your own Lossless.dll to enable frame generation later</string>
<string name="lossless_scaling_install">Install Lossless.dll</string>
<string name="lossless_scaling_install_description">Frame generation needs your own legal copy of Lossless.dll from Lossless Scaling</string>
<string name="lossless_scaling_replace_description">Select a different copy of Lossless.dll</string>
<string name="frame_generation_support">Frame generation</string>
<string name="frame_generation_supported">Supported</string>
<string name="frame_generation_unsupported">Unsupported (no Vulkan memory model)</string>
<string name="lossless_scaling">Lossless Scaling</string>
<string name="lossless_scaling_description">Provide your own copy of Lossless.dll to enable frame generation</string>
<string name="lossless_scaling_installed">Installed</string>
<string name="lossless_scaling_not_installed">Not installed</string>
<string name="lossless_scaling_installed_description">Lossless.dll is installed and contains every shader frame generation needs.</string>
<string name="lossless_scaling_replace">Replace</string>
<string name="lossless_scaling_remove">Remove</string>
<string name="lossless_scaling_remove_description">Delete the installed Lossless.dll and its prepared shaders</string>
<string name="lossless_scaling_remove_confirmation">Frame generation will stop working until you install Lossless.dll again. Your original file is not affected.</string>
<string name="lossless_scaling_installing">Preparing frame generation shaders…</string>
<string name="lossless_scaling_install_success">Lossless.dll installed successfully</string>
<string name="lossless_scaling_install_failed">Could not install Lossless.dll</string>
<string name="error_lossless_copy_failed">The selected file could not be copied.</string>
<string name="error_lossless_unreadable">The selected file could not be read.</string>
<string name="error_lossless_not_pe">The selected file is not a Windows library. Select Lossless.dll from your Lossless Scaling installation.</string>
<string name="error_lossless_missing_shaders">This copy of Lossless.dll does not contain the frame generation shaders. Update Lossless Scaling and try again.</string>
<string name="error_lossless_translation_failed">The frame generation shaders could not be translated. This version of Lossless Scaling is not supported yet.</string>
<string name="error_lossless_cache_failed">The translated shaders could not be written to storage. Check that there is free space available.</string>
<string name="advanced_settings">Advanced settings</string>
<string name="settings_description">Configure emulator settings</string>
<string name="search_recently_played">Recently played</string>
+3
View File
@@ -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"
+1
View File
@@ -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);
+1
View File
@@ -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.
+35 -8
View File
@@ -352,7 +352,7 @@ struct Values {
true};
SwitchableSetting<ScalingFilter> scaling_filter{linkage,
ScalingFilter::Bilinear,
ScalingFilter::NearestNeighbor,
"scaling_filter",
Category::Renderer,
Specialization::Default,
@@ -388,6 +388,38 @@ struct Values {
true,
true};
SwitchableSetting<bool> frame_gen{linkage, false, "frame_gen", Category::Renderer,
Specialization::Default, true, false};
SwitchableSetting<u32, true> frame_gen_multiplier{linkage,
2,
2,
4,
"frame_gen_multiplier",
Category::Renderer,
Specialization::Countable,
true,
false,
&frame_gen};
SwitchableSetting<u32, true> frame_gen_flow_scale{linkage,
75,
25,
100,
"frame_gen_flow_scale",
Category::Renderer,
Specialization::Countable |
Specialization::Percentage,
true,
true,
&frame_gen};
SwitchableSetting<bool> frame_gen_fp16{linkage, true, "frame_gen_fp16", Category::Renderer,
Specialization::Default, true, false, &frame_gen};
SwitchableSetting<bool> frame_gen_dump_flow{linkage, false, "frame_gen_dump_flow",
Category::Renderer};
SwitchableSetting<bool> use_asynchronous_gpu_emulation{linkage,
#ifdef __ANDROID__
false,
@@ -569,13 +601,8 @@ struct Values {
SwitchableSetting<bool> emulate_bgr565{linkage, false, "emulate_bgr565",
Category::RendererHacks};
SwitchableSetting<bool> rescale_hack{linkage,
#ifdef __ANDROID__
true,
#else
false,
#endif
"rescale_hack", Category::RendererHacks};
SwitchableSetting<bool> rescale_hack{linkage, false, "rescale_hack",
Category::RendererHacks};
SwitchableSetting<bool> enable_gpu_buffer_readback{linkage,
false,
"enable_gpu_buffer_readback",
+25
View File
@@ -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,26 @@ 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_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 +369,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)
+575
View File
@@ -0,0 +1,575 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <cstring>
#include <optional>
#include <span>
#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<const u8> image_) : image{image_} {}
template <typename T>
[[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<const u8>& out_slice) const {
if (offset > image.size() || image.size() - offset < size) {
return false;
}
out_slice = image.subspan(offset, size);
return true;
}
private:
std::span<const u8> image;
};
[[nodiscard]] std::optional<size_t> 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<size_t>(pe_offset);
}
[[nodiscard]] std::optional<size_t> 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<Section>& 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<size_t> RvaToFileOffset(std::span<const Section> 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<size_t>(section.raw_address) + relative;
}
}
return std::nullopt;
}
[[nodiscard]] bool ReadResourceEntries(const ImageReader& reader, size_t directory_offset,
std::vector<ResourceEntry>& 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<const Section> sections,
size_t leaf_offset, std::span<const u8>& 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<size_t> data_offset = RvaToFileOffset(sections, data_rva);
if (!data_offset) {
return false;
}
return reader.Slice(*data_offset, data_size, out_data);
}
using ResourceSpans = std::map<u32, std::span<const u8>>;
[[nodiscard]] bool CollectRcData(const ImageReader& reader, std::span<const Section> sections,
size_t resource_base, ResourceSpans& out_resources) {
std::vector<ResourceEntry> 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<ResourceEntry> 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<ResourceEntry> 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<const u8> 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<u32> PerformanceShaderIds() {
std::vector<u32> 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 <typename Map>
[[nodiscard]] bool HasPerformanceShaders(const Map& resources) {
const std::vector<u32> 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 <typename Map>
[[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<u32> adopted = AdoptSpirvModule(hit->second);
if (adopted.empty()) {
return LosslessStatus::TranslationFailed;
}
out_modules.emplace(id, std::move(adopted));
continue;
}
std::vector<u32> 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<u32>(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<u32> 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<u8>& 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<size_t>(file.GetSize()));
if (out_image.empty() || file.Read(out_image) != out_image.size()) {
return LosslessStatus::UnreadableFile;
}
return LosslessStatus::Ok;
}
[[nodiscard]] LosslessStatus ParseShaderSpans(std::span<const u8> image,
ResourceSpans& out_resources) {
const ImageReader reader{image};
const std::optional<size_t> pe_offset = FindPeHeader(reader);
if (!pe_offset) {
return LosslessStatus::NotPortableExecutable;
}
const std::optional<size_t> data_directory =
FindDataDirectory(reader, *pe_offset + 4 + COFF_HEADER_SIZE);
if (!data_directory) {
return LosslessStatus::NotPortableExecutable;
}
std::vector<Section> 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<size_t> 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<u8> 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<u8>{data.begin(), data.end()});
}
return LosslessStatus::Ok;
}
LosslessStatus ValidateLosslessDll(const std::filesystem::path& path) {
std::vector<u8> 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<u8> 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<u8> 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<const char*>(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<u32>(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<u32>(out_modules.size()),
.variant = static_cast<u32>(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
+69
View File
@@ -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 <array>
#include <filesystem>
#include <map>
#include <vector>
#include "common/common_types.h"
namespace VideoCore::FrameGen {
enum class LosslessStatus : u32 {
Ok,
NotInstalled,
UnreadableFile,
NotPortableExecutable,
MissingShaders,
TranslationFailed,
CacheUnusable,
};
using ShaderResources = std::map<u32, std::vector<u8>>;
using ShaderModules = std::map<u32, std::vector<u32>>;
enum class ShaderVariant : u32 {
TranslatedDxbc,
NativeFp32,
NativeFp16,
};
namespace PerformanceShader {
constexpr u32 MIPMAPS = 255;
constexpr u32 GENERATE = 256;
constexpr std::array<u32, 4> ALPHA{290, 291, 292, 293};
constexpr std::array<u32, 5> BETA{298, 299, 300, 301, 302};
constexpr std::array<u32, 5> GAMMA{280, 282, 283, 284, 285};
constexpr std::array<u32, 10> 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
+129
View File
@@ -0,0 +1,129 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <cstring>
#include <map>
#include <tuple>
#include <dxbc_modinfo.h>
#include <dxbc_module.h>
#include <dxbc_reader.h>
#include <thirdparty/spirv.hpp>
#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<u32> 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<u32>(i);
}
}
void RenumberBindingsInOrder(std::vector<u32>& words) {
struct Slot {
u32 set;
u32 binding;
size_t literal_offset;
};
std::map<u32, u32> sets;
std::vector<Slot> 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<u32>(i);
}
}
} // Anonymous namespace
bool IsSpirvModule(std::span<const u8> 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<u32> AdoptSpirvModule(std::span<const u8> blob) {
if (!IsSpirvModule(blob)) {
return {};
}
std::vector<u32> words(blob.size() / sizeof(u32));
std::memcpy(words.data(), blob.data(), blob.size());
RenumberBindingsInOrder(words);
return words;
}
std::vector<u32> TranslateComputeShader(std::span<const u8> dxbc) {
if (dxbc.empty()) {
return {};
}
try {
dxvk::DxbcReader reader{reinterpret_cast<const char*>(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<u32>{code.data(), code.data() + code.dwords()};
} catch (...) {
return {};
}
}
} // namespace VideoCore::FrameGen
+19
View File
@@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <span>
#include <vector>
#include "common/common_types.h"
namespace VideoCore::FrameGen {
[[nodiscard]] bool IsSpirvModule(std::span<const u8> blob);
[[nodiscard]] std::vector<u32> AdoptSpirvModule(std::span<const u8> blob);
[[nodiscard]] std::vector<u32> TranslateComputeShader(std::span<const u8> dxbc);
} // namespace VideoCore::FrameGen
@@ -0,0 +1,332 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <string>
#include <vector>
#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;
[[nodiscard]] f32 ConfiguredFlowScale() {
return static_cast<f32>(Settings::values.frame_gen_flow_scale.GetValue()) / 100.0f;
}
[[nodiscard]] size_t ConfiguredGenerations() {
const u32 multiplier = std::clamp<u32>(Settings::values.frame_gen_multiplier.GetValue(),
LSFG_MIN_MULTIPLIER, LSFG_MAX_MULTIPLIER);
return multiplier - 1;
}
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<const u8> 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<const u8> pixels) {
const size_t expected = static_cast<size_t>(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<const u8> 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<const u8> pixels, bool blue_first) {
const size_t pixel_count = static_cast<size_t>(extent.width) * extent.height;
if (pixels.size() < pixel_count * COLOR_CHANNELS) {
return;
}
std::vector<u8> 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, bool generate) {
generated = false;
if (unavailable || !Settings::values.frame_gen.GetValue()) {
return;
}
if (!frame->storage_view) {
unavailable = true;
return;
}
if (!shaders) {
shaders.emplace(device);
if (!shaders->IsValid()) {
unavailable = true;
return;
}
}
const VkExtent2D extent{.width = frame->width, .height = frame->height};
if (!chain || built_extent.width != extent.width || built_extent.height != extent.height ||
built_format != format || built_flow_scale != ConfiguredFlowScale() ||
built_generations != ConfiguredGenerations()) {
Rebuild(device, extent, format);
}
const u64 count = frame_count++;
last_count = count;
generated = generate && count + 1 >= LSFG_REQUIRED_FRAMES;
scheduler.RequestOutsideRenderPassOperationContext();
scheduler.Record([this, source = *frame->image, extent, count,
dispatch = generated](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() const {
if (unavailable || !Settings::values.frame_gen.GetValue()) {
return 0;
}
return ConfiguredGenerations();
}
size_t FrameGen::GeneratedFrameCount() const {
return generated && chain ? chain->GenerationCount() : 0;
}
void FrameGen::GenerateInto(const Device& device, Frame* destination, size_t generation) {
chain->SetTarget(device, generation, destination->index, *destination->storage_view);
const VkExtent2D extent{.width = destination->width, .height = destination->height};
scheduler.RequestOutsideRenderPassOperationContext();
scheduler.Record([this, count = last_count, generation, target = destination->index,
image = *destination->image, extent](vk::CommandBuffer cmdbuf) {
chain->DispatchGeneration(cmdbuf, count, generation, target, image, extent);
});
}
void FrameGen::Rebuild(const Device& device, VkExtent2D extent, VkFormat format) {
scheduler.Finish();
chain.reset();
built_flow_scale = ConfiguredFlowScale();
built_generations = ConfiguredGenerations();
chain.emplace(device, memory_allocator, *shaders, extent, format, built_flow_scale,
built_generations);
built_extent = extent;
built_format = format;
frame_count = 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<VkDeviceSize>(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<u8> 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_t>(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
@@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <optional>
#include "common/common_types.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, bool generate);
[[nodiscard]] size_t WantedGenerations() const;
[[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);
void DumpDebugImages(u64 count);
MemoryAllocator& memory_allocator;
Scheduler& scheduler;
std::optional<LsfgShaders> shaders;
std::optional<LsfgChain> chain;
VkExtent2D built_extent{};
VkFormat built_format{VK_FORMAT_UNDEFINED};
f32 built_flow_scale{};
size_t built_generations{};
u64 frame_count{};
u64 last_count{};
bool generated{};
bool unavailable{};
bool dumped{};
};
} // namespace Vulkan
@@ -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 <vector>
#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<VkDescriptorSetLayout> 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
@@ -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 <array>
#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<LsfgPass, LSFG_ALPHA_STAGES> 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<VkDescriptorSet, LSFG_ALPHA_STAGES - 1> descriptor_sets{};
std::array<VkDescriptorSet, LSFG_HISTORY_SLOTS> last_descriptor_sets{};
vk::DescriptorSets owned_sets;
LsfgImage temp1;
LsfgImage temp2;
LsfgImagePair temp3;
LsfgImageHistory out_images;
};
} // namespace Vulkan
@@ -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 <vector>
#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<VkDescriptorSetLayout> 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
@@ -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 <array>
#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<LsfgPass, LSFG_BETA_STAGES> passes;
std::array<VkDescriptorSet, LSFG_HISTORY_SLOTS> first_descriptor_sets{};
std::array<VkDescriptorSet, LSFG_BETA_STAGES - 1> descriptor_sets{};
vk::DescriptorSets owned_sets;
LsfgImagePair temp1;
LsfgImagePair temp2;
std::array<LsfgImage, LSFG_BETA_OUTPUTS> out_images;
};
} // namespace Vulkan
@@ -0,0 +1,102 @@
// 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 <algorithm>
#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_GENERATION = 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, size_t generation_count_)
: generation_count{generation_count_},
resources{device, memory_allocator, flow_scale},
descriptor_pool{CreateLsfgDescriptorPool(
device, FIXED_DESCRIPTOR_SETS +
DESCRIPTOR_SETS_PER_GENERATION * static_cast<u32>(generation_count))} {
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(), generation_count);
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(), generation_count);
}
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(), generation_count);
}
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,
u32 target, VkImage image, VkExtent2D extent) {
for (size_t i = 0; i < LSFG_MIP_LEVELS; ++i) {
gamma[i].Dispatch(cmdbuf, frame_count, generation);
if (i >= FIRST_DELTA_LEVEL) {
delta[i - FIRST_DELTA_LEVEL].Dispatch(cmdbuf, frame_count, generation);
}
}
generate.Dispatch(cmdbuf, frame_count, generation, target, image, extent);
}
} // namespace Vulkan
@@ -0,0 +1,91 @@
// 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 <array>
#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, size_t generation_count_);
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,
u32 target, VkImage image, VkExtent2D extent);
void SetTarget(const Device& device, size_t generation, u32 target, VkImageView view) {
generate.SetTarget(device, generation, target, view);
}
[[nodiscard]] LsfgImage& Input(u64 frame_count) {
return frames[frame_count % frames.size()];
}
[[nodiscard]] size_t GenerationCount() const {
return generation_count;
}
[[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:
size_t generation_count{};
LsfgResources resources;
vk::DescriptorPool descriptor_pool;
LsfgImagePair frames;
LsfgMipmaps mipmaps;
LsfgAlphaPasses alpha_passes;
std::array<LsfgAlpha, LSFG_MIP_LEVELS> alpha;
LsfgBeta beta;
std::array<LsfgGamma, LSFG_MIP_LEVELS> gamma;
std::array<LsfgDelta, LSFG_DELTA_INSTANCES> delta;
LsfgGenerate generate;
};
} // namespace Vulkan
@@ -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 <algorithm>
#include <cstring>
#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<u32, 2> 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<u32, 3> 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<u64>(address_mode) | (static_cast<u64>(compare_op) << 8) |
(static_cast<u64>(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(&timestamp_bits, &timestamp, sizeof(timestamp_bits));
const u64 key = static_cast<u64>(timestamp_bits) | (static_cast<u64>(first_iter) << 32) |
(static_cast<u64>(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<u8> 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<VkDescriptorType> types;
for (const auto& [count, type] : bindings) {
types.insert(types.end(), count, type);
}
descriptor_count = static_cast<u32>(types.size());
descriptor_set_layout = CreateWrappedDescriptorSetLayout(
device, std::span<const VkDescriptorType>{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
@@ -0,0 +1,210 @@
// 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 <array>
#include <deque>
#include <initializer_list>
#include <map>
#include <utility>
#include <vector>
#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_MIN_MULTIPLIER = 2;
constexpr size_t LSFG_MAX_MULTIPLIER = 4;
constexpr size_t LSFG_MAX_GENERATIONS = LSFG_MAX_MULTIPLIER - 1;
constexpr size_t LSFG_MAX_TARGETS = 7;
[[nodiscard]] constexpr f32 LsfgTimestamp(size_t generation, size_t generation_count) {
return static_cast<f32>(generation + 1) / static_cast<f32>(generation_count + 1);
}
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<LsfgImage, 2>;
using LsfgImageHistory = std::array<LsfgImagePair, LSFG_HISTORY_SLOTS>;
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<u64, vk::Sampler> samplers;
std::map<u64, vk::Buffer> 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 <typename Range>
LsfgBarriers& WriteToReadAll(Range& images) {
for (auto& image : images) {
WriteToRead(image);
}
return *this;
}
template <typename Range>
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<VkImageMemoryBarrier> 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 <typename Range>
LsfgDescriptorWriter& AddSampledImages(const Range& images) {
for (const auto& image : images) {
AddSampledImage(image);
}
return *this;
}
template <typename Range>
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<VkDescriptorImageInfo> image_infos;
std::deque<VkDescriptorBufferInfo> buffer_infos;
std::vector<VkWriteDescriptorSet> writes;
};
using LsfgBindings = std::initializer_list<std::pair<u32, VkDescriptorType>>;
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
@@ -0,0 +1,284 @@
// 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 <vector>
#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_, size_t generation_count)
: 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<VkDescriptorSetLayout> layouts;
for (size_t generation = 0; generation < generation_count; ++generation) {
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);
generations.resize(generation_count);
size_t next = 0;
for (size_t generation = 0; generation < generation_count; ++generation) {
Generation& pass = generations[generation];
const VkBuffer buffer = resources.GetBuffer(
LsfgTimestamp(generation, generation_count), 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 generation) {
const Generation& pass = generations[generation];
const VkExtent2D extent = temp1[0].Extent();
const u32 groups_x = GroupCount(extent.width);
const u32 groups_y = GroupCount(extent.height);
const size_t slot = frame_count % LSFG_HISTORY_SLOTS;
const size_t previous_slot = (frame_count + 2) % LSFG_HISTORY_SLOTS;
LsfgBarriers(cmdbuf)
.WriteToReadAll((*inputs)[previous_slot])
.WriteToReadAll((*inputs)[slot])
.WriteToRead(previous_gamma)
.ReadToWriteAll(temp1)
.Build();
passes[0].Bind(cmdbuf, pass.first_descriptor_sets[slot]);
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_slot])
.WriteToReadAll((*inputs)[slot])
.WriteToRead(previous_gamma)
.WriteToRead(previous1)
.ReadToWriteAll(temp2)
.Build();
passes[5].Bind(cmdbuf, pass.sixth_descriptor_sets[slot]);
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
@@ -0,0 +1,64 @@
// 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 <array>
#include <vector>
#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, size_t generation_count);
void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t generation);
[[nodiscard]] LsfgImage& Output1() {
return out_image1;
}
[[nodiscard]] LsfgImage& Output2() {
return out_image2;
}
private:
struct Generation {
std::array<VkDescriptorSet, LSFG_HISTORY_SLOTS> first_descriptor_sets{};
std::array<VkDescriptorSet, LSFG_HISTORY_SLOTS> sixth_descriptor_sets{};
std::array<VkDescriptorSet, LSFG_DELTA_STAGES - 2> descriptor_sets{};
};
LsfgImageHistory* inputs{};
LsfgImage* flow_input{};
LsfgImage* previous_gamma{};
LsfgImage* previous1{};
LsfgImage* previous2{};
std::array<LsfgPass, LSFG_DELTA_STAGES> passes;
std::vector<Generation> generations;
vk::DescriptorSets owned_sets;
std::array<LsfgImage, LSFG_DELTA_TEMPS> temp1;
LsfgImagePair temp2;
LsfgImage out_image1;
LsfgImage out_image2;
};
} // namespace Vulkan
@@ -0,0 +1,186 @@
// 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 <vector>
#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_, size_t generation_count)
: 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<VkDescriptorSetLayout> layouts;
for (size_t generation = 0; generation < generation_count; ++generation) {
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);
generations.resize(generation_count);
size_t next = 0;
for (size_t generation = 0; generation < generation_count; ++generation) {
Generation& pass = generations[generation];
const VkBuffer buffer = resources.GetBuffer(
LsfgTimestamp(generation, generation_count), 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 generation) {
const Generation& pass = generations[generation];
const VkExtent2D extent = temp1[0].Extent();
const u32 groups_x = GroupCount(extent.width);
const u32 groups_y = GroupCount(extent.height);
const size_t slot = frame_count % LSFG_HISTORY_SLOTS;
const size_t previous_slot = (frame_count + 2) % LSFG_HISTORY_SLOTS;
LsfgBarriers(cmdbuf)
.WriteToReadAll((*inputs)[previous_slot])
.WriteToReadAll((*inputs)[slot])
.WriteToRead(previous)
.ReadToWriteAll(temp1)
.Build();
passes[0].Bind(cmdbuf, pass.first_descriptor_sets[slot]);
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
@@ -0,0 +1,56 @@
// 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 <array>
#include <vector>
#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,
size_t generation_count);
void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t generation);
[[nodiscard]] LsfgImage& Output() {
return out_image;
}
private:
struct Generation {
std::array<VkDescriptorSet, LSFG_HISTORY_SLOTS> first_descriptor_sets{};
std::array<VkDescriptorSet, LSFG_GAMMA_STAGES - 1> descriptor_sets{};
};
LsfgImageHistory* inputs{};
LsfgImage* flow_input{};
LsfgImage* previous{};
std::array<LsfgPass, LSFG_GAMMA_STAGES> passes;
std::vector<Generation> generations;
vk::DescriptorSets owned_sets;
std::array<LsfgImage, LSFG_GAMMA_TEMPS> temp1;
LsfgImagePair temp2;
LsfgImage out_image;
};
} // namespace Vulkan
@@ -0,0 +1,132 @@
// 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 <vector>
#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_, size_t generation_count)
: 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);
generations.resize(generation_count);
const std::vector<VkDescriptorSetLayout> layouts(
generation_count * LSFG_MAX_TARGETS * 2, pass.SetLayout());
owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts);
size_t next = 0;
for (size_t generation = 0; generation < generation_count; ++generation) {
Generation& target = generations[generation];
target.buffer = resources.GetBuffer(LsfgTimestamp(generation, generation_count));
for (auto& slot : target.targets) {
for (auto& set : slot.descriptor_sets) {
set = owned_sets[next++];
}
}
}
}
void LsfgGenerate::SetTarget(const Device& device, size_t generation, u32 target, VkImageView view) {
Target& slot = generations[generation].targets[target];
if (slot.view == view) {
return;
}
slot.view = view;
for (size_t i = 0; i < slot.descriptor_sets.size(); ++i) {
LsfgDescriptorWriter(slot.descriptor_sets[i])
.AddUniformBuffer(generations[generation].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 generation,
u32 target, VkImage image, VkExtent2D extent) {
const Target& slot = generations[generation].targets[target];
LsfgBarriers(cmdbuf)
.WriteToReadAll(*frames)
.WriteToRead(*motion)
.WriteToRead(*detail1)
.WriteToRead(*detail2)
.DiscardToWrite(image)
.Build();
pass.Bind(cmdbuf, slot.descriptor_sets[frame_count % slot.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
@@ -0,0 +1,55 @@
// 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 <array>
#include <vector>
#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, size_t generation_count);
void SetTarget(const Device& device, size_t generation, u32 target, VkImageView view);
void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t generation, u32 target,
VkImage image, VkExtent2D extent);
private:
struct Target {
std::array<VkDescriptorSet, 2> descriptor_sets{};
VkImageView view{};
};
struct Generation {
std::array<Target, LSFG_MAX_TARGETS> targets{};
VkBuffer buffer{};
};
LsfgImagePair* frames{};
LsfgImage* motion{};
LsfgImage* detail1{};
LsfgImage* detail2{};
VkSampler sampler{};
VkSampler edge_sampler{};
LsfgPass pass;
std::vector<Generation> generations;
vk::DescriptorSets owned_sets;
};
} // namespace Vulkan
@@ -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 <algorithm>
#include <vector>
#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<u32>(static_cast<f32>(input_extent.width) * flow_scale)),
.height = std::max(1u, static_cast<u32>(static_cast<f32>(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<VkDescriptorSetLayout> 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
@@ -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 <array>
#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<VkDescriptorSet, 2> descriptor_sets{};
vk::DescriptorSets owned_sets;
VkExtent2D flow_extent{};
std::array<LsfgImage, LSFG_MIP_LEVELS> out_images;
};
} // namespace Vulkan
@@ -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
@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <map>
#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<u32, vk::ShaderModule> modules;
bool valid{};
};
} // namespace Vulkan
@@ -320,15 +320,16 @@ vk::DescriptorPool CreateWrappedDescriptorPool(const Device& device, size_t max_
});
}
vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout(
const Device& device, std::initializer_list<VkDescriptorType> types) {
vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout(const Device& device,
std::span<const VkDescriptorType> types,
VkShaderStageFlags stages) {
std::vector<VkDescriptorSetLayoutBinding> bindings(types.size());
for (size_t i = 0; i < types.size(); i++) {
bindings[i] = {
.binding = static_cast<u32>(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<VkDescriptorType> types,
VkShaderStageFlags stages) {
return CreateWrappedDescriptorSetLayout(
device, std::span<const VkDescriptorType>{std::data(types), types.size()}, stages);
}
vk::DescriptorSets CreateWrappedDescriptorSets(vk::DescriptorPool& pool,
vk::Span<VkDescriptorSetLayout> 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{
@@ -38,7 +38,11 @@ vk::DescriptorPool CreateWrappedDescriptorPool(const Device& device, size_t max_
std::initializer_list<VkDescriptorType> types = {
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER});
vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout(
const Device& device, std::initializer_list<VkDescriptorType> types);
const Device& device, std::initializer_list<VkDescriptorType> types,
VkShaderStageFlags stages = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT);
vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout(const Device& device,
std::span<const VkDescriptorType> types,
VkShaderStageFlags stages);
vk::DescriptorSets CreateWrappedDescriptorSets(vk::DescriptorPool& pool,
vk::Span<VkDescriptorSetLayout> 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<vk::ShaderModule&, vk::ShaderModule&> 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<vk::ShaderModule&, vk::ShaderModule&> shaders);
@@ -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,21 @@ void RendererVulkan::Composite(std::span<const Tegra::FramebufferConfig> framebu
blit_swapchain.DrawToFrame(device, rasterizer, frame, framebuffers,
render_window.GetFramebufferLayout(), swapchain.GetImageCount(),
swapchain.GetImageViewFormat());
const size_t wanted = frame_gen.WantedGenerations();
const bool can_present_all = wanted > 0 && present_manager.AvailableExtraFrames() >= wanted;
frame_gen.Process(device, frame, swapchain.GetImageFormat(), can_present_all);
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);
@@ -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<TurboMode> turbo_mode;
Frame applet_frame;
@@ -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<const Tegra::FramebufferConfig> framebuffers,
const Layout::FramebufferLayout& layout,
@@ -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);
@@ -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,11 @@ void PresentManager::Present(Frame* frame) {
}
}
size_t PresentManager::AvailableExtraFrames() {
std::scoped_lock lock{free_mutex};
return free_queue.size();
}
void PresentManager::RecreateFrame(Frame* frame, u32 width, u32 height, VkFormat image_view_format,
VkRenderPass rd) {
auto& dld = device.GetLogical();
@@ -186,6 +205,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<VkImageUsageFlags>(VK_IMAGE_USAGE_STORAGE_BIT) : 0;
frame->image = memory_allocator.CreateImage({
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
.pNext = nullptr,
@@ -202,7 +224,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 +256,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 +351,14 @@ 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<size_t>(swapchain.GetImageCount(), 7);
const size_t generations =
Settings::values.frame_gen.GetValue()
? static_cast<size_t>(Settings::values.frame_gen_multiplier.GetValue()) - 1
: 0;
const size_t frames_per_composite = generations + 1;
image_count = std::min<size_t>(
std::max<size_t>(swapchain.GetImageCount() + generations, frames_per_composite * 2),
MAX_FRAMES_IN_FLIGHT);
}
void PresentManager::CopyToSwapchain(Frame* frame) {
@@ -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
size_t AvailableExtraFrames();
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{};
};
@@ -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();
@@ -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;
+7 -1
View File
@@ -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;
+14
View File
@@ -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<std::function<void(bool)>>& 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;
}