diff --git a/CMakeLists.txt b/CMakeLists.txt index 42ed97d6..3edc054b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -179,6 +179,7 @@ set(SOURCE_FILES MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp + MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp diff --git a/MobileGL/Defines.h b/MobileGL/Defines.h index da952c78..ef81e8bb 100644 --- a/MobileGL/Defines.h +++ b/MobileGL/Defines.h @@ -34,7 +34,7 @@ #define MOBILEGL_EGL_API MOBILEGL_API // ====================== MobileGL configurations ======================= // -#define MOBILEGL_LOG_ACTIVE_LEVEL MOBILEGL_LOG_LEVEL_FATAL +#define MOBILEGL_LOG_ACTIVE_LEVEL MOBILEGL_LOG_LEVEL_DEBUG #define MOBILEGL_LOG_ENABLE_CONSOLE 0 #define MOBILEGL_LOG_ENABLE_FILE 1 diff --git a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp index f507e468..0fe3beb4 100644 --- a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp @@ -12,6 +12,7 @@ #include "Init.h" #include #include +#include #include #include @@ -97,6 +98,66 @@ void main() { fragColor = vec4(OutColor, 1.0); })"; +const char* daily_weather_variation_vs = R"(#version 150 + +struct DailyWeatherVariation { + vec2 clouds_cumulus_coverage; + vec2 clouds_altocumulus_coverage; + vec2 clouds_cirrus_coverage; + float clouds_cumulus_congestus_amount; + float clouds_stratus_amount; + float fogginess; + float aurora_amount; + float nlc_amount; + mat2x3 aurora_colors; +}; + +in vec4 Position; +out DailyWeatherVariation daily_weather_variation; + +DailyWeatherVariation get_daily_weather_variation() { + DailyWeatherVariation daily_weather_variation; + daily_weather_variation.clouds_cumulus_coverage = vec2(1.0, 2.0); + daily_weather_variation.clouds_altocumulus_coverage = vec2(3.0, 4.0); + daily_weather_variation.clouds_cirrus_coverage = vec2(5.0, 6.0); + daily_weather_variation.clouds_cumulus_congestus_amount = 7.0; + daily_weather_variation.clouds_stratus_amount = 8.0; + daily_weather_variation.fogginess = 9.0; + daily_weather_variation.aurora_amount = 10.0; + daily_weather_variation.nlc_amount = 11.0; + daily_weather_variation.aurora_colors = mat2x3(vec3(12.0, 13.0, 14.0), vec3(15.0, 16.0, 17.0)); + return daily_weather_variation; +} + +void main() { + gl_Position = Position; + daily_weather_variation = get_daily_weather_variation(); +})"; + +const char* daily_weather_variation_fs = R"(#version 150 + +struct DailyWeatherVariation { + vec2 clouds_cumulus_coverage; + vec2 clouds_altocumulus_coverage; + vec2 clouds_cirrus_coverage; + float clouds_cumulus_congestus_amount; + float clouds_stratus_amount; + float fogginess; + float aurora_amount; + float nlc_amount; + mat2x3 aurora_colors; +}; + +in DailyWeatherVariation daily_weather_variation; +out vec4 fragColor; + +void main() { + vec3 aurora = daily_weather_variation.aurora_colors[1]; + DailyWeatherVariation variation = daily_weather_variation; + vec2 coverage = variation.clouds_cumulus_coverage + daily_weather_variation.clouds_altocumulus_coverage; + fragColor = vec4(coverage, aurora.x + variation.aurora_amount, 1.0); +})"; + TEST_F(ProgramUtilTest, CompileSimpleFragmentShader) { using namespace MG_Util::ShaderTranspiler; ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fs}; @@ -351,6 +412,110 @@ TEST_F(ProgramUtilTest, DecompProgram) { } } +TEST_F(ProgramUtilTest, FlattenDailyWeatherVariationInterfaceInSpirvPass) { + using namespace MG_Util::ShaderTranspiler; + + String vsSource = daily_weather_variation_vs; + String fsSource = daily_weather_variation_fs; + PreprocessShaderSource(ShaderStage::Vertex, vsSource); + PreprocessShaderSource(ShaderStage::Fragment, fsSource); + + ShaderAttrib vsAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = vsSource}; + auto vsRes = ShaderCompiler::CompileShader(vsAttrib); + if (!vsRes) { + ASSERT_NE(vsRes.error().errc, 0); + FAIL() << "errc: " << vsRes.error().errc << "\nlog: " << vsRes.error().log; + } + + ShaderAttrib fsAttrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fsSource}; + auto fsRes = ShaderCompiler::CompileShader(fsAttrib); + if (!fsRes) { + ASSERT_NE(fsRes.error().errc, 0); + FAIL() << "errc: " << fsRes.error().errc << "\nlog: " << fsRes.error().log; + } + + ProgramAttrib programAttrib{.shaders = {vsRes.value(), fsRes.value()}}; + auto programRes = ShaderCompiler::LinkProgram(programAttrib); + if (!programRes) { + ASSERT_NE(programRes.error().errc, 0); + FAIL() << "errc: " << programRes.error().errc << "\nlog: " << programRes.error().log; + } + + ProgramBinaryAttrib binaryAttrib{ + .shaderTypes = {GL_VERTEX_SHADER, GL_FRAGMENT_SHADER}, + .program = *programRes.value(), + }; + auto binRes = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib); + ASSERT_TRUE(binRes.has_value()); + + Vector> optimizedSpirvs; + optimizedSpirvs.reserve(binRes->size()); + for (const auto& spirv : binRes.value()) { + Vector optimized; + ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(spirv, optimized)); + optimizedSpirvs.push_back(std::move(optimized)); + } + + Vector sessions(optimizedSpirvs.size()); + for (SizeT i = 0; i < optimizedSpirvs.size(); ++i) { + sessions[i] = SpvcSession(optimizedSpirvs[i], SessionUsageBit::Transpile); + } + + auto vertexSource = ShaderCompiler::DecompileShader(sessions[0]); + auto fragmentSource = ShaderCompiler::DecompileShader(sessions[1]); + ASSERT_TRUE(vertexSource.has_value()); + ASSERT_TRUE(fragmentSource.has_value()); + + EXPECT_EQ(vertexSource->find("out DailyWeatherVariation "), std::string::npos); + EXPECT_EQ(fragmentSource->find("in DailyWeatherVariation "), std::string::npos); + EXPECT_NE(vertexSource->find("daily_weather_variation_clouds_cumulus_coverage"), std::string::npos); + EXPECT_NE(vertexSource->find("daily_weather_variation_aurora_colors"), std::string::npos); + EXPECT_NE(fragmentSource->find("daily_weather_variation_clouds_altocumulus_coverage"), std::string::npos); + EXPECT_NE(fragmentSource->find("daily_weather_variation_aurora_colors"), std::string::npos); + + const struct ExpectedInterface { + const char* name; + uint32_t location; + } expectedInterfaces[] = { + {"daily_weather_variation_clouds_cumulus_coverage", 0}, + {"daily_weather_variation_clouds_altocumulus_coverage", 1}, + {"daily_weather_variation_clouds_cirrus_coverage", 2}, + {"daily_weather_variation_clouds_cumulus_congestus_amount", 3}, + {"daily_weather_variation_clouds_stratus_amount", 4}, + {"daily_weather_variation_fogginess", 5}, + {"daily_weather_variation_aurora_amount", 6}, + {"daily_weather_variation_nlc_amount", 7}, + {"daily_weather_variation_aurora_colors", 8}, + }; + + const auto vsOutputs = sessions[0].GetShaderInterface(SPVC_RESOURCE_TYPE_STAGE_OUTPUT); + const auto fsInputs = sessions[1].GetShaderInterface(SPVC_RESOURCE_TYPE_STAGE_INPUT); + ASSERT_EQ(vsOutputs.size(), std::size(expectedInterfaces)); + ASSERT_EQ(fsInputs.size(), std::size(expectedInterfaces)); + + for (const auto& expected : expectedInterfaces) { + bool foundVertex = false; + for (const auto& output : vsOutputs) { + if (output.name == expected.name) { + foundVertex = true; + EXPECT_EQ(output.location, expected.location); + break; + } + } + EXPECT_TRUE(foundVertex) << "missing vertex output: " << expected.name; + + bool foundFragment = false; + for (const auto& input : fsInputs) { + if (input.name == expected.name) { + foundFragment = true; + EXPECT_EQ(input.location, expected.location); + break; + } + } + EXPECT_TRUE(foundFragment) << "missing fragment input: " << expected.name; + } +} + const char* blit_vs = R"(#version 460 core in vec3 Position; diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index a17d68e3..38e38c77 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -9,6 +9,7 @@ #include "ShaderCompiler.h" #include "SpirvPasses/EliminateFloatEqualsZeroPass.h" +#include "SpirvPasses/FlattenInterfaceStructPass.h" #include "spirv-tools/libspirv.h" #include "spirv-tools/optimizer.hpp" @@ -234,6 +235,7 @@ namespace MobileGL { Optimizer optimizer(SPV_ENV_VULKAN_1_1); + optimizer.RegisterPass(FlattenInterfaceStructPass::CreateFlattenInterfaceStructPass()); optimizer.RegisterPass(EliminateFloatEqualsZeroPass::CreateEliminateFloatEqualsZeroPass()); return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp index d4023cef..e764b618 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp @@ -13,253 +13,10 @@ namespace { using MobileGL::SizeT; - struct FlattenedVaryingMember { - const char* typeName; - const char* memberName; - }; - - constexpr FlattenedVaryingMember kDailyWeatherVariationMembers[] = { - {"vec2", "clouds_cumulus_coverage"}, - {"vec2", "clouds_altocumulus_coverage"}, - {"vec2", "clouds_cirrus_coverage"}, - {"float", "clouds_cumulus_congestus_amount"}, - {"float", "clouds_stratus_amount"}, - {"float", "fogginess"}, - {"float", "aurora_amount"}, - {"float", "nlc_amount"}, - {"mat2x3", "aurora_colors"}, - }; - bool IsIdentifierChar(char ch) { return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '_'; } - bool HasIdentifierBoundaries(const MobileGL::String& source, SizeT pos, SizeT length) { - const bool hasLeftBoundary = pos == 0 || !IsIdentifierChar(source[pos - 1]); - const SizeT end = pos + length; - const bool hasRightBoundary = end >= source.size() || !IsIdentifierChar(source[end]); - return hasLeftBoundary && hasRightBoundary; - } - - SizeT FindToken(const MobileGL::String& source, const MobileGL::String& token, SizeT start = 0) { - SizeT pos = start; - while ((pos = source.find(token, pos)) != MobileGL::String::npos) { - if (HasIdentifierBoundaries(source, pos, token.size())) { - return pos; - } - pos += token.size(); - } - return MobileGL::String::npos; - } - - void ReplaceTokenOccurrencesInRange(MobileGL::String& source, SizeT rangeStart, SizeT rangeEnd, - const MobileGL::String& from, const MobileGL::String& to) { - SizeT pos = rangeStart; - while ((pos = source.find(from, pos)) != MobileGL::String::npos && pos < rangeEnd) { - if (!HasIdentifierBoundaries(source, pos, from.size())) { - pos += from.size(); - continue; - } - - source.replace(pos, from.size(), to); - const auto delta = static_cast(to.size()) - static_cast(from.size()); - rangeEnd = static_cast(static_cast(rangeEnd) + delta); - pos += to.size(); - } - } - - void ReplaceAll(MobileGL::String& source, const MobileGL::String& from, const MobileGL::String& to) { - SizeT pos = 0; - while ((pos = source.find(from, pos)) != MobileGL::String::npos) { - source.replace(pos, from.size(), to); - pos += to.size(); - } - } - - MobileGL::String TrimWhitespace(const MobileGL::String& input) { - SizeT begin = 0; - while (begin < input.size() && std::isspace(static_cast(input[begin]))) { - begin++; - } - - SizeT end = input.size(); - while (end > begin && std::isspace(static_cast(input[end - 1]))) { - end--; - } - - return input.substr(begin, end - begin); - } - - bool FindFunctionBody(const MobileGL::String& source, const MobileGL::String& signature, SizeT* bodyStart, - SizeT* bodyEnd) { - const SizeT signaturePos = source.find(signature); - if (signaturePos == MobileGL::String::npos) { - return false; - } - - const SizeT bracePos = source.find('{', signaturePos + signature.size()); - if (bracePos == MobileGL::String::npos) { - return false; - } - - int depth = 1; - for (SizeT pos = bracePos + 1; pos < source.size(); pos++) { - if (source[pos] == '{') { - depth++; - } else if (source[pos] == '}') { - depth--; - if (depth == 0) { - *bodyStart = bracePos + 1; - *bodyEnd = pos; - return true; - } - } - } - - return false; - } - - void RenameDailyWeatherVariationHelperLocal(MobileGL::String& source) { - constexpr const char* kHelperSignature = "DailyWeatherVariation get_daily_weather_variation()"; - constexpr const char* kInterfaceName = "daily_weather_variation"; - constexpr const char* kLocalName = "mg_daily_weather_variation_local"; - - SizeT bodyStart = 0; - SizeT bodyEnd = 0; - if (!FindFunctionBody(source, kHelperSignature, &bodyStart, &bodyEnd)) { - return; - } - - ReplaceTokenOccurrencesInRange(source, bodyStart, bodyEnd, kInterfaceName, kLocalName); - } - - bool RewriteDailyWeatherVariationInterface(MobileGL::ShaderStage stage, MobileGL::String& source) { - using MobileGL::ShaderStage; - - if (stage != ShaderStage::Vertex && stage != ShaderStage::Fragment) { - return false; - } - - constexpr const char* kTypeName = "DailyWeatherVariation"; - constexpr const char* kInterfaceName = "daily_weather_variation"; - constexpr const char* kTempName = "mg_daily_weather_variation_tmp"; - const MobileGL::String declarationNeedle = MobileGL::String(kTypeName) + " " + kInterfaceName + ";"; - - RenameDailyWeatherVariationHelperLocal(source); - - const SizeT declarationPos = source.find(declarationNeedle); - if (declarationPos == MobileGL::String::npos) { - return false; - } - - SizeT lineStart = source.rfind('\n', declarationPos); - lineStart = (lineStart == MobileGL::String::npos) ? 0 : lineStart + 1; - SizeT lineEnd = source.find('\n', declarationPos); - if (lineEnd == MobileGL::String::npos) { - lineEnd = source.size(); - } - - const MobileGL::String declarationLine = source.substr(lineStart, lineEnd - lineStart); - MOBILEGL_ASSERT(declarationLine.find("layout(") == MobileGL::String::npos, - "PreprocessShaderSource: unexpected explicit layout on DailyWeatherVariation interface in stage=%d", - static_cast(stage)); - - const bool hasInputQualifier = declarationLine.find(" in ") != MobileGL::String::npos || - declarationLine.rfind("in ", 0) == 0; - const bool hasOutputQualifier = declarationLine.find(" out ") != MobileGL::String::npos || - declarationLine.rfind("out ", 0) == 0; - MOBILEGL_ASSERT(hasInputQualifier != hasOutputQualifier, - "PreprocessShaderSource: expected a single in/out qualifier on DailyWeatherVariation interface in stage=%d line='%s'", - static_cast(stage), declarationLine.c_str()); - - const MobileGL::String qualifierPrefix = source.substr(lineStart, declarationPos - lineStart); - MobileGL::String replacementDeclaration; - for (const auto& member : kDailyWeatherVariationMembers) { - replacementDeclaration += qualifierPrefix; - replacementDeclaration += member.typeName; - replacementDeclaration += " "; - replacementDeclaration += kInterfaceName; - replacementDeclaration += "_"; - replacementDeclaration += member.memberName; - replacementDeclaration += ";\n"; - } - source.replace(lineStart, lineEnd - lineStart + (lineEnd < source.size() ? 1 : 0), replacementDeclaration); - - SizeT assignPos = FindToken(source, kInterfaceName); - while (assignPos != MobileGL::String::npos) { - SizeT probe = assignPos + strlen(kInterfaceName); - while (probe < source.size() && std::isspace(static_cast(source[probe]))) { - probe++; - } - - if (probe >= source.size() || source[probe] != '=') { - assignPos = FindToken(source, kInterfaceName, assignPos + strlen(kInterfaceName)); - continue; - } - - SizeT statementStart = source.rfind('\n', assignPos); - statementStart = (statementStart == MobileGL::String::npos) ? 0 : statementStart + 1; - for (SizeT i = statementStart; i < assignPos; i++) { - MOBILEGL_ASSERT(std::isspace(static_cast(source[i])), - "PreprocessShaderSource: unexpected inline DailyWeatherVariation assignment in stage=%d", - static_cast(stage)); - } - - const MobileGL::String indentation = source.substr(statementStart, assignPos - statementStart); - const SizeT statementEnd = source.find(';', probe); - MOBILEGL_ASSERT(statementEnd != MobileGL::String::npos, - "PreprocessShaderSource: missing ';' after DailyWeatherVariation assignment in stage=%d", - static_cast(stage)); - - const MobileGL::String rhsExpression = TrimWhitespace(source.substr(probe + 1, statementEnd - probe - 1)); - MobileGL::String replacementStatement; - replacementStatement += indentation; - replacementStatement += "{\n"; - replacementStatement += indentation; - replacementStatement += " DailyWeatherVariation "; - replacementStatement += kTempName; - replacementStatement += " = "; - replacementStatement += rhsExpression; - replacementStatement += ";\n"; - for (const auto& member : kDailyWeatherVariationMembers) { - replacementStatement += indentation; - replacementStatement += " "; - replacementStatement += kInterfaceName; - replacementStatement += "_"; - replacementStatement += member.memberName; - replacementStatement += " = "; - replacementStatement += kTempName; - replacementStatement += "."; - replacementStatement += member.memberName; - replacementStatement += ";\n"; - } - replacementStatement += indentation; - replacementStatement += "}"; - if (statementEnd + 1 < source.size() && source[statementEnd + 1] == '\n') { - replacementStatement += "\n"; - source.replace(statementStart, statementEnd - statementStart + 2, replacementStatement); - } else { - source.replace(statementStart, statementEnd - statementStart + 1, replacementStatement); - } - - assignPos = FindToken(source, kInterfaceName, statementStart + replacementStatement.size()); - } - - for (const auto& member : kDailyWeatherVariationMembers) { - const MobileGL::String from = MobileGL::String(kInterfaceName) + "." + member.memberName; - const MobileGL::String to = MobileGL::String(kInterfaceName) + "_" + member.memberName; - ReplaceAll(source, from, to); - } - - MOBILEGL_ASSERT(source.find(MobileGL::String(kTypeName) + " " + kInterfaceName + ";") == MobileGL::String::npos, - "PreprocessShaderSource: unrewritten DailyWeatherVariation interface declaration remained in stage=%d", - static_cast(stage)); - MOBILEGL_ASSERT(source.find(MobileGL::String(kInterfaceName) + ".") == MobileGL::String::npos, - "PreprocessShaderSource: unrewritten DailyWeatherVariation member access remained in stage=%d", - static_cast(stage)); - return true; - } - bool HasSingleLineFunctionDefinition(const MobileGL::String& source, const MobileGL::String& functionName) { SizeT lineStart = 0; while (lineStart < source.size()) { @@ -351,8 +108,13 @@ namespace MobileGL { SizeT linedirPos = source.find("#line"); while (linedirPos != String::npos) { SizeT newlinePos = source.find('\n', linedirPos); - // + length of "\n" - source = source.replace(linedirPos, newlinePos - linedirPos + 1, ""); + if (newlinePos == String::npos) { + source.erase(linedirPos); + break; + } + + // Preserve a line break so adjacent preprocessor directives do not merge. + source = source.replace(linedirPos, newlinePos - linedirPos + 1, "\n"); linedirPos = source.find("#line", linedirPos); } @@ -407,8 +169,6 @@ namespace MobileGL { RenameBuiltinShadowingFunction(source, "round", "mg_round"); RenameBuiltinShadowingFunction(source, "tanh", "mg_tanh"); RenameBuiltinShadowingFunction(source, "fma", "mg_fma"); - - RewriteDailyWeatherVariationInterface(stage, source); } } // namespace ShaderTranspiler diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp new file mode 100644 index 00000000..59d170a0 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp @@ -0,0 +1,528 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "FlattenInterfaceStructPass.h" + +#include "source/opt/def_use_manager.h" +#include "source/opt/instruction.h" +#include "source/opt/ir_context.h" +#include "source/opt/module.h" +#include "source/opt/type_manager.h" +#include "source/util/string_utils.h" +#include "spirv.hpp" + +#include +#include +#include +#include + +namespace { + using spvtools::opt::IRContext; + using spvtools::opt::Instruction; + using spvtools::opt::Operand; + + constexpr const char* kTargetTypeName = "DailyWeatherVariation"; + constexpr const char* kTargetInterfaceName = "daily_weather_variation"; + + struct FlattenedMemberSpec { + const char* name; + uint32_t locationSpan; + }; + + constexpr std::array kFlattenedMembers = {{ + {"clouds_cumulus_coverage", 1}, + {"clouds_altocumulus_coverage", 1}, + {"clouds_cirrus_coverage", 1}, + {"clouds_cumulus_congestus_amount", 1}, + {"clouds_stratus_amount", 1}, + {"fogginess", 1}, + {"aurora_amount", 1}, + {"nlc_amount", 1}, + {"aurora_colors", 2}, + }}; + + struct FlattenedMemberVariable { + uint32_t memberIndex; + uint32_t typeId; + uint32_t location; + Instruction* variable; + }; + + bool HasExactName(IRContext* context, uint32_t id, const char* expected) { + auto names = context->GetNames(id); + for (auto it = names.begin(); it != names.end(); ++it) { + Instruction* const nameInst = it->second; + if (nameInst->opcode() == spv::Op::OpName && nameInst->GetInOperand(1).AsString() == expected) { + return true; + } + } + return false; + } + + bool HasExactMemberName(IRContext* context, uint32_t structTypeId, uint32_t memberIndex, const char* expected) { + Instruction* const nameInst = context->GetMemberName(structTypeId, memberIndex); + return nameInst != nullptr && nameInst->GetInOperand(2).AsString() == expected; + } + + bool IsFloat32Type(const Instruction* typeInst) { + return typeInst != nullptr && typeInst->opcode() == spv::Op::OpTypeFloat && + typeInst->GetSingleWordInOperand(0) == 32; + } + + bool IsFloatVector(const Instruction* typeInst, spvtools::opt::analysis::DefUseManager* defUseMgr, + uint32_t componentCount) { + if (typeInst == nullptr || typeInst->opcode() != spv::Op::OpTypeVector) { + return false; + } + + if (typeInst->GetSingleWordInOperand(1) != componentCount) { + return false; + } + + return IsFloat32Type(defUseMgr->GetDef(typeInst->GetSingleWordInOperand(0))); + } + + bool IsMat2x3Float(const Instruction* typeInst, spvtools::opt::analysis::DefUseManager* defUseMgr) { + if (typeInst == nullptr || typeInst->opcode() != spv::Op::OpTypeMatrix) { + return false; + } + + if (typeInst->GetSingleWordInOperand(1) != 2) { + return false; + } + + return IsFloatVector(defUseMgr->GetDef(typeInst->GetSingleWordInOperand(0)), defUseMgr, 3); + } + + bool MatchesMemberType(const Instruction* typeInst, spvtools::opt::analysis::DefUseManager* defUseMgr, + size_t memberIndex) { + switch (memberIndex) { + case 0: + case 1: + case 2: + return IsFloatVector(typeInst, defUseMgr, 2); + case 3: + case 4: + case 5: + case 6: + case 7: + return IsFloat32Type(typeInst); + case 8: + return IsMat2x3Float(typeInst, defUseMgr); + default: + return false; + } + } + + bool MatchesTargetStruct(IRContext* context, spvtools::opt::analysis::DefUseManager* defUseMgr, + const Instruction* typeInst) { + if (typeInst == nullptr || typeInst->opcode() != spv::Op::OpTypeStruct || + typeInst->NumInOperands() != kFlattenedMembers.size()) { + return false; + } + + if (!HasExactName(context, typeInst->result_id(), kTargetTypeName)) { + return false; + } + + for (size_t memberIndex = 0; memberIndex < kFlattenedMembers.size(); ++memberIndex) { + const uint32_t memberTypeId = typeInst->GetSingleWordInOperand(static_cast(memberIndex)); + if (!MatchesMemberType(defUseMgr->GetDef(memberTypeId), defUseMgr, memberIndex)) { + return false; + } + + if (!HasExactMemberName(context, typeInst->result_id(), static_cast(memberIndex), + kFlattenedMembers[memberIndex].name)) { + return false; + } + } + + return true; + } + + Instruction* GetPointeeType(spvtools::opt::analysis::DefUseManager* defUseMgr, const Instruction* variable) { + Instruction* const pointerType = defUseMgr->GetDef(variable->type_id()); + if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer) { + return nullptr; + } + + return defUseMgr->GetDef(pointerType->GetSingleWordInOperand(1)); + } + + bool ValidateAccessChainUse(IRContext* context, spvtools::opt::analysis::DefUseManager* defUseMgr, + const Instruction* accessChain) { + if (accessChain->NumInOperands() < 2) { + return false; + } + + const Instruction* const indexInst = defUseMgr->GetDef(accessChain->GetSingleWordInOperand(1)); + const auto* const indexConst = context->get_constant_mgr()->GetConstantFromInst(indexInst); + if (indexConst == nullptr) { + return false; + } + + const int64_t memberIndex = indexConst->GetSignExtendedValue(); + return memberIndex >= 0 && memberIndex < static_cast(kFlattenedMembers.size()); + } + + int64_t GetAccessChainMemberIndex(IRContext* context, spvtools::opt::analysis::DefUseManager* defUseMgr, + const Instruction* accessChain) { + const Instruction* const indexInst = defUseMgr->GetDef(accessChain->GetSingleWordInOperand(1)); + return context->get_constant_mgr()->GetConstantFromInst(indexInst)->GetSignExtendedValue(); + } + + void AddName(IRContext* context, uint32_t id, const std::string& name) { + context->AddDebug2Inst(spvtools::MakeUnique( + context, spv::Op::OpName, 0, 0, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {id}}, + {SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector(name)}})); + } + + void AddLocationDecoration(IRContext* context, uint32_t id, uint32_t location) { + context->AddAnnotationInst(spvtools::MakeUnique( + context, spv::Op::OpDecorate, 0, 0, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {id}}, + {SPV_OPERAND_TYPE_DECORATION, + {static_cast(spv::Decoration::Location)}}, + {SPV_OPERAND_TYPE_LITERAL_INTEGER, {location}}})); + } + + std::optional GetVariableDecorationLiteral(IRContext* context, uint32_t id, spv::Decoration kind) { + for (Instruction* decoration : context->get_decoration_mgr()->GetDecorationsFor(id, false)) { + if (decoration->opcode() != spv::Op::OpDecorate || + static_cast(decoration->GetSingleWordInOperand(1)) != kind || + decoration->NumInOperands() < 3) { + continue; + } + + return decoration->GetSingleWordInOperand(2); + } + + return std::nullopt; + } + + std::optional GetMemberDecorationLiteral(IRContext* context, uint32_t structTypeId, uint32_t memberIndex, + spv::Decoration kind) { + for (Instruction* decoration : context->get_decoration_mgr()->GetDecorationsFor(structTypeId, false)) { + if (decoration->opcode() != spv::Op::OpMemberDecorate || decoration->GetSingleWordInOperand(1) != memberIndex || + static_cast(decoration->GetSingleWordInOperand(2)) != kind || + decoration->NumInOperands() < 4) { + continue; + } + + return decoration->GetSingleWordInOperand(3); + } + + return std::nullopt; + } + + void CloneVariableDecorations(IRContext* context, const Instruction* source, Instruction* target) { + for (Instruction* decoration : context->get_decoration_mgr()->GetDecorationsFor(source->result_id(), false)) { + if (decoration->opcode() != spv::Op::OpDecorate) { + continue; + } + + if (static_cast(decoration->GetSingleWordInOperand(1)) == spv::Decoration::Location) { + continue; + } + + std::unique_ptr clone(decoration->Clone(context)); + clone->SetInOperand(0, {target->result_id()}); + context->AddAnnotationInst(std::move(clone)); + } + } + + void CloneMemberDecorations(IRContext* context, const Instruction* structType, uint32_t memberIndex, + Instruction* target) { + for (Instruction* decoration : context->get_decoration_mgr()->GetDecorationsFor(structType->result_id(), false)) { + if (decoration->opcode() != spv::Op::OpMemberDecorate || decoration->GetSingleWordInOperand(1) != memberIndex) { + continue; + } + + const auto kind = static_cast(decoration->GetSingleWordInOperand(2)); + if (kind == spv::Decoration::Location) { + continue; + } + + auto newDecoration = spvtools::MakeUnique( + context, spv::Op::OpDecorate, 0, 0, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {target->result_id()}}, + {SPV_OPERAND_TYPE_DECORATION, {static_cast(kind)}}}); + for (uint32_t operandIndex = 3; operandIndex < decoration->NumInOperands(); ++operandIndex) { + newDecoration->AddOperand(Operand(decoration->GetInOperand(operandIndex))); + } + context->AddAnnotationInst(std::move(newDecoration)); + } + } + + Instruction* CreateFlattenedVariable(IRContext* context, spvtools::opt::analysis::DefUseManager* defUseMgr, + spvtools::opt::analysis::TypeManager* typeMgr, + const Instruction* sourceVariable, const Instruction* structType, + uint32_t memberIndex, uint32_t location) { + const auto storageClass = static_cast(sourceVariable->GetSingleWordInOperand(0)); + const uint32_t memberTypeId = structType->GetSingleWordInOperand(memberIndex); + const uint32_t pointerTypeId = typeMgr->FindPointerToType(memberTypeId, storageClass); + const uint32_t resultId = context->module()->TakeNextIdBound(); + + auto variable = spvtools::MakeUnique( + context, spv::Op::OpVariable, pointerTypeId, resultId, + std::initializer_list{{SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast(storageClass)}}}); + Instruction* const variablePtr = variable.get(); + context->AddGlobalValue(std::move(variable)); + context->AnalyzeDefUse(variablePtr); + + CloneVariableDecorations(context, sourceVariable, variablePtr); + CloneMemberDecorations(context, structType, memberIndex, variablePtr); + AddLocationDecoration(context, resultId, location); + + std::string name = kTargetInterfaceName; + name += "_"; + name += kFlattenedMembers[memberIndex].name; + AddName(context, resultId, name); + + return variablePtr; + } + + bool ReplaceAccessChain(IRContext* context, spvtools::opt::analysis::DefUseManager* defUseMgr, + Instruction* accessChain, const std::vector& replacements) { + const int64_t memberIndex = GetAccessChainMemberIndex(context, defUseMgr, accessChain); + const Instruction* const replacementVar = replacements[static_cast(memberIndex)].variable; + + if (accessChain->NumInOperands() > 2) { + const uint32_t replacementId = context->module()->TakeNextIdBound(); + auto replacementChain = spvtools::MakeUnique( + context, accessChain->opcode(), accessChain->type_id(), replacementId, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {replacementVar->result_id()}}}); + + for (uint32_t operandIndex = 2; operandIndex < accessChain->NumInOperands(); ++operandIndex) { + replacementChain->AddOperand(Operand(accessChain->GetInOperand(operandIndex))); + } + + replacementChain->UpdateDebugInfoFrom(accessChain); + auto insertPoint = spvtools::opt::BasicBlock::iterator(accessChain).InsertBefore(std::move(replacementChain)); + context->AnalyzeDefUse(&*insertPoint); + context->set_instr_block(&*insertPoint, context->get_instr_block(accessChain)); + context->ReplaceAllUsesWith(accessChain->result_id(), replacementId); + } else { + context->ReplaceAllUsesWith(accessChain->result_id(), replacementVar->result_id()); + } + + context->KillNamesAndDecorates(accessChain->result_id()); + context->KillInst(accessChain); + return true; + } + + bool ReplaceWholeLoad(IRContext* context, Instruction* load, + const std::vector& replacements) { + spvtools::opt::BasicBlock* const block = context->get_instr_block(load); + std::vector memberLoads; + memberLoads.reserve(replacements.size()); + + auto where = spvtools::opt::BasicBlock::iterator(load); + for (const auto& replacement : replacements) { + const uint32_t loadId = context->module()->TakeNextIdBound(); + auto memberLoad = spvtools::MakeUnique( + context, spv::Op::OpLoad, replacement.typeId, loadId, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {replacement.variable->result_id()}}}); + for (uint32_t operandIndex = 1; operandIndex < load->NumInOperands(); ++operandIndex) { + memberLoad->AddOperand(Operand(load->GetInOperand(operandIndex))); + } + where = where.InsertBefore(std::move(memberLoad)); + where->UpdateDebugInfoFrom(load); + context->AnalyzeDefUse(&*where); + context->set_instr_block(&*where, block); + memberLoads.push_back(&*where); + } + + const uint32_t compositeId = context->module()->TakeNextIdBound(); + auto compositeConstruct = spvtools::MakeUnique( + context, spv::Op::OpCompositeConstruct, load->type_id(), compositeId, std::initializer_list{}); + for (Instruction* memberLoad : memberLoads) { + compositeConstruct->AddOperand({SPV_OPERAND_TYPE_ID, {memberLoad->result_id()}}); + } + + where = spvtools::opt::BasicBlock::iterator(load).InsertBefore(std::move(compositeConstruct)); + where->UpdateDebugInfoFrom(load); + context->AnalyzeDefUse(&*where); + context->set_instr_block(&*where, block); + context->ReplaceAllUsesWith(load->result_id(), compositeId); + context->KillNamesAndDecorates(load->result_id()); + context->KillInst(load); + return true; + } + + bool ReplaceWholeStore(IRContext* context, Instruction* store, + const std::vector& replacements) { + spvtools::opt::BasicBlock* const block = context->get_instr_block(store); + auto where = spvtools::opt::BasicBlock::iterator(store); + const uint32_t storedValueId = store->GetSingleWordInOperand(1); + + for (const auto& replacement : replacements) { + const uint32_t extractId = context->module()->TakeNextIdBound(); + auto extract = spvtools::MakeUnique( + context, spv::Op::OpCompositeExtract, replacement.typeId, extractId, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {storedValueId}}, + {SPV_OPERAND_TYPE_LITERAL_INTEGER, {replacement.memberIndex}}}); + auto iter = where.InsertBefore(std::move(extract)); + iter->UpdateDebugInfoFrom(store); + context->AnalyzeDefUse(&*iter); + context->set_instr_block(&*iter, block); + + auto memberStore = spvtools::MakeUnique( + context, spv::Op::OpStore, 0, 0, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {replacement.variable->result_id()}}, + {SPV_OPERAND_TYPE_ID, {extractId}}}); + for (uint32_t operandIndex = 2; operandIndex < store->NumInOperands(); ++operandIndex) { + memberStore->AddOperand(Operand(store->GetInOperand(operandIndex))); + } + iter = where.InsertBefore(std::move(memberStore)); + iter->UpdateDebugInfoFrom(store); + context->AnalyzeDefUse(&*iter); + context->set_instr_block(&*iter, block); + } + + context->KillInst(store); + return true; + } + + void RewriteEntryPoints(Instruction* targetVariable, const std::vector& replacements, + spvtools::opt::Module* module) { + for (Instruction& entryPoint : module->entry_points()) { + bool replaced = false; + std::vector newOperands; + newOperands.reserve(entryPoint.NumInOperands() + replacements.size()); + for (uint32_t operandIndex = 0; operandIndex < entryPoint.NumInOperands(); ++operandIndex) { + if (operandIndex < 3) { + newOperands.push_back(entryPoint.GetInOperand(operandIndex)); + continue; + } + + if (entryPoint.GetSingleWordInOperand(operandIndex) != targetVariable->result_id()) { + newOperands.push_back(entryPoint.GetInOperand(operandIndex)); + continue; + } + + replaced = true; + for (const auto& replacement : replacements) { + newOperands.push_back({SPV_OPERAND_TYPE_ID, {replacement.variable->result_id()}}); + } + } + + if (replaced) { + entryPoint.SetInOperands(std::move(newOperands)); + } + } + } +} // namespace + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + spvtools::opt::Pass::Status FlattenInterfaceStructPass::Process() { + using namespace spvtools; + using namespace spvtools::opt; + + IRContext* const irContext = context(); + analysis::DefUseManager* const defUseMgr = irContext->get_def_use_mgr(); + analysis::TypeManager* const typeMgr = irContext->get_type_mgr(); + + Instruction* targetVariable = nullptr; + Instruction* targetStructType = nullptr; + for (Instruction& globalInst : get_module()->types_values()) { + if (globalInst.opcode() != spv::Op::OpVariable) { + continue; + } + + const auto storageClass = static_cast(globalInst.GetSingleWordInOperand(0)); + if (storageClass != spv::StorageClass::Input && storageClass != spv::StorageClass::Output) { + continue; + } + + Instruction* const pointeeType = GetPointeeType(defUseMgr, &globalInst); + if (!MatchesTargetStruct(irContext, defUseMgr, pointeeType) || + !HasExactName(irContext, globalInst.result_id(), kTargetInterfaceName)) { + continue; + } + + targetVariable = &globalInst; + targetStructType = pointeeType; + break; + } + + if (targetVariable == nullptr || targetStructType == nullptr) { + return Status::SuccessWithoutChange; + } + + std::vector directAccessChains; + std::vector directLoads; + std::vector directStores; + const bool supportedUsers = defUseMgr->WhileEachUser(targetVariable, [&](Instruction* user) { + switch (user->opcode()) { + case spv::Op::OpAccessChain: + case spv::Op::OpInBoundsAccessChain: + directAccessChains.push_back(user); + return ValidateAccessChainUse(irContext, defUseMgr, user); + case spv::Op::OpLoad: + directLoads.push_back(user); + return true; + case spv::Op::OpStore: + directStores.push_back(user); + return true; + case spv::Op::OpName: + case spv::Op::OpDecorate: + case spv::Op::OpEntryPoint: + return true; + default: + return false; + } + }); + if (!supportedUsers) { + return Status::SuccessWithoutChange; + } + + std::vector replacements; + replacements.reserve(kFlattenedMembers.size()); + uint32_t locationCursor = + GetVariableDecorationLiteral(irContext, targetVariable->result_id(), spv::Decoration::Location) + .value_or(0u); + for (uint32_t memberIndex = 0; memberIndex < static_cast(kFlattenedMembers.size()); ++memberIndex) { + const uint32_t location = GetMemberDecorationLiteral(irContext, targetStructType->result_id(), memberIndex, + spv::Decoration::Location) + .value_or(locationCursor); + Instruction* const memberVar = CreateFlattenedVariable( + irContext, defUseMgr, typeMgr, targetVariable, targetStructType, memberIndex, location); + replacements.push_back({memberIndex, targetStructType->GetSingleWordInOperand(memberIndex), location, + memberVar}); + locationCursor = location + kFlattenedMembers[memberIndex].locationSpan; + } + + RewriteEntryPoints(targetVariable, replacements, get_module()); + + for (Instruction* accessChain : directAccessChains) { + ReplaceAccessChain(irContext, defUseMgr, accessChain, replacements); + } + + for (Instruction* load : directLoads) { + ReplaceWholeLoad(irContext, load, replacements); + } + + for (Instruction* store : directStores) { + ReplaceWholeStore(irContext, store, replacements); + } + + irContext->KillNamesAndDecorates(targetVariable->result_id()); + irContext->KillInst(targetVariable); + return Status::SuccessWithChange; + } + + spvtools::Optimizer::PassToken FlattenInterfaceStructPass::CreateFlattenInterfaceStructPass() { + return spvtools::Optimizer::PassToken(MakeUnique()); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL \ No newline at end of file diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.h new file mode 100644 index 00000000..e658333a --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.h @@ -0,0 +1,29 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#pragma once +#include "source/opt/pass.h" +#include "spirv-tools/optimizer.hpp" + +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + // This is a custom SPIR-V optimization pass that flattens the "DailyWeatherVariation" interface struct into separate variables. + // This is actually a workaround for Adreno drivers that fail to work with struct as an interface block. Mali drivers don't have this issue, but we want to keep the shader code consistent across platforms. + class FlattenInterfaceStructPass : public spvtools::opt::Pass { + public: + const char* name() const override { return "flatten-interface-struct"; } + Status Process() override; + + static spvtools::Optimizer::PassToken CreateFlattenInterfaceStructPass(); + }; + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL \ No newline at end of file