Compare commits

...

3 Commits

Author SHA1 Message Date
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
36 changed files with 1634 additions and 8 deletions
+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,8 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
RENDERER_PATCH_OLD_QCOM_DRIVERS("patch_old_qcom_drivers"),
RENDERER_VERTEX_INPUT_DYNAMIC_STATE("vertex_input_dynamic_state"),
RENDERER_SAMPLE_SHADING("sample_shading"),
RENDERER_FRAME_GEN("frame_gen"),
RENDERER_FRAME_GEN_DUMP_FLOW("frame_gen_dump_flow"),
GPU_UNSWIZZLE_ENABLED("gpu_unswizzle_enabled"),
PICTURE_IN_PICTURE("picture_in_picture"),
USE_CUSTOM_RTC("custom_rtc_enabled"),
@@ -607,6 +607,20 @@ abstract class SettingsItem(
valuesId = R.array.rendererAntiAliasingValues
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_FRAME_GEN,
titleId = R.string.frame_gen,
descriptionId = R.string.frame_gen_description
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_FRAME_GEN_DUMP_FLOW,
titleId = R.string.frame_gen_dump_flow,
descriptionId = R.string.frame_gen_dump_flow_description
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_SCREEN_LAYOUT,
@@ -33,6 +33,7 @@ import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.view.PathSetting
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
import org.yuzu.yuzu_emu.fragments.ProgressDialogFragment
import org.yuzu.yuzu_emu.utils.PathUtil
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import org.yuzu.yuzu_emu.utils.*
@@ -114,6 +115,10 @@ class SettingsFragment : Fragment() {
viewLifecycleOwner,
resetState = { settingsViewModel.setShouldReloadSettingsList(false) }
) { if (it) presenter.loadSettingsList() }
settingsViewModel.shouldShowLosslessInstaller.collect(
viewLifecycleOwner,
resetState = { settingsViewModel.setShouldShowLosslessInstaller(false) }
) { if (it) losslessDllPickerLauncher.launch(arrayOf("*/*")) }
settingsViewModel.adapterItemChanged.collect(
viewLifecycleOwner,
resetState = { settingsViewModel.setAdapterItemChanged(-1) }
@@ -267,6 +272,33 @@ private fun getPlayerIndex(): Int =
directoryPickerLauncher.launch(null)
}
private val losslessDllPickerLauncher = registerForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri ->
if (uri == null) {
return@registerForActivityResult
}
val resultStrings = resources.getStringArray(R.array.losslessDllResults)
ProgressDialogFragment.newInstance(
requireActivity(),
R.string.lossless_scaling_installing,
false
) { _, _ ->
val result = LosslessScalingHelper.install(uri)
if (result == LosslessScalingHelper.RESULT_OK) {
getString(R.string.lossless_scaling_install_success)
} else {
MessageDialogFragment.newInstance(
titleId = R.string.lossless_scaling_install_failed,
descriptionString = resultStrings[result]
)
}
}.apply {
onDialogComplete = { settingsViewModel.setShouldReloadSettingsList(true) }
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
}
private val directoryPickerLauncher = registerForActivityResult(
ActivityResultContracts.OpenDocumentTree()
) { uri ->
@@ -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,49 @@ class SettingsFragmentPresenter(
}
}
private fun addFrameGenSettings(sl: ArrayList<SettingsItem>) {
sl.apply {
add(HeaderSetting(R.string.frame_gen))
val installed = LosslessScalingHelper.isInstalled()
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 (!LosslessScalingHelper.isSupportedByGpu()) {
add(
RunnableSetting(
titleId = R.string.frame_gen_unsupported,
descriptionId = R.string.frame_gen_unsupported_description,
isRunnable = false
) {}
)
return@apply
}
if (!installed) {
return@apply
}
add(BooleanSetting.RENDERER_FRAME_GEN.key)
add(BooleanSetting.RENDERER_FRAME_GEN_DUMP_FLOW.key)
}
}
private fun isSharpnessScalingFilterSelected(): Boolean {
val needsGlobal = getNeedsGlobalForKey(IntSetting.RENDERER_SCALING_FILTER.key)
val selectedFilter = IntSetting.RENDERER_SCALING_FILTER.getInt(needsGlobal)
@@ -280,6 +324,8 @@ class SettingsFragmentPresenter(
}
add(IntSetting.RENDERER_ANTI_ALIASING.key)
addFrameGenSettings(this)
add(HeaderSetting(R.string.advanced))
add(IntSetting.RENDERER_ACCURACY.key)
@@ -36,6 +36,9 @@ 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 sliderProgress: StateFlow<Int> get() = _sliderProgress
private val _sliderProgress = MutableStateFlow(-1)
@@ -85,6 +88,10 @@ class SettingsViewModel : ViewModel() {
_shouldReloadSettingsList.value = value
}
fun setShouldShowLosslessInstaller(value: Boolean) {
_shouldShowLosslessInstaller.value = value
}
fun setSliderTextValue(value: Float, units: String) {
_sliderProgress.value = value.toInt()
_sliderTextValue.value = String.format(
@@ -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
}
}
+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) {
@@ -638,6 +638,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,35 @@
<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_description">Insert an interpolated frame between every pair of real frames using Lossless Scaling. Adds about one frame of input latency.</string>
<string name="frame_gen_dump_flow">Dump flow pyramid</string>
<string name="frame_gen_dump_flow_description">Write the optical flow mip levels 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 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_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.
+5
View File
@@ -388,6 +388,11 @@ struct Values {
true,
true};
SwitchableSetting<bool> frame_gen{linkage, false, "frame_gen", Category::Renderer,
Specialization::Default, true, true};
SwitchableSetting<bool> frame_gen_dump_flow{linkage, false, "frame_gen_dump_flow",
Category::Renderer};
SwitchableSetting<bool> use_asynchronous_gpu_emulation{linkage,
#ifdef __ANDROID__
false,
+11
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,12 @@ add_library(video_core STATIC
renderer_vulkan/present/anti_alias_pass.h
renderer_vulkan/present/filters.cpp
renderer_vulkan/present/filters.h
renderer_vulkan/present/frame_gen.cpp
renderer_vulkan/present/frame_gen.h
renderer_vulkan/present/lsfg_mipmaps.cpp
renderer_vulkan/present/lsfg_mipmaps.h
renderer_vulkan/present/lsfg_shaders.cpp
renderer_vulkan/present/lsfg_shaders.h
renderer_vulkan/present/fsr.cpp
renderer_vulkan/present/fsr.h
renderer_vulkan/present/fxaa.cpp
@@ -345,6 +355,7 @@ add_dependencies(video_core host_shaders)
target_include_directories(video_core PRIVATE ${HOST_SHADERS_INCLUDE})
target_link_libraries(video_core PRIVATE sirit::sirit)
target_link_libraries(video_core PRIVATE dxbc)
# Header-only stuff needed by all dependent targets
target_link_libraries(video_core PUBLIC Vulkan::Headers Vulkan::UtilityHeaders GPUOpen::VulkanMemoryAllocator)
+515
View File
@@ -0,0 +1,515 @@
// 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 = 1;
struct CacheHeader {
u32 magic;
u32 version;
u64 source_size;
u64 source_hash;
u32 module_count;
};
struct Section {
u32 virtual_address;
u32 virtual_size;
u32 raw_address;
u32 raw_size;
};
struct ResourceEntry {
u32 id;
u32 offset;
bool is_directory;
bool is_named;
};
class ImageReader {
public:
explicit ImageReader(std::span<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]] LosslessStatus TranslateAll(const ResourceSpans& resources,
ShaderModules& out_modules) {
out_modules.clear();
for (const u32 id : PerformanceShaderIds()) {
const auto hit = resources.find(id);
if (hit == resources.end()) {
return LosslessStatus::MissingShaders;
}
std::vector<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, ShaderModules& out_modules) {
if (!Common::FS::Exists(path)) {
return false;
}
Common::FS::IOFile file{path, Common::FS::FileAccessMode::Read,
Common::FS::FileType::BinaryFile};
CacheHeader header{};
if (!file.IsOpen() || file.Read(header) != 1) {
return false;
}
if (header.magic != CACHE_MAGIC || header.version != CACHE_VERSION ||
header.source_size != source_size || header.source_hash != source_hash) {
return false;
}
out_modules.clear();
for (u32 i = 0; i < header.module_count; ++i) {
u32 id{};
u32 word_count{};
if (file.Read(id) != 1 || file.Read(word_count) != 1 || word_count == 0) {
return false;
}
std::vector<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());
}
LosslessStatus LoadShaderModules(ShaderModules& out_modules) {
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();
if (ReadShaderCache(cache_path, source_size, source_hash, out_modules)) {
return LosslessStatus::Ok;
}
ResourceSpans spans;
const LosslessStatus parse_status = ParseShaderSpans(image, spans);
if (parse_status != LosslessStatus::Ok) {
return parse_status;
}
const LosslessStatus translate_status = TranslateAll(spans, out_modules);
if (translate_status != LosslessStatus::Ok) {
return translate_status;
}
const CacheHeader header{
.magic = CACHE_MAGIC,
.version = CACHE_VERSION,
.source_size = source_size,
.source_hash = source_hash,
.module_count = static_cast<u32>(out_modules.size()),
};
if (!WriteShaderCache(cache_path, header, out_modules)) {
void(Common::FS::RemoveFile(cache_path));
return LosslessStatus::CacheUnusable;
}
return LosslessStatus::Ok;
}
LosslessStatus BuildShaderCache() {
ShaderModules modules;
return LoadShaderModules(modules);
}
bool RemoveInstalledLosslessDll() {
const std::filesystem::path cache_path = GetShaderCachePath();
if (Common::FS::Exists(cache_path)) {
void(Common::FS::RemoveFile(cache_path));
}
const std::filesystem::path path = GetLosslessDllPath();
if (!Common::FS::Exists(path)) {
return true;
}
return Common::FS::RemoveFile(path);
}
} // namespace VideoCore::FrameGen
+54
View File
@@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// 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>>;
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};
} // namespace PerformanceShader
[[nodiscard]] std::filesystem::path GetLosslessDllPath();
[[nodiscard]] std::filesystem::path GetShaderCachePath();
[[nodiscard]] LosslessStatus ReadShaderResources(const std::filesystem::path& path,
ShaderResources& out_resources);
[[nodiscard]] LosslessStatus ValidateLosslessDll(const std::filesystem::path& path);
[[nodiscard]] LosslessStatus GetInstalledLosslessStatus();
[[nodiscard]] LosslessStatus BuildShaderCache();
[[nodiscard]] LosslessStatus LoadShaderModules(ShaderModules& out_modules);
bool RemoveInstalledLosslessDll();
} // namespace VideoCore::FrameGen
@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#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;
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);
}
}
} // Anonymous namespace
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
+15
View File
@@ -0,0 +1,15 @@
// 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]] std::vector<u32> TranslateComputeShader(std::span<const u8> dxbc);
} // namespace VideoCore::FrameGen
@@ -0,0 +1,112 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <string>
#include "common/fs/file.h"
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "common/settings.h"
#include "video_core/renderer_vulkan/present/frame_gen.h"
#include "video_core/renderer_vulkan/present/util.h"
#include "video_core/renderer_vulkan/vk_present_manager.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
#include "video_core/vulkan_common/vulkan_device.h"
namespace Vulkan {
namespace {
constexpr f32 LSFG_FLOW_SCALE = 1.0f;
void WriteGrayscalePgm(const std::filesystem::path& path, VkExtent2D extent,
std::span<const u8> pixels) {
Common::FS::IOFile file{path, Common::FS::FileAccessMode::Write,
Common::FS::FileType::BinaryFile};
if (!file.IsOpen()) {
return;
}
const std::string header =
"P5\n" + std::to_string(extent.width) + " " + std::to_string(extent.height) + "\n255\n";
if (file.Write(header) != header.size()) {
return;
}
const size_t expected = static_cast<size_t>(extent.width) * extent.height;
void(file.Write(pixels.subspan(0, std::min(expected, pixels.size()))));
void(file.Flush());
}
} // Anonymous namespace
FrameGen::FrameGen(MemoryAllocator& memory_allocator_, Scheduler& scheduler_)
: memory_allocator{memory_allocator_}, scheduler{scheduler_} {}
FrameGen::~FrameGen() = default;
void FrameGen::Process(const Device& device, Frame* frame) {
if (unavailable || !Settings::values.frame_gen.GetValue()) {
return;
}
if (!shaders) {
shaders.emplace(device);
if (!shaders->IsValid()) {
unavailable = true;
return;
}
}
const VkExtent2D extent{.width = frame->width, .height = frame->height};
if (!mipmaps || built_extent.width != extent.width || built_extent.height != extent.height) {
Rebuild(device, extent);
}
mipmaps->Dispatch(device, scheduler, *frame->image_view, frame_count);
++frame_count;
const bool dump_requested = Settings::values.frame_gen_dump_flow.GetValue();
if (!dump_requested) {
dumped = false;
} else if (!dumped) {
DumpFlowPyramid(device);
dumped = true;
}
}
void FrameGen::Rebuild(const Device& device, VkExtent2D extent) {
scheduler.Finish();
mipmaps.emplace(device, memory_allocator, *shaders, extent, LSFG_FLOW_SCALE);
built_extent = extent;
frame_count = 0;
}
void FrameGen::DumpFlowPyramid(const Device& device) {
const std::filesystem::path directory =
Common::FS::GetEdenPath(Common::FS::EdenPath::LosslessDir) / "debug";
if (!Common::FS::CreateDirs(directory)) {
return;
}
for (size_t level = 0; level < LSFG_MIP_LEVELS; ++level) {
const VkExtent2D extent = mipmaps->GetLevelExtent(level);
const VkDeviceSize size = static_cast<VkDeviceSize>(extent.width) * extent.height;
vk::Buffer readback = CreateWrappedBuffer(memory_allocator, size, MemoryUsage::Download);
scheduler.RequestOutsideRenderPassOperationContext();
scheduler.Record([image = mipmaps->GetLevelImage(level), dst = *readback,
extent](vk::CommandBuffer cmdbuf) {
DownloadColorImage(cmdbuf, image, dst,
VkExtent3D{.width = extent.width, .height = extent.height,
.depth = 1});
});
scheduler.Finish();
readback.Invalidate();
WriteGrayscalePgm(directory / ("flow_mip" + std::to_string(level) + ".pgm"), extent,
readback.Mapped());
}
}
} // namespace Vulkan
@@ -0,0 +1,41 @@
// 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_mipmaps.h"
#include "video_core/renderer_vulkan/present/lsfg_shaders.h"
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
namespace Vulkan {
class Device;
class Scheduler;
struct Frame;
class FrameGen {
public:
explicit FrameGen(MemoryAllocator& memory_allocator, Scheduler& scheduler);
~FrameGen();
void Process(const Device& device, Frame* frame);
private:
void Rebuild(const Device& device, VkExtent2D extent);
void DumpFlowPyramid(const Device& device);
MemoryAllocator& memory_allocator;
Scheduler& scheduler;
std::optional<LsfgShaders> shaders;
std::optional<LsfgMipmaps> mipmaps;
VkExtent2D built_extent{};
u64 frame_count{};
bool unavailable{};
bool dumped{};
};
} // namespace Vulkan
@@ -0,0 +1,212 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <cstring>
#include "video_core/frame_gen/lossless_dll.h"
#include "video_core/renderer_vulkan/present/lsfg_mipmaps.h"
#include "video_core/renderer_vulkan/present/lsfg_shaders.h"
#include "video_core/renderer_vulkan/present/util.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
#include "video_core/vulkan_common/vulkan_device.h"
namespace Vulkan {
namespace {
constexpr VkFormat FLOW_FORMAT = VK_FORMAT_R8_UNORM;
constexpr u32 DISPATCH_TILE_SHIFT = 6;
constexpr size_t DESCRIPTOR_SET_COUNT = 2;
struct LsfgConstants {
std::array<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);
constexpr std::array<VkDescriptorType, 3 + LSFG_MIP_LEVELS> MIPMAPS_BINDINGS{
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_DESCRIPTOR_TYPE_SAMPLER,
VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
};
vk::Sampler CreateFlowSampler(const Device& device) {
return device.GetLogical().CreateSampler(VkSamplerCreateInfo{
.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.magFilter = VK_FILTER_LINEAR,
.minFilter = VK_FILTER_LINEAR,
.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR,
.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER,
.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER,
.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER,
.mipLodBias = 0.0f,
.anisotropyEnable = VK_FALSE,
.maxAnisotropy = 0.0f,
.compareEnable = VK_FALSE,
.compareOp = VK_COMPARE_OP_NEVER,
.minLod = 0.0f,
.maxLod = VK_LOD_CLAMP_NONE,
.borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK,
.unnormalizedCoordinates = VK_FALSE,
});
}
} // Anonymous namespace
LsfgMipmaps::LsfgMipmaps(const Device& device, MemoryAllocator& memory_allocator_,
const LsfgShaders& shaders, VkExtent2D input_extent, f32 flow_scale)
: memory_allocator{memory_allocator_} {
flow_extent = VkExtent2D{
.width = std::max(1u, static_cast<u32>(static_cast<f32>(input_extent.width) * flow_scale)),
.height = std::max(1u, static_cast<u32>(static_cast<f32>(input_extent.height) * flow_scale)),
};
CreateImages(device);
CreateUniformBuffer(flow_scale);
sampler = CreateFlowSampler(device);
descriptor_pool = CreateWrappedDescriptorPool(
device, DESCRIPTOR_SET_COUNT * MIPMAPS_BINDINGS.size(), DESCRIPTOR_SET_COUNT,
{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_DESCRIPTOR_TYPE_SAMPLER,
VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE});
descriptor_set_layout = CreateWrappedDescriptorSetLayout(
device, std::span<const VkDescriptorType>{MIPMAPS_BINDINGS}, VK_SHADER_STAGE_COMPUTE_BIT);
const std::vector<VkDescriptorSetLayout> layouts(DESCRIPTOR_SET_COUNT,
*descriptor_set_layout);
descriptor_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts);
pipeline_layout = CreateWrappedPipelineLayout(device, descriptor_set_layout);
pipeline = CreateWrappedComputePipeline(
device, pipeline_layout, shaders.Get(VideoCore::FrameGen::PerformanceShader::MIPMAPS));
}
VkExtent2D LsfgMipmaps::GetLevelExtent(size_t level) const {
return VkExtent2D{
.width = std::max(1u, flow_extent.width >> level),
.height = std::max(1u, flow_extent.height >> level),
};
}
void LsfgMipmaps::CreateImages(const Device& device) {
for (size_t i = 0; i < LSFG_MIP_LEVELS; ++i) {
images[i] = CreateWrappedImage(memory_allocator, GetLevelExtent(i), FLOW_FORMAT);
image_views[i] = CreateWrappedImageView(device, images[i], FLOW_FORMAT);
}
}
void LsfgMipmaps::CreateUniformBuffer(f32 flow_scale) {
uniform_buffer = CreateWrappedBuffer(memory_allocator, sizeof(LsfgConstants),
MemoryUsage::Upload);
const LsfgConstants constants{
.input_offset = {0, 0},
.first_iter = 0,
.first_iter_s = 0,
.advanced_color_kind = 0,
.hdr_support = 0,
.resolution_inv_scale = 1.0f / flow_scale,
.timestamp = 0.0f,
.ui_threshold = 0.5f,
.padding = {0, 0, 0},
};
const std::span<u8> mapped = uniform_buffer.Mapped();
std::memcpy(mapped.data(), &constants, sizeof(constants));
uniform_buffer.Flush();
}
void LsfgMipmaps::Dispatch(const Device& device, Scheduler& scheduler, VkImageView current_view,
u64 frame_count) {
const size_t set_index = frame_count % DESCRIPTOR_SET_COUNT;
const VkDescriptorSet set = descriptor_sets[set_index];
const VkDescriptorBufferInfo buffer_info{
.buffer = *uniform_buffer,
.offset = 0,
.range = sizeof(LsfgConstants),
};
const VkDescriptorImageInfo sampler_info{
.sampler = *sampler,
.imageView = VK_NULL_HANDLE,
.imageLayout = VK_IMAGE_LAYOUT_UNDEFINED,
};
const VkDescriptorImageInfo sampled_info{
.sampler = VK_NULL_HANDLE,
.imageView = current_view,
.imageLayout = VK_IMAGE_LAYOUT_GENERAL,
};
std::array<VkDescriptorImageInfo, LSFG_MIP_LEVELS> storage_infos{};
for (size_t i = 0; i < LSFG_MIP_LEVELS; ++i) {
storage_infos[i] = VkDescriptorImageInfo{
.sampler = VK_NULL_HANDLE,
.imageView = *image_views[i],
.imageLayout = VK_IMAGE_LAYOUT_GENERAL,
};
}
std::vector<VkWriteDescriptorSet> writes;
writes.reserve(MIPMAPS_BINDINGS.size());
const auto push = [&](u32 binding, VkDescriptorType type,
const VkDescriptorImageInfo* image_info,
const VkDescriptorBufferInfo* buf_info) {
writes.push_back(VkWriteDescriptorSet{
.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
.pNext = nullptr,
.dstSet = set,
.dstBinding = binding,
.dstArrayElement = 0,
.descriptorCount = 1,
.descriptorType = type,
.pImageInfo = image_info,
.pBufferInfo = buf_info,
.pTexelBufferView = nullptr,
});
};
push(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, nullptr, &buffer_info);
push(1, VK_DESCRIPTOR_TYPE_SAMPLER, &sampler_info, nullptr);
push(2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, &sampled_info, nullptr);
for (u32 i = 0; i < LSFG_MIP_LEVELS; ++i) {
push(3 + i, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, &storage_infos[i], nullptr);
}
device.GetLogical().UpdateDescriptorSets(writes, {});
const u32 groups_x = (flow_extent.width + (1u << DISPATCH_TILE_SHIFT) - 1) >>
DISPATCH_TILE_SHIFT;
const u32 groups_y = (flow_extent.height + (1u << DISPATCH_TILE_SHIFT) - 1) >>
DISPATCH_TILE_SHIFT;
std::array<VkImage, LSFG_MIP_LEVELS> raw_images{};
for (size_t i = 0; i < LSFG_MIP_LEVELS; ++i) {
raw_images[i] = *images[i];
}
scheduler.RequestOutsideRenderPassOperationContext();
scheduler.Record([raw_images, set, groups_x, groups_y, layout = *pipeline_layout,
compute_pipeline = *pipeline](vk::CommandBuffer cmdbuf) {
for (const VkImage image : raw_images) {
TransitionImageLayout(cmdbuf, image, VK_IMAGE_LAYOUT_GENERAL,
VK_IMAGE_LAYOUT_UNDEFINED);
}
cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, compute_pipeline);
cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, layout, 0, set, {});
cmdbuf.Dispatch(groups_x, groups_y, 1);
});
}
} // namespace Vulkan
@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <array>
#include "common/common_types.h"
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
namespace Vulkan {
class Device;
class LsfgShaders;
class Scheduler;
constexpr size_t LSFG_MIP_LEVELS = 7;
class LsfgMipmaps {
public:
explicit LsfgMipmaps(const Device& device, MemoryAllocator& memory_allocator,
const LsfgShaders& shaders, VkExtent2D input_extent, f32 flow_scale);
void Dispatch(const Device& device, Scheduler& scheduler, VkImageView current_view,
u64 frame_count);
[[nodiscard]] VkImageView GetLevelView(size_t level) const {
return *image_views[level];
}
[[nodiscard]] VkImage GetLevelImage(size_t level) const {
return *images[level];
}
[[nodiscard]] VkExtent2D GetLevelExtent(size_t level) const;
private:
void CreateImages(const Device& device);
void CreateUniformBuffer(f32 flow_scale);
MemoryAllocator& memory_allocator;
VkExtent2D flow_extent{};
vk::Buffer uniform_buffer;
vk::Sampler sampler;
vk::DescriptorPool descriptor_pool;
vk::DescriptorSetLayout descriptor_set_layout;
vk::DescriptorSets descriptor_sets;
vk::PipelineLayout pipeline_layout;
vk::Pipeline pipeline;
std::array<vk::Image, LSFG_MIP_LEVELS> images;
std::array<vk::ImageView, LSFG_MIP_LEVELS> image_views;
};
} // namespace Vulkan
@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "video_core/frame_gen/lossless_dll.h"
#include "video_core/renderer_vulkan/present/lsfg_shaders.h"
#include "video_core/renderer_vulkan/present/util.h"
#include "video_core/vulkan_common/vulkan_device.h"
namespace Vulkan {
LsfgShaders::LsfgShaders(const Device& device) {
if (!device.IsVulkanMemoryModelSupported()) {
return;
}
VideoCore::FrameGen::ShaderModules code;
if (VideoCore::FrameGen::LoadShaderModules(code) != VideoCore::FrameGen::LosslessStatus::Ok) {
return;
}
for (const auto& [id, words] : code) {
modules.emplace(id, CreateWrappedShaderModule(device, words));
}
valid = true;
}
VkShaderModule LsfgShaders::Get(u32 shader_id) const {
const auto hit = modules.find(shader_id);
return hit == modules.end() ? VK_NULL_HANDLE : *hit->second;
}
} // namespace Vulkan
@@ -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,9 @@ void RendererVulkan::Composite(std::span<const Tegra::FramebufferConfig> framebu
blit_swapchain.DrawToFrame(device, rasterizer, frame, framebuffers,
render_window.GetFramebufferLayout(), swapchain.GetImageCount(),
swapchain.GetImageViewFormat());
frame_gen.Process(device, frame);
scheduler.Flush(*frame->render_ready);
present_manager.Present(frame);
@@ -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;
@@ -202,7 +202,8 @@ void PresentManager::RecreateFrame(Frame* frame, u32 width, u32 height, VkFormat
.arrayLayers = 1,
.samples = VK_SAMPLE_COUNT_1_BIT,
.tiling = VK_IMAGE_TILING_OPTIMAL,
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |
VK_IMAGE_USAGE_SAMPLED_BIT,
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
.queueFamilyIndexCount = 0,
.pQueueFamilyIndices = nullptr,
+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;