VideoCommon: add custom pipeline action
This commit is contained in:
parent
d320366954
commit
c7191382be
|
@ -668,6 +668,7 @@
|
|||
<ClInclude Include="VideoCommon\GraphicsModSystem\Config\GraphicsTarget.h" />
|
||||
<ClInclude Include="VideoCommon\GraphicsModSystem\Config\GraphicsTargetGroup.h" />
|
||||
<ClInclude Include="VideoCommon\GraphicsModSystem\Constants.h" />
|
||||
<ClInclude Include="VideoCommon\GraphicsModSystem\Runtime\Actions\CustomPipelineAction.h" />
|
||||
<ClInclude Include="VideoCommon\GraphicsModSystem\Runtime\Actions\MoveAction.h" />
|
||||
<ClInclude Include="VideoCommon\GraphicsModSystem\Runtime\Actions\PrintAction.h" />
|
||||
<ClInclude Include="VideoCommon\GraphicsModSystem\Runtime\Actions\ScaleAction.h" />
|
||||
|
@ -1281,6 +1282,7 @@
|
|||
<ClCompile Include="VideoCommon\GraphicsModSystem\Config\GraphicsModGroup.cpp" />
|
||||
<ClCompile Include="VideoCommon\GraphicsModSystem\Config\GraphicsTarget.cpp" />
|
||||
<ClCompile Include="VideoCommon\GraphicsModSystem\Config\GraphicsTargetGroup.cpp" />
|
||||
<ClCompile Include="VideoCommon\GraphicsModSystem\Runtime\Actions\CustomPipelineAction.cpp" />
|
||||
<ClCompile Include="VideoCommon\GraphicsModSystem\Runtime\Actions\MoveAction.cpp" />
|
||||
<ClCompile Include="VideoCommon\GraphicsModSystem\Runtime\Actions\PrintAction.cpp" />
|
||||
<ClCompile Include="VideoCommon\GraphicsModSystem\Runtime\Actions\ScaleAction.cpp" />
|
||||
|
|
|
@ -73,6 +73,8 @@ add_library(videocommon
|
|||
GraphicsModSystem/Config/GraphicsTargetGroup.cpp
|
||||
GraphicsModSystem/Config/GraphicsTargetGroup.h
|
||||
GraphicsModSystem/Constants.h
|
||||
GraphicsModSystem/Runtime/Actions/CustomPipelineAction.cpp
|
||||
GraphicsModSystem/Runtime/Actions/CustomPipelineAction.h
|
||||
GraphicsModSystem/Runtime/Actions/MoveAction.cpp
|
||||
GraphicsModSystem/Runtime/Actions/MoveAction.h
|
||||
GraphicsModSystem/Runtime/Actions/PrintAction.cpp
|
||||
|
|
|
@ -0,0 +1,449 @@
|
|||
// Copyright 2022 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "VideoCommon/GraphicsModSystem/Runtime/Actions/CustomPipelineAction.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/StringUtil.h"
|
||||
#include "Core/System.h"
|
||||
|
||||
#include "VideoCommon/AbstractGfx.h"
|
||||
#include "VideoCommon/Assets/CustomAssetLoader.h"
|
||||
#include "VideoCommon/Assets/DirectFilesystemAssetLibrary.h"
|
||||
#include "VideoCommon/ShaderGenCommon.h"
|
||||
#include "VideoCommon/TextureCacheBase.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
bool IsQualifier(std::string_view value)
|
||||
{
|
||||
static std::array<std::string_view, 7> qualifiers = {"attribute", "const", "highp", "lowp",
|
||||
"mediump", "uniform", "varying"};
|
||||
return std::find(qualifiers.begin(), qualifiers.end(), value) != qualifiers.end();
|
||||
}
|
||||
|
||||
bool IsBuiltInMacro(std::string_view value)
|
||||
{
|
||||
static std::array<std::string_view, 5> built_in = {"__LINE__", "__FILE__", "__VERSION__",
|
||||
"GL_core_profile", "GL_compatibility_profile"};
|
||||
return std::find(built_in.begin(), built_in.end(), value) != built_in.end();
|
||||
}
|
||||
|
||||
std::vector<std::string> GlobalConflicts(std::string_view source)
|
||||
{
|
||||
std::string_view last_identifier = "";
|
||||
std::vector<std::string> global_result;
|
||||
u32 scope = 0;
|
||||
for (u32 i = 0; i < source.size(); i++)
|
||||
{
|
||||
// If we're out of global scope, we don't care
|
||||
// about any of the details
|
||||
if (scope > 0)
|
||||
{
|
||||
if (source[i] == '{')
|
||||
{
|
||||
scope++;
|
||||
}
|
||||
else if (source[i] == '}')
|
||||
{
|
||||
scope--;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto parse_identifier = [&]() {
|
||||
const u32 start = i;
|
||||
for (; i < source.size(); i++)
|
||||
{
|
||||
if (!Common::IsAlpha(source[i]) && source[i] != '_' && !std::isdigit(source[i]))
|
||||
break;
|
||||
}
|
||||
u32 end = i;
|
||||
i--; // unwind
|
||||
return source.substr(start, end - start);
|
||||
};
|
||||
|
||||
if (Common::IsAlpha(source[i]) || source[i] == '_')
|
||||
{
|
||||
const std::string_view identifier = parse_identifier();
|
||||
if (IsQualifier(identifier))
|
||||
continue;
|
||||
if (IsBuiltInMacro(identifier))
|
||||
continue;
|
||||
last_identifier = identifier;
|
||||
}
|
||||
else if (source[i] == '#')
|
||||
{
|
||||
const auto parse_until_end_of_preprocessor = [&]() {
|
||||
bool continue_until_next_newline = false;
|
||||
for (; i < source.size(); i++)
|
||||
{
|
||||
if (source[i] == '\n')
|
||||
{
|
||||
if (continue_until_next_newline)
|
||||
continue_until_next_newline = false;
|
||||
else
|
||||
break;
|
||||
}
|
||||
else if (source[i] == '\\')
|
||||
{
|
||||
continue_until_next_newline = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
i++;
|
||||
const std::string_view identifier = parse_identifier();
|
||||
if (identifier == "define")
|
||||
{
|
||||
i++;
|
||||
// skip whitespace
|
||||
while (source[i] == ' ')
|
||||
{
|
||||
i++;
|
||||
}
|
||||
global_result.push_back(std::string{parse_identifier()});
|
||||
parse_until_end_of_preprocessor();
|
||||
}
|
||||
else
|
||||
{
|
||||
parse_until_end_of_preprocessor();
|
||||
}
|
||||
}
|
||||
else if (source[i] == '{')
|
||||
{
|
||||
scope++;
|
||||
}
|
||||
else if (source[i] == '(')
|
||||
{
|
||||
// Unlikely the user will be using layouts but...
|
||||
if (last_identifier == "layout")
|
||||
continue;
|
||||
|
||||
// Since we handle equality, we can assume the identifier
|
||||
// before '(' is a function definition
|
||||
global_result.push_back(std::string{last_identifier});
|
||||
}
|
||||
else if (source[i] == '=')
|
||||
{
|
||||
global_result.push_back(std::string{last_identifier});
|
||||
i++;
|
||||
for (; i < source.size(); i++)
|
||||
{
|
||||
if (source[i] == ';')
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (source[i] == '/')
|
||||
{
|
||||
if ((i + 1) >= source.size())
|
||||
continue;
|
||||
|
||||
if (source[i + 1] == '/')
|
||||
{
|
||||
// Go to end of line...
|
||||
for (; i < source.size(); i++)
|
||||
{
|
||||
if (source[i] == '\n')
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (source[i + 1] == '*')
|
||||
{
|
||||
// Multiline, look for first '*/'
|
||||
for (; i < source.size(); i++)
|
||||
{
|
||||
if (source[i] == '/' && source[i - 1] == '*')
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the conflicts from largest to smallest string
|
||||
// this way we can ensure smaller strings that are a substring
|
||||
// of the larger string are able to be replaced appropriately
|
||||
std::sort(global_result.begin(), global_result.end(),
|
||||
[](const std::string& first, const std::string& second) {
|
||||
return first.size() > second.size();
|
||||
});
|
||||
return global_result;
|
||||
}
|
||||
|
||||
void WriteDefines(ShaderCode* out, const std::vector<std::string>& texture_code_names,
|
||||
u32 texture_unit)
|
||||
{
|
||||
for (std::size_t i = 0; i < texture_code_names.size(); i++)
|
||||
{
|
||||
const auto& code_name = texture_code_names[i];
|
||||
out->Write("#define {}_UNIT_{{0}} {}\n", code_name, texture_unit);
|
||||
out->Write(
|
||||
"#define {0}_COORD_{{0}} float3(data.texcoord[data.texmap_to_texcoord_index[{1}]].xy, "
|
||||
"{2})\n",
|
||||
code_name, texture_unit, i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<CustomPipelineAction>
|
||||
CustomPipelineAction::Create(const picojson::value& json_data,
|
||||
std::shared_ptr<VideoCommon::CustomAssetLibrary> library)
|
||||
{
|
||||
std::vector<CustomPipelineAction::PipelinePassPassDescription> pipeline_passes;
|
||||
|
||||
const auto& passes_json = json_data.get("passes");
|
||||
if (passes_json.is<picojson::array>())
|
||||
{
|
||||
for (const auto& passes_json_val : passes_json.get<picojson::array>())
|
||||
{
|
||||
CustomPipelineAction::PipelinePassPassDescription pipeline_pass;
|
||||
if (!passes_json_val.is<picojson::object>())
|
||||
{
|
||||
ERROR_LOG_FMT(VIDEO,
|
||||
"Failed to load custom pipeline action, 'passes' has an array value that "
|
||||
"is not an object!");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto pass = passes_json_val.get<picojson::object>();
|
||||
if (!pass.contains("pixel_material_asset"))
|
||||
{
|
||||
ERROR_LOG_FMT(VIDEO,
|
||||
"Failed to load custom pipeline action, 'passes' value missing required "
|
||||
"field 'pixel_material_asset'");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto pixel_material_asset_json = pass["pixel_material_asset"];
|
||||
if (!pixel_material_asset_json.is<std::string>())
|
||||
{
|
||||
ERROR_LOG_FMT(VIDEO, "Failed to load custom pipeline action, 'passes' field "
|
||||
"'pixel_material_asset' is not a string!");
|
||||
return nullptr;
|
||||
}
|
||||
pipeline_pass.m_pixel_material_asset = pixel_material_asset_json.to_str();
|
||||
pipeline_passes.push_back(std::move(pipeline_pass));
|
||||
}
|
||||
}
|
||||
|
||||
if (pipeline_passes.empty())
|
||||
{
|
||||
ERROR_LOG_FMT(VIDEO, "Failed to load custom pipeline action, must specify at least one pass");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (pipeline_passes.size() > 1)
|
||||
{
|
||||
ERROR_LOG_FMT(
|
||||
VIDEO,
|
||||
"Failed to load custom pipeline action, multiple passes are not currently supported");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return std::make_unique<CustomPipelineAction>(std::move(library), std::move(pipeline_passes));
|
||||
}
|
||||
|
||||
CustomPipelineAction::CustomPipelineAction(
|
||||
std::shared_ptr<VideoCommon::CustomAssetLibrary> library,
|
||||
std::vector<PipelinePassPassDescription> pass_descriptions)
|
||||
: m_library(std::move(library)), m_passes_config(std::move(pass_descriptions))
|
||||
{
|
||||
m_passes.resize(m_passes_config.size());
|
||||
}
|
||||
|
||||
CustomPipelineAction::~CustomPipelineAction() = default;
|
||||
|
||||
void CustomPipelineAction::OnDrawStarted(GraphicsModActionData::DrawStarted* draw_started)
|
||||
{
|
||||
if (!draw_started) [[unlikely]]
|
||||
return;
|
||||
|
||||
if (!draw_started->custom_pixel_shader) [[unlikely]]
|
||||
return;
|
||||
|
||||
if (!m_valid)
|
||||
return;
|
||||
|
||||
if (m_passes.empty()) [[unlikely]]
|
||||
return;
|
||||
|
||||
// For now assume a single pass
|
||||
auto& pass = m_passes[0];
|
||||
|
||||
if (!pass.m_pixel_shader.m_asset) [[unlikely]]
|
||||
return;
|
||||
|
||||
const auto shader_data = pass.m_pixel_shader.m_asset->GetData();
|
||||
if (shader_data)
|
||||
{
|
||||
if (pass.m_pixel_shader.m_asset->GetLastLoadedTime() > pass.m_pixel_shader.m_cached_write_time)
|
||||
{
|
||||
const auto material = pass.m_pixel_material.m_asset->GetData();
|
||||
if (!material)
|
||||
return;
|
||||
|
||||
pass.m_pixel_shader.m_cached_write_time = pass.m_pixel_shader.m_asset->GetLastLoadedTime();
|
||||
|
||||
for (const auto& prop : material->properties)
|
||||
{
|
||||
if (!shader_data->m_properties.contains(prop.m_code_name))
|
||||
{
|
||||
ERROR_LOG_FMT(VIDEO,
|
||||
"Custom pipeline has material asset '{}' that has property '{}'"
|
||||
"that is not on shader asset '{}'",
|
||||
pass.m_pixel_material.m_asset->GetAssetId(), prop.m_code_name,
|
||||
pass.m_pixel_shader.m_asset->GetAssetId());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate shader details
|
||||
std::string color_shader_data =
|
||||
ReplaceAll(shader_data->m_shader_source, "custom_main", CUSTOM_PIXELSHADER_COLOR_FUNC);
|
||||
const auto global_conflicts = GlobalConflicts(color_shader_data);
|
||||
color_shader_data = ReplaceAll(color_shader_data, "\r\n", "\n");
|
||||
color_shader_data = ReplaceAll(color_shader_data, "{", "{{");
|
||||
color_shader_data = ReplaceAll(color_shader_data, "}", "}}");
|
||||
// First replace global conflicts with dummy strings
|
||||
// This avoids the problem where a shorter word
|
||||
// is in a longer word, ex two functions: 'execute' and 'execute_fast'
|
||||
for (std::size_t i = 0; i < global_conflicts.size(); i++)
|
||||
{
|
||||
const std::string& identifier = global_conflicts[i];
|
||||
color_shader_data =
|
||||
ReplaceAll(color_shader_data, identifier, fmt::format("_{0}_DOLPHIN_TEMP_{0}_", i));
|
||||
}
|
||||
// Now replace the temporaries with the actual value
|
||||
for (std::size_t i = 0; i < global_conflicts.size(); i++)
|
||||
{
|
||||
const std::string& identifier = global_conflicts[i];
|
||||
color_shader_data = ReplaceAll(color_shader_data, fmt::format("_{0}_DOLPHIN_TEMP_{0}_", i),
|
||||
fmt::format("{}_{{0}}", identifier));
|
||||
}
|
||||
|
||||
for (const auto& texture_code_name : m_texture_code_names)
|
||||
{
|
||||
color_shader_data =
|
||||
ReplaceAll(color_shader_data, fmt::format("{}_COORD", texture_code_name),
|
||||
fmt::format("{}_COORD_{{0}}", texture_code_name));
|
||||
color_shader_data = ReplaceAll(color_shader_data, fmt::format("{}_UNIT", texture_code_name),
|
||||
fmt::format("{}_UNIT_{{0}}", texture_code_name));
|
||||
}
|
||||
|
||||
m_last_generated_shader_code = ShaderCode{};
|
||||
WriteDefines(&m_last_generated_shader_code, m_texture_code_names, draw_started->texture_unit);
|
||||
m_last_generated_shader_code.Write("{}", color_shader_data);
|
||||
}
|
||||
CustomPixelShader custom_pixel_shader;
|
||||
custom_pixel_shader.custom_shader = m_last_generated_shader_code.GetBuffer();
|
||||
*draw_started->custom_pixel_shader = custom_pixel_shader;
|
||||
}
|
||||
}
|
||||
|
||||
void CustomPipelineAction::OnTextureCreate(GraphicsModActionData::TextureCreate* create)
|
||||
{
|
||||
if (!create->custom_textures) [[unlikely]]
|
||||
return;
|
||||
|
||||
if (!create->additional_dependencies) [[unlikely]]
|
||||
return;
|
||||
|
||||
if (m_passes_config.empty()) [[unlikely]]
|
||||
return;
|
||||
|
||||
if (m_passes.empty()) [[unlikely]]
|
||||
return;
|
||||
|
||||
m_valid = true;
|
||||
auto& loader = Core::System::GetInstance().GetCustomAssetLoader();
|
||||
|
||||
// For now assume a single pass
|
||||
const auto& pass_config = m_passes_config[0];
|
||||
auto& pass = m_passes[0];
|
||||
|
||||
if (!pass.m_pixel_material.m_asset)
|
||||
{
|
||||
pass.m_pixel_material.m_asset =
|
||||
loader.LoadMaterial(pass_config.m_pixel_material_asset, m_library);
|
||||
pass.m_pixel_material.m_cached_write_time = pass.m_pixel_material.m_asset->GetLastLoadedTime();
|
||||
}
|
||||
create->additional_dependencies->push_back(VideoCommon::CachedAsset<VideoCommon::CustomAsset>{
|
||||
pass.m_pixel_material.m_asset, pass.m_pixel_material.m_asset->GetLastLoadedTime()});
|
||||
|
||||
const auto material_data = pass.m_pixel_material.m_asset->GetData();
|
||||
if (!material_data)
|
||||
return;
|
||||
|
||||
if (!pass.m_pixel_shader.m_asset || pass.m_pixel_material.m_asset->GetLastLoadedTime() >
|
||||
pass.m_pixel_material.m_cached_write_time)
|
||||
{
|
||||
pass.m_pixel_shader.m_asset = loader.LoadPixelShader(material_data->shader_asset, m_library);
|
||||
// Note: the asset timestamp will be updated in the draw command
|
||||
}
|
||||
create->additional_dependencies->push_back(VideoCommon::CachedAsset<VideoCommon::CustomAsset>{
|
||||
pass.m_pixel_shader.m_asset, pass.m_pixel_shader.m_asset->GetLastLoadedTime()});
|
||||
|
||||
m_texture_code_names.clear();
|
||||
std::vector<VideoCommon::CachedAsset<VideoCommon::GameTextureAsset>> game_assets;
|
||||
for (const auto& property : material_data->properties)
|
||||
{
|
||||
if (property.m_type == VideoCommon::MaterialProperty::Type::Type_TextureAsset)
|
||||
{
|
||||
if (property.m_value)
|
||||
{
|
||||
if (auto* value = std::get_if<std::string>(&*property.m_value))
|
||||
{
|
||||
auto asset = loader.LoadGameTexture(*value, m_library);
|
||||
if (asset)
|
||||
{
|
||||
const auto loaded_time = asset->GetLastLoadedTime();
|
||||
game_assets.push_back(VideoCommon::CachedAsset<VideoCommon::GameTextureAsset>{
|
||||
std::move(asset), loaded_time});
|
||||
m_texture_code_names.push_back(property.m_code_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Note: we swap here instead of doing a clear + append of the member
|
||||
// variable so that any loaded assets from previous iterations
|
||||
// won't be let go
|
||||
std::swap(pass.m_game_textures, game_assets);
|
||||
|
||||
for (auto& game_texture : pass.m_game_textures)
|
||||
{
|
||||
if (game_texture.m_asset)
|
||||
{
|
||||
auto data = game_texture.m_asset->GetData();
|
||||
if (data)
|
||||
{
|
||||
if (create->texture_width != data->m_levels[0].width ||
|
||||
create->texture_height != data->m_levels[0].height)
|
||||
{
|
||||
ERROR_LOG_FMT(VIDEO,
|
||||
"Custom pipeline for texture '{}' has asset '{}' that does not match "
|
||||
"the width/height of the texture loaded. Texture {}x{} vs asset {}x{}",
|
||||
create->texture_name, game_texture.m_asset->GetAssetId(),
|
||||
create->texture_width, create->texture_height, data->m_levels[0].width,
|
||||
data->m_levels[0].height);
|
||||
m_valid = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_valid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: compare game textures and shader requirements
|
||||
|
||||
create->custom_textures->insert(create->custom_textures->end(), pass.m_game_textures.begin(),
|
||||
pass.m_game_textures.end());
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright 2022 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <picojson.h>
|
||||
|
||||
#include "VideoCommon/AbstractTexture.h"
|
||||
#include "VideoCommon/Assets/CustomAssetLibrary.h"
|
||||
#include "VideoCommon/Assets/MaterialAsset.h"
|
||||
#include "VideoCommon/Assets/ShaderAsset.h"
|
||||
#include "VideoCommon/Assets/TextureAsset.h"
|
||||
#include "VideoCommon/GraphicsModSystem/Runtime/GraphicsModAction.h"
|
||||
#include "VideoCommon/ShaderGenCommon.h"
|
||||
|
||||
class CustomPipelineAction final : public GraphicsModAction
|
||||
{
|
||||
public:
|
||||
struct PipelinePassPassDescription
|
||||
{
|
||||
std::string m_pixel_material_asset;
|
||||
};
|
||||
|
||||
static std::unique_ptr<CustomPipelineAction> Create(const picojson::value& json_data,
|
||||
std::string_view path);
|
||||
CustomPipelineAction(std::shared_ptr<VideoCommon::CustomAssetLibrary> library,
|
||||
std::vector<PipelinePassPassDescription> pass_descriptions);
|
||||
~CustomPipelineAction();
|
||||
void OnDrawStarted(GraphicsModActionData::DrawStarted*) override;
|
||||
void OnTextureCreate(GraphicsModActionData::TextureCreate*) override;
|
||||
|
||||
private:
|
||||
std::shared_ptr<VideoCommon::CustomAssetLibrary> m_library;
|
||||
std::vector<PipelinePassPassDescription> m_passes_config;
|
||||
struct PipelinePass
|
||||
{
|
||||
VideoCommon::CachedAsset<VideoCommon::MaterialAsset> m_pixel_material;
|
||||
VideoCommon::CachedAsset<VideoCommon::PixelShaderAsset> m_pixel_shader;
|
||||
std::vector<VideoCommon::CachedAsset<VideoCommon::GameTextureAsset>> m_game_textures;
|
||||
};
|
||||
std::vector<PipelinePass> m_passes;
|
||||
|
||||
ShaderCode m_last_generated_shader_code;
|
||||
|
||||
bool m_valid = true;
|
||||
|
||||
std::vector<std::string> m_texture_code_names;
|
||||
};
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
#include "VideoCommon/GraphicsModSystem/Runtime/GraphicsModActionFactory.h"
|
||||
|
||||
#include "VideoCommon/GraphicsModSystem/Runtime/Actions/CustomPipelineAction.h"
|
||||
#include "VideoCommon/GraphicsModSystem/Runtime/Actions/MoveAction.h"
|
||||
#include "VideoCommon/GraphicsModSystem/Runtime/Actions/PrintAction.h"
|
||||
#include "VideoCommon/GraphicsModSystem/Runtime/Actions/ScaleAction.h"
|
||||
|
@ -29,6 +30,10 @@ std::unique_ptr<GraphicsModAction> Create(std::string_view name, const picojson:
|
|||
{
|
||||
return ScaleAction::Create(json_data);
|
||||
}
|
||||
else if (name == "custom_pipeline")
|
||||
{
|
||||
return CustomPipelineAction::Create(json_data, path);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue