aboutsummaryrefslogtreecommitdiff
path: root/src/video_core/post_processing/fx_chain.cpp
diff options
context:
space:
mode:
authorCamilleLaVey <camillelavey99@gmail.com>2026-09-10 15:14:02 +0200
committercrueter <crueter@eden-emu.dev>2026-09-10 15:14:02 +0200
commited566919f468e45ecb56e4e61185d98142a2cd1c (patch)
tree5e9a1756c2b3fe99c9beabea0144a0e88e227bcb /src/video_core/post_processing/fx_chain.cpp
parent5f142c7926d0c7fcbbd0ce30794d72f638a43b2a (diff)
[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>
Diffstat (limited to 'src/video_core/post_processing/fx_chain.cpp')
-rw-r--r--src/video_core/post_processing/fx_chain.cpp321
1 files changed, 321 insertions, 0 deletions
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 <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