[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
This commit is contained in:
CamilleLaVey
2026-08-17 00:30:19 +02:00
committed by crueter
parent dc95cd09ee
commit 2000fdfb7b
69 changed files with 4735 additions and 38 deletions
+32
View File
@@ -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
+553
View File
@@ -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
+67
View File
@@ -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
@@ -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
+17
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,140 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
#include <vector>
#include "video_core/frame_gen/lossless_dll.h"
#include "video_core/renderer_vulkan/present/lsfg_alpha.h"
#include "video_core/renderer_vulkan/present/lsfg_shaders.h"
#include "video_core/renderer_vulkan/present/util.h"
#include "video_core/vulkan_common/vulkan_device.h"
namespace Vulkan {
namespace {
constexpr u32 DISPATCH_TILE_SHIFT = 3;
[[nodiscard]] u32 GroupCount(u32 size) {
return (size + (1u << DISPATCH_TILE_SHIFT) - 1) >> DISPATCH_TILE_SHIFT;
}
[[nodiscard]] VkExtent2D HalveExtent(VkExtent2D extent) {
return VkExtent2D{
.width = (extent.width + 1) >> 1,
.height = (extent.height + 1) >> 1,
};
}
} // Anonymous namespace
LsfgAlphaPasses::LsfgAlphaPasses(const Device& device, const LsfgShaders& shaders) {
using namespace VideoCore::FrameGen::PerformanceShader;
passes[0] = LsfgPass(device, shaders, ALPHA[0],
{{1, VK_DESCRIPTOR_TYPE_SAMPLER},
{1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE},
{1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}});
passes[1] = LsfgPass(device, shaders, ALPHA[1],
{{1, VK_DESCRIPTOR_TYPE_SAMPLER},
{1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE},
{1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}});
passes[2] = LsfgPass(device, shaders, ALPHA[2],
{{1, VK_DESCRIPTOR_TYPE_SAMPLER},
{1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE},
{2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}});
passes[3] = LsfgPass(device, shaders, ALPHA[3],
{{1, VK_DESCRIPTOR_TYPE_SAMPLER},
{2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE},
{2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}});
}
LsfgAlpha::LsfgAlpha(const Device& device, MemoryAllocator& memory_allocator,
const LsfgAlphaPasses& passes_, LsfgResources& resources,
vk::DescriptorPool& descriptor_pool, LsfgImage& input_)
: passes{&passes_}, input{&input_} {
const VkExtent2D half_extent = HalveExtent(input->Extent());
const VkExtent2D quarter_extent = HalveExtent(half_extent);
temp1 = LsfgImage(device, memory_allocator, half_extent);
temp2 = LsfgImage(device, memory_allocator, half_extent);
for (size_t i = 0; i < temp3.size(); ++i) {
temp3[i] = LsfgImage(device, memory_allocator, quarter_extent);
for (size_t j = 0; j < LSFG_HISTORY_SLOTS; ++j) {
out_images[j][i] = LsfgImage(device, memory_allocator, quarter_extent);
}
}
std::vector<VkDescriptorSetLayout> layouts;
for (size_t i = 0; i < LSFG_ALPHA_STAGES - 1; ++i) {
layouts.push_back(passes->Get(i).SetLayout());
}
for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) {
layouts.push_back(passes->Get(3).SetLayout());
}
owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts);
for (size_t i = 0; i < LSFG_ALPHA_STAGES - 1; ++i) {
descriptor_sets[i] = owned_sets[i];
}
for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) {
last_descriptor_sets[i] = owned_sets[LSFG_ALPHA_STAGES - 1 + i];
}
const VkSampler sampler = resources.GetSampler();
LsfgDescriptorWriter(descriptor_sets[0])
.AddSampler(sampler)
.AddSampledImage(*input)
.AddStorageImage(temp1)
.Build(device);
LsfgDescriptorWriter(descriptor_sets[1])
.AddSampler(sampler)
.AddSampledImage(temp1)
.AddStorageImage(temp2)
.Build(device);
LsfgDescriptorWriter(descriptor_sets[2])
.AddSampler(sampler)
.AddSampledImage(temp2)
.AddStorageImages(temp3)
.Build(device);
for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) {
LsfgDescriptorWriter(last_descriptor_sets[i])
.AddSampler(sampler)
.AddSampledImages(temp3)
.AddStorageImages(out_images[i])
.Build(device);
}
}
void LsfgAlpha::PushBarriers(LsfgBarriers& barriers, u64 frame_count, size_t stage) {
switch (stage) {
case 0:
barriers.WriteToRead(*input).ReadToWrite(temp1);
break;
case 1:
barriers.WriteToRead(temp1).ReadToWrite(temp2);
break;
case 2:
barriers.WriteToRead(temp2).ReadToWriteAll(temp3);
break;
default:
barriers.WriteToReadAll(temp3).ReadToWriteAll(out_images[frame_count % LSFG_HISTORY_SLOTS]);
break;
}
}
void LsfgAlpha::DispatchStage(vk::CommandBuffer cmdbuf, u64 frame_count, size_t stage) {
const VkExtent2D extent = stage < 2 ? temp1.Extent() : temp3[0].Extent();
const VkDescriptorSet set = stage < LSFG_ALPHA_STAGES - 1
? descriptor_sets[stage]
: last_descriptor_sets[frame_count % LSFG_HISTORY_SLOTS];
passes->Get(stage).BindSet(cmdbuf, set);
cmdbuf.Dispatch(GroupCount(extent.width), GroupCount(extent.height), 1);
}
} // namespace Vulkan
@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <array>
#include "common/common_types.h"
#include "video_core/renderer_vulkan/present/lsfg_common.h"
namespace Vulkan {
class Device;
class LsfgShaders;
constexpr size_t LSFG_ALPHA_STAGES = 4;
class LsfgAlphaPasses {
public:
LsfgAlphaPasses() = default;
LsfgAlphaPasses(const Device& device, const LsfgShaders& shaders);
[[nodiscard]] const LsfgPass& Get(size_t stage) const {
return passes[stage];
}
private:
std::array<LsfgPass, LSFG_ALPHA_STAGES> passes;
};
class LsfgAlpha {
public:
LsfgAlpha() = default;
LsfgAlpha(const Device& device, MemoryAllocator& memory_allocator,
const LsfgAlphaPasses& passes_, LsfgResources& resources,
vk::DescriptorPool& descriptor_pool, LsfgImage& input);
void PushBarriers(LsfgBarriers& barriers, u64 frame_count, size_t stage);
void DispatchStage(vk::CommandBuffer cmdbuf, u64 frame_count, size_t stage);
[[nodiscard]] LsfgImageHistory& Outputs() {
return out_images;
}
private:
const LsfgAlphaPasses* passes{};
LsfgImage* input{};
std::array<VkDescriptorSet, LSFG_ALPHA_STAGES - 1> descriptor_sets{};
std::array<VkDescriptorSet, LSFG_HISTORY_SLOTS> last_descriptor_sets{};
vk::DescriptorSets owned_sets;
LsfgImage temp1;
LsfgImage temp2;
LsfgImagePair temp3;
LsfgImageHistory out_images;
};
} // namespace Vulkan
@@ -0,0 +1,147 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
#include <vector>
#include "video_core/frame_gen/lossless_dll.h"
#include "video_core/renderer_vulkan/present/lsfg_beta.h"
#include "video_core/renderer_vulkan/present/lsfg_shaders.h"
#include "video_core/renderer_vulkan/present/util.h"
#include "video_core/vulkan_common/vulkan_device.h"
namespace Vulkan {
namespace {
constexpr u32 DISPATCH_TILE_SHIFT = 3;
constexpr u32 OUTPUT_TILE_SHIFT = 5;
[[nodiscard]] u32 GroupCount(u32 size, u32 shift) {
return (size + (1u << shift) - 1) >> shift;
}
} // Anonymous namespace
LsfgBeta::LsfgBeta(const Device& device, MemoryAllocator& memory_allocator,
const LsfgShaders& shaders, LsfgResources& resources,
vk::DescriptorPool& descriptor_pool, LsfgImageHistory& inputs_)
: inputs{&inputs_} {
using namespace VideoCore::FrameGen::PerformanceShader;
passes[0] = LsfgPass(device, shaders, BETA[0],
{{1, VK_DESCRIPTOR_TYPE_SAMPLER},
{6, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE},
{2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}});
for (size_t i = 1; i < LSFG_BETA_STAGES - 1; ++i) {
passes[i] = LsfgPass(device, shaders, BETA[i],
{{1, VK_DESCRIPTOR_TYPE_SAMPLER},
{2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE},
{2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}});
}
passes[4] = LsfgPass(device, shaders, BETA[4],
{{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER},
{1, VK_DESCRIPTOR_TYPE_SAMPLER},
{2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE},
{6, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}});
const VkExtent2D extent = (*inputs)[0][0].Extent();
for (size_t i = 0; i < temp1.size(); ++i) {
temp1[i] = LsfgImage(device, memory_allocator, extent);
temp2[i] = LsfgImage(device, memory_allocator, extent);
}
for (size_t i = 0; i < LSFG_BETA_OUTPUTS; ++i) {
const VkExtent2D level_extent{
.width = extent.width >> i,
.height = extent.height >> i,
};
out_images[i] = LsfgImage(device, memory_allocator, level_extent, LSFG_FLOW_FORMAT);
}
std::vector<VkDescriptorSetLayout> layouts;
for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) {
layouts.push_back(passes[0].SetLayout());
}
for (size_t i = 1; i < LSFG_BETA_STAGES; ++i) {
layouts.push_back(passes[i].SetLayout());
}
owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts);
for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) {
first_descriptor_sets[i] = owned_sets[i];
}
for (size_t i = 0; i < LSFG_BETA_STAGES - 1; ++i) {
descriptor_sets[i] = owned_sets[LSFG_HISTORY_SLOTS + i];
}
const VkSampler sampler = resources.GetSampler();
const VkSampler border_sampler = resources.GetSampler(
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, VK_COMPARE_OP_NEVER, true);
for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) {
LsfgDescriptorWriter(first_descriptor_sets[i])
.AddSampler(border_sampler)
.AddSampledImages((*inputs)[(i + 1) % LSFG_HISTORY_SLOTS])
.AddSampledImages((*inputs)[(i + 2) % LSFG_HISTORY_SLOTS])
.AddSampledImages((*inputs)[i % LSFG_HISTORY_SLOTS])
.AddStorageImages(temp1)
.Build(device);
}
LsfgDescriptorWriter(descriptor_sets[0])
.AddSampler(sampler)
.AddSampledImages(temp1)
.AddStorageImages(temp2)
.Build(device);
LsfgDescriptorWriter(descriptor_sets[1])
.AddSampler(sampler)
.AddSampledImages(temp2)
.AddStorageImages(temp1)
.Build(device);
LsfgDescriptorWriter(descriptor_sets[2])
.AddSampler(sampler)
.AddSampledImages(temp1)
.AddStorageImages(temp2)
.Build(device);
LsfgDescriptorWriter(descriptor_sets[3])
.AddUniformBuffer(resources.GetBuffer(0.5f), LsfgResources::BufferSize())
.AddSampler(sampler)
.AddSampledImages(temp2)
.AddStorageImages(out_images)
.Build(device);
}
void LsfgBeta::Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count) {
const VkExtent2D extent = temp1[0].Extent();
const u32 groups_x = GroupCount(extent.width, DISPATCH_TILE_SHIFT);
const u32 groups_y = GroupCount(extent.height, DISPATCH_TILE_SHIFT);
LsfgBarriers barriers(cmdbuf);
for (auto& slot : *inputs) {
barriers.WriteToReadAll(slot);
}
barriers.ReadToWriteAll(temp1).Build();
passes[0].Bind(cmdbuf, first_descriptor_sets[frame_count % LSFG_HISTORY_SLOTS]);
cmdbuf.Dispatch(groups_x, groups_y, 1);
LsfgBarriers(cmdbuf).WriteToReadAll(temp1).ReadToWriteAll(temp2).Build();
passes[1].Bind(cmdbuf, descriptor_sets[0]);
cmdbuf.Dispatch(groups_x, groups_y, 1);
LsfgBarriers(cmdbuf).WriteToReadAll(temp2).ReadToWriteAll(temp1).Build();
passes[2].Bind(cmdbuf, descriptor_sets[1]);
cmdbuf.Dispatch(groups_x, groups_y, 1);
LsfgBarriers(cmdbuf).WriteToReadAll(temp1).ReadToWriteAll(temp2).Build();
passes[3].Bind(cmdbuf, descriptor_sets[2]);
cmdbuf.Dispatch(groups_x, groups_y, 1);
LsfgBarriers(cmdbuf).WriteToReadAll(temp2).ReadToWriteAll(out_images).Build();
passes[4].Bind(cmdbuf, descriptor_sets[3]);
cmdbuf.Dispatch(GroupCount(extent.width, OUTPUT_TILE_SHIFT),
GroupCount(extent.height, OUTPUT_TILE_SHIFT), 1);
}
} // namespace Vulkan
@@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <array>
#include "common/common_types.h"
#include "video_core/renderer_vulkan/present/lsfg_common.h"
namespace Vulkan {
class Device;
class LsfgShaders;
constexpr size_t LSFG_BETA_STAGES = 5;
constexpr size_t LSFG_BETA_OUTPUTS = 6;
class LsfgBeta {
public:
LsfgBeta() = default;
LsfgBeta(const Device& device, MemoryAllocator& memory_allocator, const LsfgShaders& shaders,
LsfgResources& resources, vk::DescriptorPool& descriptor_pool,
LsfgImageHistory& inputs);
void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count);
[[nodiscard]] LsfgImage& Output(size_t level) {
return out_images[level];
}
private:
LsfgImageHistory* inputs{};
std::array<LsfgPass, LSFG_BETA_STAGES> passes;
std::array<VkDescriptorSet, LSFG_HISTORY_SLOTS> first_descriptor_sets{};
std::array<VkDescriptorSet, LSFG_BETA_STAGES - 1> descriptor_sets{};
vk::DescriptorSets owned_sets;
LsfgImagePair temp1;
LsfgImagePair temp2;
std::array<LsfgImage, LSFG_BETA_OUTPUTS> out_images;
};
} // namespace Vulkan
@@ -0,0 +1,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
@@ -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
@@ -0,0 +1,339 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <cstring>
#include "video_core/renderer_vulkan/present/lsfg_common.h"
#include "video_core/renderer_vulkan/present/lsfg_shaders.h"
#include "video_core/renderer_vulkan/present/util.h"
#include "video_core/vulkan_common/vulkan_device.h"
namespace Vulkan {
namespace {
constexpr u32 DESCRIPTORS_PER_TYPE = 4096;
struct LsfgConstants {
std::array<u32, 2> input_offset;
u32 first_iter;
u32 first_iter_s;
u32 advanced_color_kind;
u32 hdr_support;
f32 resolution_inv_scale;
f32 timestamp;
f32 ui_threshold;
std::array<u32, 3> padding;
};
static_assert(sizeof(LsfgConstants) == 48);
vk::Image CreateChainImage(MemoryAllocator& memory_allocator, VkExtent2D extent, VkFormat format) {
const VkImageCreateInfo image_ci{
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.imageType = VK_IMAGE_TYPE_2D,
.format = format,
.extent = {.width = extent.width, .height = extent.height, .depth = 1},
.mipLevels = 1,
.arrayLayers = 1,
.samples = VK_SAMPLE_COUNT_1_BIT,
.tiling = VK_IMAGE_TILING_OPTIMAL,
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
.queueFamilyIndexCount = 0,
.pQueueFamilyIndices = nullptr,
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
};
return memory_allocator.CreateImage(image_ci);
}
vk::Buffer CreateUniformBuffer(MemoryAllocator& memory_allocator, VkDeviceSize size) {
const VkBufferCreateInfo buffer_ci{
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.size = size,
.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
.queueFamilyIndexCount = 0,
.pQueueFamilyIndices = nullptr,
};
return memory_allocator.CreateBuffer(buffer_ci, MemoryUsage::Upload);
}
VkImageMemoryBarrier MakeBarrier(const LsfgImage& image, VkAccessFlags src_access,
VkAccessFlags dst_access) {
return VkImageMemoryBarrier{
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.pNext = nullptr,
.srcAccessMask = src_access,
.dstAccessMask = dst_access,
.oldLayout = image.Layout(),
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = image.Handle(),
.subresourceRange{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1,
},
};
}
} // Anonymous namespace
LsfgImage::LsfgImage(const Device& device, MemoryAllocator& memory_allocator, VkExtent2D extent_,
VkFormat format_)
: extent{std::max(1u, extent_.width), std::max(1u, extent_.height)}, format{format_} {
image = CreateChainImage(memory_allocator, extent, format);
view = CreateWrappedImageView(device, image, format);
}
LsfgBarriers& LsfgBarriers::Push(LsfgImage& image, VkAccessFlags src_access,
VkAccessFlags dst_access) {
barriers.push_back(MakeBarrier(image, src_access, dst_access));
image.SetLayout(VK_IMAGE_LAYOUT_GENERAL);
return *this;
}
LsfgBarriers& LsfgBarriers::WriteToRead(LsfgImage& image) {
return Push(image, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT);
}
LsfgBarriers& LsfgBarriers::ReadToWrite(LsfgImage& image) {
return Push(image, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_SHADER_WRITE_BIT);
}
LsfgBarriers& LsfgBarriers::WriteToRead(LsfgImage* image) {
return image == nullptr ? *this : WriteToRead(*image);
}
LsfgBarriers& LsfgBarriers::ReadToWrite(LsfgImage* image) {
return image == nullptr ? *this : ReadToWrite(*image);
}
LsfgBarriers& LsfgBarriers::DiscardToWrite(VkImage image) {
barriers.push_back(VkImageMemoryBarrier{
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.pNext = nullptr,
.srcAccessMask = 0,
.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = image,
.subresourceRange{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1,
},
});
return *this;
}
VkDeviceSize LsfgResources::BufferSize() {
return sizeof(LsfgConstants);
}
VkSampler LsfgResources::GetSampler(VkSamplerAddressMode address_mode, VkCompareOp compare_op,
bool white_border) {
const u64 key = static_cast<u64>(address_mode) | (static_cast<u64>(compare_op) << 8) |
(static_cast<u64>(white_border) << 16);
const auto it = samplers.find(key);
if (it != samplers.end()) {
return *it->second;
}
const auto [entry, inserted] =
samplers.emplace(key, CreateLsfgSampler(*device, address_mode, compare_op, white_border));
return *entry->second;
}
VkBuffer LsfgResources::GetBuffer(f32 timestamp, bool first_iter, bool first_iter_s) {
u32 timestamp_bits{};
std::memcpy(&timestamp_bits, &timestamp, sizeof(timestamp_bits));
const u64 key = static_cast<u64>(timestamp_bits) | (static_cast<u64>(first_iter) << 32) |
(static_cast<u64>(first_iter_s) << 33);
const auto it = buffers.find(key);
if (it != buffers.end()) {
return *it->second;
}
vk::Buffer buffer = CreateUniformBuffer(*memory_allocator, sizeof(LsfgConstants));
const LsfgConstants constants{
.input_offset = {0, 0},
.first_iter = first_iter ? 1u : 0u,
.first_iter_s = first_iter_s ? 1u : 0u,
.advanced_color_kind = 0,
.hdr_support = 0,
.resolution_inv_scale = 1.0f / flow_scale,
.timestamp = timestamp,
.ui_threshold = 0.5f,
.padding = {0, 0, 0},
};
const std::span<u8> mapped = buffer.Mapped();
std::memcpy(mapped.data(), &constants, sizeof(constants));
buffer.Flush();
const auto [entry, inserted] = buffers.emplace(key, std::move(buffer));
return *entry->second;
}
void LsfgBarriers::Build() {
if (barriers.empty()) {
return;
}
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, {}, {}, barriers);
barriers.clear();
}
LsfgDescriptorWriter& LsfgDescriptorWriter::PushImage(VkDescriptorType type, VkSampler sampler,
VkImageView view) {
image_infos.push_back(VkDescriptorImageInfo{
.sampler = sampler,
.imageView = view,
.imageLayout = view == VK_NULL_HANDLE ? VK_IMAGE_LAYOUT_UNDEFINED
: VK_IMAGE_LAYOUT_GENERAL,
});
writes.push_back(VkWriteDescriptorSet{
.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
.pNext = nullptr,
.dstSet = set,
.dstBinding = binding++,
.dstArrayElement = 0,
.descriptorCount = 1,
.descriptorType = type,
.pImageInfo = &image_infos.back(),
.pBufferInfo = nullptr,
.pTexelBufferView = nullptr,
});
return *this;
}
LsfgDescriptorWriter& LsfgDescriptorWriter::AddSampler(VkSampler sampler) {
return PushImage(VK_DESCRIPTOR_TYPE_SAMPLER, sampler, VK_NULL_HANDLE);
}
LsfgDescriptorWriter& LsfgDescriptorWriter::AddSampledImage(const LsfgImage& image) {
return PushImage(VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_NULL_HANDLE, image.View());
}
LsfgDescriptorWriter& LsfgDescriptorWriter::AddSampledImage(const LsfgImage* image) {
return PushImage(VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_NULL_HANDLE,
image == nullptr ? VK_NULL_HANDLE : image->View());
}
LsfgDescriptorWriter& LsfgDescriptorWriter::AddStorageImage(const LsfgImage& image) {
return PushImage(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_NULL_HANDLE, image.View());
}
LsfgDescriptorWriter& LsfgDescriptorWriter::AddStorageView(VkImageView view) {
return PushImage(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_NULL_HANDLE, view);
}
LsfgDescriptorWriter& LsfgDescriptorWriter::AddUniformBuffer(VkBuffer buffer, VkDeviceSize size) {
buffer_infos.push_back(VkDescriptorBufferInfo{
.buffer = buffer,
.offset = 0,
.range = size,
});
writes.push_back(VkWriteDescriptorSet{
.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
.pNext = nullptr,
.dstSet = set,
.dstBinding = binding++,
.dstArrayElement = 0,
.descriptorCount = 1,
.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
.pImageInfo = nullptr,
.pBufferInfo = &buffer_infos.back(),
.pTexelBufferView = nullptr,
});
return *this;
}
void LsfgDescriptorWriter::Build(const Device& device) {
if (writes.empty()) {
return;
}
device.GetLogical().UpdateDescriptorSets(writes, {});
writes.clear();
}
LsfgPass::LsfgPass(const Device& device, const LsfgShaders& shaders, u32 shader_id,
LsfgBindings bindings) {
std::vector<VkDescriptorType> types;
for (const auto& [count, type] : bindings) {
types.insert(types.end(), count, type);
}
descriptor_count = static_cast<u32>(types.size());
descriptor_set_layout = CreateWrappedDescriptorSetLayout(
device, std::span<const VkDescriptorType>{types}, VK_SHADER_STAGE_COMPUTE_BIT);
pipeline_layout = CreateWrappedPipelineLayout(device, descriptor_set_layout);
pipeline = CreateWrappedComputePipeline(device, pipeline_layout, shaders.Get(shader_id));
}
void LsfgPass::Bind(vk::CommandBuffer cmdbuf, VkDescriptorSet set) const {
BindPipeline(cmdbuf);
BindSet(cmdbuf, set);
}
void LsfgPass::BindPipeline(vk::CommandBuffer cmdbuf) const {
cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *pipeline);
}
void LsfgPass::BindSet(vk::CommandBuffer cmdbuf, VkDescriptorSet set) const {
cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *pipeline_layout, 0, set, {});
}
vk::DescriptorPool CreateLsfgDescriptorPool(const Device& device, u32 max_sets) {
return CreateWrappedDescriptorPool(
device, DESCRIPTORS_PER_TYPE, max_sets,
{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_DESCRIPTOR_TYPE_SAMPLER,
VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE});
}
vk::Sampler CreateLsfgSampler(const Device& device, VkSamplerAddressMode address_mode,
VkCompareOp compare_op, bool white_border) {
return device.GetLogical().CreateSampler(VkSamplerCreateInfo{
.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.magFilter = VK_FILTER_LINEAR,
.minFilter = VK_FILTER_LINEAR,
.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR,
.addressModeU = address_mode,
.addressModeV = address_mode,
.addressModeW = address_mode,
.mipLodBias = 0.0f,
.anisotropyEnable = VK_FALSE,
.maxAnisotropy = 0.0f,
.compareEnable = VK_FALSE,
.compareOp = compare_op,
.minLod = 0.0f,
.maxLod = VK_LOD_CLAMP_NONE,
.borderColor = white_border ? VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE
: VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK,
.unnormalizedCoordinates = VK_FALSE,
});
}
} // namespace Vulkan
@@ -0,0 +1,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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <vector>
#include "video_core/frame_gen/lossless_dll.h"
#include "video_core/renderer_vulkan/present/lsfg_mipmaps.h"
#include "video_core/renderer_vulkan/present/lsfg_shaders.h"
#include "video_core/renderer_vulkan/present/util.h"
#include "video_core/vulkan_common/vulkan_device.h"
namespace Vulkan {
namespace {
constexpr u32 DISPATCH_TILE_SHIFT = 6;
[[nodiscard]] u32 GroupCount(u32 size) {
return (size + (1u << DISPATCH_TILE_SHIFT) - 1) >> DISPATCH_TILE_SHIFT;
}
} // Anonymous namespace
LsfgMipmaps::LsfgMipmaps(const Device& device, MemoryAllocator& memory_allocator,
const LsfgShaders& shaders, LsfgResources& resources,
vk::DescriptorPool& descriptor_pool, LsfgImagePair& frames_,
f32 flow_scale)
: frames{&frames_} {
using namespace VideoCore::FrameGen::PerformanceShader;
pass = LsfgPass(device, shaders, MIPMAPS,
{{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER},
{1, VK_DESCRIPTOR_TYPE_SAMPLER},
{1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE},
{LSFG_MIP_LEVELS, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}});
const VkExtent2D input_extent = (*frames)[0].Extent();
flow_extent = VkExtent2D{
.width = std::max(1u, static_cast<u32>(static_cast<f32>(input_extent.width) * flow_scale)),
.height = std::max(1u, static_cast<u32>(static_cast<f32>(input_extent.height) * flow_scale)),
};
for (size_t i = 0; i < LSFG_MIP_LEVELS; ++i) {
const VkExtent2D level_extent{
.width = flow_extent.width >> i,
.height = flow_extent.height >> i,
};
out_images[i] = LsfgImage(device, memory_allocator, level_extent, LSFG_FLOW_FORMAT);
}
const std::vector<VkDescriptorSetLayout> layouts(descriptor_sets.size(), pass.SetLayout());
owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts);
const VkSampler sampler = resources.GetSampler();
const VkBuffer buffer = resources.GetBuffer();
for (size_t i = 0; i < descriptor_sets.size(); ++i) {
descriptor_sets[i] = owned_sets[i];
LsfgDescriptorWriter(descriptor_sets[i])
.AddUniformBuffer(buffer, LsfgResources::BufferSize())
.AddSampler(sampler)
.AddSampledImage((*frames)[i])
.AddStorageImages(out_images)
.Build(device);
}
}
void LsfgMipmaps::Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count) {
const size_t slot = frame_count % descriptor_sets.size();
LsfgBarriers(cmdbuf).WriteToRead((*frames)[slot]).ReadToWriteAll(out_images).Build();
pass.Bind(cmdbuf, descriptor_sets[slot]);
cmdbuf.Dispatch(GroupCount(flow_extent.width), GroupCount(flow_extent.height), 1);
}
} // namespace Vulkan
@@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <array>
#include "common/common_types.h"
#include "video_core/renderer_vulkan/present/lsfg_common.h"
namespace Vulkan {
class Device;
class LsfgShaders;
constexpr size_t LSFG_MIP_LEVELS = 7;
class LsfgMipmaps {
public:
LsfgMipmaps() = default;
LsfgMipmaps(const Device& device, MemoryAllocator& memory_allocator, const LsfgShaders& shaders,
LsfgResources& resources, vk::DescriptorPool& descriptor_pool,
LsfgImagePair& frames, f32 flow_scale);
void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count);
[[nodiscard]] LsfgImage& Output(size_t level) {
return out_images[level];
}
private:
LsfgImagePair* frames{};
LsfgPass pass;
std::array<VkDescriptorSet, 2> descriptor_sets{};
vk::DescriptorSets owned_sets;
VkExtent2D flow_extent{};
std::array<LsfgImage, LSFG_MIP_LEVELS> out_images;
};
} // namespace Vulkan
@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "common/settings.h"
#include "video_core/frame_gen/lossless_dll.h"
#include "video_core/renderer_vulkan/present/lsfg_shaders.h"
#include "video_core/renderer_vulkan/present/util.h"
#include "video_core/vulkan_common/vulkan_device.h"
namespace Vulkan {
LsfgShaders::LsfgShaders(const Device& device) {
if (!device.IsVulkanMemoryModelSupported() || !device.HasNullDescriptor()) {
return;
}
const bool 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
@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <map>
#include "common/common_types.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
namespace Vulkan {
class Device;
class LsfgShaders {
public:
explicit LsfgShaders(const Device& device);
[[nodiscard]] bool IsValid() const {
return valid;
}
[[nodiscard]] VkShaderModule Get(u32 shader_id) const;
private:
std::map<u32, vk::ShaderModule> modules;
bool valid{};
};
} // namespace Vulkan
@@ -320,15 +320,16 @@ vk::DescriptorPool CreateWrappedDescriptorPool(const Device& device, size_t max_
});
}
vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout(
const Device& device, std::initializer_list<VkDescriptorType> types) {
vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout(const Device& device,
std::span<const VkDescriptorType> types,
VkShaderStageFlags stages) {
std::vector<VkDescriptorSetLayoutBinding> bindings(types.size());
for (size_t i = 0; i < types.size(); i++) {
bindings[i] = {
.binding = static_cast<u32>(i),
.descriptorType = std::data(types)[i],
.descriptorType = types[i],
.descriptorCount = 1,
.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
.stageFlags = stages,
.pImmutableSamplers = nullptr,
};
}
@@ -342,6 +343,13 @@ vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout(
});
}
vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout(
const Device& device, std::initializer_list<VkDescriptorType> types,
VkShaderStageFlags stages) {
return CreateWrappedDescriptorSetLayout(
device, std::span<const VkDescriptorType>{std::data(types), types.size()}, stages);
}
vk::DescriptorSets CreateWrappedDescriptorSets(vk::DescriptorPool& pool,
vk::Span<VkDescriptorSetLayout> layouts) {
return pool.Allocate(VkDescriptorSetAllocateInfo{
@@ -353,6 +361,28 @@ vk::DescriptorSets CreateWrappedDescriptorSets(vk::DescriptorPool& pool,
});
}
vk::Pipeline CreateWrappedComputePipeline(const Device& device, vk::PipelineLayout& layout,
VkShaderModule shader) {
return device.GetLogical().CreateComputePipeline(VkComputePipelineCreateInfo{
.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.stage =
{
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.stage = VK_SHADER_STAGE_COMPUTE_BIT,
.module = shader,
.pName = "main",
.pSpecializationInfo = nullptr,
},
.layout = *layout,
.basePipelineHandle = VK_NULL_HANDLE,
.basePipelineIndex = 0,
});
}
vk::PipelineLayout CreateWrappedPipelineLayout(const Device& device,
vk::DescriptorSetLayout& layout) {
return device.GetLogical().CreatePipelineLayout(VkPipelineLayoutCreateInfo{
@@ -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);
@@ -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();
@@ -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;
@@ -85,6 +85,17 @@ void BlitScreen::SetWindowAdaptPass(const Device& device) {
}
}
void BlitScreen::PrepareFrame(const Device& device, Frame* frame,
const Layout::FramebufferLayout& layout) {
if (!window_adapt || (frame->width == layout.width && frame->height == layout.height)) {
return;
}
WaitIdle(device);
present_manager.RecreateFrame(frame, layout.width, layout.height, swapchain_view_format,
window_adapt->GetRenderPass());
}
void BlitScreen::DrawToFrame(const Device& device, RasterizerVulkan& rasterizer, Frame* frame,
std::span<const Tegra::FramebufferConfig> framebuffers,
const Layout::FramebufferLayout& layout,
@@ -60,6 +60,8 @@ public:
const Layout::FramebufferLayout& layout, size_t current_swapchain_image_count,
VkFormat current_swapchain_view_format);
void PrepareFrame(const Device& device, Frame* frame, const Layout::FramebufferLayout& layout);
[[nodiscard]] vk::Framebuffer CreateFramebuffer(const Device& device, const Layout::FramebufferLayout& layout,
VkImageView image_view,
VkFormat current_view_format);
@@ -7,6 +7,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) {
@@ -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{};
};
@@ -146,9 +146,10 @@ public:
frame_counter++;
auto target_time = start_time + frame_interval * frame_counter;
if (target_time >= now) {
constexpr auto spin_tail = std::chrono::milliseconds(1);
auto sleep_time = target_time - now;
if (sleep_time > std::chrono::milliseconds(15)) {
std::this_thread::sleep_for(sleep_time - std::chrono::milliseconds(1));
if (sleep_time > spin_tail * 2) {
std::this_thread::sleep_for(sleep_time - spin_tail);
}
while (std::chrono::steady_clock::now() < target_time) {
std::this_thread::yield();
@@ -48,6 +48,9 @@ static VkPresentModeKHR ChooseSwapPresentMode(bool has_imm, bool has_mailbox,
Settings::VSyncMode setting = [has_imm, has_mailbox]() {
// Choose Mailbox or Immediate if unlocked and those modes are supported
const auto mode = Settings::values.vsync_mode.GetValue();
if (Settings::values.frame_gen.GetValue()) {
return mode == Settings::VSyncMode::FifoRelaxed ? mode : Settings::VSyncMode::Fifo;
}
if (Settings::values.use_speed_limit.GetValue() &&
Settings::values.current_speed_mode.GetValue() != Settings::SpeedMode::Turbo) {
return mode;
+7 -1
View File
@@ -37,7 +37,8 @@ VK_DEFINE_HANDLE(VmaAllocator)
FEATURE(EXT, HostQueryReset, HOST_QUERY_RESET, host_query_reset) \
FEATURE(KHR, 8BitStorage, 8BIT_STORAGE, bit8_storage) \
FEATURE(KHR, BufferDeviceAddress, BUFFER_DEVICE_ADDRESS, buffer_device_address) \
FEATURE(KHR, TimelineSemaphore, TIMELINE_SEMAPHORE, timeline_semaphore)
FEATURE(KHR, TimelineSemaphore, TIMELINE_SEMAPHORE, timeline_semaphore) \
FEATURE(KHR, VulkanMemoryModel, VULKAN_MEMORY_MODEL, vulkan_memory_model)
#define FOR_EACH_VK_FEATURE_1_3(FEATURE) \
FEATURE(EXT, ImageRobustness, IMAGE_ROBUSTNESS, robust_image_access) \
@@ -416,6 +417,11 @@ FN_MAX_LIMIT_LIST
return features.shader_float16_int8.shaderFloat16;
}
/// Returns true if the device can run shaders built against the Vulkan memory model.
bool IsVulkanMemoryModelSupported() const {
return features.vulkan_memory_model.vulkanMemoryModel;
}
/// Returns true if the device supports int8 natively.
bool IsInt8Supported() const {
return features.shader_float16_int8.shaderInt8;