From c7191382bed2ba2c64fa5a303c2bfa61778a5d97 Mon Sep 17 00:00:00 2001 From: iwubcode Date: Sun, 9 Jul 2023 15:23:08 -0500 Subject: [PATCH] VideoCommon: add custom pipeline action --- Source/Core/DolphinLib.props | 2 + Source/Core/VideoCommon/CMakeLists.txt | 2 + .../Runtime/Actions/CustomPipelineAction.cpp | 449 ++++++++++++++++++ .../Runtime/Actions/CustomPipelineAction.h | 53 +++ .../Runtime/GraphicsModActionFactory.cpp | 5 + 5 files changed, 511 insertions(+) create mode 100644 Source/Core/VideoCommon/GraphicsModSystem/Runtime/Actions/CustomPipelineAction.cpp create mode 100644 Source/Core/VideoCommon/GraphicsModSystem/Runtime/Actions/CustomPipelineAction.h diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 3e2a4f2176..b8c7162114 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -668,6 +668,7 @@ + @@ -1281,6 +1282,7 @@ + diff --git a/Source/Core/VideoCommon/CMakeLists.txt b/Source/Core/VideoCommon/CMakeLists.txt index bd816052ba..99915a0023 100644 --- a/Source/Core/VideoCommon/CMakeLists.txt +++ b/Source/Core/VideoCommon/CMakeLists.txt @@ -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 diff --git a/Source/Core/VideoCommon/GraphicsModSystem/Runtime/Actions/CustomPipelineAction.cpp b/Source/Core/VideoCommon/GraphicsModSystem/Runtime/Actions/CustomPipelineAction.cpp new file mode 100644 index 0000000000..b774324a58 --- /dev/null +++ b/Source/Core/VideoCommon/GraphicsModSystem/Runtime/Actions/CustomPipelineAction.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 +#include + +#include + +#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 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 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 GlobalConflicts(std::string_view source) +{ + std::string_view last_identifier = ""; + std::vector 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& 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::Create(const picojson::value& json_data, + std::shared_ptr library) +{ + std::vector pipeline_passes; + + const auto& passes_json = json_data.get("passes"); + if (passes_json.is()) + { + for (const auto& passes_json_val : passes_json.get()) + { + CustomPipelineAction::PipelinePassPassDescription pipeline_pass; + if (!passes_json_val.is()) + { + 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(); + 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()) + { + 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(std::move(library), std::move(pipeline_passes)); +} + +CustomPipelineAction::CustomPipelineAction( + std::shared_ptr library, + std::vector 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{ + 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{ + pass.m_pixel_shader.m_asset, pass.m_pixel_shader.m_asset->GetLastLoadedTime()}); + + m_texture_code_names.clear(); + std::vector> 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(&*property.m_value)) + { + auto asset = loader.LoadGameTexture(*value, m_library); + if (asset) + { + const auto loaded_time = asset->GetLastLoadedTime(); + game_assets.push_back(VideoCommon::CachedAsset{ + 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()); +} diff --git a/Source/Core/VideoCommon/GraphicsModSystem/Runtime/Actions/CustomPipelineAction.h b/Source/Core/VideoCommon/GraphicsModSystem/Runtime/Actions/CustomPipelineAction.h new file mode 100644 index 0000000000..6de0af8afe --- /dev/null +++ b/Source/Core/VideoCommon/GraphicsModSystem/Runtime/Actions/CustomPipelineAction.h @@ -0,0 +1,53 @@ +// Copyright 2022 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include + +#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 Create(const picojson::value& json_data, + std::string_view path); + CustomPipelineAction(std::shared_ptr library, + std::vector pass_descriptions); + ~CustomPipelineAction(); + void OnDrawStarted(GraphicsModActionData::DrawStarted*) override; + void OnTextureCreate(GraphicsModActionData::TextureCreate*) override; + +private: + std::shared_ptr m_library; + std::vector m_passes_config; + struct PipelinePass + { + VideoCommon::CachedAsset m_pixel_material; + VideoCommon::CachedAsset m_pixel_shader; + std::vector> m_game_textures; + }; + std::vector m_passes; + + ShaderCode m_last_generated_shader_code; + + bool m_valid = true; + + std::vector m_texture_code_names; +}; diff --git a/Source/Core/VideoCommon/GraphicsModSystem/Runtime/GraphicsModActionFactory.cpp b/Source/Core/VideoCommon/GraphicsModSystem/Runtime/GraphicsModActionFactory.cpp index 7b97155d8a..4055265e5a 100644 --- a/Source/Core/VideoCommon/GraphicsModSystem/Runtime/GraphicsModActionFactory.cpp +++ b/Source/Core/VideoCommon/GraphicsModSystem/Runtime/GraphicsModActionFactory.cpp @@ -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 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; }