diff options
Diffstat (limited to 'src/video_core/post_processing')
| -rw-r--r-- | src/video_core/post_processing/GenerateBundledEffects.cmake | 50 | ||||
| -rw-r--r-- | src/video_core/post_processing/GenerateBundledPresets.cmake | 50 | ||||
| -rw-r--r-- | src/video_core/post_processing/fx_chain.cpp | 321 | ||||
| -rw-r--r-- | src/video_core/post_processing/fx_chain.h | 86 | ||||
| -rw-r--r-- | src/video_core/post_processing/fx_compile.cpp | 105 | ||||
| -rw-r--r-- | src/video_core/post_processing/fx_compile.h | 29 | ||||
| -rw-r--r-- | src/video_core/post_processing/fx_effect.cpp | 307 | ||||
| -rw-r--r-- | src/video_core/post_processing/fx_effect.h | 77 | ||||
| -rw-r--r-- | src/video_core/post_processing/fx_preset.cpp | 261 | ||||
| -rw-r--r-- | src/video_core/post_processing/fx_preset.h | 43 |
10 files changed, 1329 insertions, 0 deletions
diff --git a/src/video_core/post_processing/GenerateBundledEffects.cmake b/src/video_core/post_processing/GenerateBundledEffects.cmake new file mode 100644 index 0000000000..d3aca02571 --- /dev/null +++ b/src/video_core/post_processing/GenerateBundledEffects.cmake @@ -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 +") diff --git a/src/video_core/post_processing/GenerateBundledPresets.cmake b/src/video_core/post_processing/GenerateBundledPresets.cmake new file mode 100644 index 0000000000..369a46748d --- /dev/null +++ b/src/video_core/post_processing/GenerateBundledPresets.cmake @@ -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 +") 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 diff --git a/src/video_core/post_processing/fx_chain.h b/src/video_core/post_processing/fx_chain.h new file mode 100644 index 0000000000..066f7e428d --- /dev/null +++ b/src/video_core/post_processing/fx_chain.h @@ -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 diff --git a/src/video_core/post_processing/fx_compile.cpp b/src/video_core/post_processing/fx_compile.cpp new file mode 100644 index 0000000000..5ee11e9187 --- /dev/null +++ b/src/video_core/post_processing/fx_compile.cpp @@ -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 diff --git a/src/video_core/post_processing/fx_compile.h b/src/video_core/post_processing/fx_compile.h new file mode 100644 index 0000000000..f4b70f3a2c --- /dev/null +++ b/src/video_core/post_processing/fx_compile.h @@ -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 diff --git a/src/video_core/post_processing/fx_effect.cpp b/src/video_core/post_processing/fx_effect.cpp new file mode 100644 index 0000000000..7bb36b0467 --- /dev/null +++ b/src/video_core/post_processing/fx_effect.cpp @@ -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 diff --git a/src/video_core/post_processing/fx_effect.h b/src/video_core/post_processing/fx_effect.h new file mode 100644 index 0000000000..bedf68809a --- /dev/null +++ b/src/video_core/post_processing/fx_effect.h @@ -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 diff --git a/src/video_core/post_processing/fx_preset.cpp b/src/video_core/post_processing/fx_preset.cpp new file mode 100644 index 0000000000..bd66a7d807 --- /dev/null +++ b/src/video_core/post_processing/fx_preset.cpp @@ -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 diff --git a/src/video_core/post_processing/fx_preset.h b/src/video_core/post_processing/fx_preset.h new file mode 100644 index 0000000000..23372b9495 --- /dev/null +++ b/src/video_core/post_processing/fx_preset.h @@ -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 |
