// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later #include #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 catalog; bool catalog_scanned = false; const reshadefx::annotation* FindAnnotation(const std::vector& 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& 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& 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(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 SplitItems(const std::string& items) { std::vector 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(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(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 GetFxIncludePaths(const std::filesystem::path& effect_path) { const auto root = GetFxRootDirectory(); std::vector 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 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& 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