From ed566919f468e45ecb56e4e61185d98142a2cd1c Mon Sep 17 00:00:00 2001 From: CamilleLaVey Date: Thu, 10 Sep 2026 15:14:02 +0200 Subject: [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 Reviewed-by: Maufeat --- src/video_core/post_processing/fx_chain.cpp | 321 ++++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 src/video_core/post_processing/fx_chain.cpp (limited to 'src/video_core/post_processing/fx_chain.cpp') diff --git a/src/video_core/post_processing/fx_chain.cpp b/src/video_core/post_processing/fx_chain.cpp new file mode 100644 index 0000000000..882fdaf726 --- /dev/null +++ b/src/video_core/post_processing/fx_chain.cpp @@ -0,0 +1,321 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +#include + +#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 Split(std::string_view value, char separator) { + std::vector 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 ParseFxChain(std::string_view value) { + std::vector 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 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 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 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(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(index) + delta; + if (target < 0 || target >= static_cast(entries.size())) { + return; + } + std::swap(entries[index], entries[static_cast(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 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& value) { + std::scoped_lock lock{mutex}; + if (index >= entries.size()) { + return; + } + entries[index].values[std::string(uniform)] = value; +} + +std::array 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> 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 -- cgit v1.2.3