[Fix] (GL): fail the link when a stage exceeds its uniform or storage block limits

This commit is contained in:
2026-10-12 06:46:05 -04:00
parent 106190e2ff
commit ba6db60c09
5 changed files with 249 additions and 5 deletions
+10 -5
View File
@@ -118,23 +118,28 @@ namespace MobileGL::MG_Impl::GLImpl {
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VERTEX_UNIFORM_COMPONENTS);
constexpr GLint kFrontendMaxVertexUniformVectors =
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VERTEX_UNIFORM_VECTORS);
constexpr GLint kFrontendMaxVertexUniformBlocks = 14;
constexpr GLint kFrontendMaxVertexUniformBlocks =
static_cast<GLint>(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<GLint>(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<GLint>(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<GLint>(MG_Util::ShaderTranspiler::MAX_GRAPHICS_STAGE_UNIFORM_BLOCKS);
constexpr GLint kFrontendMaxTessEvaluationUniformBlocks =
static_cast<GLint>(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
@@ -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_<STAGE>_* 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<MobileGL::String, Array<BlockUse, kStageCount>> 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<BlockUse, kStageCount>* use = nullptr;
for (SizeT stage = 0; stage < kStageCount; ++stage) {
if ((static_cast<unsigned>(block.stages) & (1u << static_cast<unsigned>(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
@@ -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_<STAGE>_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
+44
View File
@@ -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<StorageBlockLimitsBackend>();
}
~ScopedLimitsBackend() { MG_Backend::pActiveBackendObject = Move(m_previous); }
private:
UniquePtr<MG_Backend::BackendObject> 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 {
@@ -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