diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props
index 1a398f3bf5..4ddf550edf 100644
--- a/Source/Core/DolphinLib.props
+++ b/Source/Core/DolphinLib.props
@@ -637,6 +637,7 @@
+
@@ -1252,6 +1253,7 @@
+
diff --git a/Source/Core/VideoCommon/Assets/CustomAssetLibrary.h b/Source/Core/VideoCommon/Assets/CustomAssetLibrary.h
index eb78e9f770..196372fab8 100644
--- a/Source/Core/VideoCommon/Assets/CustomAssetLibrary.h
+++ b/Source/Core/VideoCommon/Assets/CustomAssetLibrary.h
@@ -11,6 +11,7 @@
namespace VideoCommon
{
class CustomTextureData;
+struct PixelShaderData;
// This class provides functionality to load
// specific data (like textures). Where this data
@@ -45,5 +46,8 @@ public:
// Loads a texture as a game texture, providing additional checks like confirming
// each mip level size is correct and that the format is consistent across the data
LoadInfo LoadGameTexture(const AssetID& asset_id, CustomTextureData* data);
+
+ // Loads a pixel shader
+ virtual LoadInfo LoadPixelShader(const AssetID& asset_id, PixelShaderData* data) = 0;
};
} // namespace VideoCommon
diff --git a/Source/Core/VideoCommon/Assets/CustomAssetLoader.cpp b/Source/Core/VideoCommon/Assets/CustomAssetLoader.cpp
index bb404064d2..ce76a368d2 100644
--- a/Source/Core/VideoCommon/Assets/CustomAssetLoader.cpp
+++ b/Source/Core/VideoCommon/Assets/CustomAssetLoader.cpp
@@ -90,4 +90,11 @@ CustomAssetLoader::LoadGameTexture(const CustomAssetLibrary::AssetID& asset_id,
{
return LoadOrCreateAsset(asset_id, m_game_textures, std::move(library));
}
+
+std::shared_ptr
+CustomAssetLoader::LoadPixelShader(const CustomAssetLibrary::AssetID& asset_id,
+ std::shared_ptr library)
+{
+ return LoadOrCreateAsset(asset_id, m_pixel_shaders, std::move(library));
+}
} // namespace VideoCommon
diff --git a/Source/Core/VideoCommon/Assets/CustomAssetLoader.h b/Source/Core/VideoCommon/Assets/CustomAssetLoader.h
index 6e4596b8b2..cca83af960 100644
--- a/Source/Core/VideoCommon/Assets/CustomAssetLoader.h
+++ b/Source/Core/VideoCommon/Assets/CustomAssetLoader.h
@@ -12,6 +12,7 @@
#include "Common/Flag.h"
#include "Common/WorkQueueThread.h"
#include "VideoCommon/Assets/CustomAsset.h"
+#include "VideoCommon/Assets/ShaderAsset.h"
#include "VideoCommon/Assets/TextureAsset.h"
namespace VideoCommon
@@ -42,6 +43,9 @@ public:
std::shared_ptr LoadGameTexture(const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr library);
+ std::shared_ptr LoadPixelShader(const CustomAssetLibrary::AssetID& asset_id,
+ std::shared_ptr library);
+
private:
// TODO C++20: use a 'derived_from' concept against 'CustomAsset' when available
template
@@ -74,6 +78,7 @@ private:
std::map> m_textures;
std::map> m_game_textures;
+ std::map> m_pixel_shaders;
std::thread m_asset_monitor_thread;
Common::Flag m_asset_monitor_thread_shutdown;
diff --git a/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.cpp b/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.cpp
index 9c126bf9e1..c234b4088e 100644
--- a/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.cpp
+++ b/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.cpp
@@ -10,6 +10,7 @@
#include "Common/Logging/Log.h"
#include "Common/StringUtil.h"
#include "VideoCommon/Assets/CustomTextureData.h"
+#include "VideoCommon/Assets/ShaderAsset.h"
namespace VideoCommon
{
@@ -49,6 +50,100 @@ DirectFilesystemAssetLibrary::GetLastAssetWriteTime(const AssetID& asset_id) con
return {};
}
+CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadPixelShader(const AssetID& asset_id,
+ PixelShaderData* data)
+{
+ const auto asset_map = GetAssetMapForID(asset_id);
+
+ // Asset map for a pixel shader is the shader and some metadata
+ if (asset_map.size() != 2)
+ {
+ ERROR_LOG_FMT(VIDEO, "Asset '{}' expected to have two files mapped!", asset_id);
+ return {};
+ }
+
+ const auto metadata = asset_map.find("metadata");
+ const auto shader = asset_map.find("shader");
+ if (metadata == asset_map.end())
+ {
+ ERROR_LOG_FMT(VIDEO, "Asset '{}' expected to have a metadata entry mapped!", asset_id);
+ return {};
+ }
+
+ if (shader == asset_map.end())
+ {
+ ERROR_LOG_FMT(VIDEO, "Asset '{}' expected to have a shader entry mapped!", asset_id);
+ return {};
+ }
+
+ std::size_t metadata_size;
+ {
+ std::error_code ec;
+ metadata_size = std::filesystem::file_size(metadata->second, ec);
+ if (ec)
+ {
+ ERROR_LOG_FMT(VIDEO,
+ "Asset '{}' error - failed to get shader metadata file size with error '{}'!",
+ asset_id, ec);
+ return {};
+ }
+ }
+ std::size_t shader_size;
+ {
+ std::error_code ec;
+ shader_size = std::filesystem::file_size(shader->second, ec);
+ if (ec)
+ {
+ ERROR_LOG_FMT(VIDEO,
+ "Asset '{}' error - failed to get shader source file size with error '{}'!",
+ asset_id, ec);
+ return {};
+ }
+ }
+ const auto approx_mem_size = metadata_size + shader_size;
+
+ if (!File::ReadFileToString(shader->second.string(), data->m_shader_source))
+ {
+ ERROR_LOG_FMT(VIDEO, "Asset '{}' error - failed to load the shader file '{}',", asset_id,
+ shader->second.string());
+ return {};
+ }
+
+ std::string json_data;
+ if (!File::ReadFileToString(metadata->second.string(), json_data))
+ {
+ ERROR_LOG_FMT(VIDEO, "Asset '{}' error - failed to load the json file '{}',", asset_id,
+ metadata->second.string());
+ return {};
+ }
+
+ picojson::value root;
+ const auto error = picojson::parse(root, json_data);
+
+ if (!error.empty())
+ {
+ ERROR_LOG_FMT(VIDEO,
+ "Asset '{}' error - failed to load the json file '{}', due to parse error: {}",
+ asset_id, metadata->second.string(), error);
+ return {};
+ }
+ if (!root.is())
+ {
+ ERROR_LOG_FMT(
+ VIDEO,
+ "Asset '{}' error - failed to load the json file '{}', due to root not being an object!",
+ asset_id, metadata->second.string());
+ return {};
+ }
+
+ const auto& root_obj = root.get();
+
+ if (!PixelShaderData::FromJson(asset_id, root_obj, data))
+ return {};
+
+ return LoadInfo{approx_mem_size, GetLastAssetWriteTime(asset_id)};
+}
+
CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const AssetID& asset_id,
CustomTextureData* data)
{
diff --git a/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.h b/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.h
index 4f02d107cf..9a842c6988 100644
--- a/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.h
+++ b/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.h
@@ -22,6 +22,7 @@ public:
using AssetMap = std::map;
LoadInfo LoadTexture(const AssetID& asset_id, CustomTextureData* data) override;
+ LoadInfo LoadPixelShader(const AssetID& asset_id, PixelShaderData* data) override;
// Gets the latest time from amongst all the files in the asset map
TimeType GetLastAssetWriteTime(const AssetID& asset_id) const override;
diff --git a/Source/Core/VideoCommon/Assets/ShaderAsset.cpp b/Source/Core/VideoCommon/Assets/ShaderAsset.cpp
new file mode 100644
index 0000000000..d09e572bab
--- /dev/null
+++ b/Source/Core/VideoCommon/Assets/ShaderAsset.cpp
@@ -0,0 +1,147 @@
+// Copyright 2023 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "VideoCommon/Assets/ShaderAsset.h"
+
+#include "Common/Logging/Log.h"
+#include "VideoCommon/Assets/CustomAssetLibrary.h"
+
+namespace VideoCommon
+{
+bool ParseShaderProperties(const VideoCommon::CustomAssetLibrary::AssetID& asset_id,
+ const picojson::array& properties_data,
+ std::map* shader_properties)
+{
+ if (!shader_properties) [[unlikely]]
+ return false;
+
+ for (const auto& property_data : properties_data)
+ {
+ VideoCommon::ShaderProperty property;
+ if (!property_data.is())
+ {
+ ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse json, property is not the right json type",
+ asset_id);
+ return false;
+ }
+ const auto& property_data_obj = property_data.get();
+
+ const auto type_iter = property_data_obj.find("type");
+ if (type_iter == property_data_obj.end())
+ {
+ ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse json, property entry 'type' not found",
+ asset_id);
+ return false;
+ }
+ if (!type_iter->second.is())
+ {
+ ERROR_LOG_FMT(VIDEO,
+ "Asset '{}' failed to parse json, property entry 'type' is not "
+ "the right json type",
+ asset_id);
+ return false;
+ }
+ std::string type = type_iter->second.to_str();
+ std::transform(type.begin(), type.end(), type.begin(),
+ [](unsigned char c) { return std::tolower(c); });
+
+ if (type == "sampler2d")
+ {
+ property.m_type = ShaderProperty::Type::Type_Sampler2D;
+ }
+ else
+ {
+ ERROR_LOG_FMT(VIDEO,
+ "Asset '{}' failed to parse json, property entry 'description' is "
+ "an invalid option",
+ asset_id);
+ return false;
+ }
+
+ const auto description_iter = property_data_obj.find("description");
+ if (description_iter == property_data_obj.end())
+ {
+ ERROR_LOG_FMT(VIDEO,
+ "Asset '{}' failed to parse json, property entry 'description' not found",
+ asset_id);
+ return false;
+ }
+ if (!description_iter->second.is())
+ {
+ ERROR_LOG_FMT(VIDEO,
+ "Asset '{}' failed to parse json, property entry 'description' is not "
+ "the right json type",
+ asset_id);
+ return false;
+ }
+ property.m_description = description_iter->second.to_str();
+
+ const auto code_name_iter = property_data_obj.find("code_name");
+ if (code_name_iter == property_data_obj.end())
+ {
+ ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse json, property entry 'code_name' not found",
+ asset_id);
+ return false;
+ }
+ if (!code_name_iter->second.is())
+ {
+ ERROR_LOG_FMT(VIDEO,
+ "Asset '{}' failed to parse json, property entry 'code_name' is not "
+ "the right json type",
+ asset_id);
+ return false;
+ }
+ shader_properties->try_emplace(code_name_iter->second.to_str(), std::move(property));
+ }
+
+ return true;
+}
+
+bool PixelShaderData::FromJson(const VideoCommon::CustomAssetLibrary::AssetID& asset_id,
+ const picojson::object& json, PixelShaderData* data)
+{
+ const auto properties_iter = json.find("properties");
+ if (properties_iter == json.end())
+ {
+ ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse json, 'properties' not found", asset_id);
+ return false;
+ }
+ if (!properties_iter->second.is())
+ {
+ ERROR_LOG_FMT(VIDEO, "Asset '{}' failed to parse json, 'properties' is not the right json type",
+ asset_id);
+ return false;
+ }
+ const auto& properties_array = properties_iter->second.get();
+ if (!ParseShaderProperties(asset_id, properties_array, &data->m_properties))
+ return false;
+
+ for (const auto& [name, property] : data->m_properties)
+ {
+ if (data->m_shader_source.find(name) == std::string::npos)
+ {
+ ERROR_LOG_FMT(
+ VIDEO,
+ "Asset '{}' failed to parse json, the code name '{}' defined in the metadata was not "
+ "found in the shader source",
+ asset_id, name);
+ return false;
+ }
+ }
+
+ return true;
+}
+CustomAssetLibrary::LoadInfo PixelShaderAsset::LoadImpl(const CustomAssetLibrary::AssetID& asset_id)
+{
+ auto potential_data = std::make_shared();
+ const auto loaded_info = m_owning_library->LoadPixelShader(asset_id, potential_data.get());
+ if (loaded_info.m_bytes_loaded == 0)
+ return {};
+ {
+ std::lock_guard lk(m_data_lock);
+ m_loaded = true;
+ m_data = std::move(potential_data);
+ }
+ return loaded_info;
+}
+} // namespace VideoCommon
diff --git a/Source/Core/VideoCommon/Assets/ShaderAsset.h b/Source/Core/VideoCommon/Assets/ShaderAsset.h
new file mode 100644
index 0000000000..98712f0d78
--- /dev/null
+++ b/Source/Core/VideoCommon/Assets/ShaderAsset.h
@@ -0,0 +1,47 @@
+// Copyright 2023 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include