diff options
| author | CamilleLaVey <camillelavey99@gmail.com> | 2026-08-17 00:30:19 +0200 |
|---|---|---|
| committer | crueter <crueter@eden-emu.dev> | 2026-08-17 00:30:19 +0200 |
| commit | 2000fdfb7b27e5e2546098b44268fc74887b22a5 (patch) | |
| tree | 924d533de5e75f9f08e02f67bddc946690ea5665 /src/video_core | |
| parent | dc95cd09eea9749250fe31a3072684d341d19417 (diff) | |
[vulkan, android] Initial implementation of LSFG-VK (#4263)
Pretty much self explanatory, own implementation based on LSFG-VK available with some additional reverse engineer work for the actual use of shaders inside Lossless.dll, we don't store/ bundle any tools that bypass security and self ownership: to use this feature is required to own your own copy of Lossless Scaling and I encourage to support developers behind this work.
Special thanks:
1.- THS - Original developer behind Lossless Scaling
2.- PancakeTAS - Developer behind LSFG-VK
3.- The Big Smolio The Sleeper (@gidoly)
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4263
Diffstat (limited to 'src/video_core')
38 files changed, 3770 insertions, 13 deletions
diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 6720195c0e..c86849837b 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -267,6 +267,38 @@ add_library(video_core STATIC video_core.h ) +if (ENABLE_LSFG) + target_sources(video_core PRIVATE + frame_gen/lossless_dll.cpp + frame_gen/lossless_dll.h + frame_gen/lsfg_translate.cpp + frame_gen/lsfg_translate.h + renderer_vulkan/present/frame_gen.cpp + renderer_vulkan/present/frame_gen.h + renderer_vulkan/present/frame_gen_pacer.cpp + renderer_vulkan/present/frame_gen_pacer.h + renderer_vulkan/present/lsfg_alpha.cpp + renderer_vulkan/present/lsfg_alpha.h + renderer_vulkan/present/lsfg_beta.cpp + renderer_vulkan/present/lsfg_beta.h + renderer_vulkan/present/lsfg_chain.cpp + renderer_vulkan/present/lsfg_chain.h + renderer_vulkan/present/lsfg_common.cpp + renderer_vulkan/present/lsfg_common.h + renderer_vulkan/present/lsfg_delta.cpp + renderer_vulkan/present/lsfg_delta.h + renderer_vulkan/present/lsfg_gamma.cpp + renderer_vulkan/present/lsfg_gamma.h + renderer_vulkan/present/lsfg_generate.cpp + renderer_vulkan/present/lsfg_generate.h + renderer_vulkan/present/lsfg_mipmaps.cpp + renderer_vulkan/present/lsfg_mipmaps.h + renderer_vulkan/present/lsfg_shaders.cpp + renderer_vulkan/present/lsfg_shaders.h + ) + target_compile_definitions(video_core PUBLIC HAS_LSFG) +endif() + if (ENABLE_OPENGL) target_sources(video_core PRIVATE renderer_opengl/present/filters.cpp diff --git a/src/video_core/frame_gen/lossless_dll.cpp b/src/video_core/frame_gen/lossless_dll.cpp new file mode 100644 index 0000000000..5d2d08ffb3 --- /dev/null +++ b/src/video_core/frame_gen/lossless_dll.cpp @@ -0,0 +1,553 @@ +// 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) { + return variant == ShaderVariant::NativeFp16 ? PerformanceShader::NATIVE_FP16_OFFSET + : PerformanceShader::NATIVE_FP32_OFFSET; +} + +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]] std::optional<ShaderVariant> SelectVariant(const ResourceSpans& resources, + bool allow_fp16, bool prefer_fp16) { + if (prefer_fp16 && HasNativeVariant(resources, ShaderVariant::NativeFp16)) { + return ShaderVariant::NativeFp16; + } + if (HasNativeVariant(resources, ShaderVariant::NativeFp32)) { + return ShaderVariant::NativeFp32; + } + if (allow_fp16 && HasNativeVariant(resources, ShaderVariant::NativeFp16)) { + return ShaderVariant::NativeFp16; + } + return std::nullopt; +} + +[[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; + } + std::vector<u32> adopted = AdoptSpirvModule(hit->second); + if (adopted.empty()) { + return LosslessStatus::TranslationFailed; + } + out_modules.emplace(id, std::move(adopted)); + } + 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()); +} + +LosslessStatus LoadShaderModules(ShaderModules& out_modules, bool allow_fp16, 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 std::optional<ShaderVariant> variant = SelectVariant(spans, allow_fp16, prefer_fp16); + if (!variant) { + return LosslessStatus::MissingShaders; + } + + 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, true); +} + +bool RemoveInstalledLosslessDll() { + const std::filesystem::path cache_path = GetShaderCachePath(); + if (Common::FS::Exists(cache_path)) { + void(Common::FS::RemoveFile(cache_path)); + } + + const std::filesystem::path path = GetLosslessDllPath(); + if (!Common::FS::Exists(path)) { + return true; + } + return Common::FS::RemoveFile(path); +} + +} // namespace VideoCore::FrameGen diff --git a/src/video_core/frame_gen/lossless_dll.h b/src/video_core/frame_gen/lossless_dll.h new file mode 100644 index 0000000000..1d583d008f --- /dev/null +++ b/src/video_core/frame_gen/lossless_dll.h @@ -0,0 +1,67 @@ +// 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 { + NativeFp32 = 1, + NativeFp16 = 2, +}; + +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]] LosslessStatus LoadShaderModules(ShaderModules& out_modules, + bool allow_fp16 = false, + bool prefer_fp16 = false); + +bool RemoveInstalledLosslessDll(); + +} // namespace VideoCore::FrameGen diff --git a/src/video_core/frame_gen/lsfg_translate.cpp b/src/video_core/frame_gen/lsfg_translate.cpp new file mode 100644 index 0000000000..4063465d93 --- /dev/null +++ b/src/video_core/frame_gen/lsfg_translate.cpp @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <algorithm> +#include <cstring> +#include <map> +#include <tuple> + +#include "video_core/frame_gen/lsfg_translate.h" + +namespace VideoCore::FrameGen { + +namespace { + +constexpr u32 SPIRV_MAGIC = 0x07230203; +constexpr u32 SPIRV_WORD_COUNT_SHIFT = 16; +constexpr u32 SPIRV_OPCODE_MASK = 0xffff; +constexpr u32 SPIRV_OP_FUNCTION = 54; +constexpr u32 SPIRV_OP_DECORATE = 71; +constexpr u32 SPIRV_DECORATION_BINDING = 33; +constexpr u32 SPIRV_DECORATION_DESCRIPTOR_SET = 34; + +constexpr u32 DECORATION_LITERAL_WORD = 3; +constexpr size_t SPIRV_HEADER_WORDS = 5; + +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] >> SPIRV_WORD_COUNT_SHIFT; + const u32 opcode = words[offset] & SPIRV_OPCODE_MASK; + if (length == 0 || offset + length > words.size()) { + return; + } + if (opcode == SPIRV_OP_FUNCTION) { + break; + } + if (opcode == SPIRV_OP_DECORATE && length >= 4) { + if (words[offset + 2] == SPIRV_DECORATION_DESCRIPTOR_SET) { + sets[words[offset + 1]] = words[offset + 3]; + } else if (words[offset + 2] == SPIRV_DECORATION_BINDING) { + 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 == SPIRV_MAGIC; +} + +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; +} + +} // namespace VideoCore::FrameGen diff --git a/src/video_core/frame_gen/lsfg_translate.h b/src/video_core/frame_gen/lsfg_translate.h new file mode 100644 index 0000000000..5d21128c3e --- /dev/null +++ b/src/video_core/frame_gen/lsfg_translate.h @@ -0,0 +1,17 @@ +// 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); + +} // namespace VideoCore::FrameGen diff --git a/src/video_core/renderer_vulkan/present/frame_gen.cpp b/src/video_core/renderer_vulkan/present/frame_gen.cpp new file mode 100644 index 0000000000..b0d16819e5 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/frame_gen.cpp @@ -0,0 +1,361 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <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; +constexpr u32 LSFG_RECURRENCE_FRAMES = 2; + +[[nodiscard]] f32 ManualFlowScale() { + return static_cast<f32>(Settings::values.frame_gen_flow_scale.GetValue()) / 100.0f; +} + +[[nodiscard]] f32 ConfiguredFlowScale(VkExtent2D guest_extent, VkExtent2D presented_extent) { + if (!Settings::values.frame_gen_flow_scale_auto.GetValue()) { + return ManualFlowScale(); + } + if (guest_extent.width == 0 || presented_extent.width == 0) { + return 1.0f; + } + + const f32 rendered_width = static_cast<f32>(guest_extent.width) * + Settings::values.resolution_info.up_factor; + const f32 ratio = rendered_width / static_cast<f32>(presented_extent.width); + + constexpr f32 FLOW_SCALE_STEPS = 20.0f; + const f32 stepped = std::ceil(ratio * FLOW_SCALE_STEPS) / FLOW_SCALE_STEPS; + return std::clamp(stepped, 0.25f, 1.0f); +} + +bool IsBlueFirst(VkFormat format) { + return format == VK_FORMAT_B8G8R8A8_UNORM || format == VK_FORMAT_B8G8R8A8_SRGB; +} + +VkDeviceSize BytesPerTexel(VkFormat format) { + switch (format) { + case VK_FORMAT_R8_UNORM: + return 1; + case VK_FORMAT_R16G16B16A16_SFLOAT: + return 8; + default: + return COLOR_CHANNELS; + } +} + +void WritePortablePixmap(const std::filesystem::path& path, const std::string& magic, + VkExtent2D extent, std::span<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, + VkExtent2D guest_extent) { + generated = false; + + if (unavailable || !Settings::values.frame_gen.GetValue()) { + if (chain) { + scheduler.Finish(); + chain.reset(); + } + warm_streak = 0; + return; + } + + if (!frame->storage_view) { + unavailable = true; + return; + } + + if (!shaders) { + shaders.emplace(device); + if (!shaders->IsValid()) { + unavailable = true; + return; + } + } + + peak_guest_extent.width = std::max(peak_guest_extent.width, guest_extent.width); + peak_guest_extent.height = std::max(peak_guest_extent.height, guest_extent.height); + + const VkExtent2D extent{.width = frame->width, .height = frame->height}; + const f32 flow_scale = ConfiguredFlowScale(peak_guest_extent, extent); + if (!chain || built_extent.width != extent.width || built_extent.height != extent.height || + built_format != format || built_flow_scale != flow_scale) { + Rebuild(device, extent, format, flow_scale); + } + + const u64 count = frame_count++; + last_count = count; + last_generations = plan.generations; + + const bool warm = plan.warm && count + 1 >= LSFG_REQUIRED_FRAMES; + warm_streak = warm ? warm_streak + 1 : 0; + generated = warm && warm_streak >= LSFG_RECURRENCE_FRAMES && plan.generations > 0; + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([this, source = *frame->image, extent, count, + dispatch = warm](vk::CommandBuffer cmdbuf) { + CopyPresentedFrame(cmdbuf, source, chain->Input(count), extent); + if (dispatch) { + chain->DispatchShared(cmdbuf, count); + } + }); + + const bool dump_requested = generated && Settings::values.frame_gen_dump_flow.GetValue(); + if (!dump_requested) { + dumped = false; + } else if (!dumped) { + DumpDebugImages(count); + dumped = true; + } +} + +size_t FrameGen::WantedGenerations(size_t capacity) { + if (unavailable) { + plan = {}; + return 0; + } + plan = pacer.Plan(capacity); + return plan.generations; +} + +size_t FrameGen::GeneratedFrameCount() const { + return generated ? last_generations : 0; +} + +void FrameGen::GenerateInto(const Device& device, Frame* destination, size_t generation) { + chain->SetTarget(device, last_generations, generation, destination->index, + *destination->storage_view); + + const VkExtent2D extent{.width = destination->width, .height = destination->height}; + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([this, count = last_count, generation_count = last_generations, generation, + target = destination->index, image = *destination->image, + extent](vk::CommandBuffer cmdbuf) { + chain->DispatchGeneration(cmdbuf, count, generation_count, generation, target, image, + extent); + }); +} + +void FrameGen::Rebuild(const Device& device, VkExtent2D extent, VkFormat format, f32 flow_scale) { + scheduler.Finish(); + chain.reset(); + + built_flow_scale = flow_scale; + + chain.emplace(device, memory_allocator, *shaders, extent, format, built_flow_scale); + built_extent = extent; + built_format = format; + frame_count = 0; + warm_streak = 0; + generated = false; +} + +void FrameGen::DumpDebugImages(u64 count) { + const std::filesystem::path directory = + Common::FS::GetEdenPath(Common::FS::EdenPath::LosslessDir) / "debug"; + if (!Common::FS::CreateDirs(directory)) { + return; + } + + const auto dump = [&](const std::string& name, LsfgImage& image) { + const VkExtent2D extent = image.Extent(); + const VkFormat format = image.Format(); + const VkDeviceSize texel_size = BytesPerTexel(format); + const VkDeviceSize size = + static_cast<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 diff --git a/src/video_core/renderer_vulkan/present/frame_gen.h b/src/video_core/renderer_vulkan/present/frame_gen.h new file mode 100644 index 0000000000..19c6d04289 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/frame_gen.h @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <optional> + +#include "common/common_types.h" +#include "video_core/renderer_vulkan/present/frame_gen_pacer.h" +#include "video_core/renderer_vulkan/present/lsfg_chain.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/vulkan_common/vulkan_memory_allocator.h" + +namespace Vulkan { + +class Device; +class Scheduler; +struct Frame; + +class FrameGen { +public: + explicit FrameGen(MemoryAllocator& memory_allocator, Scheduler& scheduler); + ~FrameGen(); + + void Process(const Device& device, Frame* frame, VkFormat format, VkExtent2D guest_extent); + + [[nodiscard]] size_t WantedGenerations(size_t capacity); + + [[nodiscard]] size_t GeneratedFrameCount() const; + + void GenerateInto(const Device& device, Frame* destination, size_t generation); + +private: + void Rebuild(const Device& device, VkExtent2D extent, VkFormat format, f32 flow_scale); + void DumpDebugImages(u64 count); + + MemoryAllocator& memory_allocator; + Scheduler& scheduler; + + std::optional<LsfgShaders> shaders; + std::optional<LsfgChain> chain; + FrameGenPacer pacer; + FrameGenPlan plan{}; + VkExtent2D peak_guest_extent{}; + VkExtent2D built_extent{}; + VkFormat built_format{VK_FORMAT_UNDEFINED}; + f32 built_flow_scale{}; + u64 frame_count{}; + u64 last_count{}; + size_t last_generations{}; + u32 warm_streak{}; + bool generated{}; + bool unavailable{}; + bool dumped{}; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/frame_gen_pacer.cpp b/src/video_core/renderer_vulkan/present/frame_gen_pacer.cpp new file mode 100644 index 0000000000..78a8c83b5a --- /dev/null +++ b/src/video_core/renderer_vulkan/present/frame_gen_pacer.cpp @@ -0,0 +1,240 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <algorithm> +#include <cmath> +#include <utility> + +#include "common/settings.h" +#include "video_core/renderer_vulkan/present/frame_gen_pacer.h" + +namespace Vulkan { + +namespace { + +using Clock = std::chrono::steady_clock; + +constexpr f32 INTERVAL_SMOOTHING = 0.25f; +constexpr f32 MINIMUM_BASE_RATE = 10.0f; +constexpr f32 BURST_CADENCE_RATIO = 3.0f; +constexpr f32 BURST_TARGET_RATIO = 2.0f; +constexpr f32 PROBE_THROUGHPUT_TOLERANCE = 0.95f; +constexpr f32 PROBE_BASE_COLLAPSE_RATIO = 0.70f; +constexpr f32 PROBE_MARGINAL_GAIN = 1.15f; +constexpr f32 TARGET_SATISFIED_RATIO = 0.95f; +constexpr f32 UNLOADED_BASE_RETENTION = 0.75f; +constexpr f32 CREDIT_EPSILON = 1.0e-4f; +constexpr u32 MAX_PROBE_FAILURES = 4; + +constexpr auto STABILIZATION_DURATION = std::chrono::seconds(1); +constexpr auto PROBE_DURATION = std::chrono::seconds(1); +constexpr auto DEFICIT_DURATION = std::chrono::seconds(1); +constexpr auto PROBE_STEP_DELAY = std::chrono::milliseconds(250); + +[[nodiscard]] Clock::duration ProbeBackoff(u32 failures) { + switch (failures) { + case 1: + return std::chrono::seconds(5); + case 2: + return std::chrono::seconds(15); + case 3: + return std::chrono::seconds(30); + default: + return std::chrono::seconds(60); + } +} + +} // Anonymous namespace + +FrameGenPlan FrameGenPacer::Plan(size_t capacity) { + const size_t ceiling = std::min(capacity, Settings::FrameGenMaxGenerations()); + if (ceiling == 0) { + Reset(); + return {}; + } + + const Clock::time_point now = Clock::now(); + const size_t previous_generations = std::exchange(issued_generations, 0); + if (!last_frame) { + last_frame = now; + return {}; + } + + const Clock::duration interval = now - *last_frame; + const f32 interval_seconds = std::chrono::duration<f32>(interval).count(); + last_frame = now; + + if (interval_seconds <= 0.0f) { + Stabilize(now); + return {}; + } + + const f32 target_rate = static_cast<f32>(Settings::values.frame_gen_target_rate.GetValue()); + + if (smoothed_interval > 0.0f) { + f32 burst_threshold = BURST_CADENCE_RATIO / smoothed_interval; + if (target_rate > 0.0f) { + burst_threshold = std::max(burst_threshold, target_rate * BURST_TARGET_RATIO); + } + if (1.0f / interval_seconds > burst_threshold) { + DeferEvaluations(interval); + output_credit = 0.0f; + return {}; + } + } + + if (interval_seconds > 1.0f / MINIMUM_BASE_RATE) { + Stabilize(now); + return {}; + } + + smoothed_interval = smoothed_interval > 0.0f + ? smoothed_interval + + (interval_seconds - smoothed_interval) * INTERVAL_SMOOTHING + : interval_seconds; + + if (previous_generations == 0) { + const f32 measured = 1.0f / smoothed_interval; + unloaded_base_rate = + unloaded_base_rate > 0.0f + ? unloaded_base_rate + (measured - unloaded_base_rate) * INTERVAL_SMOOTHING + : measured; + } + + if (stable_until) { + if (now < *stable_until) { + return {}; + } + stable_until.reset(); + } + + if (target_rate == 0.0f) { + limit = std::min(Settings::FrameGenGenerations(), ceiling); + output_credit = 0.0f; + issued_generations = limit; + return {.generations = limit, .warm = limit > 0}; + } + + UpdateLimit(now, 1.0f / smoothed_interval, target_rate, ceiling); + + const size_t allowed = std::min(limit, ceiling); + const f32 desired_outputs = smoothed_interval * target_rate; + if (allowed == 0 || desired_outputs <= 1.0f) { + output_credit = 0.0f; + return {}; + } + + output_credit += desired_outputs; + const size_t outputs = + std::max<size_t>(1, static_cast<size_t>(std::floor(output_credit + CREDIT_EPSILON))); + const size_t generations = std::min(outputs - 1, allowed); + + output_credit -= static_cast<f32>(generations + 1); + if (output_credit < 0.0f) { + output_credit = 0.0f; + } else if (generations == allowed && output_credit >= 1.0f) { + output_credit = std::fmod(output_credit, 1.0f); + } + + issued_generations = generations; + return {.generations = generations, .warm = true}; +} + +void FrameGenPacer::UpdateLimit(Clock::time_point now, f32 base_rate, f32 target_rate, + size_t ceiling) { + limit = std::min(limit, ceiling); + + if (probe_until) { + if (now < *probe_until) { + return; + } + probe_until.reset(); + output_credit = 0.0f; + + const f32 previous_output = + std::min(target_rate, probe_base_rate * static_cast<f32>(probe_previous_limit + 1)); + const f32 current_output = + std::min(target_rate, base_rate * static_cast<f32>(limit + 1)); + + const bool throughput_regressed = + current_output < previous_output * PROBE_THROUGHPUT_TOLERANCE; + const bool collapsed_for_marginal_gain = + base_rate < probe_base_rate * PROBE_BASE_COLLAPSE_RATIO && + current_output < previous_output * PROBE_MARGINAL_GAIN; + const bool emulation_slowed = unloaded_base_rate > 0.0f && + base_rate < unloaded_base_rate * UNLOADED_BASE_RETENTION; + + if (throughput_regressed || collapsed_for_marginal_gain || emulation_slowed) { + limit = probe_previous_limit; + probe_failures = std::min(probe_failures + 1, MAX_PROBE_FAILURES); + next_probe = now + ProbeBackoff(probe_failures); + deficit_since.reset(); + return; + } + + probe_failures = 0; + next_probe = now + PROBE_STEP_DELAY; + } + + if (base_rate * static_cast<f32>(limit + 1) >= target_rate * TARGET_SATISFIED_RATIO || + limit >= ceiling) { + deficit_since.reset(); + return; + } + + if (!deficit_since) { + deficit_since = now; + return; + } + if (now - *deficit_since < DEFICIT_DURATION) { + return; + } + if (next_probe && now < *next_probe) { + return; + } + + probe_previous_limit = limit; + probe_base_rate = base_rate; + ++limit; + probe_until = now + PROBE_DURATION; + deficit_since.reset(); + output_credit = 0.0f; +} + +void FrameGenPacer::DeferEvaluations(Clock::duration amount) { + const auto defer = [amount](std::optional<Clock::time_point>& deadline) { + if (deadline) { + *deadline += amount; + } + }; + defer(stable_until); + defer(probe_until); + defer(next_probe); + deficit_since.reset(); +} + +void FrameGenPacer::Stabilize(Clock::time_point now) { + stable_until = now + STABILIZATION_DURATION; + probe_until.reset(); + deficit_since.reset(); + smoothed_interval = 0.0f; + output_credit = 0.0f; +} + +void FrameGenPacer::Reset() { + last_frame.reset(); + stable_until.reset(); + probe_until.reset(); + next_probe.reset(); + deficit_since.reset(); + smoothed_interval = 0.0f; + output_credit = 0.0f; + probe_base_rate = 0.0f; + unloaded_base_rate = 0.0f; + issued_generations = 0; + probe_previous_limit = 0; + limit = 0; + probe_failures = 0; +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/frame_gen_pacer.h b/src/video_core/renderer_vulkan/present/frame_gen_pacer.h new file mode 100644 index 0000000000..7f4b23920c --- /dev/null +++ b/src/video_core/renderer_vulkan/present/frame_gen_pacer.h @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <chrono> +#include <optional> + +#include "common/common_types.h" + +namespace Vulkan { + +struct FrameGenPlan { + size_t generations{}; + bool warm{}; +}; + +class FrameGenPacer { +public: + [[nodiscard]] FrameGenPlan Plan(size_t capacity); + + void Reset(); + +private: + using Clock = std::chrono::steady_clock; + + void Stabilize(Clock::time_point now); + void DeferEvaluations(Clock::duration amount); + void UpdateLimit(Clock::time_point now, f32 base_rate, f32 target_rate, size_t ceiling); + + std::optional<Clock::time_point> last_frame; + std::optional<Clock::time_point> stable_until; + std::optional<Clock::time_point> probe_until; + std::optional<Clock::time_point> next_probe; + std::optional<Clock::time_point> deficit_since; + f32 smoothed_interval{}; + f32 output_credit{}; + f32 probe_base_rate{}; + f32 unloaded_base_rate{}; + size_t issued_generations{}; + size_t probe_previous_limit{}; + size_t limit{}; + u32 probe_failures{}; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_alpha.cpp b/src/video_core/renderer_vulkan/present/lsfg_alpha.cpp new file mode 100644 index 0000000000..a735dd11f4 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_alpha.cpp @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <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 diff --git a/src/video_core/renderer_vulkan/present/lsfg_alpha.h b/src/video_core/renderer_vulkan/present/lsfg_alpha.h new file mode 100644 index 0000000000..1dc8864431 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_alpha.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <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 diff --git a/src/video_core/renderer_vulkan/present/lsfg_beta.cpp b/src/video_core/renderer_vulkan/present/lsfg_beta.cpp new file mode 100644 index 0000000000..6dddc7c21d --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_beta.cpp @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <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 diff --git a/src/video_core/renderer_vulkan/present/lsfg_beta.h b/src/video_core/renderer_vulkan/present/lsfg_beta.h new file mode 100644 index 0000000000..1e24b235cf --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_beta.h @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <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 diff --git a/src/video_core/renderer_vulkan/present/lsfg_chain.cpp b/src/video_core/renderer_vulkan/present/lsfg_chain.cpp new file mode 100644 index 0000000000..972b13a1c4 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_chain.cpp @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <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_SLOT = 112; +constexpr size_t FIRST_DELTA_LEVEL = 4; + +} // Anonymous namespace + +LsfgChain::LsfgChain(const Device& device, MemoryAllocator& memory_allocator, + const LsfgShaders& shaders, VkExtent2D extent, VkFormat format, + f32 flow_scale) + : resources{device, memory_allocator, flow_scale}, + descriptor_pool{CreateLsfgDescriptorPool( + device, FIXED_DESCRIPTOR_SETS + + DESCRIPTOR_SETS_PER_SLOT * static_cast<u32>(LSFG_GENERATION_SLOTS))} { + for (auto& image : frames) { + image = LsfgImage(device, memory_allocator, extent, format); + } + + mipmaps = LsfgMipmaps(device, memory_allocator, shaders, resources, descriptor_pool, frames, + flow_scale); + + alpha_passes = LsfgAlphaPasses(device, shaders); + for (size_t i = 0; i < LSFG_MIP_LEVELS; ++i) { + alpha[i] = LsfgAlpha(device, memory_allocator, alpha_passes, resources, descriptor_pool, + mipmaps.Output(i)); + } + + beta = LsfgBeta(device, memory_allocator, shaders, resources, descriptor_pool, + alpha[0].Outputs()); + + for (size_t i = 0; i < LSFG_MIP_LEVELS; ++i) { + const size_t level = LSFG_MIP_LEVELS - 1 - i; + gamma[i] = LsfgGamma(device, memory_allocator, shaders, resources, descriptor_pool, + alpha[level].Outputs(), + beta.Output(std::min(level, LSFG_BETA_OUTPUTS - 1)), + i == 0 ? nullptr : &gamma[i - 1].Output()); + + if (i < FIRST_DELTA_LEVEL) { + continue; + } + + const size_t index = i - FIRST_DELTA_LEVEL; + delta[index] = LsfgDelta( + device, memory_allocator, shaders, resources, descriptor_pool, alpha[level].Outputs(), + beta.Output(level), i == FIRST_DELTA_LEVEL ? nullptr : &gamma[i - 1].Output(), + i == FIRST_DELTA_LEVEL ? nullptr : &delta[index - 1].Output1(), + i == FIRST_DELTA_LEVEL ? nullptr : &delta[index - 1].Output2()); + } + + generate = LsfgGenerate(device, shaders, resources, descriptor_pool, frames, + gamma[LSFG_MIP_LEVELS - 1].Output(), + delta[LSFG_DELTA_INSTANCES - 1].Output1(), + delta[LSFG_DELTA_INSTANCES - 1].Output2()); +} + +void LsfgChain::DispatchShared(vk::CommandBuffer cmdbuf, u64 frame_count) { + mipmaps.Dispatch(cmdbuf, frame_count); + + for (size_t stage = 0; stage < LSFG_ALPHA_STAGES; ++stage) { + LsfgBarriers barriers(cmdbuf); + for (auto& level : alpha) { + level.PushBarriers(barriers, frame_count, stage); + } + barriers.Build(); + + alpha_passes.Get(stage).BindPipeline(cmdbuf); + for (auto& level : alpha) { + level.DispatchStage(cmdbuf, frame_count, stage); + } + } + + beta.Dispatch(cmdbuf, frame_count); +} + +void LsfgChain::DispatchGeneration(vk::CommandBuffer cmdbuf, u64 frame_count, + size_t generation_count, size_t generation, u32 target, + VkImage image, VkExtent2D extent) { + const size_t slot = LsfgGenerationSlot(generation_count, generation); + for (size_t i = 0; i < LSFG_MIP_LEVELS; ++i) { + gamma[i].Dispatch(cmdbuf, frame_count, slot); + if (i >= FIRST_DELTA_LEVEL) { + delta[i - FIRST_DELTA_LEVEL].Dispatch(cmdbuf, frame_count, slot); + } + } + generate.Dispatch(cmdbuf, frame_count, slot, target, image, extent); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_chain.h b/src/video_core/renderer_vulkan/present/lsfg_chain.h new file mode 100644 index 0000000000..c8879f74a9 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_chain.h @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <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); + + LsfgChain(const LsfgChain&) = delete; + LsfgChain& operator=(const LsfgChain&) = delete; + + void DispatchShared(vk::CommandBuffer cmdbuf, u64 frame_count); + + void DispatchGeneration(vk::CommandBuffer cmdbuf, u64 frame_count, size_t generation_count, + size_t generation, u32 target, VkImage image, VkExtent2D extent); + + void SetTarget(const Device& device, size_t generation_count, size_t generation, u32 target, + VkImageView view) { + generate.SetTarget(device, LsfgGenerationSlot(generation_count, generation), target, view); + } + + [[nodiscard]] LsfgImage& Input(u64 frame_count) { + return frames[frame_count % frames.size()]; + } + + [[nodiscard]] LsfgImage& FlowLevel(size_t level) { + return mipmaps.Output(level); + } + + [[nodiscard]] LsfgImage& AlphaOutput(size_t level, u64 frame_count, size_t index) { + return alpha[level].Outputs()[frame_count % LSFG_HISTORY_SLOTS][index]; + } + + [[nodiscard]] LsfgImage& BetaOutput(size_t level) { + return beta.Output(level); + } + + [[nodiscard]] LsfgImage& GammaOutput(size_t index) { + return gamma[index].Output(); + } + + [[nodiscard]] LsfgImage& DeltaOutput1(size_t index) { + return delta[index].Output1(); + } + + [[nodiscard]] LsfgImage& DeltaOutput2(size_t index) { + return delta[index].Output2(); + } + +private: + LsfgResources resources; + vk::DescriptorPool descriptor_pool; + + LsfgImagePair frames; + LsfgMipmaps mipmaps; + LsfgAlphaPasses alpha_passes; + std::array<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 diff --git a/src/video_core/renderer_vulkan/present/lsfg_common.cpp b/src/video_core/renderer_vulkan/present/lsfg_common.cpp new file mode 100644 index 0000000000..d2df2755e5 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_common.cpp @@ -0,0 +1,339 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <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(×tamp_bits, ×tamp, 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 diff --git a/src/video_core/renderer_vulkan/present/lsfg_common.h b/src/video_core/renderer_vulkan/present/lsfg_common.h new file mode 100644 index 0000000000..d1e3271a6d --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_common.h @@ -0,0 +1,227 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <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_MAX_TARGETS = 7; +constexpr size_t LSFG_MAX_GENERATIONS = 3; + +constexpr size_t LSFG_GENERATION_SLOTS = LSFG_MAX_GENERATIONS * (LSFG_MAX_GENERATIONS + 1) / 2; + +[[nodiscard]] constexpr size_t LsfgGenerationSlot(size_t generation_count, size_t generation) { + return (generation_count - 1) * generation_count / 2 + generation; +} + +[[nodiscard]] constexpr f32 LsfgTimestamp(size_t generation, size_t generation_count) { + return static_cast<f32>(generation + 1) / static_cast<f32>(generation_count + 1); +} + +[[nodiscard]] constexpr size_t LsfgSlotCount(size_t slot) { + size_t count = 1; + while (LsfgGenerationSlot(count + 1, 0) <= slot) { + ++count; + } + return count; +} + +[[nodiscard]] constexpr f32 LsfgSlotTimestamp(size_t slot) { + const size_t count = LsfgSlotCount(slot); + return LsfgTimestamp(slot - LsfgGenerationSlot(count, 0), count); +} + +class LsfgImage { +public: + LsfgImage() = default; + LsfgImage(const Device& device, MemoryAllocator& memory_allocator, VkExtent2D extent_, + VkFormat format = LSFG_DEFAULT_FORMAT); + + [[nodiscard]] VkImage Handle() const { + return *image; + } + + [[nodiscard]] VkImageView View() const { + return *view; + } + + [[nodiscard]] VkExtent2D Extent() const { + return extent; + } + + [[nodiscard]] VkFormat Format() const { + return format; + } + + [[nodiscard]] VkImageLayout Layout() const { + return layout; + } + + void SetLayout(VkImageLayout new_layout) { + layout = new_layout; + } + +private: + vk::Image image; + vk::ImageView view; + VkExtent2D extent{}; + VkFormat format{VK_FORMAT_UNDEFINED}; + VkImageLayout layout{VK_IMAGE_LAYOUT_UNDEFINED}; +}; + +using LsfgImagePair = std::array<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 diff --git a/src/video_core/renderer_vulkan/present/lsfg_delta.cpp b/src/video_core/renderer_vulkan/present/lsfg_delta.cpp new file mode 100644 index 0000000000..dc6eec383f --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_delta.cpp @@ -0,0 +1,283 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <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_) + : 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 slot = 0; slot < LSFG_GENERATION_SLOTS; ++slot) { + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + layouts.push_back(passes[0].SetLayout()); + } + for (size_t i = 1; i <= 4; ++i) { + layouts.push_back(passes[i].SetLayout()); + } + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + layouts.push_back(passes[5].SetLayout()); + } + for (size_t i = 6; i < LSFG_DELTA_STAGES; ++i) { + layouts.push_back(passes[i].SetLayout()); + } + } + owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts); + + const VkSampler sampler = resources.GetSampler(); + const VkSampler border_sampler = resources.GetSampler( + VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, VK_COMPARE_OP_NEVER, true); + const VkSampler edge_sampler = + resources.GetSampler(VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, VK_COMPARE_OP_ALWAYS, false); + size_t next = 0; + for (size_t slot = 0; slot < LSFG_GENERATION_SLOTS; ++slot) { + Generation& pass = generations[slot]; + const VkBuffer buffer = + resources.GetBuffer(LsfgSlotTimestamp(slot), false, previous_gamma == nullptr); + + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + pass.first_descriptor_sets[i] = owned_sets[next++]; + } + for (size_t i = 0; i < 4; ++i) { + pass.descriptor_sets[i] = owned_sets[next++]; + } + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + pass.sixth_descriptor_sets[i] = owned_sets[next++]; + } + for (size_t i = 4; i < LSFG_DELTA_STAGES - 2; ++i) { + pass.descriptor_sets[i] = owned_sets[next++]; + } + + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + LsfgDescriptorWriter(pass.first_descriptor_sets[i]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(border_sampler) + .AddSampler(edge_sampler) + .AddSampledImages((*inputs)[(i + 2) % LSFG_HISTORY_SLOTS]) + .AddSampledImages((*inputs)[i % LSFG_HISTORY_SLOTS]) + .AddSampledImage(previous_gamma) + .AddStorageImages(temp1) + .Build(device); + LsfgDescriptorWriter(pass.sixth_descriptor_sets[i]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(border_sampler) + .AddSampler(edge_sampler) + .AddSampledImages((*inputs)[(i + 2) % LSFG_HISTORY_SLOTS]) + .AddSampledImages((*inputs)[i % LSFG_HISTORY_SLOTS]) + .AddSampledImage(previous_gamma) + .AddSampledImage(previous1) + .AddStorageImage(temp2[0]) + .Build(device); + } + LsfgDescriptorWriter(pass.descriptor_sets[0]) + .AddSampler(sampler) + .AddSampledImages(temp1) + .AddStorageImages(temp2) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[1]) + .AddSampler(sampler) + .AddSampledImages(temp2) + .AddStorageImage(temp1[0]) + .AddStorageImage(temp1[1]) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[2]) + .AddSampler(sampler) + .AddSampledImage(temp1[0]) + .AddSampledImage(temp1[1]) + .AddStorageImages(temp2) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[3]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(sampler) + .AddSampler(edge_sampler) + .AddSampledImages(temp2) + .AddSampledImage(previous_gamma) + .AddSampledImage(*flow_input) + .AddStorageImage(out_image1) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[4]) + .AddSampler(sampler) + .AddSampledImage(temp2[0]) + .AddStorageImage(temp1[0]) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[5]) + .AddSampler(sampler) + .AddSampledImage(temp1[0]) + .AddStorageImage(temp2[0]) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[6]) + .AddSampler(sampler) + .AddSampledImage(temp2[0]) + .AddStorageImage(temp1[0]) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[7]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(sampler) + .AddSampler(edge_sampler) + .AddSampledImage(temp1[0]) + .AddSampledImage(previous2) + .AddStorageImage(out_image2) + .Build(device); + } +} + +void LsfgDelta::Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t slot) { + const Generation& pass = generations[slot]; + + const VkExtent2D extent = temp1[0].Extent(); + const u32 groups_x = GroupCount(extent.width); + const u32 groups_y = GroupCount(extent.height); + + const size_t history = frame_count % LSFG_HISTORY_SLOTS; + const size_t previous_history = (frame_count + 2) % LSFG_HISTORY_SLOTS; + + LsfgBarriers(cmdbuf) + .WriteToReadAll((*inputs)[previous_history]) + .WriteToReadAll((*inputs)[history]) + .WriteToRead(previous_gamma) + .ReadToWriteAll(temp1) + .Build(); + passes[0].Bind(cmdbuf, pass.first_descriptor_sets[history]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp1).ReadToWriteAll(temp2).Build(); + passes[1].Bind(cmdbuf, pass.descriptor_sets[0]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp2).ReadToWriteAll(temp1).Build(); + passes[2].Bind(cmdbuf, pass.descriptor_sets[1]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp1).ReadToWriteAll(temp2).Build(); + passes[3].Bind(cmdbuf, pass.descriptor_sets[2]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToReadAll(temp2) + .WriteToRead(previous_gamma) + .WriteToRead(*flow_input) + .ReadToWrite(out_image1) + .Build(); + passes[4].Bind(cmdbuf, pass.descriptor_sets[3]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToReadAll((*inputs)[previous_history]) + .WriteToReadAll((*inputs)[history]) + .WriteToRead(previous_gamma) + .WriteToRead(previous1) + .ReadToWriteAll(temp2) + .Build(); + passes[5].Bind(cmdbuf, pass.sixth_descriptor_sets[history]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToReadAll(temp2) + .ReadToWrite(temp1[0]) + .ReadToWrite(temp1[1]) + .Build(); + passes[6].Bind(cmdbuf, pass.descriptor_sets[4]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToRead(temp1[0]) + .WriteToRead(temp1[1]) + .ReadToWriteAll(temp2) + .Build(); + passes[7].Bind(cmdbuf, pass.descriptor_sets[5]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToReadAll(temp2) + .ReadToWrite(temp1[0]) + .ReadToWrite(temp1[1]) + .Build(); + passes[8].Bind(cmdbuf, pass.descriptor_sets[6]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToRead(temp1[0]) + .WriteToRead(temp1[1]) + .WriteToRead(previous2) + .ReadToWrite(out_image2) + .Build(); + passes[9].Bind(cmdbuf, pass.descriptor_sets[7]); + cmdbuf.Dispatch(groups_x, groups_y, 1); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_delta.h b/src/video_core/renderer_vulkan/present/lsfg_delta.h new file mode 100644 index 0000000000..1bbcfe6745 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_delta.h @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <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_DELTA_STAGES = 10; +constexpr size_t LSFG_DELTA_TEMPS = 3; + +class LsfgDelta { +public: + LsfgDelta() = default; + LsfgDelta(const Device& device, MemoryAllocator& memory_allocator, const LsfgShaders& shaders, + LsfgResources& resources, vk::DescriptorPool& descriptor_pool, + LsfgImageHistory& inputs, LsfgImage& flow_input, LsfgImage* previous_gamma, + LsfgImage* previous1, LsfgImage* previous2); + + void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t slot); + + [[nodiscard]] LsfgImage& Output1() { + return out_image1; + } + + [[nodiscard]] LsfgImage& Output2() { + return out_image2; + } + +private: + struct Generation { + std::array<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::array<Generation, LSFG_GENERATION_SLOTS> generations{}; + vk::DescriptorSets owned_sets; + + std::array<LsfgImage, LSFG_DELTA_TEMPS> temp1; + LsfgImagePair temp2; + LsfgImage out_image1; + LsfgImage out_image2; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_gamma.cpp b/src/video_core/renderer_vulkan/present/lsfg_gamma.cpp new file mode 100644 index 0000000000..067d124b6f --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_gamma.cpp @@ -0,0 +1,185 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <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_) + : 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 slot = 0; slot < LSFG_GENERATION_SLOTS; ++slot) { + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + layouts.push_back(passes[0].SetLayout()); + } + for (size_t i = 1; i < LSFG_GAMMA_STAGES; ++i) { + layouts.push_back(passes[i].SetLayout()); + } + } + owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts); + + const VkSampler sampler = resources.GetSampler(); + const VkSampler border_sampler = resources.GetSampler( + VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, VK_COMPARE_OP_NEVER, true); + const VkSampler edge_sampler = + resources.GetSampler(VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, VK_COMPARE_OP_ALWAYS, false); + + size_t next = 0; + for (size_t slot = 0; slot < LSFG_GENERATION_SLOTS; ++slot) { + Generation& pass = generations[slot]; + const VkBuffer buffer = + resources.GetBuffer(LsfgSlotTimestamp(slot), previous == nullptr); + + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + pass.first_descriptor_sets[i] = owned_sets[next++]; + } + for (size_t i = 0; i < LSFG_GAMMA_STAGES - 1; ++i) { + pass.descriptor_sets[i] = owned_sets[next++]; + } + + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + LsfgDescriptorWriter(pass.first_descriptor_sets[i]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(border_sampler) + .AddSampler(edge_sampler) + .AddSampledImages((*inputs)[(i + 2) % LSFG_HISTORY_SLOTS]) + .AddSampledImages((*inputs)[i % LSFG_HISTORY_SLOTS]) + .AddSampledImage(previous) + .AddStorageImages(temp1) + .Build(device); + } + LsfgDescriptorWriter(pass.descriptor_sets[0]) + .AddSampler(sampler) + .AddSampledImages(temp1) + .AddStorageImages(temp2) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[1]) + .AddSampler(sampler) + .AddSampledImages(temp2) + .AddStorageImage(temp1[0]) + .AddStorageImage(temp1[1]) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[2]) + .AddSampler(sampler) + .AddSampledImage(temp1[0]) + .AddSampledImage(temp1[1]) + .AddStorageImages(temp2) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[3]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(sampler) + .AddSampler(edge_sampler) + .AddSampledImages(temp2) + .AddSampledImage(previous) + .AddSampledImage(*flow_input) + .AddStorageImage(out_image) + .Build(device); + } +} + +void LsfgGamma::Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t slot) { + const Generation& pass = generations[slot]; + + const VkExtent2D extent = temp1[0].Extent(); + const u32 groups_x = GroupCount(extent.width); + const u32 groups_y = GroupCount(extent.height); + + const size_t history = frame_count % LSFG_HISTORY_SLOTS; + const size_t previous_history = (frame_count + 2) % LSFG_HISTORY_SLOTS; + + LsfgBarriers(cmdbuf) + .WriteToReadAll((*inputs)[previous_history]) + .WriteToReadAll((*inputs)[history]) + .WriteToRead(previous) + .ReadToWriteAll(temp1) + .Build(); + passes[0].Bind(cmdbuf, pass.first_descriptor_sets[history]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp1).ReadToWriteAll(temp2).Build(); + passes[1].Bind(cmdbuf, pass.descriptor_sets[0]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToReadAll(temp2) + .ReadToWrite(temp1[0]) + .ReadToWrite(temp1[1]) + .Build(); + passes[2].Bind(cmdbuf, pass.descriptor_sets[1]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToRead(temp1[0]) + .WriteToRead(temp1[1]) + .ReadToWriteAll(temp2) + .Build(); + passes[3].Bind(cmdbuf, pass.descriptor_sets[2]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToReadAll(temp2) + .WriteToRead(previous) + .WriteToRead(*flow_input) + .ReadToWrite(out_image) + .Build(); + passes[4].Bind(cmdbuf, pass.descriptor_sets[3]); + cmdbuf.Dispatch(groups_x, groups_y, 1); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_gamma.h b/src/video_core/renderer_vulkan/present/lsfg_gamma.h new file mode 100644 index 0000000000..848c380bfe --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_gamma.h @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <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_GAMMA_STAGES = 5; +constexpr size_t LSFG_GAMMA_TEMPS = 3; + +class LsfgGamma { +public: + LsfgGamma() = default; + LsfgGamma(const Device& device, MemoryAllocator& memory_allocator, const LsfgShaders& shaders, + LsfgResources& resources, vk::DescriptorPool& descriptor_pool, + LsfgImageHistory& inputs, LsfgImage& flow_input, LsfgImage* previous); + + void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t slot); + + [[nodiscard]] LsfgImage& Output() { + return out_image; + } + +private: + struct Generation { + std::array<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::array<Generation, LSFG_GENERATION_SLOTS> generations{}; + vk::DescriptorSets owned_sets; + + std::array<LsfgImage, LSFG_GAMMA_TEMPS> temp1; + LsfgImagePair temp2; + LsfgImage out_image; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_generate.cpp b/src/video_core/renderer_vulkan/present/lsfg_generate.cpp new file mode 100644 index 0000000000..4473b59a2f --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_generate.cpp @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <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_) + : frames{&frames_}, motion{&motion_}, detail1{&detail1_}, detail2{&detail2_} { + using namespace VideoCore::FrameGen::PerformanceShader; + + pass = LsfgPass(device, shaders, GENERATE, + {{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLER}, + {5, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + + sampler = resources.GetSampler(); + edge_sampler = + resources.GetSampler(VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, VK_COMPARE_OP_ALWAYS, false); + + const std::vector<VkDescriptorSetLayout> layouts( + LSFG_GENERATION_SLOTS * LSFG_MAX_TARGETS * 2, pass.SetLayout()); + owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts); + + size_t next = 0; + for (size_t slot = 0; slot < LSFG_GENERATION_SLOTS; ++slot) { + Generation& target = generations[slot]; + target.buffer = resources.GetBuffer(LsfgSlotTimestamp(slot)); + + for (auto& entry : target.targets) { + for (auto& set : entry.descriptor_sets) { + set = owned_sets[next++]; + } + } + } +} + +void LsfgGenerate::SetTarget(const Device& device, size_t slot, u32 target, VkImageView view) { + Target& entry = generations[slot].targets[target]; + if (entry.view == view) { + return; + } + entry.view = view; + + for (size_t i = 0; i < entry.descriptor_sets.size(); ++i) { + LsfgDescriptorWriter(entry.descriptor_sets[i]) + .AddUniformBuffer(generations[slot].buffer, LsfgResources::BufferSize()) + .AddSampler(sampler) + .AddSampler(edge_sampler) + .AddSampledImage((*frames)[1 - i]) + .AddSampledImage((*frames)[i]) + .AddSampledImage(*motion) + .AddSampledImage(*detail1) + .AddSampledImage(*detail2) + .AddStorageView(view) + .Build(device); + } +} + +void LsfgGenerate::Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t slot, u32 target, + VkImage image, VkExtent2D extent) { + const Target& entry = generations[slot].targets[target]; + + LsfgBarriers(cmdbuf) + .WriteToReadAll(*frames) + .WriteToRead(*motion) + .WriteToRead(*detail1) + .WriteToRead(*detail2) + .DiscardToWrite(image) + .Build(); + + pass.Bind(cmdbuf, entry.descriptor_sets[frame_count % entry.descriptor_sets.size()]); + cmdbuf.Dispatch(GroupCount(extent.width), GroupCount(extent.height), 1); + + const std::array after{MakeTargetBarrier( + image, VK_ACCESS_SHADER_WRITE_BIT, + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT, + VK_IMAGE_LAYOUT_GENERAL)}; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, {}, {}, after); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_generate.h b/src/video_core/renderer_vulkan/present/lsfg_generate.h new file mode 100644 index 0000000000..75cbd8d2e4 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_generate.h @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <array> + +#include "common/common_types.h" +#include "video_core/renderer_vulkan/present/lsfg_common.h" + +namespace Vulkan { + +class Device; +class LsfgShaders; + +class LsfgGenerate { +public: + LsfgGenerate() = default; + LsfgGenerate(const Device& device, const LsfgShaders& shaders, LsfgResources& resources, + vk::DescriptorPool& descriptor_pool, LsfgImagePair& frames, LsfgImage& motion, + LsfgImage& detail1, LsfgImage& detail2); + + void SetTarget(const Device& device, size_t slot, u32 target, VkImageView view); + + void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t slot, u32 target, + VkImage image, VkExtent2D extent); + +private: + struct Target { + std::array<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::array<Generation, LSFG_GENERATION_SLOTS> generations{}; + vk::DescriptorSets owned_sets; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_mipmaps.cpp b/src/video_core/renderer_vulkan/present/lsfg_mipmaps.cpp new file mode 100644 index 0000000000..3f31f58d68 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_mipmaps.cpp @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <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 diff --git a/src/video_core/renderer_vulkan/present/lsfg_mipmaps.h b/src/video_core/renderer_vulkan/present/lsfg_mipmaps.h new file mode 100644 index 0000000000..7fa5b7b224 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_mipmaps.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <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 diff --git a/src/video_core/renderer_vulkan/present/lsfg_shaders.cpp b/src/video_core/renderer_vulkan/present/lsfg_shaders.cpp new file mode 100644 index 0000000000..aafe0d6419 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_shaders.cpp @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "common/settings.h" +#include "video_core/frame_gen/lossless_dll.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +LsfgShaders::LsfgShaders(const Device& device) { + if (!device.IsVulkanMemoryModelSupported() || !device.HasNullDescriptor()) { + return; + } + + const bool allow_fp16 = device.IsFloat16Supported(); + const bool prefer_fp16 = allow_fp16 && Settings::values.frame_gen_fp16.GetValue(); + + VideoCore::FrameGen::ShaderModules code; + if (VideoCore::FrameGen::LoadShaderModules(code, allow_fp16, prefer_fp16) != + VideoCore::FrameGen::LosslessStatus::Ok) { + return; + } + + for (const auto& [id, words] : code) { + modules.emplace(id, CreateWrappedShaderModule(device, words)); + } + valid = true; +} + +VkShaderModule LsfgShaders::Get(u32 shader_id) const { + const auto hit = modules.find(shader_id); + return hit == modules.end() ? VK_NULL_HANDLE : *hit->second; +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_shaders.h b/src/video_core/renderer_vulkan/present/lsfg_shaders.h new file mode 100644 index 0000000000..cab37110d6 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_shaders.h @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <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 diff --git a/src/video_core/renderer_vulkan/present/util.cpp b/src/video_core/renderer_vulkan/present/util.cpp index 2b83902bc0..11250687d6 100644 --- a/src/video_core/renderer_vulkan/present/util.cpp +++ b/src/video_core/renderer_vulkan/present/util.cpp @@ -320,15 +320,16 @@ vk::DescriptorPool CreateWrappedDescriptorPool(const Device& device, size_t max_ }); } -vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout( - const Device& device, std::initializer_list<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{ diff --git a/src/video_core/renderer_vulkan/present/util.h b/src/video_core/renderer_vulkan/present/util.h index 38cc6203c5..ecc263286d 100644 --- a/src/video_core/renderer_vulkan/present/util.h +++ b/src/video_core/renderer_vulkan/present/util.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project @@ -38,7 +38,11 @@ vk::DescriptorPool CreateWrappedDescriptorPool(const Device& device, size_t max_ std::initializer_list<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); diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp index b0a7d0c579..d815278c85 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp +++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp @@ -49,6 +49,23 @@ constexpr VkExtent2D CaptureImageSize{ .height = VideoCore::Capture::LinearHeight, }; +#ifdef HAS_LSFG +[[nodiscard]] VkExtent2D GuestExtent(std::span<const Tegra::FramebufferConfig> framebuffers) { + if (framebuffers.empty()) { + return VkExtent2D{}; + } + + const auto& framebuffer = framebuffers.front(); + if (framebuffer.crop_rect.IsEmpty()) { + return VkExtent2D{.width = framebuffer.width, .height = framebuffer.height}; + } + return VkExtent2D{ + .width = static_cast<u32>(framebuffer.crop_rect.GetWidth()), + .height = static_cast<u32>(framebuffer.crop_rect.GetHeight()), + }; +} +#endif + constexpr VkExtent3D CaptureImageExtent{ .width = VideoCore::Capture::LinearWidth, .height = VideoCore::Capture::LinearHeight, @@ -155,7 +172,11 @@ 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) +#ifdef HAS_LSFG + , frame_gen(memory_allocator, scheduler) +#endif +{ if (Settings::values.renderer_force_max_clock.GetValue() && device.ShouldBoostClocks()) { turbo_mode.emplace(instance, dld); @@ -191,9 +212,28 @@ void RendererVulkan::Composite(std::span<const Tegra::FramebufferConfig> framebu blit_swapchain.DrawToFrame(device, rasterizer, frame, framebuffers, render_window.GetFramebufferLayout(), swapchain.GetImageCount(), swapchain.GetImageViewFormat()); + +#ifdef HAS_LSFG + void(frame_gen.WantedGenerations(present_manager.MaxExtraFrames())); + + frame_gen.Process(device, frame, swapchain.GetImageFormat(), GuestExtent(framebuffers)); + + const size_t generated_frames = frame_gen.GeneratedFrameCount(); + for (size_t generation = 0; generation < generated_frames; ++generation) { + Frame* generated = present_manager.GetRenderFrame(); + blit_swapchain.PrepareFrame(device, generated, render_window.GetFramebufferLayout()); + frame_gen.GenerateInto(device, generated, generation); + scheduler.Flush(*generated->render_ready); + present_manager.Present(generated); + } +#endif + scheduler.Flush(*frame->render_ready); present_manager.Present(frame); +#ifdef HAS_LSFG + scheduler.DispatchWork(); +#endif gpu.RendererFrameEndNotify(); rasterizer.TickFrame(); diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.h b/src/video_core/renderer_vulkan/renderer_vulkan.h index 4fb88b29de..5125fadcd7 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.h +++ b/src/video_core/renderer_vulkan/renderer_vulkan.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project @@ -13,6 +13,9 @@ #include "common/dynamic_library.h" #include "video_core/host1x/gpu_device_memory_manager.h" #include "video_core/renderer_base.h" +#ifdef HAS_LSFG +#include "video_core/renderer_vulkan/present/frame_gen.h" +#endif #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 +98,9 @@ private: BlitScreen blit_capture; BlitScreen blit_applet; RasterizerVulkan rasterizer; +#ifdef HAS_LSFG + FrameGen frame_gen; +#endif std::optional<TurboMode> turbo_mode; Frame applet_frame; diff --git a/src/video_core/renderer_vulkan/vk_blit_screen.cpp b/src/video_core/renderer_vulkan/vk_blit_screen.cpp index 05314fea27..96eeeb8045 100644 --- a/src/video_core/renderer_vulkan/vk_blit_screen.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_screen.cpp @@ -85,6 +85,17 @@ void BlitScreen::SetWindowAdaptPass(const Device& device) { } } +void BlitScreen::PrepareFrame(const Device& device, Frame* frame, + const Layout::FramebufferLayout& layout) { + if (!window_adapt || (frame->width == layout.width && frame->height == layout.height)) { + return; + } + + WaitIdle(device); + present_manager.RecreateFrame(frame, layout.width, layout.height, swapchain_view_format, + window_adapt->GetRenderPass()); +} + void BlitScreen::DrawToFrame(const Device& device, RasterizerVulkan& rasterizer, Frame* frame, std::span<const Tegra::FramebufferConfig> framebuffers, const Layout::FramebufferLayout& layout, diff --git a/src/video_core/renderer_vulkan/vk_blit_screen.h b/src/video_core/renderer_vulkan/vk_blit_screen.h index e50f0bcf33..56b429ae53 100644 --- a/src/video_core/renderer_vulkan/vk_blit_screen.h +++ b/src/video_core/renderer_vulkan/vk_blit_screen.h @@ -60,6 +60,8 @@ public: const Layout::FramebufferLayout& layout, size_t current_swapchain_image_count, VkFormat current_swapchain_view_format); + void PrepareFrame(const Device& device, Frame* frame, const Layout::FramebufferLayout& layout); + [[nodiscard]] vk::Framebuffer CreateFramebuffer(const Device& device, const Layout::FramebufferLayout& layout, VkImageView image_view, VkFormat current_view_format); diff --git a/src/video_core/renderer_vulkan/vk_present_manager.cpp b/src/video_core/renderer_vulkan/vk_present_manager.cpp index 90ebb55d38..523e6d02cb 100644 --- a/src/video_core/renderer_vulkan/vk_present_manager.cpp +++ b/src/video_core/renderer_vulkan/vk_present_manager.cpp @@ -7,6 +7,9 @@ #include "common/settings.h" #include "common/thread.h" #include "core/frontend/emu_window.h" +#ifdef HAS_LSFG +#include "video_core/renderer_vulkan/present/lsfg_common.h" +#endif #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 +22,23 @@ namespace Vulkan { namespace { +constexpr size_t MAX_FRAMES_IN_FLIGHT = 7; +#ifdef HAS_LSFG +static_assert(MAX_FRAMES_IN_FLIGHT <= LSFG_MAX_TARGETS); +#endif + +bool CanStoreToFrame(const vk::PhysicalDevice& physical_device, VkFormat format) { +#ifdef HAS_LSFG + 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; +#else + return false; +#endif +} + 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 +130,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 +148,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 +201,10 @@ void PresentManager::Present(Frame* frame) { } } +size_t PresentManager::MaxExtraFrames() const { + return image_count - 1; +} + void PresentManager::RecreateFrame(Frame* frame, u32 width, u32 height, VkFormat image_view_format, VkRenderPass rd) { auto& dld = device.GetLogical(); @@ -186,6 +212,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 +231,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 +263,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 +358,15 @@ 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); +#ifdef HAS_LSFG + const size_t generations = Settings::FrameGenMaxGenerations(); + const size_t queued_composites = Settings::values.frame_gen_queue_target.GetValue() + 1; + image_count = + std::clamp<size_t>((generations + 1) * queued_composites, swapchain.GetImageCount(), + MAX_FRAMES_IN_FLIGHT); +#else + image_count = std::min<size_t>(swapchain.GetImageCount(), MAX_FRAMES_IN_FLIGHT); +#endif } void PresentManager::CopyToSwapchain(Frame* frame) { diff --git a/src/video_core/renderer_vulkan/vk_present_manager.h b/src/video_core/renderer_vulkan/vk_present_manager.h index c51f8ed77f..577a82098a 100644 --- a/src/video_core/renderer_vulkan/vk_present_manager.h +++ b/src/video_core/renderer_vulkan/vk_present_manager.h @@ -28,8 +28,10 @@ class Swapchain; struct Frame { u32 width; u32 height; + u32 index; vk::Image image; vk::ImageView image_view; + vk::ImageView storage_view; vk::Framebuffer framebuffer; vk::CommandBuffer cmdbuf; vk::Semaphore render_ready; @@ -60,6 +62,9 @@ public: /// Waits for the present thread to finish presenting all queued frames. void WaitPresent(); + /// How many additional frames can be queued without stalling the render thread + [[nodiscard]] size_t MaxExtraFrames() const; + private: void PresentThread(std::stop_token token); @@ -90,6 +95,7 @@ private: std::mutex free_mutex; std::jthread present_thread; bool blit_supported; + bool storage_supported; bool use_present_thread; std::size_t image_count{}; }; diff --git a/src/video_core/renderer_vulkan/vk_scheduler.h b/src/video_core/renderer_vulkan/vk_scheduler.h index b810862f73..c2aa3fb996 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.h +++ b/src/video_core/renderer_vulkan/vk_scheduler.h @@ -146,9 +146,10 @@ public: frame_counter++; auto target_time = start_time + frame_interval * frame_counter; if (target_time >= now) { + constexpr auto spin_tail = std::chrono::milliseconds(1); auto sleep_time = target_time - now; - if (sleep_time > std::chrono::milliseconds(15)) { - std::this_thread::sleep_for(sleep_time - std::chrono::milliseconds(1)); + if (sleep_time > spin_tail * 2) { + std::this_thread::sleep_for(sleep_time - spin_tail); } while (std::chrono::steady_clock::now() < target_time) { std::this_thread::yield(); diff --git a/src/video_core/renderer_vulkan/vk_swapchain.cpp b/src/video_core/renderer_vulkan/vk_swapchain.cpp index fdbad16721..fea5398557 100644 --- a/src/video_core/renderer_vulkan/vk_swapchain.cpp +++ b/src/video_core/renderer_vulkan/vk_swapchain.cpp @@ -48,6 +48,9 @@ static VkPresentModeKHR ChooseSwapPresentMode(bool has_imm, bool has_mailbox, Settings::VSyncMode setting = [has_imm, has_mailbox]() { // Choose Mailbox or Immediate if unlocked and those modes are supported const auto mode = Settings::values.vsync_mode.GetValue(); + if (Settings::values.frame_gen.GetValue()) { + return mode == Settings::VSyncMode::FifoRelaxed ? mode : Settings::VSyncMode::Fifo; + } if (Settings::values.use_speed_limit.GetValue() && Settings::values.current_speed_mode.GetValue() != Settings::SpeedMode::Turbo) { return mode; diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index 2708281297..1ae073c279 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -37,7 +37,8 @@ VK_DEFINE_HANDLE(VmaAllocator) FEATURE(EXT, HostQueryReset, HOST_QUERY_RESET, host_query_reset) \ FEATURE(KHR, 8BitStorage, 8BIT_STORAGE, bit8_storage) \ FEATURE(KHR, BufferDeviceAddress, BUFFER_DEVICE_ADDRESS, buffer_device_address) \ - FEATURE(KHR, TimelineSemaphore, TIMELINE_SEMAPHORE, timeline_semaphore) + FEATURE(KHR, TimelineSemaphore, TIMELINE_SEMAPHORE, timeline_semaphore) \ + FEATURE(KHR, VulkanMemoryModel, VULKAN_MEMORY_MODEL, vulkan_memory_model) #define FOR_EACH_VK_FEATURE_1_3(FEATURE) \ FEATURE(EXT, ImageRobustness, IMAGE_ROBUSTNESS, robust_image_access) \ @@ -416,6 +417,11 @@ FN_MAX_LIMIT_LIST return features.shader_float16_int8.shaderFloat16; } + /// Returns true if the device can run shaders built against the Vulkan memory model. + bool IsVulkanMemoryModelSupported() const { + return features.vulkan_memory_model.vulkanMemoryModel; + } + /// Returns true if the device supports int8 natively. bool IsInt8Supported() const { return features.shader_float16_int8.shaderInt8; |
