mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Fix] (GL): fail the link when a stage exceeds its uniform or storage block limits
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user