From ba6db60c098c65b43f52aaa42c9d340593889c7c Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Mon, 12 Oct 2026 06:17:39 -0400 Subject: [PATCH] [Fix] (GL): fail the link when a stage exceeds its uniform or storage block limits --- MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp | 15 ++- .../GLState/ProgramState/ProgramLinkTask.cpp | 107 ++++++++++++++++++ .../MG_Test/Program/ProgramInterfaceTest.cpp | 83 ++++++++++++++ MobileGL/MG_Test/Program/ProgramTest.cpp | 44 +++++++ MobileGL/MG_Util/ShaderTranspiler/Types.h | 5 + 5 files changed, 249 insertions(+), 5 deletions(-) diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index c4808aa6..de7c6ee5 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -118,23 +118,28 @@ namespace MobileGL::MG_Impl::GLImpl { static_cast(MG_Util::ShaderTranspiler::MAX_VERTEX_UNIFORM_COMPONENTS); constexpr GLint kFrontendMaxVertexUniformVectors = static_cast(MG_Util::ShaderTranspiler::MAX_VERTEX_UNIFORM_VECTORS); - constexpr GLint kFrontendMaxVertexUniformBlocks = 14; + constexpr GLint kFrontendMaxVertexUniformBlocks = + static_cast(MG_Util::ShaderTranspiler::MAX_GRAPHICS_STAGE_UNIFORM_BLOCKS); constexpr GLint kFrontendMaxVertexOutputComponents = 64; constexpr GLint kFrontendMaxFragmentInputComponents = 128; constexpr GLint kFrontendMaxFragmentUniformComponents = 4096; constexpr GLint kFrontendMaxFragmentUniformVectors = 256; - constexpr GLint kFrontendMaxFragmentUniformBlocks = 14; + constexpr GLint kFrontendMaxFragmentUniformBlocks = + static_cast(MG_Util::ShaderTranspiler::MAX_GRAPHICS_STAGE_UNIFORM_BLOCKS); constexpr GLint kFrontendMaxGeometryInputComponents = 64; constexpr GLint kFrontendMaxGeometryOutputComponents = 128; constexpr GLint kFrontendMaxGeometryTextureImageUnits = 16; constexpr GLint kFrontendMaxGeometryUniformComponents = 1024; - constexpr GLint kFrontendMaxGeometryUniformBlocks = 14; + constexpr GLint kFrontendMaxGeometryUniformBlocks = + static_cast(MG_Util::ShaderTranspiler::MAX_GRAPHICS_STAGE_UNIFORM_BLOCKS); // ARB_geometry_shader4's per-invocation count. No TBuiltInResource field and no // gl_MaxGeometryShaderInvocations built-in exists to keep in step, so this is a getter // answer only; 32 is the GL 4.6 core minimum (table 23.57). constexpr GLint kFrontendMaxGeometryShaderInvocations = 32; - constexpr GLint kFrontendMaxTessControlUniformBlocks = 14; - constexpr GLint kFrontendMaxTessEvaluationUniformBlocks = 14; + constexpr GLint kFrontendMaxTessControlUniformBlocks = + static_cast(MG_Util::ShaderTranspiler::MAX_GRAPHICS_STAGE_UNIFORM_BLOCKS); + constexpr GLint kFrontendMaxTessEvaluationUniformBlocks = + static_cast(MG_Util::ShaderTranspiler::MAX_GRAPHICS_STAGE_UNIFORM_BLOCKS); // The compute stage's share of the combined sum below. Compute's own per-stage answer is // backend-derived (GL_MAX_COMPUTE_UNIFORM_BLOCKS reads dynamicParameters), so this is not // what that query returns - it is the GL 4.3 core minimum, present here only so the diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp index 6cbd47ab..826adda6 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp @@ -351,6 +351,106 @@ namespace { return {}; } + // GL 4.6 core 7.6.2 / 7.8: LinkProgram FAILS when a stage uses more active uniform blocks or + // shader storage blocks than its GL_MAX__* limit, or the stages together more than the + // GL_MAX_COMBINED_* one (a block used by two stages counts once per stage). glslang counts + // neither, so a program over a limit used to report LINK_STATUS = TRUE and then fail inside + // the backend - a fragment stage with eight storage blocks against an advertised four linked + // cleanly and rendered nothing. + // + // The limits are the ones glGetIntegerv answers (the storage family clamped to the backend's + // binding count as the getter clamps it); the synthesized global uniform block holds + // default-block uniforms and is not a GL block at all. An instance array costs one block per + // element, reconciled per name exactly as image arrays are above. + // + // Atomic counter buffers are deliberately NOT counted here. Their pre-fragment limits are a + // frontend 0 while every backend serves a counter as a lowered storage block in any stage, + // so enforcing that number would only start refusing programs that run today. + static MobileGL::String ValidateBlockLimits(glslang::TProgram& reflection, + const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) { + using MobileGL::Array; + using MobileGL::Int; + using MobileGL::SizeT; + using MobileGL::UnorderedMap; + namespace Transpiler = MobileGL::MG_Util::ShaderTranspiler; + + static constexpr EShLanguage kStages[] = {EShLangVertex, EShLangTessControl, EShLangTessEvaluation, + EShLangGeometry, EShLangFragment, EShLangCompute}; + static constexpr const char* kStageNames[] = {"VERTEX", "TESS_CONTROL", "TESS_EVALUATION", + "GEOMETRY", "FRAGMENT", "COMPUTE"}; + constexpr SizeT kStageCount = sizeof(kStages) / sizeof(kStages[0]); + + enum Family : SizeT { kUniform = 0, kStorage, kFamilyCount }; + static constexpr const char* kFamilyNames[] = {"UNIFORM_BLOCKS", "SHADER_STORAGE_BLOCKS"}; + static constexpr const char* kFamilyNouns[] = {"uniform blocks", "shader storage blocks"}; + + const auto& p = env.params; + const Int storageBindings = std::max(p.MaxShaderStorageBufferBindings, 0); + const auto clampStorage = [storageBindings](Int limit) { return std::min(std::max(limit, 0), storageBindings); }; + const Int graphicsUniform = Transpiler::MAX_GRAPHICS_STAGE_UNIFORM_BLOCKS; + const Int combinedUniform = 6 * graphicsUniform; + const Int limits[kFamilyCount][kStageCount] = { + {graphicsUniform, graphicsUniform, graphicsUniform, graphicsUniform, graphicsUniform, + std::min(std::max(p.MaxComputeUniformBlocks, 0), combinedUniform)}, + {clampStorage(p.MaxVertexShaderStorageBlocks), clampStorage(p.MaxTessControlShaderStorageBlocks), + clampStorage(p.MaxTessEvaluationShaderStorageBlocks), clampStorage(p.MaxGeometryShaderStorageBlocks), + clampStorage(p.MaxFragmentShaderStorageBlocks), clampStorage(p.MaxComputeShaderStorageBlocks)}, + }; + const Int combinedLimits[kFamilyCount] = {combinedUniform, clampStorage(p.MaxCombinedShaderStorageBlocks)}; + + struct BlockUse { + Int entries = 0; + Int declared = 0; + }; + UnorderedMap> useByName[kFamilyCount]; + + const Int blockCount = reflection.getNumUniformBlocks(); + for (Int i = 0; i < blockCount; ++i) { + const auto& block = reflection.getUniformBlock(i); + if (std::strstr(block.name.c_str(), Transpiler::GLOBAL_UBO_NAME) != nullptr) continue; + if (IsAtomicCounterBlockName(block.name)) continue; + const Family family = IsStorageBlock(block) ? kStorage : kUniform; + Int elements = 1; + if (const glslang::TType* type = block.getType(); type != nullptr && type->isSizedArray()) { + elements = type->getCumulativeArraySize(); + } + Array* use = nullptr; + for (SizeT stage = 0; stage < kStageCount; ++stage) { + if ((static_cast(block.stages) & (1u << static_cast(kStages[stage]))) == 0) { + continue; + } + if (use == nullptr) use = &useByName[family][StripArrayElementSuffix(block.name)]; + ++(*use)[stage].entries; + (*use)[stage].declared = std::max((*use)[stage].declared, elements); + } + } + + for (SizeT family = 0; family < kFamilyCount; ++family) { + Int counts[kStageCount] = {}; + for (const auto& entry : useByName[family]) { + for (SizeT stage = 0; stage < kStageCount; ++stage) { + counts[stage] += std::max(entry.second[stage].entries, entry.second[stage].declared); + } + } + Int combined = 0; + for (SizeT stage = 0; stage < kStageCount; ++stage) { + combined += counts[stage]; + if (counts[stage] > limits[family][stage]) { + return std::format("This program uses {} active {} in one stage, more than the {} " + "GL_MAX_{}_{} allows.", + counts[stage], kFamilyNouns[family], limits[family][stage], + kStageNames[stage], kFamilyNames[family]); + } + } + if (combined > combinedLimits[family]) { + return std::format("This program uses {} active {} across its stages, more than the {} " + "GL_MAX_COMBINED_{} allows.", + combined, kFamilyNouns[family], combinedLimits[family], kFamilyNames[family]); + } + } + return {}; + } + static bool IsBuiltInPipelineOutput(const glslang::TObjectReflection& output) { const auto* type = output.getType(); return type && type->getQualifier().builtIn != glslang::EbvNone; @@ -1144,6 +1244,13 @@ namespace MobileGL::MG_State::GLState { return false; } + if (String blockLimitError = ValidateBlockLimits(*artifacts.program, env); !blockLimitError.empty()) { + artifacts.infoLog = Move(blockLimitError); + DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog)); + ProgramObject::ResetLinkArtifacts(artifacts); + return false; + } + // ---------- GL-facing index spaces (relaxed-parse cleanup) ---------- // Blocks first: global-UBO membership drives the uniform filter below. The // synthesized MGL_GLOBAL_UBO is a transpiler artifact - its members are GL diff --git a/MobileGL/MG_Test/Program/ProgramInterfaceTest.cpp b/MobileGL/MG_Test/Program/ProgramInterfaceTest.cpp index 8f6e1c86..3ac4306b 100644 --- a/MobileGL/MG_Test/Program/ProgramInterfaceTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramInterfaceTest.cpp @@ -905,6 +905,89 @@ void main() { } } + // The same rule for blocks (GL 4.6 core 7.6.2 / 7.8): a stage using more shader storage + // blocks than GL_MAX__SHADER_STORAGE_BLOCKS fails the link. glslang does not count + // them, so a fragment stage with eight blocks against an advertised four reported + // LINK_STATUS = TRUE and then failed inside the backend. Sized off the advertised limit. + TEST_F(ProgramInterfaceTest, StorageBlocksOverAStageLimitFailToLink) { + GLint maxFragmentBlocks = 0; + GLint maxCombinedBlocks = 0; + GetIntegerv(GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS, &maxFragmentBlocks); + GetIntegerv(GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, &maxCombinedBlocks); + ClearErrors(); + ASSERT_GT(maxFragmentBlocks, 0); + + // Bindings wrap at the limit: two blocks may share a binding point, and a binding past + // GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS would fail for a different reason. + const auto linkWithFragmentBlocks = [maxFragmentBlocks](GLint count) { + std::string source = "#version 430\nout vec4 color;\n"; + std::string sum = "0.0"; + for (GLint i = 0; i < count; ++i) { + const std::string n = std::to_string(i); + source += "layout(std430, binding = " + std::to_string(i % maxFragmentBlocks) + ") buffer B" + n + + " { float v" + n + "; };\n"; + sum += " + v" + n; + } + source += "void main() { color = vec4(" + sum + "); }\n"; + return MakeProgram(kSimpleVs, source.c_str()); + }; + + const GLuint over = linkWithFragmentBlocks(maxFragmentBlocks + 1); + LinkProgram(over); + GLint status = -1; + GetProgramiv(over, GL_LINK_STATUS, &status); + EXPECT_EQ(status, GL_FALSE); + char log[4096] = ""; + GetProgramInfoLog(over, sizeof(log), nullptr, log); + EXPECT_NE(std::string(log).find("GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS"), std::string::npos) + << "info log was: " << log; + ClearErrors(); + + if (maxFragmentBlocks <= maxCombinedBlocks) { + const GLuint atLimit = linkWithFragmentBlocks(maxFragmentBlocks); + LinkProgram(atLimit); + ExpectLinked(atLimit); + ClearErrors(); + } + } + + // Uniform blocks are counted the same way, and the synthesized block that carries the + // default-block uniforms is not one of them. + TEST_F(ProgramInterfaceTest, UniformBlocksOverAStageLimitFailToLink) { + GLint maxFragmentBlocks = 0; + GetIntegerv(GL_MAX_FRAGMENT_UNIFORM_BLOCKS, &maxFragmentBlocks); + ClearErrors(); + ASSERT_GT(maxFragmentBlocks, 0); + + const auto linkWithFragmentBlocks = [](GLint count) { + std::string source = "#version 430\nout vec4 color;\nuniform float loose;\n"; + std::string sum = "loose"; + for (GLint i = 0; i < count; ++i) { + const std::string n = std::to_string(i); + source += "layout(std140) uniform U" + n + " { float u" + n + "; };\n"; + sum += " + u" + n; + } + source += "void main() { color = vec4(" + sum + "); }\n"; + return MakeProgram(kSimpleVs, source.c_str()); + }; + + const GLuint over = linkWithFragmentBlocks(maxFragmentBlocks + 1); + LinkProgram(over); + GLint status = -1; + GetProgramiv(over, GL_LINK_STATUS, &status); + EXPECT_EQ(status, GL_FALSE); + char log[4096] = ""; + GetProgramInfoLog(over, sizeof(log), nullptr, log); + EXPECT_NE(std::string(log).find("GL_MAX_FRAGMENT_UNIFORM_BLOCKS"), std::string::npos) + << "info log was: " << log; + ClearErrors(); + + const GLuint atLimit = linkWithFragmentBlocks(maxFragmentBlocks); + LinkProgram(atLimit); + ExpectLinked(atLimit); + ClearErrors(); + } + // glGetProgramiv(GL_ACTIVE_ATOMIC_COUNTER_BUFFERS) and glGetActiveAtomicCounterBufferiv are // the pre-4.3 spelling of the interface above, and the spec requires the two to agree. // Neither did: the first counted glslang's atomic counter UNIFORMS - zero, because the diff --git a/MobileGL/MG_Test/Program/ProgramTest.cpp b/MobileGL/MG_Test/Program/ProgramTest.cpp index f65876f3..8332d5f8 100644 --- a/MobileGL/MG_Test/Program/ProgramTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramTest.cpp @@ -410,7 +410,51 @@ void main() { // BUFFER block, so SPIRV-Reflect sees an ordinary storage descriptor for it and the // backend's space has always contained it - even though GL's own GL_SHADER_STORAGE_BLOCK // enumeration excludes it. The archive must follow the BACKEND's space, not GL's. +namespace { + // A limits-only backend: what the link checks against comes from GetDynamicParameters, and the + // no-backend defaults are the spec minimums, which give the vertex stage no storage blocks. + class StorageBlockLimitsBackend final : public MG_Backend::BackendObject { + public: + StorageBlockLimitsBackend() { + m_params.MaxVertexShaderStorageBlocks = 8; + m_params.MaxCombinedShaderStorageBlocks = 16; + m_params.MaxShaderStorageBufferBindings = 16; + } + void Initialize() override {} + Bool InitCapabilities() override { return true; } + Bool InitWindowSurface() override { return true; } + const RendererInfo& GetRendererInfo() const override { + static RendererInfo info = {}; + return info; + } + String GetBackendAPIVersionString() const override { return {}; } + const MG_Backend::GlobalBackendFunctionsTable& GetBackendFunctions() const override { + static MG_Backend::GlobalBackendFunctionsTable table = {}; + return table; + } + const MG_Backend::DynamicBackendParameters& GetDynamicParameters() const override { return m_params; } + BackendType GetBackendType() const override { return BackendType::Unknown; } + + private: + MG_Backend::DynamicBackendParameters m_params{}; + }; + + class ScopedLimitsBackend { + public: + ScopedLimitsBackend(): m_previous(Move(MG_Backend::pActiveBackendObject)) { + MG_Backend::pActiveBackendObject = MakeUnique(); + } + ~ScopedLimitsBackend() { MG_Backend::pActiveBackendObject = Move(m_previous); } + + private: + UniquePtr m_previous; + }; +} // namespace + TEST_F(ProgramTest, TheArchivesStorageBlockOrderIsTheOneSpirvReflectProduces) { + // The vertex stage below uses storage blocks, which a link only accepts against a backend + // that advertises them. + const ScopedLimitsBackend limits; char infoLog[1024] = ""; const char* vsSrc = R"(#version 460 core layout(std430, binding = 3) buffer Blocks { diff --git a/MobileGL/MG_Util/ShaderTranspiler/Types.h b/MobileGL/MG_Util/ShaderTranspiler/Types.h index a8ef9a91..c9242414 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/Types.h +++ b/MobileGL/MG_Util/ShaderTranspiler/Types.h @@ -80,6 +80,11 @@ namespace MobileGL { inline constexpr Int MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE = 8; inline constexpr Int MAX_ATOMIC_COUNTERS_PER_STAGE = 8; + // GL_MAX_{VERTEX,TESS_CONTROL,TESS_EVALUATION,GEOMETRY,FRAGMENT}_UNIFORM_BLOCKS. No + // built-in constant expands from these, but the link fails a program that uses more + // (ProgramLinkTask's block-count check), so the getter and the check share one number. + inline constexpr Int MAX_GRAPHICS_STAGE_UNIFORM_BLOCKS = 14; + // ---- Tessellation per-stage resource limits ---- // // Here for exactly the reason the atomic-counter block above is here. GL 4.6 requires