diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props
index b164c6dda1..d8b446bbc2 100644
--- a/Source/Core/DolphinLib.props
+++ b/Source/Core/DolphinLib.props
@@ -631,6 +631,11 @@
+
+
+
+
+
@@ -1210,6 +1215,11 @@
+
+
+
+
+
diff --git a/Source/Core/VideoCommon/CMakeLists.txt b/Source/Core/VideoCommon/CMakeLists.txt
index 51e51e672d..0b79a77519 100644
--- a/Source/Core/VideoCommon/CMakeLists.txt
+++ b/Source/Core/VideoCommon/CMakeLists.txt
@@ -39,6 +39,16 @@ add_library(videocommon
GeometryShaderGen.h
GeometryShaderManager.cpp
GeometryShaderManager.h
+ GraphicsModSystem/Config/GraphicsMod.cpp
+ GraphicsModSystem/Config/GraphicsMod.h
+ GraphicsModSystem/Config/GraphicsModFeature.cpp
+ GraphicsModSystem/Config/GraphicsModFeature.h
+ GraphicsModSystem/Config/GraphicsModGroup.cpp
+ GraphicsModSystem/Config/GraphicsModGroup.h
+ GraphicsModSystem/Config/GraphicsTarget.cpp
+ GraphicsModSystem/Config/GraphicsTarget.h
+ GraphicsModSystem/Config/GraphicsTargetGroup.cpp
+ GraphicsModSystem/Config/GraphicsTargetGroup.h
GraphicsModSystem/Constants.h
HiresTextures.cpp
HiresTextures.h
diff --git a/Source/Core/VideoCommon/GraphicsModSystem/Config/GraphicsMod.cpp b/Source/Core/VideoCommon/GraphicsModSystem/Config/GraphicsMod.cpp
new file mode 100644
index 0000000000..c7dc5aab87
--- /dev/null
+++ b/Source/Core/VideoCommon/GraphicsModSystem/Config/GraphicsMod.cpp
@@ -0,0 +1,291 @@
+// Copyright 2022 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "VideoCommon/GraphicsModSystem/Config/GraphicsMod.h"
+
+#include
+
+#include "Common/CommonPaths.h"
+#include "Common/FileUtil.h"
+#include "Common/Logging/Log.h"
+#include "Common/StringUtil.h"
+
+#include "VideoCommon/GraphicsModSystem/Constants.h"
+
+std::optional GraphicsModConfig::Create(const std::string& file_path,
+ Source source)
+{
+ std::string json_data;
+ if (!File::ReadFileToString(file_path, json_data))
+ {
+ ERROR_LOG_FMT(VIDEO, "Failed to load graphics mod json file '{}'", file_path);
+ return std::nullopt;
+ }
+
+ picojson::value root;
+ const auto error = picojson::parse(root, json_data);
+
+ if (!error.empty())
+ {
+ ERROR_LOG_FMT(VIDEO, "Failed to load graphics mod json file '{}' due to parse error: {}",
+ file_path, error);
+ return std::nullopt;
+ }
+
+ GraphicsModConfig result;
+ if (!result.DeserializeFromConfig(root))
+ {
+ return std::nullopt;
+ }
+ result.m_source = source;
+ if (source == Source::User)
+ {
+ const std::string base_path = File::GetUserPath(D_GRAPHICSMOD_IDX);
+ if (base_path.size() > file_path.size())
+ {
+ ERROR_LOG_FMT(
+ VIDEO,
+ "Failed to load graphics mod json file '{}' due to it not matching the base path: {}",
+ file_path, base_path);
+ return std::nullopt;
+ }
+ result.m_relative_path = file_path.substr(base_path.size());
+ }
+ else
+ {
+ const std::string base_path = File::GetSysDirectory() + DOLPHIN_SYSTEM_GRAPHICS_MOD_DIR;
+ if (base_path.size() > file_path.size())
+ {
+ ERROR_LOG_FMT(
+ VIDEO,
+ "Failed to load graphics mod json file '{}' due to it not matching the base path: {}",
+ file_path, base_path);
+ return std::nullopt;
+ }
+ result.m_relative_path = file_path.substr(base_path.size());
+ }
+
+ return result;
+}
+
+std::optional GraphicsModConfig::Create(const picojson::object* obj)
+{
+ if (!obj)
+ return std::nullopt;
+
+ const auto source_it = obj->find("source");
+ if (source_it == obj->end())
+ {
+ return std::nullopt;
+ }
+ const std::string source_str = source_it->second.to_str();
+
+ const auto path_it = obj->find("path");
+ if (path_it == obj->end())
+ {
+ return std::nullopt;
+ }
+ const std::string relative_path = path_it->second.to_str();
+
+ if (source_str == "system")
+ {
+ return Create(fmt::format("{}{}{}", File::GetSysDirectory(), DOLPHIN_SYSTEM_GRAPHICS_MOD_DIR,
+ relative_path),
+ Source::System);
+ }
+ else
+ {
+ return Create(File::GetUserPath(D_GRAPHICSMOD_IDX) + relative_path, Source::User);
+ }
+}
+
+std::string GraphicsModConfig::GetAbsolutePath() const
+{
+ if (m_source == Source::System)
+ {
+ return WithUnifiedPathSeparators(fmt::format("{}{}{}", File::GetSysDirectory(),
+ DOLPHIN_SYSTEM_GRAPHICS_MOD_DIR, m_relative_path));
+ }
+ else
+ {
+ return WithUnifiedPathSeparators(File::GetUserPath(D_GRAPHICSMOD_IDX) + m_relative_path);
+ }
+}
+
+bool GraphicsModConfig::DeserializeFromConfig(const picojson::value& value)
+{
+ const auto& meta = value.get("meta");
+ if (meta.is())
+ {
+ const auto& title = meta.get("title");
+ if (title.is())
+ {
+ m_title = title.to_str();
+ }
+
+ const auto& author = meta.get("author");
+ if (author.is())
+ {
+ m_author = author.to_str();
+ }
+
+ const auto& description = meta.get("description");
+ if (description.is())
+ {
+ m_description = description.to_str();
+ }
+ }
+
+ const auto& groups = value.get("groups");
+ if (groups.is())
+ {
+ for (const auto& group_val : groups.get())
+ {
+ if (!group_val.is())
+ {
+ ERROR_LOG_FMT(
+ VIDEO, "Failed to load mod configuration file, specified group is not a json object");
+ return false;
+ }
+ GraphicsTargetGroupConfig group;
+ if (!group.DeserializeFromConfig(group_val.get()))
+ {
+ return false;
+ }
+
+ m_groups.push_back(group);
+ }
+ }
+
+ const auto& features = value.get("features");
+ if (features.is())
+ {
+ for (const auto& feature_val : features.get())
+ {
+ if (!feature_val.is())
+ {
+ ERROR_LOG_FMT(
+ VIDEO, "Failed to load mod configuration file, specified feature is not a json object");
+ return false;
+ }
+ GraphicsModFeatureConfig feature;
+ if (!feature.DeserializeFromConfig(feature_val.get()))
+ {
+ return false;
+ }
+
+ m_features.push_back(feature);
+ }
+ }
+
+ return true;
+}
+
+void GraphicsModConfig::SerializeToProfile(picojson::object* obj) const
+{
+ if (!obj)
+ return;
+
+ auto& json_obj = *obj;
+ switch (m_source)
+ {
+ case Source::User:
+ {
+ json_obj["source"] = picojson::value{"user"};
+ }
+ break;
+ case Source::System:
+ {
+ json_obj["source"] = picojson::value{"system"};
+ }
+ break;
+ };
+
+ json_obj["path"] = picojson::value{m_relative_path};
+
+ picojson::array serialized_groups;
+ for (const auto& group : m_groups)
+ {
+ picojson::object serialized_group;
+ group.SerializeToProfile(&serialized_group);
+ serialized_groups.push_back(picojson::value{serialized_group});
+ }
+ json_obj["groups"] = picojson::value{serialized_groups};
+
+ picojson::array serialized_features;
+ for (const auto& feature : m_features)
+ {
+ picojson::object serialized_feature;
+ feature.SerializeToProfile(&serialized_feature);
+ serialized_features.push_back(picojson::value{serialized_feature});
+ }
+ json_obj["features"] = picojson::value{serialized_features};
+
+ json_obj["enabled"] = picojson::value{m_enabled};
+
+ json_obj["weight"] = picojson::value{static_cast(m_weight)};
+}
+
+void GraphicsModConfig::DeserializeFromProfile(const picojson::object& obj)
+{
+ if (const auto it = obj.find("groups"); it != obj.end())
+ {
+ if (it->second.is())
+ {
+ auto serialized_groups = it->second.get();
+ if (serialized_groups.size() != m_groups.size())
+ return;
+
+ for (std::size_t i = 0; i < serialized_groups.size(); i++)
+ {
+ const auto& serialized_group_val = serialized_groups[i];
+ if (serialized_group_val.is())
+ {
+ const auto& serialized_group = serialized_group_val.get();
+ m_groups[i].DeserializeFromProfile(serialized_group);
+ }
+ }
+ }
+ }
+
+ if (const auto it = obj.find("features"); it != obj.end())
+ {
+ if (it->second.is())
+ {
+ auto serialized_features = it->second.get();
+ if (serialized_features.size() != m_features.size())
+ return;
+
+ for (std::size_t i = 0; i < serialized_features.size(); i++)
+ {
+ const auto& serialized_feature_val = serialized_features[i];
+ if (serialized_feature_val.is())
+ {
+ const auto& serialized_feature = serialized_feature_val.get();
+ m_features[i].DeserializeFromProfile(serialized_feature);
+ }
+ }
+ }
+ }
+
+ if (const auto it = obj.find("enabled"); it != obj.end())
+ {
+ if (it->second.is())
+ {
+ m_enabled = it->second.get();
+ }
+ }
+
+ if (const auto it = obj.find("weight"); it != obj.end())
+ {
+ if (it->second.is())
+ {
+ m_weight = static_cast(it->second.get());
+ }
+ }
+}
+
+bool GraphicsModConfig::operator<(const GraphicsModConfig& other) const
+{
+ return m_weight < other.m_weight;
+}
diff --git a/Source/Core/VideoCommon/GraphicsModSystem/Config/GraphicsMod.h b/Source/Core/VideoCommon/GraphicsModSystem/Config/GraphicsMod.h
new file mode 100644
index 0000000000..f4f6859cb3
--- /dev/null
+++ b/Source/Core/VideoCommon/GraphicsModSystem/Config/GraphicsMod.h
@@ -0,0 +1,45 @@
+// Copyright 2022 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include
+#include
+#include
+
+#include
+
+#include "VideoCommon/GraphicsModSystem/Config/GraphicsModFeature.h"
+#include "VideoCommon/GraphicsModSystem/Config/GraphicsTargetGroup.h"
+
+struct GraphicsModConfig
+{
+ std::string m_title;
+ std::string m_author;
+ std::string m_description;
+ bool m_enabled = false;
+ u16 m_weight = 0;
+ std::string m_relative_path;
+
+ enum class Source
+ {
+ User,
+ System
+ };
+ Source m_source = Source::User;
+
+ std::vector m_groups;
+ std::vector m_features;
+
+ static std::optional Create(const std::string& file, Source source);
+ static std::optional Create(const picojson::object* obj);
+
+ std::string GetAbsolutePath() const;
+
+ bool DeserializeFromConfig(const picojson::value& value);
+
+ void SerializeToProfile(picojson::object* value) const;
+ void DeserializeFromProfile(const picojson::object& value);
+
+ bool operator<(const GraphicsModConfig& other) const;
+};
diff --git a/Source/Core/VideoCommon/GraphicsModSystem/Config/GraphicsModFeature.cpp b/Source/Core/VideoCommon/GraphicsModSystem/Config/GraphicsModFeature.cpp
new file mode 100644
index 0000000000..3fa75eceba
--- /dev/null
+++ b/Source/Core/VideoCommon/GraphicsModSystem/Config/GraphicsModFeature.cpp
@@ -0,0 +1,48 @@
+// Copyright 2022 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "VideoCommon/GraphicsModSystem/Config/GraphicsModFeature.h"
+
+#include "Common/Logging/Log.h"
+
+bool GraphicsModFeatureConfig::DeserializeFromConfig(const picojson::object& obj)
+{
+ if (auto group_iter = obj.find("group"); group_iter != obj.end())
+ {
+ if (!group_iter->second.is())
+ {
+ ERROR_LOG_FMT(
+ VIDEO,
+ "Failed to load mod configuration file, specified feature's group is not a string");
+ return false;
+ }
+ m_group = group_iter->second.get();
+ }
+
+ if (auto action_iter = obj.find("action"); action_iter != obj.end())
+ {
+ if (!action_iter->second.is())
+ {
+ ERROR_LOG_FMT(
+ VIDEO,
+ "Failed to load mod configuration file, specified feature's action is not a string");
+ return false;
+ }
+ m_action = action_iter->second.get();
+ }
+
+ if (auto action_data_iter = obj.find("action_data"); action_data_iter != obj.end())
+ {
+ m_action_data = action_data_iter->second;
+ }
+
+ return true;
+}
+
+void GraphicsModFeatureConfig::SerializeToProfile(picojson::object*) const
+{
+}
+
+void GraphicsModFeatureConfig::DeserializeFromProfile(const picojson::object&)
+{
+}
diff --git a/Source/Core/VideoCommon/GraphicsModSystem/Config/GraphicsModFeature.h b/Source/Core/VideoCommon/GraphicsModSystem/Config/GraphicsModFeature.h
new file mode 100644
index 0000000000..af71bf35a7
--- /dev/null
+++ b/Source/Core/VideoCommon/GraphicsModSystem/Config/GraphicsModFeature.h
@@ -0,0 +1,20 @@
+// Copyright 2022 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include
+
+#include
+
+struct GraphicsModFeatureConfig
+{
+ std::string m_group;
+ std::string m_action;
+ picojson::value m_action_data;
+
+ bool DeserializeFromConfig(const picojson::object& value);
+
+ void SerializeToProfile(picojson::object* value) const;
+ void DeserializeFromProfile(const picojson::object& value);
+};
diff --git a/Source/Core/VideoCommon/GraphicsModSystem/Config/GraphicsModGroup.cpp b/Source/Core/VideoCommon/GraphicsModSystem/Config/GraphicsModGroup.cpp
new file mode 100644
index 0000000000..0d1ae30fb6
--- /dev/null
+++ b/Source/Core/VideoCommon/GraphicsModSystem/Config/GraphicsModGroup.cpp
@@ -0,0 +1,191 @@
+// Copyright 2022 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "VideoCommon/GraphicsModSystem/Config/GraphicsModGroup.h"
+
+#include