diff --git a/CMakeLists.txt b/CMakeLists.txt index 75c34a0f..fe5df977 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -188,6 +188,7 @@ set(SOURCE_FILES MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp + MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index cc09bbfe..3c92a249 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -312,34 +312,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return targetEnv; } - // Cheap raw-word scan for an `OpDecorate BuiltIn InstanceIndex` decoration. Used - // only to decide whether to warn when shaderDrawParameters is unavailable; a false - // negative merely suppresses a diagnostic. - Bool SpirvDeclaresInstanceIndexBuiltin(const Vector& spirv) { - constexpr Uint32 kSpirvMagicNumber = 0x07230203u; - constexpr SizeT kHeaderWordCount = 5; - if (spirv.size() <= kHeaderWordCount || spirv[0] != kSpirvMagicNumber) { - return false; - } - - SizeT wordIndex = kHeaderWordCount; - while (wordIndex < spirv.size()) { - const Uint32 firstWord = spirv[wordIndex]; - const Uint32 wordCount = firstWord >> 16; - const auto opcode = static_cast(firstWord & 0xffffu); - if (wordCount == 0 || wordIndex + wordCount > spirv.size()) { - break; - } - if (opcode == spv::Op::OpDecorate && wordCount >= 4 && - static_cast(spirv[wordIndex + 2]) == spv::Decoration::BuiltIn && - static_cast(spirv[wordIndex + 3]) == spv::BuiltIn::InstanceIndex) { - return true; - } - wordIndex += wordCount; - } - return false; - } - Bool IsInterfaceVariableStaticallyUsed(const Vector& spirv, Uint32 spirvId) { if (spirv.empty() || spirvId == 0) { return false; @@ -1234,6 +1206,25 @@ namespace MobileGL::MG_Backend::DirectVulkan { return false; } + // glslang's relaxed-Vulkan mode maps GL's gl_InstanceID onto the InstanceIndex builtin. + // Without shaderDrawParameters there is no gl_BaseInstance to subtract, so such a shader + // cannot be corrected and instanced draws with a non-zero baseInstance misrender; this + // detects the case so the user gets one warning instead of silent corruption. + Bool ProgramFactory::ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule) { + for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) { + const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex]; + for (Uint32 variableIndex = 0; variableIndex < entryPoint.input_variable_count; ++variableIndex) { + const SpvReflectInterfaceVariable* variable = entryPoint.input_variables[variableIndex]; + if (variable != nullptr && + (variable->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0 && + variable->built_in == SpvBuiltInInstanceIndex) { + return true; + } + } + } + return false; + } + VkShaderStageFlagBits ProgramFactory::ToVkStage(ShaderStage stage) { switch (stage) { case ShaderStage::Vertex: @@ -1481,6 +1472,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { continue; } + if (!m_shaderDrawParametersEnabled && ReflectedReadsInstanceIndexBuiltin(reflectModule)) { + static Bool s_warnedInstanceIndexUnsupported = false; + if (!s_warnedInstanceIndexUnsupported) { + s_warnedInstanceIndexUnsupported = true; + MGLOG_W("ProgramFactory: shaderDrawParameters is unavailable; gl_InstanceID cannot be " + "rebased and instanced draws with a non-zero baseInstance may render incorrectly"); + } + } + uint32_t inputCount = 0; SpvReflectResult reflectResult = spvReflectEnumerateInputVariables(&reflectModule, &inputCount, nullptr); MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS, @@ -1892,8 +1892,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { moduleSpirvs[i], invariantSpirv)) { moduleSpirvs[i] = std::move(invariantSpirv); } else { - MGLOG_W("ProgramFactory: position-invariant decoration failed for program %u; " - "keeping the original module", + // The pass round-trips through SPIRV-Tools IR, so an unparseable module + // fails open and keeps the undecorated words - which silently reinstates + // the multi-pass invariance bug rather than breaking anything loudly. + MGLOG_E("ProgramFactory: position-invariant decoration failed for program %u; " + "keeping the original module - multi-pass depth-equality chains " + "(e.g. MC 26.3 OIT clouds) may drop primitives on this device", program.GetExternalIndex()); } } @@ -1902,24 +1906,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { // gl_InstanceIndex, which wrongly includes the draw's baseInstance. Rebase vertex-stage // loads to (InstanceIndex - BaseInstance) so shaders observe GL semantics. Reflection // below runs on the rebased words so the added BaseInstance builtin stays consistent. - if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex) { - if (m_shaderDrawParametersEnabled) { - Vector rebasedSpirv; - if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i], - rebasedSpirv)) { - moduleSpirvs[i] = std::move(rebasedSpirv); - } else { - MGLOG_E("ProgramFactory: failed to rebase gl_InstanceID for program %u; " - "instanced draws with a non-zero baseInstance may render incorrectly", - program.GetExternalIndex()); - } - } else if (SpirvDeclaresInstanceIndexBuiltin(moduleSpirvs[i])) { - static Bool s_warnedInstanceIndexUnsupported = false; - if (!s_warnedInstanceIndexUnsupported) { - s_warnedInstanceIndexUnsupported = true; - MGLOG_W("ProgramFactory: shaderDrawParameters is unavailable; gl_InstanceID cannot be " - "rebased and instanced draws with a non-zero baseInstance may render incorrectly"); - } + // The unsupported-device counterpart of this rebase (warning when a shader reads + // the builtin but shaderDrawParameters is missing) rides along with + // ReflectVertexInputs, which already reflects this stage. + if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex && + m_shaderDrawParametersEnabled) { + Vector rebasedSpirv; + if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i], + rebasedSpirv)) { + moduleSpirvs[i] = std::move(rebasedSpirv); + } else { + MGLOG_E("ProgramFactory: failed to rebase gl_InstanceID for program %u; " + "instanced draws with a non-zero baseInstance may render incorrectly", + program.GetExternalIndex()); } } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h index 5df2b52b..fa8f5cca 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h @@ -223,6 +223,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { // can be pinned by tests. A false negative loses the exemption, so such a shader is // stripped conservatively and forfeits its depth write. static Bool ReflectedFragmentReplacesDepth(const SpvReflectShaderModule& reflectModule); + // True when an entry point reads the InstanceIndex builtin. Only gates a diagnostic: + // without shaderDrawParameters such a shader cannot have gl_InstanceID rebased. + static Bool ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule); private: struct ProgramLookupCache { diff --git a/MobileGL/MG_Test/CMakeLists.txt b/MobileGL/MG_Test/CMakeLists.txt index 041f4eea..1e73ede8 100644 --- a/MobileGL/MG_Test/CMakeLists.txt +++ b/MobileGL/MG_Test/CMakeLists.txt @@ -73,6 +73,7 @@ add_subdirectory(VertexArray) add_subdirectory(Program) add_subdirectory(Query) add_subdirectory(Pipeline) +add_subdirectory(ShaderTranspiler) if (ENABLE_INTEGRATION_TESTS) add_subdirectory(Backend/DirectVulkan) endif() diff --git a/MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp b/MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp index 750af2a1..ca908e5f 100644 --- a/MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp +++ b/MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp @@ -92,6 +92,93 @@ namespace { 0x00000005u, 0x0003003eu, 0x00000009u, 0x0000000bu, 0x000100fdu, 0x00010038u, }; + + // glslangValidator -V output for a vertex shader reading gl_InstanceIndex: + // #version 450 + // layout(location = 0) in vec4 inPos; + // void main() { gl_Position = inPos + vec4(float(gl_InstanceIndex)); } + constexpr Uint32 kInstanceIndexVertexSpirv[] = { + 0x07230203u, 0x00010000u, 0x0008000bu, 0x0000001bu, 0x00000000u, 0x00020011u, + 0x00000001u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, + 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x0008000fu, 0x00000000u, + 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000011u, 0x00000014u, + 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, + 0x00000000u, 0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u, + 0x00000000u, 0x00060006u, 0x0000000bu, 0x00000000u, 0x505f6c67u, 0x7469736fu, + 0x006e6f69u, 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u, 0x746e696fu, + 0x657a6953u, 0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, + 0x4470696cu, 0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u, + 0x435f6c67u, 0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u, 0x0000000du, + 0x00000000u, 0x00040005u, 0x00000011u, 0x6f506e69u, 0x00000073u, 0x00070005u, + 0x00000014u, 0x495f6c67u, 0x6174736eu, 0x4965636eu, 0x7865646eu, 0x00000000u, + 0x00030047u, 0x0000000bu, 0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u, + 0x0000000bu, 0x00000000u, 0x00050048u, 0x0000000bu, 0x00000001u, 0x0000000bu, + 0x00000001u, 0x00050048u, 0x0000000bu, 0x00000002u, 0x0000000bu, 0x00000003u, + 0x00050048u, 0x0000000bu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, + 0x00000011u, 0x0000001eu, 0x00000000u, 0x00040047u, 0x00000014u, 0x0000000bu, + 0x0000002bu, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, + 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, + 0x00000004u, 0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu, + 0x00000008u, 0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, + 0x00000009u, 0x0006001eu, 0x0000000bu, 0x00000007u, 0x00000006u, 0x0000000au, + 0x0000000au, 0x00040020u, 0x0000000cu, 0x00000003u, 0x0000000bu, 0x0004003bu, + 0x0000000cu, 0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u, + 0x00000001u, 0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040020u, + 0x00000010u, 0x00000001u, 0x00000007u, 0x0004003bu, 0x00000010u, 0x00000011u, + 0x00000001u, 0x00040020u, 0x00000013u, 0x00000001u, 0x0000000eu, 0x0004003bu, + 0x00000013u, 0x00000014u, 0x00000001u, 0x00040020u, 0x00000019u, 0x00000003u, + 0x00000007u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, + 0x000200f8u, 0x00000005u, 0x0004003du, 0x00000007u, 0x00000012u, 0x00000011u, + 0x0004003du, 0x0000000eu, 0x00000015u, 0x00000014u, 0x0004006fu, 0x00000006u, + 0x00000016u, 0x00000015u, 0x00070050u, 0x00000007u, 0x00000017u, 0x00000016u, + 0x00000016u, 0x00000016u, 0x00000016u, 0x00050081u, 0x00000007u, 0x00000018u, + 0x00000012u, 0x00000017u, 0x00050041u, 0x00000019u, 0x0000001au, 0x0000000du, + 0x0000000fu, 0x0003003eu, 0x0000001au, 0x00000018u, 0x000100fdu, 0x00010038u, + }; + + + // Same, but reading gl_VertexIndex instead: a DIFFERENT input builtin. glslang emits + // this for GL's gl_VertexID, so nearly every real vertex shader has one - it is what + // separates "declares some builtin" from "declares the InstanceIndex builtin". + constexpr Uint32 kVertexIndexVertexSpirv[] = { + 0x07230203u, 0x00010000u, 0x0008000bu, 0x0000001bu, 0x00000000u, 0x00020011u, + 0x00000001u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, + 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x0008000fu, 0x00000000u, + 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000011u, 0x00000014u, + 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, + 0x00000000u, 0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u, + 0x00000000u, 0x00060006u, 0x0000000bu, 0x00000000u, 0x505f6c67u, 0x7469736fu, + 0x006e6f69u, 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u, 0x746e696fu, + 0x657a6953u, 0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, + 0x4470696cu, 0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u, + 0x435f6c67u, 0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u, 0x0000000du, + 0x00000000u, 0x00040005u, 0x00000011u, 0x6f506e69u, 0x00000073u, 0x00060005u, + 0x00000014u, 0x565f6c67u, 0x65747265u, 0x646e4978u, 0x00007865u, 0x00030047u, + 0x0000000bu, 0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu, + 0x00000000u, 0x00050048u, 0x0000000bu, 0x00000001u, 0x0000000bu, 0x00000001u, + 0x00050048u, 0x0000000bu, 0x00000002u, 0x0000000bu, 0x00000003u, 0x00050048u, + 0x0000000bu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x00000011u, + 0x0000001eu, 0x00000000u, 0x00040047u, 0x00000014u, 0x0000000bu, 0x0000002au, + 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u, + 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u, + 0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu, 0x00000008u, + 0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, 0x00000009u, + 0x0006001eu, 0x0000000bu, 0x00000007u, 0x00000006u, 0x0000000au, 0x0000000au, + 0x00040020u, 0x0000000cu, 0x00000003u, 0x0000000bu, 0x0004003bu, 0x0000000cu, + 0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u, 0x00000001u, + 0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040020u, 0x00000010u, + 0x00000001u, 0x00000007u, 0x0004003bu, 0x00000010u, 0x00000011u, 0x00000001u, + 0x00040020u, 0x00000013u, 0x00000001u, 0x0000000eu, 0x0004003bu, 0x00000013u, + 0x00000014u, 0x00000001u, 0x00040020u, 0x00000019u, 0x00000003u, 0x00000007u, + 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, + 0x00000005u, 0x0004003du, 0x00000007u, 0x00000012u, 0x00000011u, 0x0004003du, + 0x0000000eu, 0x00000015u, 0x00000014u, 0x0004006fu, 0x00000006u, 0x00000016u, + 0x00000015u, 0x00070050u, 0x00000007u, 0x00000017u, 0x00000016u, 0x00000016u, + 0x00000016u, 0x00000016u, 0x00050081u, 0x00000007u, 0x00000018u, 0x00000012u, + 0x00000017u, 0x00050041u, 0x00000019u, 0x0000001au, 0x0000000du, 0x0000000fu, + 0x0003003eu, 0x0000001au, 0x00000018u, 0x000100fdu, 0x00010038u, + }; + // Owns the reflection module so each test case cleans up after itself. class ReflectModule { public: @@ -339,3 +426,31 @@ TEST(ReflectedFragmentReplacesDepth, ReflectedFlagFlipsTheStripDecision) { payload.fragmentReplacesDepth = ProgramFactory::ReflectedFragmentReplacesDepth(depthWriter.Get()); EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload)); } + + +// --- InstanceIndex reflection feeding the shaderDrawParameters diagnostic --- + +TEST(ReflectedReadsInstanceIndexBuiltin, TrueForAShaderReadingInstanceIndex) { + const ReflectModule module(kInstanceIndexVertexSpirv); + ASSERT_TRUE(module.Created()); + EXPECT_TRUE(ProgramFactory::ReflectedReadsInstanceIndexBuiltin(module.Get())); +} + +TEST(ReflectedReadsInstanceIndexBuiltin, FalseForAShaderReadingADifferentBuiltin) { + // Discriminates the builtin's identity, not merely its presence: weakening the check to + // "has any BuiltIn decoration" would fire the diagnostic on every real vertex shader. + const ReflectModule module(kVertexIndexVertexSpirv); + ASSERT_TRUE(module.Created()); + EXPECT_FALSE(ProgramFactory::ReflectedReadsInstanceIndexBuiltin(module.Get())); +} + +TEST(ReflectedReadsInstanceIndexBuiltin, FalseForAShaderWithNoInputBuiltins) { + const ReflectModule module(kPlainFragmentSpirv); + ASSERT_TRUE(module.Created()); + EXPECT_FALSE(ProgramFactory::ReflectedReadsInstanceIndexBuiltin(module.Get())); +} + +TEST(ReflectedReadsInstanceIndexBuiltin, FalseForAnEmptyModule) { + SpvReflectShaderModule emptyModule{}; + EXPECT_FALSE(ProgramFactory::ReflectedReadsInstanceIndexBuiltin(emptyModule)); +} diff --git a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt new file mode 100644 index 00000000..714f8d51 --- /dev/null +++ b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.14) + +add_executable( + SpirvPassTest + SpirvPassTest.cpp +) + +target_include_directories(SpirvPassTest PRIVATE + ${MGL_ROOT}/include + ${MGL_ROOT}/MobileGL + ${MGL_ROOT}/3rdparty/SPIRV-Reflect +) + +target_link_libraries( + SpirvPassTest PRIVATE + GTest::gtest_main + ${LINK_LIBRARIES} +) + +if (MSVC) + target_compile_options(SpirvPassTest PRIVATE /Zc:preprocessor) +endif() + +include(GoogleTest) +gtest_discover_tests(SpirvPassTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) diff --git a/MobileGL/MG_Test/ShaderTranspiler/SpirvPassTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/SpirvPassTest.cpp new file mode 100644 index 00000000..87c16ed2 --- /dev/null +++ b/MobileGL/MG_Test/ShaderTranspiler/SpirvPassTest.cpp @@ -0,0 +1,170 @@ +// MobileGL - MobileGL/MG_Test/ShaderTranspiler/SpirvPassTest.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 + +#include + +#include + +using namespace MobileGL; +using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler; + +namespace { + // glslangValidator -V output. Both are vertex shaders writing gl_Position through + // the gl_PerVertex block, i.e. the Position builtin arrives as OpMemberDecorate rather + // than a plain OpDecorate - the shape real glslang output actually takes. + + // #version 450 + // layout(location = 0) in vec4 inPos; + // void main() { gl_Position = inPos; } + constexpr Uint32 kPlainVertexSpirv[] = { + 0x07230203u, 0x00010000u, 0x0008000bu, 0x00000015u, 0x00000000u, 0x00020011u, + 0x00000001u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, + 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x0007000fu, 0x00000000u, + 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000011u, 0x00030003u, + 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, + 0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u, 0x00000000u, + 0x00060006u, 0x0000000bu, 0x00000000u, 0x505f6c67u, 0x7469736fu, 0x006e6f69u, + 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u, 0x746e696fu, 0x657a6953u, + 0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, 0x4470696cu, + 0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u, 0x435f6c67u, + 0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u, 0x0000000du, 0x00000000u, + 0x00040005u, 0x00000011u, 0x6f506e69u, 0x00000073u, 0x00030047u, 0x0000000bu, + 0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu, 0x00000000u, + 0x00050048u, 0x0000000bu, 0x00000001u, 0x0000000bu, 0x00000001u, 0x00050048u, + 0x0000000bu, 0x00000002u, 0x0000000bu, 0x00000003u, 0x00050048u, 0x0000000bu, + 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x00000011u, 0x0000001eu, + 0x00000000u, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, + 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, + 0x00000004u, 0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu, + 0x00000008u, 0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, + 0x00000009u, 0x0006001eu, 0x0000000bu, 0x00000007u, 0x00000006u, 0x0000000au, + 0x0000000au, 0x00040020u, 0x0000000cu, 0x00000003u, 0x0000000bu, 0x0004003bu, + 0x0000000cu, 0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u, + 0x00000001u, 0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040020u, + 0x00000010u, 0x00000001u, 0x00000007u, 0x0004003bu, 0x00000010u, 0x00000011u, + 0x00000001u, 0x00040020u, 0x00000013u, 0x00000003u, 0x00000007u, 0x00050036u, + 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, + 0x0004003du, 0x00000007u, 0x00000012u, 0x00000011u, 0x00050041u, 0x00000013u, + 0x00000014u, 0x0000000du, 0x0000000fu, 0x0003003eu, 0x00000014u, 0x00000012u, + 0x000100fdu, 0x00010038u, + }; + + // ... plus `invariant gl_Position;` - already carries OpMemberDecorate %gl_PerVertex 0 + // Invariant, so the pass must not add a duplicate. + constexpr Uint32 kAlreadyInvariantVertexSpirv[] = { + 0x07230203u, 0x00010000u, 0x0008000bu, 0x00000015u, 0x00000000u, 0x00020011u, + 0x00000001u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, + 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x0007000fu, 0x00000000u, + 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000011u, 0x00030003u, + 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, + 0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u, 0x00000000u, + 0x00060006u, 0x0000000bu, 0x00000000u, 0x505f6c67u, 0x7469736fu, 0x006e6f69u, + 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u, 0x746e696fu, 0x657a6953u, + 0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, 0x4470696cu, + 0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u, 0x435f6c67u, + 0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u, 0x0000000du, 0x00000000u, + 0x00040005u, 0x00000011u, 0x6f506e69u, 0x00000073u, 0x00030047u, 0x0000000bu, + 0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu, 0x00000000u, + 0x00040048u, 0x0000000bu, 0x00000000u, 0x00000012u, 0x00050048u, 0x0000000bu, + 0x00000001u, 0x0000000bu, 0x00000001u, 0x00050048u, 0x0000000bu, 0x00000002u, + 0x0000000bu, 0x00000003u, 0x00050048u, 0x0000000bu, 0x00000003u, 0x0000000bu, + 0x00000004u, 0x00040047u, 0x00000011u, 0x0000001eu, 0x00000000u, 0x00020013u, + 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, + 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u, 0x00040015u, + 0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu, 0x00000008u, 0x00000009u, + 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, 0x00000009u, 0x0006001eu, + 0x0000000bu, 0x00000007u, 0x00000006u, 0x0000000au, 0x0000000au, 0x00040020u, + 0x0000000cu, 0x00000003u, 0x0000000bu, 0x0004003bu, 0x0000000cu, 0x0000000du, + 0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u, 0x00000001u, 0x0004002bu, + 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040020u, 0x00000010u, 0x00000001u, + 0x00000007u, 0x0004003bu, 0x00000010u, 0x00000011u, 0x00000001u, 0x00040020u, + 0x00000013u, 0x00000003u, 0x00000007u, 0x00050036u, 0x00000002u, 0x00000004u, + 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x00000007u, + 0x00000012u, 0x00000011u, 0x00050041u, 0x00000013u, 0x00000014u, 0x0000000du, + 0x0000000fu, 0x0003003eu, 0x00000014u, 0x00000012u, 0x000100fdu, 0x00010038u, + }; + + // OpMemberDecorate + constexpr Uint32 kOpMemberDecorate = 72; + constexpr Uint32 kDecorationInvariant = 18; + constexpr Uint32 kSpirvHeaderWordCount = 5; + + // Test-side reference walker. Deliberately independent of the production code so a bug in + // the pass cannot hide behind the same helper; only used to count what the pass emitted. + Uint32 CountInvariantMemberDecorations(const Vector& spirv) { + Uint32 count = 0; + for (SizeT i = kSpirvHeaderWordCount; i < spirv.size();) { + const Uint32 wordCount = spirv[i] >> 16; + const Uint32 opcode = spirv[i] & 0xFFFFu; + if (wordCount == 0 || i + wordCount > spirv.size()) { + break; + } + if (opcode == kOpMemberDecorate && wordCount >= 4 && spirv[i + 3] == kDecorationInvariant) { + ++count; + } + i += wordCount; + } + return count; + } + + template + Vector ToVector(const Uint32 (&words)[WordCount]) { + return Vector(words, words + WordCount); + } +} // namespace + +// --- DecoratePositionInvariantPass --- + +TEST(DecoratePositionInvariant, AddsInvariantToThePositionMember) { + const Vector input = ToVector(kPlainVertexSpirv); + ASSERT_EQ(CountInvariantMemberDecorations(input), 0u); + + Vector output; + ASSERT_TRUE(ShaderCompiler::DecoratePositionInvariantForVulkan(input, output)); + EXPECT_EQ(CountInvariantMemberDecorations(output), 1u); +} + +TEST(DecoratePositionInvariant, DoesNotDuplicateAnExistingInvariant) { + const Vector input = ToVector(kAlreadyInvariantVertexSpirv); + ASSERT_EQ(CountInvariantMemberDecorations(input), 1u); + + Vector output; + ASSERT_TRUE(ShaderCompiler::DecoratePositionInvariantForVulkan(input, output)); + EXPECT_EQ(CountInvariantMemberDecorations(output), 1u); + // The pass reports SuccessWithoutChange here, and SPIRV-Tools asserts (in assert-enabled + // builds) that such a run round-trips byte-identically. Pin that from the outside so an + // assert-enabled CI build cannot be the first thing to discover a violation. + EXPECT_EQ(output, input); +} + +TEST(DecoratePositionInvariant, IsIdempotent) { + Vector once; + ASSERT_TRUE(ShaderCompiler::DecoratePositionInvariantForVulkan(ToVector(kPlainVertexSpirv), once)); + Vector twice; + ASSERT_TRUE(ShaderCompiler::DecoratePositionInvariantForVulkan(once, twice)); + EXPECT_EQ(CountInvariantMemberDecorations(twice), 1u); +} + +TEST(DecoratePositionInvariant, OutputStaysAReflectableModule) { + Vector output; + ASSERT_TRUE(ShaderCompiler::DecoratePositionInvariantForVulkan(ToVector(kPlainVertexSpirv), output)); + + SpvReflectShaderModule module{}; + ASSERT_EQ(spvReflectCreateShaderModule(output.size() * sizeof(Uint32), output.data(), &module), + SPV_REFLECT_RESULT_SUCCESS); + EXPECT_EQ(module.entry_point_count, 1u); + spvReflectDestroyShaderModule(&module); +} + +TEST(DecoratePositionInvariant, RejectsGarbageInput) { + const Vector notSpirv{0xdeadbeefu, 0u, 0u, 0u, 0u}; + Vector output; + EXPECT_FALSE(ShaderCompiler::DecoratePositionInvariantForVulkan(notSpirv, output)); +} diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 66538393..a2f4ebbd 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -16,6 +16,7 @@ #include "SpirvPasses/FlattenInterfaceStructPass.h" #include "SpirvPasses/RenameSamplerFunctionParameterPass.h" #include "SpirvPasses/DecomposeWorkgroupVec3Pass.h" +#include "SpirvPasses/DecoratePositionInvariantPass.h" #include "SpirvPasses/LowerDrawParametersPass.h" #include "SpirvPasses/RebaseInstanceIndexPass.h" #include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h" @@ -347,71 +348,14 @@ namespace MobileGL { bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector& inputBinary, Vector& outputBinary) { - static constexpr Uint32 kHeaderWords = 5; - static constexpr Uint32 kOpDecorate = 71; - static constexpr Uint32 kOpMemberDecorate = 72; - static constexpr Uint32 kDecorationInvariant = 18; - static constexpr Uint32 kDecorationBuiltIn = 11; - static constexpr Uint32 kBuiltInPosition = 0; - if (inputBinary.size() < kHeaderWords) { - return false; - } + using namespace spvtools; + OptimizerOptions options; + options.set_run_validator(false); - // First pass: find targets that already carry Invariant so we never duplicate. - struct MemberKey { - Uint32 id; - Uint32 member; - bool operator==(const MemberKey& o) const { return id == o.id && member == o.member; } - }; - Vector invariantIds; - Vector invariantMembers; - for (SizeT i = kHeaderWords; i < inputBinary.size();) { - const Uint32 word0 = inputBinary[i]; - const Uint32 opcode = word0 & 0xFFFFu; - const Uint32 length = word0 >> 16; - if (length == 0 || i + length > inputBinary.size()) { - return false; - } - if (opcode == kOpDecorate && length >= 3 && inputBinary[i + 2] == kDecorationInvariant) { - invariantIds.push_back(inputBinary[i + 1]); - } else if (opcode == kOpMemberDecorate && length >= 4 && - inputBinary[i + 3] == kDecorationInvariant) { - invariantMembers.push_back({inputBinary[i + 1], inputBinary[i + 2]}); - } - i += length; - } + Optimizer optimizer(SPV_ENV_VULKAN_1_1); + optimizer.RegisterPass(DecoratePositionInvariantPass::CreateDecoratePositionInvariantPass()); - outputBinary.clear(); - outputBinary.reserve(inputBinary.size() + 8); - outputBinary.insert(outputBinary.end(), inputBinary.begin(), inputBinary.begin() + kHeaderWords); - for (SizeT i = kHeaderWords; i < inputBinary.size();) { - const Uint32 word0 = inputBinary[i]; - const Uint32 opcode = word0 & 0xFFFFu; - const Uint32 length = word0 >> 16; - outputBinary.insert(outputBinary.end(), inputBinary.begin() + i, - inputBinary.begin() + i + length); - if (opcode == kOpDecorate && length == 4 && - inputBinary[i + 2] == kDecorationBuiltIn && inputBinary[i + 3] == kBuiltInPosition) { - const Uint32 target = inputBinary[i + 1]; - if (std::find(invariantIds.begin(), invariantIds.end(), target) == invariantIds.end()) { - outputBinary.push_back((3u << 16) | kOpDecorate); - outputBinary.push_back(target); - outputBinary.push_back(kDecorationInvariant); - } - } else if (opcode == kOpMemberDecorate && length == 5 && - inputBinary[i + 3] == kDecorationBuiltIn && inputBinary[i + 4] == kBuiltInPosition) { - const MemberKey key{inputBinary[i + 1], inputBinary[i + 2]}; - if (std::find(invariantMembers.begin(), invariantMembers.end(), key) == - invariantMembers.end()) { - outputBinary.push_back((4u << 16) | kOpMemberDecorate); - outputBinary.push_back(key.id); - outputBinary.push_back(key.member); - outputBinary.push_back(kDecorationInvariant); - } - } - i += length; - } - return true; + return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); } bool ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan( diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp new file mode 100644 index 00000000..f418dc9e --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp @@ -0,0 +1,139 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.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 "DecoratePositionInvariantPass.h" + +#include "spirv.hpp" +#include "source/opt/instruction.h" +#include "source/opt/ir_context.h" +#include "source/util/make_unique.h" + +#include +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + namespace { + using spvtools::opt::Instruction; + using spvtools::opt::IRContext; + using spvtools::opt::Operand; + + // Identifies one member of a decorated struct (gl_PerVertex's Position slot). + struct MemberKey { + uint32_t id = 0; + uint32_t member = 0; + bool operator==(const MemberKey& other) const { + return id == other.id && member == other.member; + } + }; + + // OpDecorate [literals...] + // OpMemberDecorate [literals...] + constexpr uint32_t kDecorateTargetOperand = 0; + constexpr uint32_t kDecorateDecorationOperand = 1; + constexpr uint32_t kDecorateBuiltInOperand = 2; + constexpr uint32_t kMemberDecorateStructOperand = 0; + constexpr uint32_t kMemberDecorateMemberOperand = 1; + constexpr uint32_t kMemberDecorateDecorationOperand = 2; + constexpr uint32_t kMemberDecorateBuiltInOperand = 3; + } // namespace + + spvtools::opt::Pass::Status DecoratePositionInvariantPass::Process() { + auto* irContext = context(); + + // Collect first: AddAnnotationInst mutates the list being walked. + std::vector invariantIds; + std::vector invariantMembers; + std::vector positionIds; + std::vector positionMembers; + + for (const Instruction& annotation : irContext->annotations()) { + if (annotation.opcode() == spv::Op::OpDecorate) { + if (annotation.NumInOperands() <= kDecorateDecorationOperand) { + continue; + } + const auto decoration = static_cast( + annotation.GetSingleWordInOperand(kDecorateDecorationOperand)); + const uint32_t target = annotation.GetSingleWordInOperand(kDecorateTargetOperand); + if (decoration == spv::Decoration::Invariant) { + invariantIds.push_back(target); + } else if (decoration == spv::Decoration::BuiltIn && + annotation.NumInOperands() > kDecorateBuiltInOperand && + static_cast(annotation.GetSingleWordInOperand( + kDecorateBuiltInOperand)) == spv::BuiltIn::Position) { + positionIds.push_back(target); + } + } else if (annotation.opcode() == spv::Op::OpMemberDecorate) { + if (annotation.NumInOperands() <= kMemberDecorateDecorationOperand) { + continue; + } + const auto decoration = static_cast( + annotation.GetSingleWordInOperand(kMemberDecorateDecorationOperand)); + const MemberKey key{ + annotation.GetSingleWordInOperand(kMemberDecorateStructOperand), + annotation.GetSingleWordInOperand(kMemberDecorateMemberOperand)}; + if (decoration == spv::Decoration::Invariant) { + invariantMembers.push_back(key); + } else if (decoration == spv::Decoration::BuiltIn && + annotation.NumInOperands() > kMemberDecorateBuiltInOperand && + static_cast(annotation.GetSingleWordInOperand( + kMemberDecorateBuiltInOperand)) == spv::BuiltIn::Position) { + positionMembers.push_back(key); + } + } + } + + Bool changed = false; + + for (const uint32_t target : positionIds) { + if (std::find(invariantIds.begin(), invariantIds.end(), target) != invariantIds.end()) { + continue; + } + irContext->AddAnnotationInst(spvtools::MakeUnique( + irContext, spv::Op::OpDecorate, 0, 0, + std::initializer_list{ + {SPV_OPERAND_TYPE_ID, {target}}, + {SPV_OPERAND_TYPE_DECORATION, + {static_cast(spv::Decoration::Invariant)}}})); + // Guard against a second Position decoration on the same target. + invariantIds.push_back(target); + changed = true; + } + + for (const MemberKey& key : positionMembers) { + if (std::find(invariantMembers.begin(), invariantMembers.end(), key) != + invariantMembers.end()) { + continue; + } + irContext->AddAnnotationInst(spvtools::MakeUnique( + irContext, spv::Op::OpMemberDecorate, 0, 0, + std::initializer_list{ + {SPV_OPERAND_TYPE_ID, {key.id}}, + {SPV_OPERAND_TYPE_LITERAL_INTEGER, {key.member}}, + {SPV_OPERAND_TYPE_DECORATION, + {static_cast(spv::Decoration::Invariant)}}})); + invariantMembers.push_back(key); + changed = true; + } + + if (!changed) { + return Status::SuccessWithoutChange; + } + + irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone); + return Status::SuccessWithChange; + } + + spvtools::Optimizer::PassToken + DecoratePositionInvariantPass::CreateDecoratePositionInvariantPass() { + return spvtools::Optimizer::PassToken(MakeUnique()); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.h new file mode 100644 index 00000000..bc374f97 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.h @@ -0,0 +1,35 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.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 { + // Adds the Invariant decoration to every Position builtin output. GL apps + // routinely rely on cross-program position invariance for multi-pass equality + // depth tests - MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the + // depth its own first pass wrote - and a driver that optimizes each pipeline + // separately may otherwise vary the position math between passes, dropping whole + // primitives from the later ones. Both the plain (OpDecorate on a Position + // variable) and the block-member (OpMemberDecorate on gl_PerVertex) spellings are + // handled; targets that already carry Invariant are left alone. DirectVulkan only. + class DecoratePositionInvariantPass : public spvtools::opt::Pass { + public: + const char* name() const override { return "decorate-position-invariant"; } + Status Process() override; + + static spvtools::Optimizer::PassToken CreateDecoratePositionInvariantPass(); + }; + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL