diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 2df8db54..15e6a30b 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -4909,9 +4909,32 @@ namespace MobileGL::MG_Backend::DirectGLES { // fails to compile and every draw made with it silently renders nothing. // Gated on the module actually declaring the output, so no other stage pays an // optimizer round trip for it. + // One parse of the module answers every armed pass gate below. The per-gate + // Declares* probes each cost a BuildModule per stage, and on a driver where both + // gates are armed (Mali: no GL_OES_viewport_array AND integer multisample + // squeezed to 1) the doubled parse made compile-heavy workloads ~10% slower. + // Probing the pre-lowering module is sound for both gates: demoting + // gl_ViewportIndex neither adds nor removes multisampled image types. + // Recomputed here rather than calling GL_Getter's GetAdvertisedMaxSamples(): + // this is backend code and must not reach into the GL frontend. 4 is that + // translation unit's kFrontendMaxSamples, which is the source of truth - + // keep the two in step. + constexpr Int kFrontendMaxSamples = 4; + const Int advertisedMaxSamples = + std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples); + const Bool viewportLoweringArmed = !g_GLESCapabilities.SupportsViewportArray; + const Bool sampleClampArmed = + g_GLESCapabilities.MaxColorTextureSamples < advertisedMaxSamples || + g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples || + g_GLESCapabilities.MaxDepthTextureSamples < advertisedMaxSamples; + MG_Util::ShaderTranspiler::ShaderCompiler::SpirvGateFeatures spirvGates; + if (viewportLoweringArmed || sampleClampArmed) { + spirvGates = MG_Util::ShaderTranspiler::ShaderCompiler::ProbeSpirvGateFeatures( + *effectiveSpirv); + } + Vector loweredViewportSpirv; - if (!g_GLESCapabilities.SupportsViewportArray && - MG_Util::ShaderTranspiler::ShaderCompiler::DeclaresViewportIndexBuiltin(*effectiveSpirv) && + if (viewportLoweringArmed && spirvGates.WritesViewportIndexOutput && MG_Util::ShaderTranspiler::ShaderCompiler::LowerViewportIndexForEssl( *effectiveSpirv, loweredViewportSpirv, enableSpirvValidation) && !loweredViewportSpirv.empty()) { @@ -4937,28 +4960,15 @@ namespace MobileGL::MG_Backend::DirectGLES { // optimizer round trip for it. DirectVulkan is deliberately not given this: it // allocates the sample count it was asked for, so its modules are already right. Vector clampedSampleSpirv; - { - // Recomputed here rather than calling GL_Getter's GetAdvertisedMaxSamples(): - // this is backend code and must not reach into the GL frontend. 4 is that - // translation unit's kFrontendMaxSamples, which is the source of truth - - // keep the two in step. - constexpr Int kFrontendMaxSamples = 4; - const Int advertisedMaxSamples = - std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples); - if ((g_GLESCapabilities.MaxColorTextureSamples < advertisedMaxSamples || - g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples || - g_GLESCapabilities.MaxDepthTextureSamples < advertisedMaxSamples) && - MG_Util::ShaderTranspiler::ShaderCompiler::DeclaresMultisampledImage( - *effectiveSpirv) && - MG_Util::ShaderTranspiler::ShaderCompiler::ClampMultisampleFetchesForEssl( - *effectiveSpirv, clampedSampleSpirv, - g_GLESCapabilities.MaxColorTextureSamples, - g_GLESCapabilities.MaxIntegerSamples, - g_GLESCapabilities.MaxDepthTextureSamples, advertisedMaxSamples, - enableSpirvValidation) && - !clampedSampleSpirv.empty()) { - effectiveSpirv = &clampedSampleSpirv; - } + if (sampleClampArmed && spirvGates.DeclaresMultisampledImage && + MG_Util::ShaderTranspiler::ShaderCompiler::ClampMultisampleFetchesForEssl( + *effectiveSpirv, clampedSampleSpirv, + g_GLESCapabilities.MaxColorTextureSamples, + g_GLESCapabilities.MaxIntegerSamples, + g_GLESCapabilities.MaxDepthTextureSamples, advertisedMaxSamples, + enableSpirvValidation) && + !clampedSampleSpirv.empty()) { + effectiveSpirv = &clampedSampleSpirv; } // GLSL ES has no ARRAY vertex inputs, and SPIRV-Cross refuses the whole module diff --git a/MobileGL/MG_Test/ShaderTranspiler/ClampMultisampleFetchTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/ClampMultisampleFetchTest.cpp index 83f36bcf..4c538317 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/ClampMultisampleFetchTest.cpp +++ b/MobileGL/MG_Test/ShaderTranspiler/ClampMultisampleFetchTest.cpp @@ -247,6 +247,26 @@ TEST_F(ClampMultisampleFetchTest, TheProbeAnswersOnlyForAMultisampledImage) { EXPECT_FALSE(ShaderCompiler::DeclaresMultisampledImage({})); } +// The combined probe answers both gate questions from one parse; it must agree with the +// per-gate probes on the same modules and stay quiet for an empty stage. +TEST_F(ClampMultisampleFetchTest, TheCombinedProbeAgreesWithThePerGateOnes) { + const Vector integerMs = CompileFragment(kIntegerMultisampleFetch); + ASSERT_FALSE(integerMs.empty()); + const auto msFeatures = ShaderCompiler::ProbeSpirvGateFeatures(integerMs); + EXPECT_TRUE(msFeatures.DeclaresMultisampledImage); + EXPECT_FALSE(msFeatures.WritesViewportIndexOutput); + + const Vector plain = CompileFragment(kNoMultisampleFetch); + ASSERT_FALSE(plain.empty()); + const auto plainFeatures = ShaderCompiler::ProbeSpirvGateFeatures(plain); + EXPECT_FALSE(plainFeatures.DeclaresMultisampledImage); + EXPECT_FALSE(plainFeatures.WritesViewportIndexOutput); + + const auto emptyFeatures = ShaderCompiler::ProbeSpirvGateFeatures({}); + EXPECT_FALSE(emptyFeatures.DeclaresMultisampledImage); + EXPECT_FALSE(emptyFeatures.WritesViewportIndexOutput); +} + // The overwhelming majority of modules. Behind the probe they never reach the pass at all, but the // pass has to be inert for them on its own, or a future caller that forgets the gate silently // re-serialises every shader in the program. diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 26420811..7cf8722a 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -672,6 +672,28 @@ namespace MobileGL { return ClampMultisampleFetchPass::DeclaresMultisampledImage(binary); } + ShaderCompiler::SpirvGateFeatures ShaderCompiler::ProbeSpirvGateFeatures( + const Vector& binary) { + SpirvGateFeatures features; + if (binary.empty()) { + return features; + } + std::unique_ptr context = spvtools::BuildModule( + SPV_ENV_VULKAN_1_1, + [](spv_message_level_t, const char*, const spv_position_t&, const char*) {}, + binary.data(), binary.size()); + if (!context) { + // Unparseable here means unusable downstream too; let the ordinary transpile + // path produce the error rather than inventing a verdict from it. + return features; + } + features.WritesViewportIndexOutput = + LowerViewportIndexPass::DeclaresViewportIndexBuiltin(context.get()); + features.DeclaresMultisampledImage = + ClampMultisampleFetchPass::DeclaresMultisampledImage(context.get()); + return features; + } + bool ShaderCompiler::SplitArrayVertexInputsForEssl(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index 3cabe30c..94cc8ddd 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -65,6 +65,16 @@ namespace MobileGL { // above has anything to do. The gate that keeps every other stage off an // optimizer round trip it does not need. static bool DeclaresMultisampledImage(const Vector& binary); + // Both gate questions above answered from ONE parse. Every armed gate costs a + // BuildModule per shader stage, and on a driver where both are armed (Mali: no + // GL_OES_viewport_array AND integer multisample squeezed to 1) the separate + // probes made compile-heavy workloads measurably slower - ReservedNames-class + // CTS cases paid ~10%. Callers with more than one armed gate use this instead. + struct SpirvGateFeatures { + Bool WritesViewportIndexOutput = false; + Bool DeclaresMultisampledImage = false; + }; + static SpirvGateFeatures ProbeSpirvGateFeatures(const Vector& binary); // Replaces an ARRAY vertex input with one input per element at consecutive // locations, seeding a Private copy of the array so indexed reads still work. // GLSL ES has no array vertex inputs and SPIRV-Cross refuses the whole module diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp index c1e2cbf7..21bb30bb 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp @@ -198,6 +198,10 @@ namespace MobileGL { // path produce the error rather than inventing a verdict from it. return false; } + return DeclaresMultisampledImage(context.get()); + } + + bool ClampMultisampleFetchPass::DeclaresMultisampledImage(IRContext* context) { for (const Instruction& type : context->module()->types_values()) { if (IsMultisampledImageType(&type)) { return true; diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.h index 4823baca..8d4fb722 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.h +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.h @@ -74,6 +74,10 @@ namespace MobileGL { // that read a multisample texture directly. static bool DeclaresMultisampledImage(const Vector& binary); + // Same question answered from an already-built module, so one parse can feed + // several gates (ShaderCompiler::ProbeSpirvGateFeatures). + static bool DeclaresMultisampledImage(spvtools::opt::IRContext* context); + static spvtools::Optimizer::PassToken CreateClampMultisampleFetchPass( Int32 maxColorSamples, Int32 maxIntegerSamples, Int32 maxDepthSamples, Int32 advertisedMaxSamples); diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp index e0074197..93a63dba 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp @@ -138,9 +138,13 @@ namespace MobileGL { // path produce the error rather than inventing a verdict from it. return false; } + return DeclaresViewportIndexBuiltin(context.get()); + } + + bool LowerViewportIndexPass::DeclaresViewportIndexBuiltin(IRContext* context) { for (const Instruction& annotation : context->annotations()) { if (IsViewportIndexBuiltinDecoration(annotation) && - GetDecoratedViewportIndexOutput(context.get(), annotation) != nullptr) { + GetDecoratedViewportIndexOutput(context, annotation) != nullptr) { return true; } } diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.h index fcb207ed..9e8f9017 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.h +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.h @@ -44,6 +44,10 @@ namespace MobileGL { // but the handful that route viewports from the shader. static bool DeclaresViewportIndexBuiltin(const Vector& binary); + // Same question answered from an already-built module, so one parse can feed + // several gates (ShaderCompiler::ProbeSpirvGateFeatures). + static bool DeclaresViewportIndexBuiltin(spvtools::opt::IRContext* context); + static spvtools::Optimizer::PassToken CreateLowerViewportIndexPass(); }; } // namespace ShaderTranspiler