[vulkan, android] Add feature of post-processing shaders on Android (#4348)

Very self-explanatory; implementation of the feature for post-processing shaders for Android (at least for now); will allow users to enhance the graphic quality of video games based on the use of multiple shaders adjustable, presets and more, inspired on the PPSSPP implementation, this feature adds 22 customizable shaders (1 pass) that lives on the overlay of the screen, which means that aside of the capability to chain multiple shaders on the screen, this doesn't have depths (pixel/ depth z-buffer) so it ensures the performance with it's use. Few shaders were a faithful port from PPSSPP with their respective attribution on the shader headers for their respective owners; and there are adaptations from public references/ cinematographic (Anime4K) and the rest are my own addition.

_Special Credits:_

1.- PPSSPP Team for their contribution on the public references for shaders: Henrik Rydgard, ShadX, SimoneT, KillaMaaki and guest(r).
2.- Niklas Haas.
3.- bloc97.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4348
Reviewed-by: lizzie <lizzie@eden-emu.dev>
Reviewed-by: Maufeat <sahyno1996@gmail.com>
This commit is contained in:
CamilleLaVey
2026-09-10 15:14:02 +02:00
committed by crueter
parent 5f142c7926
commit ed566919f4
94 changed files with 7079 additions and 0 deletions
+44
View File
@@ -302,6 +302,50 @@ if (ENABLE_LSFG)
target_compile_definitions(video_core PUBLIC HAS_LSFG)
endif()
if (ENABLE_RESHADE)
set(BUNDLED_FX_DIR ${CMAKE_SOURCE_DIR}/dist/post_shaders)
set(BUNDLED_FX_HEADER ${CMAKE_CURRENT_BINARY_DIR}/bundled_fx_effects.h)
file(GLOB BUNDLED_FX_FILES ${BUNDLED_FX_DIR}/*.fx)
add_custom_command(
OUTPUT ${BUNDLED_FX_HEADER}
COMMAND ${CMAKE_COMMAND} -P
${CMAKE_CURRENT_SOURCE_DIR}/post_processing/GenerateBundledEffects.cmake
${BUNDLED_FX_DIR} ${BUNDLED_FX_HEADER}
DEPENDS ${BUNDLED_FX_FILES}
)
set(BUNDLED_PRESET_DIR ${CMAKE_SOURCE_DIR}/dist/post_presets)
set(BUNDLED_PRESET_HEADER ${CMAKE_CURRENT_BINARY_DIR}/bundled_fx_presets.h)
file(GLOB BUNDLED_PRESET_FILES ${BUNDLED_PRESET_DIR}/*.fxp)
add_custom_command(
OUTPUT ${BUNDLED_PRESET_HEADER}
COMMAND ${CMAKE_COMMAND} -P
${CMAKE_CURRENT_SOURCE_DIR}/post_processing/GenerateBundledPresets.cmake
${BUNDLED_PRESET_DIR} ${BUNDLED_PRESET_HEADER}
DEPENDS ${BUNDLED_PRESET_FILES}
)
target_sources(video_core PRIVATE
post_processing/fx_chain.cpp
post_processing/fx_chain.h
post_processing/fx_compile.cpp
post_processing/fx_compile.h
post_processing/fx_effect.cpp
post_processing/fx_effect.h
post_processing/fx_preset.cpp
post_processing/fx_preset.h
renderer_vulkan/present/post_process.cpp
renderer_vulkan/present/post_process.h
${BUNDLED_FX_HEADER}
${BUNDLED_PRESET_HEADER}
)
target_include_directories(video_core PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(video_core PRIVATE reshadefx::reshadefx)
target_compile_definitions(video_core PUBLIC HAS_RESHADE)
endif()
if (ENABLE_OPENGL)
target_sources(video_core PRIVATE
renderer_opengl/present/filters.cpp
@@ -0,0 +1,50 @@
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
set(EFFECT_DIR ${CMAKE_ARGV3})
set(HEADER_FILE ${CMAKE_ARGV4})
file(GLOB EFFECT_FILES ${EFFECT_DIR}/*.fx)
list(SORT EFFECT_FILES)
set(ENTRIES "")
foreach(EFFECT_FILE IN LISTS EFFECT_FILES)
get_filename_component(EFFECT_NAME ${EFFECT_FILE} NAME)
file(READ ${EFFECT_FILE} EFFECT_BODY)
string(REGEX REPLACE ";" "{{SEMICOLON}}" EFFECT_BODY "${EFFECT_BODY}")
string(REGEX REPLACE "\n" ";" EFFECT_BODY "${EFFECT_BODY}")
set(EFFECT_TEXT "")
foreach(LINE IN LISTS EFFECT_BODY)
string(CONCAT EFFECT_TEXT "${EFFECT_TEXT}" " R\"(${LINE}\n)\"\n")
endforeach()
string(REGEX REPLACE "{{SEMICOLON}}" ";" EFFECT_TEXT "${EFFECT_TEXT}")
string(CONCAT ENTRIES "${ENTRIES}"
" {\n \"${EFFECT_NAME}\",\n${EFFECT_TEXT} },\n")
endforeach()
get_filename_component(OUTPUT_DIR ${HEADER_FILE} DIRECTORY)
make_directory(${OUTPUT_DIR})
file(WRITE ${HEADER_FILE}
"// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <string_view>
namespace VideoCore {
struct BundledFxEffect {
std::string_view name;
std::string_view source;
};
constexpr BundledFxEffect BUNDLED_FX_EFFECTS[]{
${ENTRIES}};
} // namespace VideoCore
")
@@ -0,0 +1,50 @@
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
set(PRESET_DIR ${CMAKE_ARGV3})
set(HEADER_FILE ${CMAKE_ARGV4})
file(GLOB PRESET_FILES ${PRESET_DIR}/*.fxp)
list(SORT PRESET_FILES)
set(ENTRIES "")
foreach(PRESET_FILE IN LISTS PRESET_FILES)
get_filename_component(PRESET_NAME ${PRESET_FILE} NAME)
file(READ ${PRESET_FILE} PRESET_BODY)
string(REGEX REPLACE ";" "{{SEMICOLON}}" PRESET_BODY "${PRESET_BODY}")
string(REGEX REPLACE "\n" ";" PRESET_BODY "${PRESET_BODY}")
set(PRESET_TEXT "")
foreach(LINE IN LISTS PRESET_BODY)
string(CONCAT PRESET_TEXT "${PRESET_TEXT}" " R\"(${LINE}\n)\"\n")
endforeach()
string(REGEX REPLACE "{{SEMICOLON}}" ";" PRESET_TEXT "${PRESET_TEXT}")
string(CONCAT ENTRIES "${ENTRIES}"
" {\n \"${PRESET_NAME}\",\n${PRESET_TEXT} },\n")
endforeach()
get_filename_component(OUTPUT_DIR ${HEADER_FILE} DIRECTORY)
make_directory(${OUTPUT_DIR})
file(WRITE ${HEADER_FILE}
"// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <string_view>
namespace VideoCore {
struct BundledFxPreset {
std::string_view name;
std::string_view source;
};
constexpr BundledFxPreset BUNDLED_FX_PRESETS[]{
${ENTRIES}};
} // namespace VideoCore
")
+321
View File
@@ -0,0 +1,321 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <cstdlib>
#include <fmt/format.h>
#include "common/settings.h"
#include "video_core/post_processing/fx_chain.h"
#include "video_core/post_processing/fx_effect.h"
namespace VideoCore {
namespace {
std::vector<std::string_view> Split(std::string_view value, char separator) {
std::vector<std::string_view> out;
size_t start = 0;
while (start <= value.size()) {
size_t end = value.find(separator, start);
if (end == std::string_view::npos) {
end = value.size();
}
out.push_back(value.substr(start, end - start));
start = end + 1;
}
return out;
}
bool IsSerializableName(std::string_view value) {
return value.find_first_of(";|,=") == std::string_view::npos;
}
} // Anonymous namespace
std::vector<FxChainEntry> ParseFxChain(std::string_view value) {
std::vector<FxChainEntry> parsed;
for (const std::string_view record : Split(value, ';')) {
if (record.empty()) {
continue;
}
const auto fields = Split(record, '|');
if (fields.size() < 2 || fields[0].empty() || fields[1].empty()) {
continue;
}
FxChainEntry entry;
entry.file = std::string(fields[0]);
entry.technique = std::string(fields[1]);
if (fields.size() >= 3) {
for (const std::string_view assignment : Split(fields[2], ',')) {
const size_t equals = assignment.find('=');
if (equals == std::string_view::npos) {
continue;
}
const std::string name(assignment.substr(0, equals));
if (name.empty()) {
continue;
}
std::array<f32, 4> components{};
size_t index = 0;
for (const std::string_view piece : Split(assignment.substr(equals + 1), '/')) {
if (index >= components.size()) {
break;
}
const std::string text(piece);
if (!text.empty()) {
components[index] = std::strtof(text.c_str(), nullptr);
}
++index;
}
entry.values.emplace(name, components);
}
}
parsed.push_back(std::move(entry));
}
return parsed;
}
std::string SerializeFxChain(std::span<const FxChainEntry> entries) {
std::string out;
for (const auto& entry : entries) {
if (!IsSerializableName(entry.file) || !IsSerializableName(entry.technique)) {
continue;
}
if (!out.empty()) {
out += ';';
}
out += fmt::format("{}|{}|", entry.file, entry.technique);
bool first = true;
for (const auto& [name, value] : entry.values) {
if (!IsSerializableName(name)) {
continue;
}
if (!first) {
out += ',';
}
first = false;
out += name;
out += '=';
for (size_t i = 0; i < value.size(); ++i) {
if (i > 0) {
out += '/';
}
out += fmt::format("{}", value[i]);
}
}
}
return out;
}
void UseGlobalFxSettings() {
Settings::values.post_shader_chain.SetGlobal(true);
Settings::values.post_shader_preset.SetGlobal(true);
Settings::values.post_shader_enabled.SetGlobal(true);
}
void UsePerGameFxSettings() {
const std::string chain = Settings::values.post_shader_chain.GetValue();
const std::string preset = Settings::values.post_shader_preset.GetValue();
const bool enabled = Settings::values.post_shader_enabled.GetValue();
Settings::values.post_shader_chain.SetGlobal(false);
Settings::values.post_shader_preset.SetGlobal(false);
Settings::values.post_shader_enabled.SetGlobal(false);
Settings::values.post_shader_chain.SetValue(chain);
Settings::values.post_shader_preset.SetValue(preset);
Settings::values.post_shader_enabled.SetValue(enabled);
}
FxChain& FxChain::Instance() {
static FxChain instance;
return instance;
}
FxChainSnapshot FxChain::Snapshot() const {
std::scoped_lock lock{mutex};
return FxChainSnapshot{
.entries = entries,
.generation = generation.load(std::memory_order_relaxed),
};
}
std::vector<FxChainEntry> FxChain::Entries() const {
std::scoped_lock lock{mutex};
return entries;
}
size_t FxChain::Size() const {
std::scoped_lock lock{mutex};
return entries.size();
}
void FxChain::Append(std::string_view file, std::string_view technique) {
{
std::scoped_lock lock{mutex};
FxChainEntry entry;
entry.file = std::string(file);
entry.technique = std::string(technique);
entries.push_back(std::move(entry));
}
generation.fetch_add(1, std::memory_order_relaxed);
}
void FxChain::Replace(size_t index, std::string_view file, std::string_view technique) {
{
std::scoped_lock lock{mutex};
if (index >= entries.size()) {
return;
}
if (entries[index].file == file && entries[index].technique == technique) {
return;
}
FxChainEntry entry;
entry.file = std::string(file);
entry.technique = std::string(technique);
entries[index] = std::move(entry);
}
generation.fetch_add(1, std::memory_order_relaxed);
}
void FxChain::Remove(size_t index) {
{
std::scoped_lock lock{mutex};
if (index >= entries.size()) {
return;
}
entries.erase(entries.begin() + static_cast<std::ptrdiff_t>(index));
}
generation.fetch_add(1, std::memory_order_relaxed);
}
void FxChain::Move(size_t index, int delta) {
{
std::scoped_lock lock{mutex};
if (index >= entries.size()) {
return;
}
const std::ptrdiff_t target = static_cast<std::ptrdiff_t>(index) + delta;
if (target < 0 || target >= static_cast<std::ptrdiff_t>(entries.size())) {
return;
}
std::swap(entries[index], entries[static_cast<size_t>(target)]);
}
generation.fetch_add(1, std::memory_order_relaxed);
}
void FxChain::Clear() {
{
std::scoped_lock lock{mutex};
entries.clear();
}
generation.fetch_add(1, std::memory_order_relaxed);
}
void FxChain::SetEntries(std::vector<FxChainEntry> next) {
{
std::scoped_lock lock{mutex};
entries = std::move(next);
}
generation.fetch_add(1, std::memory_order_relaxed);
}
void FxChain::SetValue(size_t index, std::string_view uniform, const std::array<f32, 4>& value) {
std::scoped_lock lock{mutex};
if (index >= entries.size()) {
return;
}
entries[index].values[std::string(uniform)] = value;
}
std::array<f32, 4> FxChain::GetValue(size_t index, std::string_view uniform) const {
std::scoped_lock lock{mutex};
if (index >= entries.size()) {
return {};
}
const auto it = entries[index].values.find(std::string(uniform));
if (it == entries[index].values.end()) {
return {};
}
return it->second;
}
std::map<std::string, std::array<f32, 4>> FxChain::EntryValues(size_t index) const {
std::scoped_lock lock{mutex};
if (index >= entries.size()) {
return {};
}
return entries[index].values;
}
bool FxChain::HasValue(size_t index, std::string_view uniform) const {
std::scoped_lock lock{mutex};
if (index >= entries.size()) {
return false;
}
return entries[index].values.contains(std::string(uniform));
}
void FxChain::ResetValues(size_t index) {
std::scoped_lock lock{mutex};
if (index >= entries.size()) {
return;
}
entries[index].values.clear();
}
void FxChain::LoadFromSettings() {
auto parsed = ParseFxChain(Settings::values.post_shader_chain.GetValue());
std::scoped_lock lock{mutex};
if (entries == parsed) {
return;
}
entries = std::move(parsed);
generation.fetch_add(1, std::memory_order_relaxed);
}
void FxChain::StoreToSettings() const {
std::string serialized;
{
std::scoped_lock lock{mutex};
serialized = SerializeFxChain(entries);
}
Settings::values.post_shader_chain.SetValue(serialized);
}
void FxChain::DropUnknownEntries() {
bool changed = false;
{
std::scoped_lock lock{mutex};
const auto removed = std::remove_if(entries.begin(), entries.end(), [](const FxChainEntry& entry) {
const FxEffectDesc* effect = FindFxEffect(entry.file);
if (effect == nullptr || !effect->Valid()) {
return true;
}
return std::find(effect->techniques.begin(), effect->techniques.end(),
entry.technique) == effect->techniques.end();
});
if (removed != entries.end()) {
entries.erase(removed, entries.end());
changed = true;
}
}
if (changed) {
generation.fetch_add(1, std::memory_order_relaxed);
}
}
} // namespace VideoCore
+86
View File
@@ -0,0 +1,86 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <array>
#include <atomic>
#include <map>
#include <mutex>
#include <span>
#include <string>
#include <string_view>
#include <vector>
#include "common/common_types.h"
namespace VideoCore {
struct FxChainEntry {
std::string file;
std::string technique;
std::map<std::string, std::array<f32, 4>> values;
bool operator==(const FxChainEntry&) const = default;
};
struct FxChainSnapshot {
std::vector<FxChainEntry> entries;
u64 generation{};
};
std::vector<FxChainEntry> ParseFxChain(std::string_view value);
std::string SerializeFxChain(std::span<const FxChainEntry> entries);
void UseGlobalFxSettings();
void UsePerGameFxSettings();
class FxChain {
public:
static FxChain& Instance();
FxChainSnapshot Snapshot() const;
std::vector<FxChainEntry> Entries() const;
size_t Size() const;
void Append(std::string_view file, std::string_view technique);
void Replace(size_t index, std::string_view file, std::string_view technique);
void Remove(size_t index);
void Move(size_t index, int delta);
void Clear();
void SetEntries(std::vector<FxChainEntry> next);
void SetValue(size_t index, std::string_view uniform, const std::array<f32, 4>& value);
std::array<f32, 4> GetValue(size_t index, std::string_view uniform) const;
std::map<std::string, std::array<f32, 4>> EntryValues(size_t index) const;
bool HasValue(size_t index, std::string_view uniform) const;
void ResetValues(size_t index);
void LoadFromSettings();
void StoreToSettings() const;
void DropUnknownEntries();
private:
FxChain() = default;
mutable std::mutex mutex;
std::vector<FxChainEntry> entries;
std::atomic<u64> generation{1};
};
} // namespace VideoCore
@@ -0,0 +1,105 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <cstring>
#include <memory>
#include <set>
#include "effect_codegen.hpp"
#include "effect_parser.hpp"
#include "effect_preprocessor.hpp"
#include "common/fs/fs.h"
#include "common/fs/fs_util.h"
#include "video_core/post_processing/fx_compile.h"
#include "video_core/post_processing/fx_effect.h"
namespace VideoCore {
FxCompileResult CompileFxEffect(const std::filesystem::path& path, u32 width, u32 height,
u32 color_bit_depth) {
FxCompileResult result;
if (!Common::FS::Exists(path)) {
result.error = "Effect file not found: " + Common::FS::PathToUTF8String(path);
return result;
}
reshadefx::preprocessor preprocessor;
preprocessor.add_macro_definition("__RESHADE__", "60800");
preprocessor.add_macro_definition("__RESHADE_PERFORMANCE_MODE__", "1");
preprocessor.add_macro_definition("__RENDERER__", "0x20000");
preprocessor.add_macro_definition("__VENDOR__", "0");
preprocessor.add_macro_definition("__DEVICE__", "0");
preprocessor.add_macro_definition("__APPLICATION__", "0");
preprocessor.add_macro_definition("BUFFER_WIDTH", std::to_string(width));
preprocessor.add_macro_definition("BUFFER_HEIGHT", std::to_string(height));
preprocessor.add_macro_definition("BUFFER_RCP_WIDTH", "(1.0 / BUFFER_WIDTH)");
preprocessor.add_macro_definition("BUFFER_RCP_HEIGHT", "(1.0 / BUFFER_HEIGHT)");
preprocessor.add_macro_definition("BUFFER_COLOR_DEPTH", std::to_string(color_bit_depth));
preprocessor.add_macro_definition("BUFFER_COLOR_BIT_DEPTH", std::to_string(color_bit_depth));
for (const auto& include : GetFxIncludePaths(path)) {
preprocessor.add_include_path(include);
}
if (!preprocessor.append_file(path)) {
result.error = preprocessor.errors();
if (result.error.empty()) {
result.error = "Failed to preprocess " + Common::FS::PathToUTF8String(path);
}
return result;
}
std::unique_ptr<reshadefx::codegen> backend(
reshadefx::create_codegen_spirv(true, false, false, false, true));
reshadefx::parser parser;
if (!parser.parse(preprocessor.output(), backend.get())) {
result.error = parser.errors();
if (result.error.empty()) {
result.error = "Failed to parse " + Common::FS::PathToUTF8String(path);
}
return result;
}
result.module = backend->module();
std::set<std::string> wanted;
for (const auto& technique : result.module.techniques) {
for (const auto& pass : technique.passes) {
if (!pass.vs_entry_point.empty()) {
wanted.insert(pass.vs_entry_point);
}
if (!pass.ps_entry_point.empty()) {
wanted.insert(pass.ps_entry_point);
}
}
}
for (const auto& name : wanted) {
std::string binary;
std::string assembly;
std::string errors;
if (!backend->assemble_code_for_entry_point(name, binary, assembly, errors)) {
result.error = "Failed to assemble entry point '" + name + "': " + errors;
return result;
}
if (binary.size() % sizeof(u32) != 0) {
result.error = "Entry point '" + name + "' produced a malformed SPIR-V module";
return result;
}
std::vector<u32> words(binary.size() / sizeof(u32));
std::memcpy(words.data(), binary.data(), binary.size());
result.entry_points.emplace(name, std::move(words));
}
if (result.entry_points.empty()) {
result.error = "Effect declares no usable entry points";
}
return result;
}
} // namespace VideoCore
@@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <filesystem>
#include <map>
#include <string>
#include <vector>
#include "common/common_types.h"
#include "effect_module.hpp"
namespace VideoCore {
struct FxCompileResult {
reshadefx::effect_module module;
std::map<std::string, std::vector<u32>> entry_points;
std::string error;
bool Succeeded() const {
return error.empty() && !entry_points.empty();
}
};
FxCompileResult CompileFxEffect(const std::filesystem::path& path, u32 width, u32 height,
u32 color_bit_depth);
} // namespace VideoCore
@@ -0,0 +1,307 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include "bundled_fx_effects.h"
#include "common/fs/file.h"
#include "common/fs/fs.h"
#include "common/fs/fs_util.h"
#include "common/fs/path_util.h"
#include "common/logging.h"
#include "video_core/post_processing/fx_compile.h"
#include "video_core/post_processing/fx_effect.h"
namespace VideoCore {
namespace {
constexpr u32 CATALOG_PROBE_WIDTH = 1280;
constexpr u32 CATALOG_PROBE_HEIGHT = 720;
constexpr u32 CATALOG_PROBE_DEPTH = 8;
std::vector<FxEffectDesc> catalog;
bool catalog_scanned = false;
const reshadefx::annotation* FindAnnotation(const std::vector<reshadefx::annotation>& annotations,
std::string_view name) {
const auto it = std::find_if(annotations.begin(), annotations.end(),
[&](const reshadefx::annotation& a) { return a.name == name; });
if (it == annotations.end()) {
return nullptr;
}
return &*it;
}
std::string AnnotationString(const std::vector<reshadefx::annotation>& annotations,
std::string_view name) {
const reshadefx::annotation* a = FindAnnotation(annotations, name);
if (a == nullptr) {
return std::string();
}
return a->value.string_data;
}
bool AnnotationFloat(const std::vector<reshadefx::annotation>& annotations, std::string_view name,
f32& out) {
const reshadefx::annotation* a = FindAnnotation(annotations, name);
if (a == nullptr) {
return false;
}
if (a->type.is_floating_point()) {
out = a->value.as_float[0];
return true;
}
if (a->type.is_integral()) {
out = static_cast<f32>(a->value.as_int[0]);
return true;
}
return false;
}
FxUiType ParseUiType(std::string_view value) {
if (value == "slider") {
return FxUiType::Slider;
}
if (value == "drag") {
return FxUiType::Drag;
}
if (value == "combo") {
return FxUiType::Combo;
}
if (value == "radio") {
return FxUiType::Radio;
}
if (value == "check" || value == "checkbox") {
return FxUiType::CheckBox;
}
if (value == "color") {
return FxUiType::Color;
}
if (value == "input") {
return FxUiType::InputBox;
}
return FxUiType::Hidden;
}
std::vector<std::string> SplitItems(const std::string& items) {
std::vector<std::string> out;
std::string current;
for (const char c : items) {
if (c == '\0') {
out.push_back(current);
current.clear();
continue;
}
current += c;
}
if (!current.empty()) {
out.push_back(current);
}
return out;
}
FxUniformDesc DescribeUniform(const reshadefx::uniform& info) {
FxUniformDesc desc;
desc.name = info.name;
desc.components = std::min<u32>(info.type.components(), 4);
if (info.type.is_boolean()) {
desc.kind = FxUniformKind::Boolean;
} else if (info.type.is_integral()) {
desc.kind = FxUniformKind::Integer;
} else {
desc.kind = FxUniformKind::Floating;
}
desc.label = AnnotationString(info.annotations, "ui_label");
if (desc.label.empty()) {
desc.label = info.name;
}
desc.tooltip = AnnotationString(info.annotations, "ui_tooltip");
desc.category = AnnotationString(info.annotations, "ui_category");
desc.ui_type = ParseUiType(AnnotationString(info.annotations, "ui_type"));
desc.items = SplitItems(AnnotationString(info.annotations, "ui_items"));
if (desc.kind == FxUniformKind::Boolean) {
desc.ui_min = 0.0f;
desc.ui_max = 1.0f;
desc.ui_step = 1.0f;
} else if (desc.kind == FxUniformKind::Integer) {
desc.ui_min = 0.0f;
desc.ui_max = 100.0f;
desc.ui_step = 1.0f;
}
void(AnnotationFloat(info.annotations, "ui_min", desc.ui_min));
void(AnnotationFloat(info.annotations, "ui_max", desc.ui_max));
void(AnnotationFloat(info.annotations, "ui_step", desc.ui_step));
if (desc.ui_step <= 0.0f) {
desc.ui_step = 0.01f;
if (desc.kind != FxUniformKind::Floating) {
desc.ui_step = 1.0f;
}
}
if (desc.ui_max < desc.ui_min) {
std::swap(desc.ui_min, desc.ui_max);
}
if (info.has_initializer_value) {
for (u32 i = 0; i < desc.components; ++i) {
if (desc.kind == FxUniformKind::Floating) {
desc.default_value[i] = info.initializer_value.as_float[i];
} else {
desc.default_value[i] = static_cast<f32>(info.initializer_value.as_int[i]);
}
}
}
return desc;
}
FxEffectDesc DescribeEffect(const std::filesystem::path& path, const std::filesystem::path& root) {
FxEffectDesc desc;
desc.file = Common::FS::PathToUTF8String(std::filesystem::relative(path, root));
desc.name = Common::FS::PathToUTF8String(path.stem());
const auto compiled =
CompileFxEffect(path, CATALOG_PROBE_WIDTH, CATALOG_PROBE_HEIGHT, CATALOG_PROBE_DEPTH);
if (!compiled.Succeeded()) {
desc.error = compiled.error;
return desc;
}
for (const auto& technique : compiled.module.techniques) {
desc.techniques.push_back(technique.name);
}
if (!compiled.module.techniques.empty()) {
const auto& annotations = compiled.module.techniques.front().annotations;
desc.label = AnnotationString(annotations, "ui_label");
desc.description = AnnotationString(annotations, "ui_tooltip");
}
if (desc.label.empty()) {
desc.label = desc.name;
}
for (const auto& uniform : compiled.module.uniforms) {
FxUniformDesc uniform_desc = DescribeUniform(uniform);
if (uniform_desc.ui_type == FxUiType::Hidden) {
continue;
}
desc.uniforms.push_back(std::move(uniform_desc));
}
return desc;
}
} // Anonymous namespace
std::filesystem::path GetFxRootDirectory() {
return Common::FS::GetEdenPath(Common::FS::EdenPath::PostShaderDir);
}
std::vector<std::filesystem::path> GetFxIncludePaths(const std::filesystem::path& effect_path) {
const auto root = GetFxRootDirectory();
std::vector<std::filesystem::path> paths;
paths.push_back(effect_path.parent_path());
paths.push_back(root);
paths.push_back(root / "Shaders");
const auto last = std::unique(paths.begin(), paths.end());
paths.erase(last, paths.end());
return paths;
}
std::filesystem::path ResolveFxTexturePath(const std::filesystem::path& effect_path,
std::string_view source) {
const auto root = GetFxRootDirectory();
const std::filesystem::path name{source};
const std::array candidates{
effect_path.parent_path() / name,
root / "Textures" / name,
root / name,
};
for (const auto& candidate : candidates) {
if (Common::FS::Exists(candidate)) {
return candidate;
}
}
return std::filesystem::path();
}
void ReloadFxCatalog() {
catalog.clear();
catalog_scanned = true;
const auto root = GetFxRootDirectory();
if (!Common::FS::Exists(root) && !Common::FS::CreateDirs(root)) {
return;
}
for (const auto& bundled : BUNDLED_FX_EFFECTS) {
const auto path = root / bundled.name;
if (Common::FS::Exists(path)) {
continue;
}
void(Common::FS::WriteStringToFile(path, Common::FS::FileType::TextFile, bundled.source));
}
std::vector<std::filesystem::path> effect_files;
Common::FS::IterateDirEntriesRecursively(
root,
[&](const std::filesystem::directory_entry& entry) {
if (entry.path().extension() == ".fx") {
effect_files.push_back(entry.path());
}
return true;
},
Common::FS::DirEntryFilter::File);
std::sort(effect_files.begin(), effect_files.end());
for (const auto& file : effect_files) {
FxEffectDesc desc = DescribeEffect(file, root);
if (!desc.error.empty()) {
LOG_WARNING(Render, "Post-processing effect '{}' failed to compile:\n{}", desc.file,
desc.error);
}
catalog.push_back(std::move(desc));
}
const size_t usable = std::count_if(catalog.begin(), catalog.end(),
[](const FxEffectDesc& d) { return d.Valid(); });
LOG_INFO(Render, "Loaded {} post-processing effects ({} usable)", catalog.size(), usable);
}
const std::vector<FxEffectDesc>& GetFxCatalog() {
if (!catalog_scanned) {
ReloadFxCatalog();
}
return catalog;
}
const FxEffectDesc* FindFxEffect(std::string_view file) {
const auto& effects = GetFxCatalog();
const auto it = std::find_if(effects.begin(), effects.end(),
[&](const FxEffectDesc& d) { return d.file == file; });
if (it == effects.end()) {
return nullptr;
}
return &*it;
}
const FxUniformDesc* FindFxUniform(const FxEffectDesc& effect, std::string_view name) {
const auto it = std::find_if(effect.uniforms.begin(), effect.uniforms.end(),
[&](const FxUniformDesc& u) { return u.name == name; });
if (it == effect.uniforms.end()) {
return nullptr;
}
return &*it;
}
} // namespace VideoCore
@@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <array>
#include <filesystem>
#include <string>
#include <string_view>
#include <vector>
#include "common/common_types.h"
namespace VideoCore {
enum class FxUniformKind {
Boolean,
Integer,
Floating,
};
enum class FxUiType {
Hidden,
Slider,
Drag,
Combo,
Radio,
CheckBox,
Color,
InputBox,
};
struct FxUniformDesc {
std::string name;
std::string label;
std::string tooltip;
std::string category;
FxUniformKind kind{FxUniformKind::Floating};
u32 components{1};
FxUiType ui_type{FxUiType::Hidden};
f32 ui_min{0.0f};
f32 ui_max{1.0f};
f32 ui_step{0.01f};
std::vector<std::string> items;
std::array<f32, 4> default_value{};
};
struct FxEffectDesc {
std::string file;
std::string name;
std::string label;
std::string description;
std::vector<std::string> techniques;
std::vector<FxUniformDesc> uniforms;
std::string error;
bool Valid() const {
return error.empty() && !techniques.empty();
}
};
std::filesystem::path GetFxRootDirectory();
std::vector<std::filesystem::path> GetFxIncludePaths(const std::filesystem::path& effect_path);
std::filesystem::path ResolveFxTexturePath(const std::filesystem::path& effect_path,
std::string_view source);
void ReloadFxCatalog();
const std::vector<FxEffectDesc>& GetFxCatalog();
const FxEffectDesc* FindFxEffect(std::string_view file);
const FxUniformDesc* FindFxUniform(const FxEffectDesc& effect, std::string_view name);
} // namespace VideoCore
@@ -0,0 +1,261 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include "bundled_fx_presets.h"
#include "common/fs/file.h"
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "common/settings.h"
#include "video_core/post_processing/fx_chain.h"
#include "video_core/post_processing/fx_preset.h"
namespace VideoCore {
namespace {
std::vector<FxPresetDesc> preset_catalog;
bool preset_catalog_scanned = false;
std::string Trim(std::string_view value) {
size_t begin = 0;
while (begin < value.size() && (value[begin] == ' ' || value[begin] == '\t')) {
++begin;
}
size_t end = value.size();
while (end > begin) {
const char c = value[end - 1];
if (c != ' ' && c != '\t' && c != '\r' && c != '\n') {
break;
}
--end;
}
return std::string(value.substr(begin, end - begin));
}
FxPresetDesc ParsePreset(std::string_view text) {
FxPresetDesc desc;
size_t start = 0;
while (start <= text.size()) {
size_t end = text.find('\n', start);
if (end == std::string_view::npos) {
end = text.size();
}
const std::string_view line = text.substr(start, end - start);
start = end + 1;
const size_t equals = line.find('=');
if (equals == std::string_view::npos) {
continue;
}
const std::string key = Trim(line.substr(0, equals));
const std::string value = Trim(line.substr(equals + 1));
if (key == "name") {
desc.name = value;
} else if (key == "description") {
desc.description = value;
} else if (key == "chain") {
desc.chain = value;
}
}
return desc;
}
std::string PresetFileName(std::string_view name) {
std::string out;
for (const char c : name) {
if (c == ' ') {
out += '_';
continue;
}
const bool keep = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '-' || c == '_';
if (keep) {
out += c;
}
}
if (out.empty()) {
out = "preset";
}
return out + ".fxp";
}
std::string ComposePreset(const FxPresetDesc& desc) {
std::string out;
out += "name=";
out += desc.name;
out += '\n';
out += "description=";
out += desc.description;
out += '\n';
out += "chain=";
out += desc.chain;
out += '\n';
return out;
}
} // Anonymous namespace
std::filesystem::path GetFxPresetDirectory() {
return Common::FS::GetEdenPath(Common::FS::EdenPath::PostPresetDir);
}
void ReloadFxPresetCatalog() {
preset_catalog.clear();
preset_catalog_scanned = true;
const auto root = GetFxPresetDirectory();
if (!Common::FS::Exists(root) && !Common::FS::CreateDirs(root)) {
return;
}
std::vector<std::string> bundled_names;
for (const auto& bundled : BUNDLED_FX_PRESETS) {
const auto path = root / bundled.name;
bundled_names.emplace_back(bundled.name);
if (Common::FS::Exists(path)) {
continue;
}
void(Common::FS::WriteStringToFile(path, Common::FS::FileType::TextFile, bundled.source));
}
std::vector<std::filesystem::path> preset_files;
Common::FS::IterateDirEntriesRecursively(
root,
[&](const std::filesystem::directory_entry& entry) {
if (entry.path().extension() == ".fxp") {
preset_files.push_back(entry.path());
}
return true;
},
Common::FS::DirEntryFilter::File);
std::sort(preset_files.begin(), preset_files.end());
for (const auto& file : preset_files) {
const std::string text =
Common::FS::ReadStringFromFile(file, Common::FS::FileType::TextFile);
FxPresetDesc desc = ParsePreset(text);
if (desc.name.empty() || desc.chain.empty()) {
continue;
}
desc.file = Common::FS::PathToUTF8String(file.filename());
desc.bundled = std::find(bundled_names.begin(), bundled_names.end(), desc.file) !=
bundled_names.end();
preset_catalog.push_back(std::move(desc));
}
}
const std::vector<FxPresetDesc>& GetFxPresetCatalog() {
if (!preset_catalog_scanned) {
ReloadFxPresetCatalog();
}
return preset_catalog;
}
const FxPresetDesc* FindFxPreset(std::string_view name) {
const auto& presets = GetFxPresetCatalog();
const auto it = std::find_if(presets.begin(), presets.end(),
[&](const FxPresetDesc& p) { return p.name == name; });
if (it == presets.end()) {
return nullptr;
}
return &*it;
}
bool ApplyFxPreset(std::string_view name) {
const FxPresetDesc* preset = FindFxPreset(name);
if (preset == nullptr) {
return false;
}
FxChain::Instance().SetEntries(ParseFxChain(preset->chain));
FxChain::Instance().DropUnknownEntries();
FxChain::Instance().StoreToSettings();
SetActiveFxPreset(preset->name);
return true;
}
bool SaveFxPreset(std::string_view name, std::string_view description) {
if (!IsFxPresetName(name)) {
return false;
}
const auto root = GetFxPresetDirectory();
if (!Common::FS::Exists(root) && !Common::FS::CreateDirs(root)) {
return false;
}
FxPresetDesc desc;
desc.name = std::string(name);
desc.description = std::string(description);
desc.chain = SerializeFxChain(FxChain::Instance().Entries());
const std::string body = ComposePreset(desc);
const std::string file = PresetFileName(name);
if (Common::FS::WriteStringToFile(root / file, Common::FS::FileType::TextFile, body) !=
body.size()) {
return false;
}
ReloadFxPresetCatalog();
SetActiveFxPreset(name);
return true;
}
bool DeleteFxPreset(std::string_view name) {
const FxPresetDesc* preset = FindFxPreset(name);
if (preset == nullptr || preset->bundled) {
return false;
}
const auto path = GetFxPresetDirectory() / preset->file;
if (!Common::FS::RemoveFile(path)) {
return false;
}
if (GetActiveFxPreset() == name) {
SetActiveFxPreset(std::string_view());
}
ReloadFxPresetCatalog();
return true;
}
bool IsFxPresetName(std::string_view name) {
if (name.empty()) {
return false;
}
return name.find_first_of("\r\n=") == std::string_view::npos;
}
std::string GetActiveFxPreset() {
return Settings::values.post_shader_preset.GetValue();
}
void SetActiveFxPreset(std::string_view name) {
Settings::values.post_shader_preset.SetValue(std::string(name));
}
bool IsActiveFxPresetModified() {
const std::string active = GetActiveFxPreset();
if (active.empty()) {
return false;
}
const FxPresetDesc* preset = FindFxPreset(active);
if (preset == nullptr) {
return true;
}
return SerializeFxChain(FxChain::Instance().Entries()) !=
SerializeFxChain(ParseFxChain(preset->chain));
}
} // namespace VideoCore
@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <filesystem>
#include <string>
#include <string_view>
#include <vector>
namespace VideoCore {
struct FxPresetDesc {
std::string file;
std::string name;
std::string description;
std::string chain;
bool bundled{};
};
std::filesystem::path GetFxPresetDirectory();
void ReloadFxPresetCatalog();
const std::vector<FxPresetDesc>& GetFxPresetCatalog();
const FxPresetDesc* FindFxPreset(std::string_view name);
bool ApplyFxPreset(std::string_view name);
bool SaveFxPreset(std::string_view name, std::string_view description);
bool DeleteFxPreset(std::string_view name);
bool IsFxPresetName(std::string_view name);
std::string GetActiveFxPreset();
void SetActiveFxPreset(std::string_view name);
bool IsActiveFxPresetModified();
} // namespace VideoCore
@@ -18,6 +18,10 @@
#include "video_core/renderer_vulkan/present/sgsr.h"
#include "video_core/renderer_vulkan/present/fxaa.h"
#include "video_core/renderer_vulkan/present/layer.h"
#ifdef HAS_RESHADE
#include "video_core/post_processing/fx_chain.h"
#include "video_core/renderer_vulkan/present/post_process.h"
#endif
#include "video_core/renderer_vulkan/present/present_push_constants.h"
#include "video_core/renderer_vulkan/present/smaa.h"
#include "video_core/renderer_vulkan/present/util.h"
@@ -93,6 +97,9 @@ void Layer::ConfigureDraw(const Device& device, PresentPushConstants* out_push_c
RefreshResources(device, framebuffer);
SetAntiAliasPass(device);
#ifdef HAS_RESHADE
SetPostProcessPass(device);
#endif
// Finish any pending renderpass
scheduler.RequestOutsideRenderPassOperationContext();
@@ -115,6 +122,12 @@ void Layer::ConfigureDraw(const Device& device, PresentPushConstants* out_push_c
smaa->Draw(device, scheduler, image_index, &source_image, &source_image_view);
}
#ifdef HAS_RESHADE
if (post_process.has_value()) {
post_process->Draw(device, scheduler, image_index, &source_image, &source_image_view);
}
#endif
auto crop_rect = Tegra::NormalizeCrop(framebuffer, texture_width, texture_height);
const VkExtent2D render_extent{
.width = scaled_width,
@@ -214,6 +227,43 @@ void Layer::SetAntiAliasPass(const Device& device) {
}
}
#ifdef HAS_RESHADE
void Layer::SetPostProcessPass(const Device& device) {
const VkExtent2D render_area{
.width = Settings::values.resolution_info.ScaleUp(raw_width),
.height = Settings::values.resolution_info.ScaleUp(raw_height),
};
const u64 generation = VideoCore::FxChain::Instance().Snapshot().generation;
const bool enabled = Settings::values.post_shader_enabled.GetValue();
if (post_process_generation == generation && post_process_enabled == enabled &&
post_process_extent.width == render_area.width &&
post_process_extent.height == render_area.height) {
return;
}
for (const u64 tick : resource_ticks) {
scheduler.Wait(tick);
}
post_process_generation = generation;
post_process_enabled = enabled;
post_process_extent = render_area;
post_process.reset();
if (!enabled || VideoCore::FxChain::Instance().Size() == 0) {
return;
}
post_process.emplace(device, memory_allocator, scheduler, image_count, render_area);
if (post_process->Empty()) {
post_process.reset();
}
}
#endif
void Layer::ReleaseRawImages() {
for (const u64 tick : resource_ticks) {
scheduler.Wait(tick);
@@ -15,6 +15,9 @@
#include "video_core/renderer_vulkan/present/fsr.h"
#include "video_core/renderer_vulkan/present/sgsr.h"
#include "video_core/renderer_vulkan/present/fxaa.h"
#ifdef HAS_RESHADE
#include "video_core/renderer_vulkan/present/post_process.h"
#endif
#include "video_core/renderer_vulkan/present/smaa.h"
namespace Layout {
@@ -66,6 +69,9 @@ private:
void RefreshResources(const Device& device, const Tegra::FramebufferConfig& framebuffer);
void SetAntiAliasPass(const Device& device);
#ifdef HAS_RESHADE
void SetPostProcessPass(const Device& device);
#endif
void ReleaseRawImages();
u64 CalculateBufferSize(const Tegra::FramebufferConfig& framebuffer) const;
@@ -95,6 +101,12 @@ private:
Settings::AntiAliasing anti_alias_setting{};
std::variant<std::monostate, FXAA, SMAA> anti_alias{};
std::variant<std::monostate, SGSR, FSR> sr_filter{};
#ifdef HAS_RESHADE
std::optional<PostProcessChain> post_process{};
u64 post_process_generation{};
bool post_process_enabled{};
VkExtent2D post_process_extent{};
#endif
std::vector<u64> resource_ticks{};
};
@@ -0,0 +1,899 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <cstring>
#include <random>
#include "common/fs/fs.h"
#include "common/fs/fs_util.h"
#include "common/logging.h"
#include "video_core/post_processing/fx_chain.h"
#include "video_core/post_processing/fx_compile.h"
#include "video_core/post_processing/fx_effect.h"
#include "video_core/renderer_vulkan/present/post_process.h"
#include "video_core/renderer_vulkan/present/util.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
#include "video_core/vulkan_common/vulkan_device.h"
namespace Vulkan {
namespace {
constexpr VkFormat BACKBUFFER_FORMAT = VK_FORMAT_R16G16B16A16_SFLOAT;
constexpr size_t NO_TEXTURE = ~size_t{0};
VkFormat ToVkFormat(reshadefx::texture_format format) {
switch (format) {
case reshadefx::texture_format::r8:
return VK_FORMAT_R8_UNORM;
case reshadefx::texture_format::r16f:
return VK_FORMAT_R16_SFLOAT;
case reshadefx::texture_format::r32f:
return VK_FORMAT_R32_SFLOAT;
case reshadefx::texture_format::rg8:
return VK_FORMAT_R8G8_UNORM;
case reshadefx::texture_format::rg16:
return VK_FORMAT_R16G16_UNORM;
case reshadefx::texture_format::rg16f:
return VK_FORMAT_R16G16_SFLOAT;
case reshadefx::texture_format::rg32f:
return VK_FORMAT_R32G32_SFLOAT;
case reshadefx::texture_format::rgba8:
return VK_FORMAT_R8G8B8A8_UNORM;
case reshadefx::texture_format::rgba16:
return VK_FORMAT_R16G16B16A16_UNORM;
case reshadefx::texture_format::rgba16f:
return VK_FORMAT_R16G16B16A16_SFLOAT;
case reshadefx::texture_format::rgba32f:
return VK_FORMAT_R32G32B32A32_SFLOAT;
case reshadefx::texture_format::rgb10a2:
return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
default:
return VK_FORMAT_R8G8B8A8_UNORM;
}
}
VkSamplerAddressMode ToAddressMode(reshadefx::texture_address_mode mode) {
switch (mode) {
case reshadefx::texture_address_mode::wrap:
return VK_SAMPLER_ADDRESS_MODE_REPEAT;
case reshadefx::texture_address_mode::mirror:
return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
case reshadefx::texture_address_mode::border:
return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER;
case reshadefx::texture_address_mode::clamp:
default:
return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
}
}
VkBlendFactor ToBlendFactor(reshadefx::blend_factor func) {
switch (func) {
case reshadefx::blend_factor::zero:
return VK_BLEND_FACTOR_ZERO;
case reshadefx::blend_factor::source_color:
return VK_BLEND_FACTOR_SRC_COLOR;
case reshadefx::blend_factor::source_alpha:
return VK_BLEND_FACTOR_SRC_ALPHA;
case reshadefx::blend_factor::one_minus_source_color:
return VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR;
case reshadefx::blend_factor::one_minus_source_alpha:
return VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
case reshadefx::blend_factor::dest_color:
return VK_BLEND_FACTOR_DST_COLOR;
case reshadefx::blend_factor::dest_alpha:
return VK_BLEND_FACTOR_DST_ALPHA;
case reshadefx::blend_factor::one_minus_dest_color:
return VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR;
case reshadefx::blend_factor::one_minus_dest_alpha:
return VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA;
case reshadefx::blend_factor::one:
default:
return VK_BLEND_FACTOR_ONE;
}
}
VkBlendOp ToBlendOp(reshadefx::blend_op op) {
switch (op) {
case reshadefx::blend_op::subtract:
return VK_BLEND_OP_SUBTRACT;
case reshadefx::blend_op::reverse_subtract:
return VK_BLEND_OP_REVERSE_SUBTRACT;
case reshadefx::blend_op::min:
return VK_BLEND_OP_MIN;
case reshadefx::blend_op::max:
return VK_BLEND_OP_MAX;
case reshadefx::blend_op::add:
default:
return VK_BLEND_OP_ADD;
}
}
VkPrimitiveTopology ToTopology(reshadefx::primitive_topology topology) {
switch (topology) {
case reshadefx::primitive_topology::point_list:
return VK_PRIMITIVE_TOPOLOGY_POINT_LIST;
case reshadefx::primitive_topology::line_list:
return VK_PRIMITIVE_TOPOLOGY_LINE_LIST;
case reshadefx::primitive_topology::line_strip:
return VK_PRIMITIVE_TOPOLOGY_LINE_STRIP;
case reshadefx::primitive_topology::triangle_strip:
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
case reshadefx::primitive_topology::triangle_list:
default:
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
}
}
vk::RenderPass CreateFxRenderPass(const Device& device, VkFormat format, bool clear) {
VkAttachmentLoadOp load_op = VK_ATTACHMENT_LOAD_OP_LOAD;
VkImageLayout initial_layout = VK_IMAGE_LAYOUT_GENERAL;
if (clear) {
load_op = VK_ATTACHMENT_LOAD_OP_CLEAR;
initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
}
const VkAttachmentDescription attachment{
.flags = 0,
.format = format,
.samples = VK_SAMPLE_COUNT_1_BIT,
.loadOp = load_op,
.storeOp = VK_ATTACHMENT_STORE_OP_STORE,
.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
.initialLayout = initial_layout,
.finalLayout = VK_IMAGE_LAYOUT_GENERAL,
};
const VkAttachmentReference reference{
.attachment = 0,
.layout = VK_IMAGE_LAYOUT_GENERAL,
};
const VkSubpassDescription subpass{
.flags = 0,
.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
.inputAttachmentCount = 0,
.pInputAttachments = nullptr,
.colorAttachmentCount = 1,
.pColorAttachments = &reference,
.pResolveAttachments = nullptr,
.pDepthStencilAttachment = nullptr,
.preserveAttachmentCount = 0,
.pPreserveAttachments = nullptr,
};
return device.GetLogical().CreateRenderPass(VkRenderPassCreateInfo{
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.attachmentCount = 1,
.pAttachments = &attachment,
.subpassCount = 1,
.pSubpasses = &subpass,
.dependencyCount = 0,
.pDependencies = nullptr,
});
}
vk::Pipeline CreateFxPipeline(const Device& device, vk::RenderPass& renderpass,
vk::PipelineLayout& layout, VkShaderModule vertex_shader,
VkShaderModule fragment_shader,
const reshadefx::pass& pass) {
const std::array<VkPipelineShaderStageCreateInfo, 2> stages{{
{
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.stage = VK_SHADER_STAGE_VERTEX_BIT,
.module = vertex_shader,
.pName = pass.vs_entry_point.c_str(),
.pSpecializationInfo = nullptr,
},
{
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.stage = VK_SHADER_STAGE_FRAGMENT_BIT,
.module = fragment_shader,
.pName = pass.ps_entry_point.c_str(),
.pSpecializationInfo = nullptr,
},
}};
constexpr VkPipelineVertexInputStateCreateInfo vertex_input{
.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.vertexBindingDescriptionCount = 0,
.pVertexBindingDescriptions = nullptr,
.vertexAttributeDescriptionCount = 0,
.pVertexAttributeDescriptions = nullptr,
};
const VkPipelineInputAssemblyStateCreateInfo input_assembly{
.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.topology = ToTopology(pass.topology),
.primitiveRestartEnable = VK_FALSE,
};
constexpr VkPipelineViewportStateCreateInfo viewport_state{
.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.viewportCount = 1,
.pViewports = nullptr,
.scissorCount = 1,
.pScissors = nullptr,
};
constexpr VkPipelineRasterizationStateCreateInfo rasterization{
.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.depthClampEnable = VK_FALSE,
.rasterizerDiscardEnable = VK_FALSE,
.polygonMode = VK_POLYGON_MODE_FILL,
.cullMode = VK_CULL_MODE_NONE,
.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE,
.depthBiasEnable = VK_FALSE,
.depthBiasConstantFactor = 0.0f,
.depthBiasClamp = 0.0f,
.depthBiasSlopeFactor = 0.0f,
.lineWidth = 1.0f,
};
constexpr VkPipelineMultisampleStateCreateInfo multisampling{
.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT,
.sampleShadingEnable = VK_FALSE,
.minSampleShading = 0.0f,
.pSampleMask = nullptr,
.alphaToCoverageEnable = VK_FALSE,
.alphaToOneEnable = VK_FALSE,
};
VkBool32 blend_enable = VK_FALSE;
if (pass.blend_enable[0]) {
blend_enable = VK_TRUE;
}
const VkPipelineColorBlendAttachmentState blending{
.blendEnable = blend_enable,
.srcColorBlendFactor = ToBlendFactor(pass.source_color_blend_factor[0]),
.dstColorBlendFactor = ToBlendFactor(pass.dest_color_blend_factor[0]),
.colorBlendOp = ToBlendOp(pass.color_blend_op[0]),
.srcAlphaBlendFactor = ToBlendFactor(pass.source_alpha_blend_factor[0]),
.dstAlphaBlendFactor = ToBlendFactor(pass.dest_alpha_blend_factor[0]),
.alphaBlendOp = ToBlendOp(pass.alpha_blend_op[0]),
.colorWriteMask = static_cast<VkColorComponentFlags>(pass.render_target_write_mask[0] & 0xF),
};
const VkPipelineColorBlendStateCreateInfo color_blend{
.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.logicOpEnable = VK_FALSE,
.logicOp = VK_LOGIC_OP_COPY,
.attachmentCount = 1,
.pAttachments = &blending,
.blendConstants = {0.0f, 0.0f, 0.0f, 0.0f},
};
constexpr std::array dynamic_states{
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR,
};
const VkPipelineDynamicStateCreateInfo dynamic_state{
.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.dynamicStateCount = static_cast<u32>(dynamic_states.size()),
.pDynamicStates = dynamic_states.data(),
};
return device.GetLogical().CreateGraphicsPipeline(VkGraphicsPipelineCreateInfo{
.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.stageCount = static_cast<u32>(stages.size()),
.pStages = stages.data(),
.pVertexInputState = &vertex_input,
.pInputAssemblyState = &input_assembly,
.pTessellationState = nullptr,
.pViewportState = &viewport_state,
.pRasterizationState = &rasterization,
.pMultisampleState = &multisampling,
.pDepthStencilState = nullptr,
.pColorBlendState = &color_blend,
.pDynamicState = &dynamic_state,
.layout = *layout,
.renderPass = *renderpass,
.subpass = 0,
.basePipelineHandle = nullptr,
.basePipelineIndex = 0,
});
}
} // Anonymous namespace
PostProcessChain::PostProcessChain(const Device& device, MemoryAllocator& allocator,
Scheduler& scheduler, size_t image_count, VkExtent2D extent)
: m_extent(extent)
, m_image_count(u32(image_count))
{
m_start = std::chrono::steady_clock::now();
m_previous = m_start;
CreatePingPongImages(device, allocator);
m_fallback_sampler = CreateWrappedSampler(device);
m_fallback_image = CreateWrappedImage(allocator, VkExtent2D{1, 1}, VK_FORMAT_R8G8B8A8_UNORM);
m_fallback_view = CreateWrappedImageView(device, m_fallback_image, VK_FORMAT_R8G8B8A8_UNORM);
if (!BuildEffects(device, allocator, scheduler)) {
m_effects.clear();
}
}
PostProcessChain::~PostProcessChain() = default;
bool PostProcessChain::Empty() const {
return m_effects.empty();
}
void PostProcessChain::CreatePingPongImages(const Device& device, MemoryAllocator& allocator) {
m_frames.resize(m_image_count);
for (auto& frame : m_frames) {
for (size_t i = 0; i < frame.images.size(); ++i) {
frame.images[i] = CreateWrappedImage(allocator, m_extent, BACKBUFFER_FORMAT);
frame.views[i] = CreateWrappedImageView(device, frame.images[i], BACKBUFFER_FORMAT);
}
}
}
bool PostProcessChain::BuildEffects(const Device& device, MemoryAllocator& allocator,
Scheduler& scheduler) {
const auto snapshot = VideoCore::FxChain::Instance().Snapshot();
if (snapshot.entries.empty()) {
return true;
}
const auto root = VideoCore::GetFxRootDirectory();
for (size_t entry_index = 0; entry_index < snapshot.entries.size(); ++entry_index) {
const auto& entry = snapshot.entries[entry_index];
const auto path = root / entry.file;
const auto compiled = VideoCore::CompileFxEffect(path, m_extent.width, m_extent.height, 8);
if (!compiled.Succeeded()) {
LOG_ERROR(Render_Vulkan, "Post-processing effect '{}' failed to compile:\n{}",
entry.file, compiled.error);
continue;
}
const auto& module = compiled.module;
const auto technique = std::find_if(
module.techniques.begin(), module.techniques.end(),
[&](const reshadefx::technique& t) { return t.name == entry.technique; });
if (technique == module.techniques.end()) {
LOG_ERROR(Render_Vulkan, "Effect '{}' has no technique '{}'", entry.file,
entry.technique);
continue;
}
Effect effect;
effect.entry_index = entry_index;
effect.file = entry.file;
effect.uniform_size = module.total_uniform_size;
for (const auto& [name, words] : compiled.entry_points) {
effect.shaders.emplace(name, CreateWrappedShaderModule(device, words));
}
for (const auto& texture : module.textures) {
Texture out;
out.name = texture.unique_name;
out.extent = VkExtent2D{texture.width, texture.height};
out.format = ToVkFormat(texture.format);
if (texture.semantic == "COLOR") {
out.is_backbuffer = true;
effect.textures.push_back(std::move(out));
continue;
}
if (texture.semantic == "DEPTH") {
effect.textures.push_back(std::move(out));
continue;
}
out.image = CreateWrappedImage(allocator, out.extent, out.format);
out.view = CreateWrappedImageView(device, out.image, out.format);
effect.textures.push_back(std::move(out));
}
for (const auto& sampler : module.samplers) {
Sampler out;
out.texture_index = NO_TEXTURE;
for (size_t i = 0; i < effect.textures.size(); ++i) {
if (effect.textures[i].name == sampler.texture_name) {
out.texture_index = i;
break;
}
}
VkFilter mag_filter = VK_FILTER_LINEAR;
VkFilter min_filter = VK_FILTER_LINEAR;
VkSamplerMipmapMode mip_mode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
const u32 filter = static_cast<u32>(sampler.filter);
if ((filter & 0x10) == 0) {
min_filter = VK_FILTER_NEAREST;
}
if ((filter & 0x04) == 0) {
mag_filter = VK_FILTER_NEAREST;
}
if ((filter & 0x01) == 0) {
mip_mode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
}
out.sampler = device.GetLogical().CreateSampler(VkSamplerCreateInfo{
.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.magFilter = mag_filter,
.minFilter = min_filter,
.mipmapMode = mip_mode,
.addressModeU = ToAddressMode(sampler.address_u),
.addressModeV = ToAddressMode(sampler.address_v),
.addressModeW = ToAddressMode(sampler.address_w),
.mipLodBias = sampler.lod_bias,
.anisotropyEnable = VK_FALSE,
.maxAnisotropy = 1.0f,
.compareEnable = VK_FALSE,
.compareOp = VK_COMPARE_OP_NEVER,
.minLod = sampler.min_lod,
.maxLod = sampler.max_lod,
.borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK,
.unnormalizedCoordinates = VK_FALSE,
});
effect.samplers.push_back(std::move(out));
}
for (const auto& uniform : module.uniforms) {
UniformWrite write;
write.name = uniform.name;
write.offset = uniform.offset;
write.components = std::min<u32>(uniform.type.components(), 4);
write.kind = UniformKind::Floating;
if (uniform.type.is_boolean()) {
write.kind = UniformKind::Boolean;
} else if (uniform.type.is_integral()) {
write.kind = UniformKind::Integer;
}
for (const auto& annotation : uniform.annotations) {
if (annotation.name != "source") {
continue;
}
const std::string& source = annotation.value.string_data;
if (source == "frametime") {
write.source = UniformSource::FrameTime;
} else if (source == "framecount") {
write.source = UniformSource::FrameCount;
} else if (source == "timer") {
write.source = UniformSource::Timer;
} else if (source == "random") {
write.source = UniformSource::Random;
} else if (source == "pingpong") {
write.source = UniformSource::PingPong;
}
}
if (uniform.has_initializer_value) {
for (u32 i = 0; i < write.components; ++i) {
if (write.kind == UniformKind::Floating) {
write.fallback[i] = uniform.initializer_value.as_float[i];
} else {
write.fallback[i] = static_cast<f32>(uniform.initializer_value.as_int[i]);
}
}
}
write.args = {0.0f, 1.0f, 1.0f, 0.0f};
for (const auto& annotation : uniform.annotations) {
if (annotation.name == "min" && annotation.type.is_floating_point()) {
write.args[0] = annotation.value.as_float[0];
}
if (annotation.name == "max" && annotation.type.is_floating_point()) {
write.args[1] = annotation.value.as_float[0];
}
if (annotation.name == "step" && annotation.type.is_floating_point()) {
write.args[2] = annotation.value.as_float[0];
}
}
write.state = write.args[0];
effect.uniforms.push_back(std::move(write));
}
effect.uniform_layout = CreateWrappedDescriptorSetLayout(
device, std::array{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER},
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT);
for (const auto& pass : technique->passes) {
Pass out;
out.num_vertices = pass.num_vertices;
out.clear = pass.clear_render_targets != 0;
out.target_texture = NO_TEXTURE;
out.extent = m_extent;
const std::string& target = pass.render_target_names[0];
if (target.empty()) {
out.writes_backbuffer = true;
} else {
for (size_t i = 0; i < effect.textures.size(); ++i) {
if (effect.textures[i].name == target) {
out.target_texture = i;
out.extent = effect.textures[i].extent;
break;
}
}
if (out.target_texture == NO_TEXTURE) {
LOG_WARNING(Render_Vulkan, "Effect '{}' pass targets unknown texture '{}'",
entry.file, target);
out.writes_backbuffer = true;
}
}
if (pass.viewport_width != 0 && pass.viewport_height != 0) {
out.extent = VkExtent2D{pass.viewport_width, pass.viewport_height};
}
VkFormat target_format = BACKBUFFER_FORMAT;
if (!out.writes_backbuffer) {
target_format = effect.textures[out.target_texture].format;
}
u32 binding_count = 0;
for (const auto& binding : pass.sampler_bindings) {
SamplerBinding entry_binding;
entry_binding.binding = binding.entry_point_binding;
entry_binding.sampler_index = binding.index;
out.sampler_bindings.push_back(entry_binding);
binding_count = std::max<u32>(binding_count, binding.entry_point_binding + 1);
}
const std::vector<VkDescriptorType> sampler_types(
std::max<size_t>(binding_count, 1), VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
out.sampler_layout = CreateWrappedDescriptorSetLayout(
device, sampler_types, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT);
const std::array set_layouts{*effect.uniform_layout, *out.sampler_layout};
out.pipeline_layout =
device.GetLogical().CreatePipelineLayout(VkPipelineLayoutCreateInfo{
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.setLayoutCount = static_cast<u32>(set_layouts.size()),
.pSetLayouts = set_layouts.data(),
.pushConstantRangeCount = 0,
.pPushConstantRanges = nullptr,
});
const auto vertex_shader = effect.shaders.find(pass.vs_entry_point);
const auto fragment_shader = effect.shaders.find(pass.ps_entry_point);
if (vertex_shader == effect.shaders.end() ||
fragment_shader == effect.shaders.end()) {
LOG_WARNING(Render_Vulkan, "Effect '{}' pass references a missing entry point",
entry.file);
continue;
}
out.renderpass = CreateFxRenderPass(device, target_format, out.clear);
out.pipeline = CreateFxPipeline(device, out.renderpass, out.pipeline_layout,
*vertex_shader->second, *fragment_shader->second, pass);
if (out.writes_backbuffer) {
out.backbuffer_slot = static_cast<u32>(effect.backbuffer_pass_count % 2);
++effect.backbuffer_pass_count;
for (u32 image = 0; image < m_image_count; ++image) {
for (size_t slot = 0; slot < 2; ++slot) {
out.framebuffers.push_back(CreateWrappedFramebuffer(
device, out.renderpass, m_frames[image].views[slot], out.extent));
}
}
} else {
out.framebuffers.push_back(
CreateWrappedFramebuffer(device, out.renderpass,
effect.textures[out.target_texture].view, out.extent));
}
effect.passes.push_back(std::move(out));
}
if (effect.passes.empty()) {
LOG_WARNING(Render_Vulkan, "Effect '{}' technique '{}' has no passes", entry.file,
entry.technique);
continue;
}
const u32 buffer_size = std::max<u32>(effect.uniform_size, 4);
for (u32 i = 0; i < m_image_count; ++i) {
effect.uniform_buffers.push_back(
CreateWrappedBuffer(allocator, buffer_size, MemoryUsage::Upload));
}
size_t sampler_descriptor_count = 0;
size_t sampler_set_count = 0;
for (const auto& pass : effect.passes) {
sampler_descriptor_count +=
m_image_count * std::max<size_t>(pass.sampler_bindings.size(), 1);
sampler_set_count += m_image_count;
}
effect.descriptor_pool = CreateWrappedDescriptorPool(
device, m_image_count + sampler_descriptor_count, m_image_count + sampler_set_count,
{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER});
const std::vector<VkDescriptorSetLayout> uniform_layouts(m_image_count,
*effect.uniform_layout);
effect.uniform_sets = CreateWrappedDescriptorSets(effect.descriptor_pool, uniform_layouts);
for (auto& pass : effect.passes) {
const std::vector<VkDescriptorSetLayout> layouts(m_image_count, *pass.sampler_layout);
pass.sampler_sets = CreateWrappedDescriptorSets(effect.descriptor_pool, layouts);
}
m_effects.push_back(std::move(effect));
}
return true;
}
void PostProcessChain::PrepareImages(const Device& device, Scheduler& scheduler) {
if (m_images_ready) {
return;
}
scheduler.Record([this](vk::CommandBuffer cmdbuf) {
ClearColorImage(cmdbuf, *m_fallback_image);
for (auto& frame : m_frames) {
for (auto& image : frame.images) {
ClearColorImage(cmdbuf, *image);
}
}
for (auto& effect : m_effects) {
for (auto& texture : effect.textures) {
if (texture.image) {
ClearColorImage(cmdbuf, *texture.image);
}
}
}
});
scheduler.Finish();
m_images_ready = true;
}
void PostProcessChain::UpdateUniforms(Effect& effect, size_t image_index, f32 delta_seconds) {
if (effect.uniform_size == 0) {
return;
}
static thread_local std::mt19937 rng{std::random_device{}()};
std::vector<u8> staging(effect.uniform_size, 0);
const f32 elapsed =
std::chrono::duration<f32>(std::chrono::steady_clock::now() - m_start).count();
const auto overrides = VideoCore::FxChain::Instance().EntryValues(effect.entry_index);
for (auto& uniform : effect.uniforms) {
std::array<f32, 4> value = uniform.fallback;
const auto override = overrides.find(uniform.name);
if (override != overrides.end()) {
value = override->second;
}
switch (uniform.source) {
case UniformSource::FrameTime:
value[0] = delta_seconds * 1000.0f;
break;
case UniformSource::FrameCount:
value[0] = static_cast<f32>(m_frame_count);
break;
case UniformSource::Timer:
value[0] = elapsed * 1000.0f;
break;
case UniformSource::Random: {
const int low = static_cast<int>(uniform.args[0]);
int high = static_cast<int>(uniform.args[1]);
if (high <= low) {
high = low + 1;
}
std::uniform_int_distribution<int> dist(low, high);
value[0] = static_cast<f32>(dist(rng));
break;
}
case UniformSource::PingPong: {
const f32 min_value = uniform.args[0];
f32 max_value = uniform.args[1];
if (max_value <= min_value) {
max_value = min_value + 1.0f;
}
f32 step = uniform.args[2];
if (step == 0.0f) {
step = 1.0f;
}
uniform.state += uniform.direction * step * delta_seconds;
if (uniform.state >= max_value) {
uniform.state = max_value;
uniform.direction = -1.0f;
}
if (uniform.state <= min_value) {
uniform.state = min_value;
uniform.direction = 1.0f;
}
value[0] = uniform.state;
value[1] = uniform.direction;
break;
}
case UniformSource::Value:
default:
break;
}
for (u32 i = 0; i < uniform.components; ++i) {
const size_t offset = uniform.offset + i * sizeof(u32);
if (offset + sizeof(u32) > staging.size()) {
break;
}
if (uniform.kind == UniformKind::Floating) {
const f32 element = value[i];
std::memcpy(staging.data() + offset, &element, sizeof(f32));
} else {
const s32 element = static_cast<s32>(value[i]);
std::memcpy(staging.data() + offset, &element, sizeof(s32));
}
}
}
const std::span<u8> mapped = effect.uniform_buffers[image_index].Mapped();
if (mapped.size() >= staging.size()) {
std::memcpy(mapped.data(), staging.data(), staging.size());
effect.uniform_buffers[image_index].Flush();
}
}
void PostProcessChain::UpdateDescriptors(const Device& device, Effect& effect, Pass& pass,
size_t image_index, VkImageView backbuffer_view) {
std::vector<VkDescriptorImageInfo> image_infos;
std::vector<VkWriteDescriptorSet> writes;
image_infos.reserve(pass.sampler_bindings.size() + 1);
const VkDescriptorSet sampler_set = pass.sampler_sets[image_index];
for (const auto& binding : pass.sampler_bindings) {
VkImageView view = *m_fallback_view;
VkSampler handle = *m_fallback_sampler;
if (binding.sampler_index < effect.samplers.size()) {
const Sampler& sampler = effect.samplers[binding.sampler_index];
if (sampler.sampler) {
handle = *sampler.sampler;
}
if (sampler.texture_index != NO_TEXTURE) {
const Texture& texture = effect.textures[sampler.texture_index];
if (texture.is_backbuffer) {
view = backbuffer_view;
} else if (texture.view) {
view = *texture.view;
}
}
}
writes.push_back(
CreateWriteDescriptorSet(image_infos, handle, view, sampler_set, binding.binding));
}
const VkDescriptorBufferInfo buffer_info{
.buffer = *effect.uniform_buffers[image_index],
.offset = 0,
.range = VK_WHOLE_SIZE,
};
writes.push_back(VkWriteDescriptorSet{
.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
.pNext = nullptr,
.dstSet = effect.uniform_sets[image_index],
.dstBinding = 0,
.dstArrayElement = 0,
.descriptorCount = 1,
.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
.pImageInfo = nullptr,
.pBufferInfo = &buffer_info,
.pTexelBufferView = nullptr,
});
device.GetLogical().UpdateDescriptorSets(writes, {});
}
void PostProcessChain::Draw(const Device& device, Scheduler& scheduler, size_t image_index,
VkImage* inout_image, VkImageView* inout_image_view) {
if (m_effects.empty()) {
return;
}
PrepareImages(device, scheduler);
const auto now = std::chrono::steady_clock::now();
const f32 delta_seconds = std::chrono::duration<f32>(now - m_previous).count();
m_previous = now;
++m_frame_count;
FrameImages& frame = m_frames[image_index];
VkImage current_image = *inout_image;
VkImageView current_view = *inout_image_view;
u32 slot = 0;
for (auto& effect : m_effects) {
UpdateUniforms(effect, image_index, delta_seconds);
for (size_t pass_index = 0; pass_index < effect.passes.size(); ++pass_index) {
Pass& pass = effect.passes[pass_index];
UpdateDescriptors(device, effect, pass, image_index, current_view);
VkFramebuffer framebuffer{};
VkImage target_image{};
if (pass.writes_backbuffer) {
const u32 target_slot = (slot + 1) % 2;
framebuffer = *pass.framebuffers[image_index * 2 + target_slot];
target_image = *frame.images[target_slot];
} else {
framebuffer = *pass.framebuffers[0];
target_image = *effect.textures[pass.target_texture].image;
}
const VkImage source_image = current_image;
const VkRenderPass renderpass = *pass.renderpass;
const VkPipeline pipeline = *pass.pipeline;
const VkPipelineLayout layout = *pass.pipeline_layout;
const VkDescriptorSet uniform_set = effect.uniform_sets[image_index];
const VkDescriptorSet sampler_set = pass.sampler_sets[image_index];
const VkExtent2D extent = pass.extent;
const u32 vertices = pass.num_vertices;
scheduler.RequestOutsideRenderPassOperationContext();
scheduler.Record([=](vk::CommandBuffer cmdbuf) {
TransitionImageLayout(cmdbuf, source_image, VK_IMAGE_LAYOUT_GENERAL);
TransitionImageLayout(cmdbuf, target_image, VK_IMAGE_LAYOUT_GENERAL);
BeginRenderPass(cmdbuf, renderpass, framebuffer, extent);
cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0,
std::array{uniform_set, sampler_set}, {});
cmdbuf.Draw(vertices, 1, 0, 0);
cmdbuf.EndRenderPass();
TransitionImageLayout(cmdbuf, target_image, VK_IMAGE_LAYOUT_GENERAL);
});
if (pass.writes_backbuffer) {
slot = (slot + 1) % 2;
current_image = *frame.images[slot];
current_view = *frame.views[slot];
}
}
}
*inout_image = current_image;
*inout_image_view = current_view;
}
} // namespace Vulkan
@@ -0,0 +1,140 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <array>
#include <chrono>
#include <map>
#include <string>
#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 Scheduler;
class PostProcessChain {
public:
explicit PostProcessChain(const Device& device, MemoryAllocator& allocator, Scheduler& scheduler,
size_t image_count, VkExtent2D extent);
~PostProcessChain();
void Draw(const Device& device, Scheduler& scheduler, size_t image_index, VkImage* inout_image,
VkImageView* inout_image_view);
bool Empty() const;
private:
enum class UniformKind : u32 {
Boolean,
Integer,
Floating,
};
enum class UniformSource : u32 {
Value,
FrameTime,
FrameCount,
Timer,
Random,
PingPong,
};
struct UniformWrite {
std::string name;
u32 offset{};
u32 components{};
UniformKind kind{UniformKind::Floating};
UniformSource source{UniformSource::Value};
std::array<f32, 4> fallback{};
std::array<f32, 4> args{};
f32 state{};
f32 direction{1.0f};
};
struct Texture {
std::string name;
vk::Image image{};
vk::ImageView view{};
VkExtent2D extent{};
VkFormat format{};
bool is_backbuffer{};
};
struct Sampler {
vk::Sampler sampler{};
size_t texture_index{};
};
struct SamplerBinding {
u32 binding{};
size_t sampler_index{};
};
struct Pass {
vk::RenderPass renderpass{};
vk::Pipeline pipeline{};
vk::DescriptorSetLayout sampler_layout{};
vk::PipelineLayout pipeline_layout{};
vk::DescriptorSets sampler_sets{};
std::vector<SamplerBinding> sampler_bindings{};
std::vector<vk::Framebuffer> framebuffers{};
size_t target_texture{};
VkExtent2D extent{};
u32 num_vertices{3};
bool clear{};
bool writes_backbuffer{};
u32 backbuffer_slot{};
};
struct Effect {
size_t entry_index{};
std::string file{};
std::map<std::string, vk::ShaderModule> shaders{};
std::vector<Texture> textures{};
std::vector<Sampler> samplers{};
std::vector<Pass> passes{};
std::vector<UniformWrite> uniforms{};
u32 uniform_size{};
std::vector<vk::Buffer> uniform_buffers{};
vk::DescriptorSetLayout uniform_layout{};
vk::DescriptorPool descriptor_pool{};
vk::DescriptorSets uniform_sets{};
size_t backbuffer_pass_count{};
u32 backbuffer_slots{1};
};
struct FrameImages {
std::array<vk::Image, 2> images{};
std::array<vk::ImageView, 2> views{};
};
bool BuildEffects(const Device& device, MemoryAllocator& allocator, Scheduler& scheduler);
void CreatePingPongImages(const Device& device, MemoryAllocator& allocator);
void PrepareImages(const Device& device, Scheduler& scheduler);
void UpdateUniforms(Effect& effect, size_t image_index, f32 delta_seconds);
void UpdateDescriptors(const Device& device, Effect& effect, Pass& pass, size_t image_index,
VkImageView backbuffer_view);
const VkExtent2D m_extent;
const u32 m_image_count;
std::vector<Effect> m_effects{};
std::vector<FrameImages> m_frames{};
vk::Sampler m_fallback_sampler{};
vk::Image m_fallback_image{};
vk::ImageView m_fallback_view{};
std::chrono::steady_clock::time_point m_start{};
std::chrono::steady_clock::time_point m_previous{};
u64 m_frame_count{};
bool m_images_ready{};
};
} // namespace Vulkan
@@ -24,6 +24,9 @@
#include "video_core/gpu.h"
#include "video_core/present.h"
#include "video_core/renderer_vulkan/present/util.h"
#ifdef HAS_RESHADE
#include "video_core/post_processing/fx_chain.h"
#endif
#include "video_core/renderer_vulkan/renderer_vulkan.h"
#include "video_core/renderer_vulkan/vk_blit_screen.h"
#include "video_core/renderer_vulkan/vk_rasterizer.h"
@@ -183,6 +186,10 @@ try
scheduler.RegisterOnSubmit([this] { turbo_mode->QueueSubmitted(); });
}
#ifdef HAS_RESHADE
VideoCore::FxChain::Instance().LoadFromSettings();
#endif
Report();
} catch (const vk::Exception& exception) {
LOG_ERROR(Render_Vulkan, "Vulkan initialization failed with error: {}", exception.what());